From 1aef774c556026f634183ca4d85a3551dd83b651 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Wed, 7 Oct 2026 00:40:53 +0200 Subject: [PATCH 001/281] =?UTF-8?q?key.ui.fx:=20M0=20scaffolding=20?= =?UTF-8?q?=E2=80=94=20runnable=20JavaFX=20shell?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a new Gradle module key.ui.fx that reimplements the Swing UI (key.ui) with JavaFX (v25.0.4). No existing program code is touched; only build config (settings.gradle include, version catalog) is added. - org.openjfx.javafxplugin with modules javafx.base/controls/graphics (no FXML, no Swing) - MainEx (picocli) mirroring key.ui's CLI entry: --experimental and --threads are functional, --auto exits with an explicit not-yet-implemented message - minimal MainApplication shell (menu bar, status bar) - copied fonts/ and images/ resources from key.ui - --enable-native-access=javafx.graphics for JDK 25+ warnings Verified: compile, spotless, run (window stays up), --auto failure. --- gradle/libs.versions.toml | 15 +- key.ui.fx/build.gradle | 75 ++ .../java/de/uka/ilkd/key/core/fx/MainEx.java | 113 +++ .../uka/ilkd/key/gui/fx/MainApplication.java | 60 ++ .../uka/ilkd/key/gui/images/closed-cached.png | Bin 0 -> 857 bytes .../uka/ilkd/key/gui/images/ekey-brackets.gif | Bin 0 -> 3373 bytes .../gui/images/ekey-mono-inverted-icon.gif | Bin 0 -> 227 bytes .../de/uka/ilkd/key/gui/images/ekey-mono.gif | Bin 0 -> 1392 bytes .../key/gui/images/interactiveAppLogo.png | Bin 0 -> 315 bytes .../key/gui/images/key-color-icon-square.gif | Bin 0 -> 2864 bytes .../key/gui/images/key-color-icon-square.png | Bin 0 -> 8276 bytes .../de/uka/ilkd/key/gui/images/key-color.png | Bin 0 -> 18292 bytes .../ilkd/key/gui/images/key-shadow-2.10.png | Bin 0 -> 85512 bytes .../ilkd/key/gui/images/key-shadow-2.12.png | Bin 0 -> 88787 bytes .../ilkd/key/gui/images/key-shadow-2.8.png | Bin 0 -> 99751 bytes .../ilkd/key/gui/images/key-shadow-3.0.png | Bin 0 -> 94495 bytes .../de/uka/ilkd/key/gui/images/key-shadow.png | Bin 0 -> 97720 bytes .../ilkd/key/gui/images/keyinteractive.gif | Bin 0 -> 9549 bytes .../de/uka/ilkd/key/gui/images/keylinked.gif | Bin 0 -> 1392 bytes .../de/uka/ilkd/key/gui/images/keyproved.gif | Bin 0 -> 2085 bytes .../uka/ilkd/key/gui/images/questionIcon.png | Bin 0 -> 10837 bytes .../uka/ilkd/key/gui/images/scriptAppLogo.png | Bin 0 -> 1115 bytes .../uka/ilkd/key/gui/images/scriptAppLogo.svg | 72 ++ .../de/uka/ilkd/key/gui/images/toolbar/README | 3 + .../ilkd/key/gui/images/toolbar/abandon.png | Bin 0 -> 33495 bytes .../key/gui/images/toolbar/autoModeStart.png | Bin 0 -> 8064 bytes .../key/gui/images/toolbar/autoModeStop.png | Bin 0 -> 6210 bytes .../de/uka/ilkd/key/gui/images/toolbar/ce.png | Bin 0 -> 24858 bytes .../ilkd/key/gui/images/toolbar/config.png | Bin 0 -> 34200 bytes .../key/gui/images/toolbar/decProcArrow.png | Bin 0 -> 171 bytes .../uka/ilkd/key/gui/images/toolbar/edit.png | Bin 0 -> 16499 bytes .../key/gui/images/toolbar/expandGoals.png | Bin 0 -> 8457 bytes .../ilkd/key/gui/images/toolbar/go-next.png | Bin 0 -> 17526 bytes .../key/gui/images/toolbar/go-previous.png | Bin 0 -> 17733 bytes .../ilkd/key/gui/images/toolbar/goalBack.png | Bin 0 -> 17218 bytes .../ilkd/key/gui/images/toolbar/heatmap.png | Bin 0 -> 900 bytes .../uka/ilkd/key/gui/images/toolbar/help.png | Bin 0 -> 31781 bytes .../uka/ilkd/key/gui/images/toolbar/jml.png | Bin 0 -> 1374 bytes .../key/gui/images/toolbar/junit_logo.png | Bin 0 -> 1208 bytes .../ilkd/key/gui/images/toolbar/macros.png | Bin 0 -> 18793 bytes .../uka/ilkd/key/gui/images/toolbar/mgt.png | Bin 0 -> 5381 bytes .../uka/ilkd/key/gui/images/toolbar/minus.png | Bin 0 -> 2837 bytes .../gui/images/toolbar/oneStepSimplifier.png | Bin 0 -> 598 bytes .../gui/images/toolbar/oneStepSimplifier.svg | 738 ++++++++++++++++++ .../uka/ilkd/key/gui/images/toolbar/open.png | Bin 0 -> 14175 bytes .../key/gui/images/toolbar/openMostRecent.png | Bin 0 -> 27595 bytes .../uka/ilkd/key/gui/images/toolbar/plus.png | Bin 0 -> 5366 bytes .../key/gui/images/toolbar/properties.png | Bin 0 -> 12522 bytes .../key/gui/images/toolbar/pruneProof.png | Bin 0 -> 29042 bytes .../uka/ilkd/key/gui/images/toolbar/quit.png | Bin 0 -> 23410 bytes .../ilkd/key/gui/images/toolbar/recent.png | Bin 0 -> 41960 bytes .../ilkd/key/gui/images/toolbar/saveFile.png | Bin 0 -> 26285 bytes .../ilkd/key/gui/images/toolbar/search.png | Bin 0 -> 25482 bytes .../ilkd/key/gui/images/toolbar/search2.png | Bin 0 -> 38841 bytes .../key/gui/images/toolbar/statistics.png | Bin 0 -> 19095 bytes .../uka/ilkd/key/gui/images/toolbar/stop.png | Bin 0 -> 23714 bytes .../de/uka/ilkd/key/gui/images/toolbar/tg.png | Bin 0 -> 30989 bytes .../ilkd/key/gui/images/toolbar/toolbox.png | Bin 0 -> 13275 bytes .../uka/ilkd/key/gui/images/toolbar/uml.png | Bin 0 -> 993 bytes .../Font Awesome 6 Brands-Regular-400.otf | Bin 0 -> 525600 bytes .../fonts/Font Awesome 6 Free-Regular-400.otf | Bin 0 -> 191952 bytes .../fonts/Font Awesome 6 Free-Solid-900.otf | Bin 0 -> 1031648 bytes .../resources/fonts/MaterialIcons-Regular.ttf | Bin 0 -> 128180 bytes key.ui.fx/src/main/resources/fonts/entypo.ttf | Bin 0 -> 75800 bytes .../resources/fonts/fontawesome-webfont.ttf | Bin 0 -> 165548 bytes .../src/main/resources/fonts/typicons.ttf | Bin 0 -> 99920 bytes settings.gradle | 1 + 67 files changed, 1076 insertions(+), 1 deletion(-) create mode 100644 key.ui.fx/build.gradle create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/core/fx/MainEx.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/MainApplication.java create mode 100644 key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/closed-cached.png create mode 100644 key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/ekey-brackets.gif create mode 100644 key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/ekey-mono-inverted-icon.gif create mode 100644 key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/ekey-mono.gif create mode 100644 key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/interactiveAppLogo.png create mode 100644 key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/key-color-icon-square.gif create mode 100644 key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/key-color-icon-square.png create mode 100644 key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/key-color.png create mode 100644 key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/key-shadow-2.10.png create mode 100644 key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/key-shadow-2.12.png create mode 100644 key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/key-shadow-2.8.png create mode 100644 key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/key-shadow-3.0.png create mode 100644 key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/key-shadow.png create mode 100644 key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/keyinteractive.gif create mode 100644 key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/keylinked.gif create mode 100644 key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/keyproved.gif create mode 100644 key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/questionIcon.png create mode 100644 key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/scriptAppLogo.png create mode 100644 key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/scriptAppLogo.svg create mode 100644 key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/toolbar/README create mode 100644 key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/toolbar/abandon.png create mode 100644 key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/toolbar/autoModeStart.png create mode 100644 key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/toolbar/autoModeStop.png create mode 100644 key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/toolbar/ce.png create mode 100644 key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/toolbar/config.png create mode 100644 key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/toolbar/decProcArrow.png create mode 100644 key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/toolbar/edit.png create mode 100644 key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/toolbar/expandGoals.png create mode 100644 key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/toolbar/go-next.png create mode 100644 key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/toolbar/go-previous.png create mode 100644 key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/toolbar/goalBack.png create mode 100644 key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/toolbar/heatmap.png create mode 100644 key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/toolbar/help.png create mode 100644 key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/toolbar/jml.png create mode 100644 key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/toolbar/junit_logo.png create mode 100644 key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/toolbar/macros.png create mode 100644 key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/toolbar/mgt.png create mode 100644 key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/toolbar/minus.png create mode 100644 key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/toolbar/oneStepSimplifier.png create mode 100644 key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/toolbar/oneStepSimplifier.svg create mode 100644 key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/toolbar/open.png create mode 100644 key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/toolbar/openMostRecent.png create mode 100644 key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/toolbar/plus.png create mode 100644 key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/toolbar/properties.png create mode 100644 key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/toolbar/pruneProof.png create mode 100644 key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/toolbar/quit.png create mode 100644 key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/toolbar/recent.png create mode 100644 key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/toolbar/saveFile.png create mode 100644 key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/toolbar/search.png create mode 100644 key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/toolbar/search2.png create mode 100644 key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/toolbar/statistics.png create mode 100644 key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/toolbar/stop.png create mode 100644 key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/toolbar/tg.png create mode 100644 key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/toolbar/toolbox.png create mode 100644 key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/toolbar/uml.png create mode 100644 key.ui.fx/src/main/resources/fonts/Font Awesome 6 Brands-Regular-400.otf create mode 100644 key.ui.fx/src/main/resources/fonts/Font Awesome 6 Free-Regular-400.otf create mode 100644 key.ui.fx/src/main/resources/fonts/Font Awesome 6 Free-Solid-900.otf create mode 100644 key.ui.fx/src/main/resources/fonts/MaterialIcons-Regular.ttf create mode 100644 key.ui.fx/src/main/resources/fonts/entypo.ttf create mode 100644 key.ui.fx/src/main/resources/fonts/fontawesome-webfont.ttf create mode 100644 key.ui.fx/src/main/resources/fonts/typicons.ttf diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7ddff7e2730..e6f297adabb 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -32,6 +32,11 @@ miglayout = "11.4.3" picocli = "4.7.7" docking-frames = "1.1.3p1" +# JavaFX user interface (key.ui.fx) +javafx = "25.0.4" +richtextfx = "0.11.5" +openjfx-plugin = "0.1.0" + # External tools scala-isabelle = "0.4.5" @@ -80,6 +85,13 @@ miglayout-swing = { module = "com.miglayout:miglayout-swing", version.ref = "mig docking-frames-common = { module = "org.key-project:docking-frames-common", version.ref = "docking-frames" } docking-frames-core = { module = "org.key-project:docking-frames-core", version.ref = "docking-frames" } +# JavaFX UI components (key.ui.fx) +richtextfx = { module = "org.fxmisc.richtext:richtextfx", version.ref = "richtextfx" } +javafx-base = { module = "org.openjfx:javafx-base", version.ref = "javafx" } +javafx-controls = { module = "org.openjfx:javafx-controls", version.ref = "javafx" } +javafx-graphics = { module = "org.openjfx:javafx-graphics", version.ref = "javafx" } +javafx-web = { module = "org.openjfx:javafx-web", version.ref = "javafx" } + # CLI picocli = { module = "info.picocli:picocli", version.ref = "picocli" } @@ -98,4 +110,5 @@ spotless = { id = "com.diffplug.spotless", version.ref = "spotless" } checkerframework = { id = "org.checkerframework", version.ref = "checkerframework-gradle" } maven-publish = { id = "com.vanniktech.maven.publish", version.ref = "maven-publish" } shadow = { id = "com.gradleup.shadow", version.ref = "shadow" } -license-report = { id = "com.github.jk1.dependency-license-report", version = "3.1.4" } \ No newline at end of file +license-report = { id = "com.github.jk1.dependency-license-report", version = "3.1.4" } +openjfx = { id = "org.openjfx.javafxplugin", version.ref = "openjfx-plugin" } \ No newline at end of file diff --git a/key.ui.fx/build.gradle b/key.ui.fx/build.gradle new file mode 100644 index 00000000000..bf586481ca9 --- /dev/null +++ b/key.ui.fx/build.gradle @@ -0,0 +1,75 @@ +/* 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 */ + +plugins { + id 'application' + + // JavaFX is not bundled with the JDK; this plugin resolves the matching + // platform-specific runtime artifacts (see gradle/libs.versions.toml). + alias(libs.plugins.openjfx) + + // Used to create a single executable jar file with all dependencies + // see task "shadowJar" below + // https://github.com/GradleUp/shadow + alias(libs.plugins.shadow) +} + +description = "JavaFX user interface for the deductive verification of Java programs" + +javafx { + version = libs.versions.javafx.get() + // key.ui.fx intentionally avoids javafx-fxml and javafx-swing: + // all UIs are built in code (no FXML) and no Swing components are used. + modules = ['javafx.base', 'javafx.controls', 'javafx.graphics'] +} + +dependencies { + implementation project(":key.core") + + // Java/JML source editor with syntax highlighting (Milestone M2). + implementation(libs.richtextfx) + + implementation(libs.picocli) + + // logging implementation used by the slf4j facade + implementation(libs.logback.classic) +} + +application { + mainClass.set("de.uka.ilkd.key.core.fx.MainEx") +} + +run { + systemProperties["key.examples.dir"] = "$rootDir/key.ui/examples" + systemProperties["KeyDebugFlag"] = "on" + + // enable assertions + jvmArgs += "-ea" + + // JavaFX 25 loads its native libraries (glass, prism) via restricted methods; + // allow native access to silence the JDK warning (mandatory on future JDKs). + jvmArgs += "--enable-native-access=javafx.graphics" +} + +tasks.register('runExperimental', JavaExec) { + group = "application" + classpath = sourceSets.main.runtimeClasspath + mainClass.set("de.uka.ilkd.key.core.fx.MainEx") + args("--experimental") +} + +tasks.register('runDebug', JavaExec) { + systemProperties["key.examples.dir"] = "$rootDir/key.ui/examples" + systemProperties["KeyDebugFlag"] = "on" +} + +/* + * Packaging note (M0): + * A JavaFX application does not assemble into a plain "fat jar" simply by + * shading the Java bytecode: the platform-specific native runtime + * (libglass, libprism, ...) comes from classifier jars and must be bundled + * with jlink/jpackage. Shadow/jlink packaging for key.ui.fx is therefore a + * later milestone; until then run via `./gradlew :key.ui.fx:run`. + */ 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/MainApplication.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/MainApplication.java new file mode 100644 index 00000000000..07b032505b3 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/MainApplication.java @@ -0,0 +1,60 @@ +/* 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.application.Platform; +import javafx.geometry.Pos; +import javafx.scene.Scene; +import javafx.scene.control.Label; +import javafx.scene.control.Menu; +import javafx.scene.control.MenuBar; +import javafx.scene.control.MenuItem; +import javafx.scene.control.SeparatorMenuItem; +import javafx.scene.layout.BorderPane; +import javafx.stage.Stage; + +/** + * The JavaFX application of KeY, counter-part of {@code de.uka.ilkd.key.gui.MainWindow} in the + * Swing module {@code key.ui}. + *

+ * Milestone M0: this is a deliberately minimal, runnable shell. The docking workspace (M1), the + * menus/toolbars, and the views (M2) replace the placeholder content stage by stage. + */ +public final class MainApplication extends Application { + + @Override + public void start(final Stage stage) { + BorderPane root = new BorderPane(); + + root.setTop(buildMenuBar()); + + Label placeholder = new Label("KeY UI (JavaFX)\n\nThe docking workspace arrives in " + + "milestone M1."); + placeholder.setAlignment(Pos.CENTER); + root.setCenter(placeholder); + + Label statusLine = new Label("Ready"); + BorderPane.setAlignment(statusLine, Pos.CENTER_LEFT); + root.setBottom(statusLine); + + stage.setTitle("KeY (JavaFX)"); + stage.setScene(new Scene(root, 1024, 768)); + stage.show(); + } + + private MenuBar buildMenuBar() { + MenuBar menuBar = new MenuBar(); + + Menu fileMenu = new Menu("File"); + MenuItem quitItem = new MenuItem("Quit"); + quitItem.setOnAction(e -> Platform.exit()); + fileMenu.getItems().add(new SeparatorMenuItem()); + fileMenu.getItems().add(quitItem); + + menuBar.getMenus().add(fileMenu); + return menuBar; + } +} diff --git a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/closed-cached.png b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/closed-cached.png new file mode 100644 index 0000000000000000000000000000000000000000..a69bf849179884c022e98b31ae9156892422243c GIT binary patch literal 857 zcmV-f1E&0mP)&Q5?tduiA?!B60|6xPvw2QW1@2fdmF(23if>hcED60Hq9z33|qzX)f9aU*QTV{?wZfRt&m(qc7h>W-%Xzor|S<3z)na79?pmsJ|&0{T+M z@{;HSBZOICkth-hWpd;x5g>zIJYC+AGSBi|_-Fq3@)vH$D>CbJdV1p_$^_1cDo{HJ*?_fZGDOnI>rT*T*h z2{kQC$JDSNx^Z=Vi@RXBYEnBm55^Umx|ZD!W=OdfY67kX8r@%JSPxVB#3+3gcwjt{ z#K6#m5RB%rSOUMW%^Fy)oT(i}Fcd*PUqCwjf}21HZjTR~%S&9TgGi;GQ7o1ajzl2< z<29;M&{K@a)mS!e5m42u>${Lo&C+~uQYHlB)iu-0G{ZJUw-Ah%C%z5IN(b7NU1*hx z(b+dlo*p&1FvZd|$ys2$98qJn9hFal_wp;<QVtOj3T8S#!I>{P)`I<^w`-Pip z3$hEUi`xrJ8!SA0d%X3FOkBLY+w9y5tt{=!oRxhYvTfzfjcvUU5U>Ou?(qvuUS5ub zo~|Cv;O=g;E9PhSUg{t61oCotK#f((HKOvu3-!-?j2AcWX08%6$#7U^MB z7|X!MjX5rU6o+f%NE{DOZuq5=qz#iUKdg({@J7p;8gAmWr*kJy3_gEWoaxbxCn`l_ zvJ~oN9VI4BRR&FJOlnAgRXJWAhefMcR}NuXeG2y5*tKLknw5zZYZbP1Ik`;y&S>`@35{BF zjHaFCo=y!r@{6LaO%I8UTXc)SM+Y~Cmpiy!76ES7w*5o6a>OUfNj?7i^783ZQP`uo z*>h#;+}V}5j(xQD(!v)||L!mR>hY2vXAC`pq59eLd%!nwAB6n+EdKcK$h;e7`c34Y z3I&qa*9?$(u+ei0viG20+a<`sNC)Bv;d*uXVAuvKT}Xn1K~PxMh8V;oB7LsiH5NW5 zT7VOX@2v=3iY~GRV~jG|l%WfBQS~DQM(sEcjR+EHAC3|jRa}ofA=zJxQz-cWlTA8F z6qYHhrIwTqSa~IK`>2wfSXyw|0GMHli4vRQ*%+2qX{re)hDK&rXPkMe>714nK)I!y zV3?TzpMClnL7wgL6NaD%7;289GZOZnq}52o=K+9DVCkEPHX$OUnr@mXsU>vEg`A>} z`e_5EhWIL9`53A|tgg1&CZ%X}#bm9v`id)$caE{kstKT){=u)p4m*otnKGa(0?`f> ztYfhl3oNr2ARAz{QCv&wt=>)>k+;^G60QT&lKYam=X!!Iw90bZt*&p360f|$viqdA zXMB<_1oOJvuf1@<8}I|a(i^3lX>5|P!nQJOAi;%IlCQcGTb!lCYG53(z^(F|tiK;q z60*pMQe3gYbWD=5$|h?(^9mfR6|>A9)4a3IJ)fMi1h10Zaddd(T&k8Vqfql2NGIL& zu|!)Cbs9l)O6t^9|7`WuSXa#|*G!9T#?xPez4Qq;SK;-afTnGy(RG-8HrZNN05%zr zdRH#mYvWC}-hA@Xchqpl&8QuF^WAQSho2g_kFv=AusGudbNx81F4ssnafVJ_q~U)9 zzPWGArBJ!$m&*-x=@GJSx@xEoUfr@iXRbNyi<)9W>Z+H_9`0kxUa6N1e%@B-x8F^= z?leePA??Xi8~o_aC(b#Q75LtH<25sXJ@!Wm$9?zSyKbKK;q%Voa>$=vxW~fhE`IE} zm2dv)&P%Pn+vscPzW2fJeE$nu85xKWZ-aewLfX~&wm*N z;Qa(R!1#3#f*Y(`{p=SS5QdO|Crn=nO~^hD;Rb)814#iRsK6MO2{kmV;iPT|zHKG2 zWtr(A0xg3>2{!OHkfC9qkQl_R4Y6Kz!Jq!mp!h-`L0*sxj(p_)A}Pl?cEOKSgk%(f7set~(vmGSAS3m7$|H=Dl$sP*C_6bqCUEkL z!@{H_2kC@WvQmfodL=AlsmfW>5|E!{AuvU0ND2~DkwPqHbA&lePcpNa(;QGTl_^bM zzEPVt(B&^%*~=+LQ=BvK<~MoiLT-*Tl{qlyIT6V-a;}q|-Gt^D{bS1`Ix3jgOp;LY zsZW50pmHUur86B^%?Y@XjHM)DLjLvl&@ZX*cX#~fK`|K7dRVY_6I!M^MbaB066yy= z%pN+kB~PHdQ=T8_Aw@m-zhC8~VcNsNL??+SHwkYsB1O{;T$;ao;*p+@;%QHPTF#r| z6qOx(oKxmG(ikXpsh-m5rhc-`Ai)5rKCCDl0o5z0Ms)>T?J83nYRY4RG^}FXrwYm% z)T}m@Nc}u&O`o|@;vG`2uG|@)1R)vBb*tt^=Ja zRSg?UmbsI9FN%+4Ioi=NE^V_S6l`S)J3W)avvsGGEKJ>M&BjhphT2J+YZW`zmMT>` zwFRp^Z&}z5a1S`+x$19${)@{CEc7+s^X+kwYu4zQ@f_8|(rwSWTHRWZi#u}ab2FJ; z;i{0k)eR+SmFHNA$`-V|ZJMdBJK5lRXrknGFQVYP(#vMRiNwX+tMF^r@Jbg<^4+26 zK6PGDrdOk=eW^bWtjz>d6Rq3(EOV=7D*y)+oC7AXfD0-%nMzoTA3o7%9gIy1_m!xH z`tF1|tbww!(Qe8bF>6n{-xJ4GwFSO0jOXj(MgCa1I^J<;7c78iX&9p=u5g2sJ3br> z8OaQ;>kp-zqbKV(#X}}BmSY@bopo7AMP~1V4>+wQb5_Mw3fD(-$mV!bc}`&tXPoJ* zCWp>Bz)ha=oylDOXOxb4N$JX{p#N;=Jy#h_aPDyg;`Qb$BRbK826Ld(`sGI_EYeKQ zw3i(XWGgGU(rxZ1rx8kOq47)2>+7?o^L%JeW7pMwPIZn&{bE_;nbGN-bYMYUYg%{M zO6aw9KZ17Z(i+>xy0$c~KMmzw6Pwb&e(kcwtZ4NKc3B7GOUwf_R2KO3K817&Td)U*?^}dfC?})}xYpk|+v|Ai+ch7sn?Z$Mo z`OQ{n9~=RdT(=Zis_=$OJL0NttuQU#>W7D13cm7tLqL}R zsJYE=j&uH-A35hdZ#mC@Uh@{f9Oy*PvdOvq^PLkt=nP-F&V^1lr{~=0yuP^9QT%J5 zKk@2M_naTO-gSi@7wlo5b{w6v(It*^d}|%;V1L1q+1_>mke2OxojcvH zzSW%Z9nf!wd)zS#_^c=0?#NEN*i|m?rRTKnG^M=SV>b9#Lw?0hM||QHKlQ*zEc2SL zx-rjQRj!MB@r>WJ<4fOfWlP@LQjhQKMPF~ym%8x&M!oHQUwPF_)mV6kH}8Az_unT! z_`=6MtaZO}qkwn$%>VrALqB|4L4VP)FLvjZ&wB8Ozx7(izPY=PYT$$Z@U*|Z^F3_& z=>B81(bWHY?af|)sFxY`9Pf1g5!Za}=YH>JB>~8N{U>Sa*L3%Xf8$4hOk#cQuzASm zfD1T)0;qtOSAH{hX$EJ0_V<00XMhhlBo~Nq$LD<}NPiiKTO&wLr?&RhE~{w zOUaZ=Ida*U1RTkSM%a{9X_e9_m0d!UCb@KFc9mc0lJ#hm8yS{dNp$50mT6g*8ibTn zvX;=bg=#66Ov#frDJyMhgFjb*bIF%YIgY}3FB9WEEmMM<2+X`D|Pnrot*uIYh* z*$dN&opd=A(3zdxDV*EMo!&W};7Ok4X`bhap6RKc>&c$&>7EPk37_#PpY!<&1poj$ DQVg_+ literal 0 HcmV?d00001 diff --git a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/ekey-mono-inverted-icon.gif b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/ekey-mono-inverted-icon.gif new file mode 100644 index 0000000000000000000000000000000000000000..68c81f366a7dc5694c8dbc094063ee1f2f64f9e7 GIT binary patch literal 227 zcmV<90382ENk%w1VK4wN0Du4h|NsC0|NkNW5kqoiVRU6=Aa`kWXdp*PO;7+K`2+z9 z0096j00000FaR(B00O0qkEzS;52K6#%=@a|6=M!^9u=0_*o2FUA|QFTfjH6V=GkAI z^LuymNpLOV#Tc~wWCkRRITJ#p37w1Tz#dGcpEAXD!oJyX5?a)vGwLYl>l zN9U`1Xua+SMAw!k$Y&TQwuR`XMtBCs7$|u~wnq54#73E9xso~b2>L{N$$9A6S#x>> dI$D+*8QaQ=IysAn8yaTD#QW>}%N0=o06XGfXBz+j literal 0 HcmV?d00001 diff --git a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/ekey-mono.gif b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/ekey-mono.gif new file mode 100644 index 0000000000000000000000000000000000000000..4c573ffecd4f116c9326a16b1bee6872625d5c4e GIT binary patch literal 1392 zcmV-$1&{hiNk%w1VJZPA0e}Di00030|NkNR1PKTL0RSuj0000g0Vn|g0{@JUsmtvT zqnxzbi?iOm`wxcVNS5Y_rs~SJ?hD8AOxN~}=lag~{tpZahs2`sh)gP%%%<}RjY_A~ zs`ZM^YPa03_X`e-$K-YS={|^`_I7nD%c!-#&xX9S( z_y`#(IZ0V*d5M{+xyjk-`3V{-I!an`U)E>J4;(@dyAW^yUW|_`vVXx zEc`n*e2knNTr8#R{0t4iTohex{XE@sz0HlBebXH-F8vKNem-8lg3g|~zmK0!!4I&XKL-H|o*T%(pu&R)4=j8rt|7#U5%W>J*o_{>jsDxzaU8bbBgknG zLri40@WjJk23siXRZvDuT>x>^#I;XbPFwea?3`tfT2NW?k_>%SRLN0R@t!1I)ow~t zRZ*!-J;g3dRZ{}BGAz|qOIT22#eNNS)=OGXYt_zU`&LX`PIJSw#Z*^rMY?;@Sa3K^hYcr|^jL95yo=*RepK0VW=faUbY8Ss^JLDW=Y)pz+4E^ft3`L5 z)EZ7~(A@Nt#(cZZY(%+r_on^Yc5T$d_xw(DoKEsX%jN7Y)ZFzo+{J}g2R_|TbV1vt zSEHW2Pj^7$-;~#W9T+hk=f$Vr;VegcKJ(W_1NWYHejEGk{|9GOUkZ(p5MUk!nsAj$ z&kb@QJUy9ImVW=q7tjVz0odROBBcbLH9TziQitEc2jWbu^(Rw_6^eKti7N6#+=Mp0 z_#lhW=tSd%jVx$RjtQP9BZWQ=Xh?<%4rwGFNgj!0i?E^SVw2uj!lRH_v!*nGF#k=9@scDd(JY);Z>#cz&tno^a~v=Zt^`${&%0 zTB+ut0vf6)euFmJBcqVcz~-cg#%ZZbS!IgjT~&;9%T`vz7pkaGjL4IzO!VoBjz~OO zrmJ>V+UioBs!CLig8r1Mu9TWsDzA~Qn9EHsth5WU|Ne!Ttg$MT1e3E&qUnpW2qMc8 zjEgc`jI<5{OOmhOYWt431d{8ItmpoS5WC|_TF$21VyY3m8Ng~$q7CR9E~_HYD-o0s z`b#Ll81S1d!T$>EC&CFi+$_Tbhi9q5_ev}$#1CT}u)-Q@9IVG5i`+5AE`Z#t$(nMU zF3S^p2&u&!pbV7DFQcj`%^Q=PFVE|$yr;|^=!}%j3me@p(8IzUsL=Hw-7VBNJK3<* zAUGYd(lHQC71sn=oi)>rYCVwGUxQsN+E=TcGTB?RU5nWdVBI#(mr~uX+;hX7tIsUx zjTYY@&|S0Ne+ynK;!hh+lj4iJoekg+5Dtsv{*ybN3gsh19yQ}DdmcCDgNx1;=|h`7 ziscfhP8jMlvksW+uY(?o=GW3*x$LuZ9=6>+!48@3yNjJP?|0uAukgbY-?{N?;?9)v z%j2D}^UqsfIQF}0pF8w#d!MHCr-NTQ`NxX?r1|HoA1nKxGv7G+<8vRq`|rb#ll+h4 zA2R<&052i>@rio*v){=8SS0<8&VZS)-S-4oJ_rsFdGV9r1hG*;{~1Mr%t4@qIG8{N zE=_}pv*3q7NC*#3kAk5S;R8unKNFr1g@I7v{9xEM7rxMU-IL+1cvv(Yrbvi8qT$he zm?a{PkbteQ;R2P293yTHhhVvq6|oda#4AdvZ(7`954(t@+JVuFB^=|BzGy}tWigFv yEF&9fw8l5SD2{RjlN;*@A~fQWOmy7iM!4ulj$|*87il3OPtw0bT8jt;0029Y$Gi{# literal 0 HcmV?d00001 diff --git a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/interactiveAppLogo.png b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/interactiveAppLogo.png new file mode 100644 index 0000000000000000000000000000000000000000..9ca42b385c838cee8739934eabf3afb585c46a75 GIT binary patch literal 315 zcmeAS@N?(olHy`uVBq!ia0vp^!ayv@!3HGlXG}j1q!^2X+?^QKos)S9a~60+7Bet#3xP1>rMq>1fP(BLp1!W^ce!MF8C8~-mlOhpq)J>PN`mv# zO3D+9QW*jgGxJLH{9Hp6%8d0)^$ZQ&KRcNNRZaJFaSXBWU)$5j+iW1fe6ya%?1kRk zM$=0cC$1FE=~$#Bdusl_eC?%wdNt+?Ee*2&v#eydl|}APi)k4jtV(QJEHB=DQQ(=u zX(^}JoMCk2LC5>aI?g^bS=|G#{;QhLE~ZfD{A*z!$HX!XN8Z=6Kkam;G*z5;S|BfW zFtnw|sp;RhSat8tJG_k=jLqfF3SOVQIb44BY^r4l?d)hfz5HDh(1{G5u6{1-oD!M< DVPI`# literal 0 HcmV?d00001 diff --git a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/key-color-icon-square.gif b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/key-color-icon-square.gif new file mode 100644 index 0000000000000000000000000000000000000000..929320bd1bedcfa6d130396c5fee6c5efc4c02d8 GIT binary patch literal 2864 zcmV-03(xdNNk%w1VE_RD0fZa?L_2;(JA6eueMUQcMmv2*JAOwyeMLNcMLc~)Jbp$z zd`3KdMm&B;JbXtyeMdZgMLm2)J$*(!d`3NeMm>H_CX>@2HM@dak03rDV0SW*g04x9i0000100966 z{@^E$WNDt{rmk%3zHlthbZy^wuJ0=9|G=P+M=TnT$fVLHY&wU_sB}uL9-mmDb<6E~ zso8MpOD>zwGjSHpZo9qe=6g=BU*k}_e$SWTO?`oafqx@|Y$F#Ii;Rtq7lwysg_3X| z7nq6wo1C4V00EsAnUEiqTzIEc8JCzAodE(1v$VCgw*sM@qnIBVs#0peORbulvbV{~ z%d-NznWn=?VbVvLugA>V+se+Hycg9)SmHbwuG!q`>$u;T809)n@j2(%$n5$0&F&Z) z^h~q>oN-Q3tbGU*E^x*R-oO}g9=^z644=Y_`Miz#C9#FTjw>iyIEb-iLWURqID*^& zWTgs<9{^Z1NmHM*ODa45aLI~f&7TNwrUaQsBFvdUlV;pV^h8IP0hCHr7yze4r$#?S zoys*~)tvMI4TRaXtU{{*$Bb1}!K_<-6D;@|m;xXx3vJ)d3c$Co->rHVRG1q!QaKHf zER_Ar*OB8zi~&!Td*NR>!{QJxn%dV92@Ie^i^f30QQyggYz=TV4tr)2B%3TmMum1CSHR-^pcb9t! zx)<%QxtsJpEZQ*fG<{=#qrG`a_Ad1hUr*V*oBHZqBv*d=1(=Y0{?Yyc;Ck*6=*E5m z_9w}L7!BxP8xST`frP5mmzP2oUO0(?`fNDgbc#$+VS^%ep$Gv2me*Qh7ZET@i(c3x z;81Ik$j&GZ;b_HcNUMYsh6c|`QQYBO7q zS0bU~KC|)Yph#gd17w*=DAHy(Otr`+nm58}M3!?VlO|Xf-f5EsWa_DdB6zAPq@ela zX{Zi-4ytIAjIJ~0qxERHPn~*-glD0c%7Eygpq3e_K8}Ve18IHQVx_82X7oX+syP2o3d5ABG8t-yReI^`3b$HH=&>rC$}6)hurjEyu>NYAkFmZct4@f}vgD(i zy+Zqeij9Wbt+w1uKrN=yHoC5_#BzgesOWB!YPTEItIfTk!nY2$`1bIvGWe;gFSr4> zaPTtxKDux+1t;w9w&r3wF*gkd{O~Oi8@jPA9ZMYVz7=~sFvKM%tis2&WL)OR4F9I` z3m5PEZj&FBJ2MO{({eM8J?`x(Gm3P5dYQ=x&U|Ef+D<6-krG%Yu^lKc@A z^%4M#q3d}$9kwIHS8bSFUqkk#fFfDAhdPXp551Se8J>YcK|Ih9_2imz zCFq8UfngFcwnP@z(0OJ+OcbYRFW)?|eJ33L1b}iF82Oy2Rz;_AXkNWOI%vxW9L5=hX)2&3uqp6&#Z zJs-kGDGIHM)6`6aAlji`6x2-=Rc8Lpf+f!hS=6Bf%cwCo8bE;xf}a-+qd7?$Kuq{E ziv`JJj9!{cmi}lFjoboh@~EZYF)XK|w5hV>0-8Cb$1gGjYES{XjsVc+npJqs`jUFj zXnmlLRB@_Ypz5S|`mL(199K;UpjD;vfT{Zft5tzwBV1a;0_l{BG}-A&jHHQ{#PF)z z)Y^ic{N%1&s3uU7$=AN>&Lp|2!U%b3R%0HeCS~KoV!3p>!uAzR@xkj`l_C_bR<%+N z)Tv&(;SSHLb#y*;;TB}6BzJJrvM{hmYC+c68Z1^LlY^}}UNPI~i55++)vRu5kh$3| z3l);G&2X3Cim8ezlR04RHvSyY%jFsYtu2Et4zTMp>rx?&wY5R&OzU0Sx-Xc&{c3rU zz%}hgH+XJc?+ogSurF|T7IT&F4ZuqV{_58V8T^8O|4W3c{>Z)ZaGwMT%me!V)OS53 zFN5<{#PH@|z!TnY57F?!2WNP@SLASrKWu|0s#m~0q-lmR+}{e5*t`c;@e)kTz8Ayb zzVYqqfo&`*7w68$JYL+jB&-A$2U%e3STGytxZ@*xM5Sj4u#+haWe8W<#^Zg1g9&C~ z%iebZ2%d(!rrZPgb{W9u4Rc}Rc49Q7QMmt29h?K(VJ=VkcXg01$($@$4|DFk$K|n+ zQ3qr!1A5L~=(C-J{;WVk<1NUFR;?Z0Ea)O4I!A^6FrGDNW*+MqqkXt3n<)=^+}d4>JIOvib|xkfgDg-Y8) zq)x_aCHAU~yEuGvk7eNXmS+z{+F6l zcuw*4j!2pDi+6U`%K`XFU`7&^7oF6J9-Yfeo*lc+&#f`pdCw<1-)5h@9mIlpa_T*S zs?Rg)2N85BZT_K^Ktb+v-apx4wi30@k@meoGbpx*wTfZ_6LUmg^_|dN?}!=YQ}CMuc*srGEu; zY>A?I;9vg$=qm1KRBK=dtP+3-=v%T!4RtVC3HTQL7gcf4fcf`;hg5VsKneI}fdF`Z z0il5lRDm4W6{GebgPX}O=yH60028blz?df literal 0 HcmV?d00001 diff --git a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/key-color-icon-square.png b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/key-color-icon-square.png new file mode 100644 index 0000000000000000000000000000000000000000..d412f9b94be34c4b3df9387b6563f9b0748ab6b5 GIT binary patch literal 8276 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 literal 0 HcmV?d00001 diff --git a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/key-color.png b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/key-color.png new file mode 100644 index 0000000000000000000000000000000000000000..0a3d036607b2495aed1bdb46349d78d8577e909b GIT binary patch literal 18292 zcmdqJgz-HkNT-JR0Cgmj2Vr=WCqvouO~H`3kB9X{XtyMM%e z!M^sz3^TLy%=4Uc-f=cuNkIw|ofsVi0%6KXi>rV@aFxL8BPuemh4ddu7Vw5_EGs1r zdVcxIZq1JewxBsmYdM2JgqSZcI86!LKwu+^i;TPk%H}I9bi(%uWQMpP5Cup^TtwAl z{xHMSLv3oRcg6K+2oVw=ku$9&9azw#9e|iCiW90mhDZVn5dDIXf+H0s@E#grNzJ+j zRY5`gRjM5aBBq4a^HJ8kq_}+s=hio za$*q1K@|D_&u?(Hu+u*t1CUP+rr9A94#jAipMgehY%PGbbxHc5}a9|+cA zQl>e|=4Y@Lv~DLt=!CDe0lNq@XM5XfM^&aR;4|-c+TgsSoZ^|O(whd#!Fn#12H73f zcl)2gFXBQ;A=9`ZBA+VNHN{(!U$l@c6OsfdoN!v#Gf5ECXy^!;wP!9+!MgqE!sVc_ z85zg9N|LR;>WWL~)hg#{0~@#Z*3wwfSTX{IdUfFladdKJNCSUEccMkC$*OrY^%XcI zp~=ctB*;T&G{UL~^$Ls1uzJ~(Jft_l!XmsYLnN&iPZsV$B_9)>7{sx8UPI#+8HV&3 z_LsGsqSLfh{e!Rk{Dtef9VJo8mHa}P<`R0gOm(4^lUZZ<`Y8_i25prT$fev(Y_Lt+ z;13Om0z&X^m}!`Qj>3ctiW*%VU!`rZHUJ!+JZbjB2PAwwr->*%w5!gVSM0&hzN?(L z6i?LCbcmyLH1fgkwE39{LVzJR5f;V#)DPR8r>2JOEn)U@JJpR6Pv#K{V6M6jnJaL# zWHz2U4@#eUSUSmwD4<9r$C~iBJHYu1&6a$5es0IXeFg8N=e7k$R4~@9-}B9x`R64? z1wPmn8|vycl#T7*e9q$Jf!dM1W$8x-^GszptZAA47OnLjMWQ=U+Q({I4e{^9d?k2y zpDkiKvi|F|>0?--cY@Kc_0ejaXw1xBVCa5F^of~975Dooottrhlnk4kANzNo@1DrR z!mNJvvCZktW+#D9cG8fP!-aBp@K5V-&`V_LG!Z1VaT5JTdgMo<31jQeh%_?eyM_I5 z620+k*w6_dX0yV=c8wJxq>xTnp%WLS>!-?}VNC|<48gB!!4gbx4=v_wG_Z=TES+0J z?^6%DQrIUp1+`TTkOxPaw#tI%{x7P>pJB<=>uxR?h^xfDc*+N7Ri{CZMk1(4SmMWE z%vF=tNR}pMZBidxjMuR!N$(~us@5`coxl4fm&1p+e^A-hh8T;ytAp(Gg0OdVb>3#< z+OVPv5<0y4*-v=gGAjL-NEBw@ns}2pduH31@Yb|0tLhvL>e9Yo!}$h#>D$$0z0f6j zc-Wj=?0BWc>xoES-vQyZ`HRk5U<&*=jb)rg?L85bS0_hatDKEEfLlLZ#nFg!#S8mV z`xY{+&DZ47GcqkiA?wt`{U?B1mz1XG9!(MgNoJWeB#MlicIP5CH{)y2i)OSwgwojjm~@c zW23HLu&&}S_Bd#BoaK@-2YtXDVJJ@Y&?8h$3(XZfp3zt=4i`O3PYtK;DR<8jysL6G z`Q#opqci-I{(?U%fT$E6xg;ttgg}1sV;R3UHq?#tDO1DRSHeA545=^C0`vW7u{KRY zH`h!a;j={3#PgS-f7tGv5UM*|?r^l$@G-n+`LAT?B&MC##wDgowbs`9O`8s}f1r0+ z0W4qeCzLI?9E`R*bX*sBmk}pa*DnbSQzdni?&{eUxj}|$_!WMQg3fn|i4SI4;kDoF z&E}R+lYiFYz0R8shr)qWR8;scb(jw1PZot&ZQyhfPktg1gl;OvKzhqT4-qh}OzHAZ z3jI1=+3rNsI;4sV6<>}P8`Zg||D{Hq-QAom{)B25Ij@VrFe~-k1awiB!Y!9%e3CLY zz`t~0@DWqr648@Oy0xW8v^M6EuehEuh0`V3$h(O!AJu<QJaTqqU>4=Mr{_U;`hctc%M4-0n)adrT-AqKVCU0KRgA}u)@^$ zvqbjtxAQu!+4v}GPg4i7F>p>XhggcE$?MP5UJj?}-enu6;#x zB59|6>Le8Y?XcS<%Jd;Ur9YRMH(2KxQ^4Afj5}>Wb+{pI#^U{a&7%hKI7eKcb=zRA4&(l*~FAZZ^7+ zDSe#u$CsQz;fSkaiVpR8r^=VWgc;hJn$(t56R9l08u7@Esh*Y(x|p)f!&NHG$`>$_ zb+p8-%>A|Cj9p{WNiw7%pOo2B8V|;LB1!({?(}6ncuwg845cUW%6k8hR7Wu!oHK&_ zZnKp$JJvH?N65uja?A@kcHAx$8dr%wNA=5!)aXhzP5`kxnXbmeUF+9m4ocPlJ-J z!;z{KqSD2AgU;DJ75e9~H!CHbd+mK>DF-4kwO#r9M{+JAbn^E!o2`pN9S>g@1S>vf ze<8Y&u1>M+*}Hs2cg!b~I!U=M%cj80Kb-|iFK9&aZ))yn7&$UFqquEEXB=rw56c?0 z4rZ}B-1$z+b$8sWZ=M!_hDR36YTe-yTEnArcW{VY;)?K^%-3hMdeGc2iVmdmzWL9|%OrRL8%+pbnuA@+mFBLp`bRI?So)6cQVC>gl}&ux?7sVOL#%`pXktOxezf-E@8n^$4*^+ zllI5#0Z@r1DGi8Mo0Qp>rxA_e$d=o9>wru0``Qey?R|C%$Q6|f(?HAXLA=W=vWK6H zt(I{*K8Zg}-H4Kz*YJ%8gO3o5*QC??SGVKw&X&oNZ~Cobo?cI=ZuT&Pyl6b5uo79f zu8T;fsBN{wt%w&(^aGals6jWhh904N%T}*(@5uxP`ZA2aoua!)vQf5dAJ_D7h{&M4 z+tLGrE3CmTX*Jp33~{P0rl8#|Z3<6ewN#OHA9HlzEHT?9g-4L_1*mk1rEHV*sG$L= zL+o!OLhYr!_jHz4s32FZ;E1nVNg$UgOS89ljsHlZkg4T7z7a*jal3AffcCJTtI=TF zq0iUN{@ar`8u_>zOK`q4zjzu0N@baJ|E!{kSpIeIPTaoHWim8te8fDVy5qw)D<{=! zfqV#3()GO3pY?VgtW-WQ8E~e4&8?V=hP1vpf3f~v;TS_kW(zHDXj+zH@y)uB>nk`{ zj1r60EMyZr?@8+4kTxuj%Ic>1#Y<---K-O(zM!xpOQNCt?`G*8$XQr<=V&{z8AuO~0$2DR=WL*0iBv6i|NEfMav5j4+p-rqiJCA~ zYR;`Oh?mJooY})|ad&S zOG$gb`nvN((|iXi{RNt1jlVDh9O8vg0w^}9rKg0zeWz(HUP+og<@@*bx!Ql7vf)ur zAbXG_k|=-U z_}Q}(vhE8wY3D@LJgZvp*X#OTF5=abXycV!{(EGcmHY{07HV!i7&0|KFNR#TjtGnh z2G9Kdy6{w2sn%!QM1g=Aq*=4LsG=7C?2$!0^40JYas{ZP9S$tBs)0hiiRqq$jV*qB z1$QG}4L07|_WrTe&FhAwp6Uc2u(d?^)bBd?<6_=Bm(Mp@bHMyr4Ap^k$j7jR?tDzl*|^`V)7apXEw z%)3O}s7z0l&8ne;1bY?V65H=yNR-pYaHT~x|F(p&^@feQP4=2LF~D9~zsm{x=LVhg z7moZ`cqr7E<5#WN_SfHG5)wp;c-d?$`cRBTf~c9jrUp(U(eX953JQU)Z0tA1ZE6G5 zOO3yJ7ja?^&ochSPistVuKdK%MP7Z(MgyG*?Uk@Z=;a5_8%QMd|Ke|94R-Wj7?YI#mH!ZpI^6L(5jR#a*K z5#inwHwmW$*#*j#Z|<-LJHwkco)U3-0FS{#GshNYPiN+9*7MW%a^ZZ3$|6eZe?C{1 zL6i#f=kWF?b-pweYI9m;0)O-c?~btC^ zcX<%z`$s~PCXd!kPXM&{D_w{I_4OYNjM4wArYiYV+xTq7ONKhG%s`A&mN|1u{4Pt$ zD!u>o0^X&>J9yYG= z+Xk{_nhu#qbq)*s`6t`jr97`NL1dG5p~C;M6F)BwjVO&z&F1QZoMM;%_5^iA%$Eeg zg?qLmSO!bt$zzax2A0FZde2X`Naxp)ghG|g)^ooWgdafcWxS{^iU5}Rkbj%+rV|=& z*pll{A!TptIbET2do;V7JA&TnObJz913mmhFAH&*PG@|8_rl!tz)p!O{-7A4AieaAOIG0d0b`-*VC)x-CfEwAvycmo2AaQ> zLaiFV#?DeGBS1fOXi|sF<|MBY2@_)6ky+&h`TO&_$EzCh<$_1kv^o{uQnv7{Ey7;G zBY*w@KJbuUlVvw+%%_40q_-V;de}lE=agz-JrNQ;s4dzw^_a0n68Qq*6{f}v(tBf{FMeUOpo_5?eN`N+PY#MxqtQ>)rZ?J z&U()Zz0+DU_hC1YnJE-iWry)I3s!!GvP_KdH~%Q)5*#8*`_a8t7yhgHQAOGP|CSGZ z(8c-dY6o2;1aY@P&W(e9UQ?2tr`1~ZOl0ikiUhIdVz6d@&HSd~|8xbb(n;AAUdACjmn0aVl2+0qlzVi5C^icUiaO@b|1Yk*?I*(@c zZWlw>NcV!@wH>`?LFSM;KL-s@S^mp5CVTxU_A$m2KhZQ3Zj};K!k=hr70f#0z$t>- z_U(2kkGh-&A9`5mAGWi$e-|Y*U?o_1j$i)1x1S=J_x-5tAYVK8XPbh$o3jIyFlNp3 z`j_gA_x)9IF%aWe_XUydr;Fl)EB9F=GpR#XVXl=0<^36XIdNo@`HX(6_jzl z{d4w^7@(aN8B>YjH5e{)ZIoj52QTL{r4Eexm1lyug#}w5p7Yttk!& zfli_)QZluT{V(VJ@yNgF2l)Zv8%!3XN@Fm!&DE%*g14p=R4;r=^T6S$oEu?KltwmBG2%;!g+7p>T3?1!d1K+shwEuB z=CHXH9sNdNe7DV*=(gh8tQ~$LBhqGYL_$zBw1;DBTkZ!sh2cJX4D(p{)6IG&A#nnJ z0}>4D(Kl+qgwEN|O2bnoZ&wX&5H6eV=43vs4malQMmm#mo|R*~7uYc4{t{)5LMkf= z4)#+18qRt}Mx+cC{lg;^M&%)3NNWgAIm9QGJOg&pW$eT@vHUh5hAly_UTKZ}y16`> zkXm>xVd9;1*9W(wfKWZ6^rOk^rsfe?fBIit=Xl71Z8*~Yl*!UP%0~RMA2w+6|U}gYFhIH_2EhPa2)Yp3&{OO*&Og3)Vmf za-Pm=Uqtnp^6xe`?MQnOG>d%4Q5SJm%(ia`UTIn#-BJt&UcuTfeh9TXW`5qEsDw;! zq#Q7!KcMe>mfVwOad7Cw2%#W=f^G$M76i@_)MRP=-?QT0sWN0d;8zbBC5Jg@?8RD7 zU=Zp5f}br~AP5?4s7e>tAPf6TG!h_1B2dS3rZFOOS^#QCTxE|| zCx+uNHBk@Wi09OQ$#jOFYAgHAA`=x;DbH3(ux65ckNc8Uu-my@-q}bI*s%%McP&RU z2?_STZl63%Vc8}b03y|F8;RFBIXtY+oakfmcsgk^EDl0&eQ0$Hx}32JN?{daxYdz8 zC30bcqNYWB27Q6smZU@boJ=J67UeKZ*m;$VhR?LLyn0FnC6pGB>m+ov1sN#~*fwDxi*JJNy2`x%l3=6|?NEX*h!z-8)S9$(CQtSRZ z_((RJn|g$l{g^=&6xx&c@8L+=-%mNE+yrO64IcK^wJpc0%xYruTZ(G=UuE3C75Yey z-zC|iO&#~{a6ex+{SHi-B;|CwiE!6YSjOmREUq;BT*unq7KRaVYpS*Rf*CmAzgkdQTluX1?OU0}XhwtCz$|ro+6`5{B;g<*zKP%fBE`Lq9ejG97 zujBXTCu*M(h=Qk`+7)?A^6@3jw;M^;nX{GO_BejPiH6-T*A;x?*c93~5si0Ntz(N~i_w=Xx z*}v%%vlL6mdT^3o7pQk*a4OFu*>ZodUI^Zp9MBX@s#&ouCW!`nK;@BqD?F^8h{3l@ zVpHhO48r8W>U=i7>eyLz21xutMh?O8RD&6w@LDa1u)82#m6yto1fE>#Z_JjG^SMAIQ5j%( zO7-gK7cOO)y)L_>^pM+Bg1(O5=0kF=zAH_y^HbD1;gVMM^b}{pr0s`eUGUjbpGYhV zA(TO@fr`JE!~RUZwA=#=x{LLUft1HL9h?5d#8toX?M>y;M4!Gi#gb9blD7{ zj4I@&_=Z5-GlL#25@eF*Sh;bZB|^LjyXqv0cm8qClzd7XJ5bL232Jj)9Wej|5NAJb z_O)+#m0OPiD$MJbS31Lv)<@~Nl<$|L5N{wl-OQ_c@2FZMmC4dXvBxB2WXx+87OUMZ zCJX{RKENq_;<-?RonT2=d;F3?|A-sOG(sk`+FV0SnX0@^_mrDP=c-6^0m3npve0Z8 zU;IGT>a7L5_rsDn-{9kdE7q5Ga$i^l^vxfeY!;;KJEqemQ#j;$Cr;nS zzJVxK$OD?UWUn4!E>Yz&;CNINLpCs!;z7TN!#FkSC{nU^rwsppwLRyZEAJNVBz;u^ z`{A>bd+o!tI=#v! zd`%&}Ot;P3 zRBjgLE))t6zi~sAhL`mk>**{Xp8&+q}g+Gue^6RB3znR8$)t2feGF6_GqfXDQEoB*> zv3T=!DseVjBt^M`(vTHy!AuvM?x5h%`e&BS%)E zB{*?}*wAdulCpxIl>t{w@~aYK3I8{?}O&?qU4YrH0_nSBEX? z2WFu;UT1oCd3Te>=7qUo&hk@OY%2WpqmkB6TxI2}AL8C!L+%K8 z(6^uq6h}TdMR0|ntU~|&1gy<7yzJhis<5qfNO^7297~UABhdmOo8S>Ya<0KSsN9e( zIAS}*@c+C3s}Y(21(ffRybT{V+R(fF&&;I1x8!@!n&!_}=0q z7?RM0f!{#z??2(5bja`cy_|u2?jt3u$Zsp;fzU4AEe7leVV$No?Z3-r28r@_wM9S)!V0XGnj za+%_oS?-f$ zd4*u4^K&q|Zt&^@KXUG--^P8w?az%5>n_y%#^4Jrpk8qWc=$nJ$B0WNO+zWeZx2Tb zlKTu+W;pStzKQ7Q%w!{q3h5btw7)$uH)rX^aAf54BWy;WY$dMxTz4gm2m>+%_CT{C zZQWNG)>uTod9cg^4#d?@%z9{fIh(;!&sYUqZ_d99i+4*+O_}$X+LkvytPN*=7*YFW zNrB)#uWWuE5M<<@NNkXqF%b1;2+$%E?mXx5nkf-Ax?i|*vy%Z(pr@N5&c^S|5z{Me zfhyC_nrb@%1&``cq+p`{O9*H7O7N@f0~xYAF_Db{xTZyl)HmRYD?oQ?#a_Rb9+N_$ z;?DvQ%YEQu>&zwCUma$05t1_?viAwf2-ju!7GcD7o*ZBOe`Pj8?0%gXMb?VOS-79b)!Jdw+5JnQ z#fuAzHf)MR3&YT)<)L&CfGQ??l7cV5bhU9N@L3G{K0a)RKRno~+Ng$4DtKi@^rV^B z!?|MuBH>{lL(5$1AM~$B939JqeD1Vg=Afo!X5VPrrT#8}m?^$Yp+l~Q3NJsyCR1nP z%2!D*72*4_a(T^2yz(cmxL@%M9L7NGYaQ4@-SA=WAy7C7!X!~R00kaFbqF<>s}4jh zk;0fD_=o3A0i01dLwG0e54_;+3=pp;SIGnhiMAdtYG}@Dq~d?4Z56v_c~^xAmOy22a}n%tlN4?#<;5pW(STLjnP8KgYE zg7~qhdJ%m5@uL1?r)itLU0;P`nR%aLAN#dlEd-=PqX@7Qm1aCMK|3Nt=5LaZG=;u2 zbu(i&h2cW7ncoU;%2+8t*j?j3d=iBfaduB>#zG)0L-4Mo z3(uu>W)~c94R{Kqb9s4jP0}*_*$`UADuVQgN{KU(LVNd=Z0>35B)aKy29^%;zYb78 zlLkk#V$AlgEIl|`jI{90pLiHxcOv@A9LnfWe{$ya!majJx(+$o9xpvj;5ia{XVyi4 zOVwMxc=Yi@>bowe{IIF^-qx|rf`>60LMwaPZBe1tQpjXuVogCVVI}jv<%77bgqskT zf9{!Msl0^V(bW||c&^if9R4afE|Rd*Z}`^x>FRn8lVaRun6tg4PZRY&T;u8D-F{zw zOGoi6ed={_-A9=|Ua2Fv`qg#QQ|n9P;(XuIJ8{s62fJq_At5Yh(6u^p*$8zlA8bWk z1<${Q{Fg`>VVEO*u~y|#KrSoFZ;j#EY8AqpRBDJ_DrOM3^57YYk-NfArKR6#?m z^dxM}Mg?g04;A(!0;9u9)>u$Z-a{PbX2#Y6xzz7rvtuJ((B+m~zX+Z7*yJa3MVDir((b9GRVa92^H!qeZcL5QBISO7(OnaBvYj}WJv`|D$P zSZ#2nliA!y`v+$8H2VnL*Z&2OwQ&l9ndB^{k`(S%)HYD zl&1Ia2OahWN664aU|XFb%fk%dutyrTVBh5?2H?Ep>!G+6FWFrDU4Lf6tOx~S^$qKa zPMR|RdGqsIjZ6VN*ix$0V+l~Ly`Cu{xr;tVFq^H#yz)_#e?r}56|&_3NT}ypkzV;w z)}-^_sP;$v&4nmR^5U=HD~B%`(; zz!J~r6(=v0UA>iUJ3Q<+u{>8$v<1;PND<19{~2D{r3V% z%03?1-fVFHI~)V~K^ag_NL2yuNh!P_J8S^RGgx|DKCJHc9+Ht^<^8R3T8~J?2uXN` zA3L)h2YJakA-%9YPD!z20898e_VBa;9u%TIs_C?fQyb2mBv-4iWnqq)>C8sR!c>R5 zP}Q0WbK@A7G?Nqmvxd4w9A`8ZuQjFq99cNsuxXyqWsGbBIUBeVh?x&S!F;Tw3gW7R zc3=atRenU@60LEt9&+KpbY8;4d02EQ;FFQNsUDttje4AM=GwOj;CSxVz;b}0(*bK5 zeRjHU)gQTa&`2|oow4GfmfLXfO&zJhXo8{<;!NCvvhsxUaUEHo?nf$XPLV-;aLXdu zr5HYcf|+E-l9120qvJB0_N<+gJf1@lt&&g-{i>%md?sRPLh!UdeL&hbLttOeN5lXD zL+;R>U?GILDi}TSySUE1a0KO~VuJ1_1OJu|kURGrKg6G$j!#?Hoxw%@D09|@N4GPAZMJ;s(c`3Lsvu)1Y0Lc4}`NO5%13A*4Fs-&gP!(U-@4O1S~%SmqF4LC3~4Oyl{R2C!e9cd zMq{_tM9H4Y|A_i%LZVJQ%;8XHXU#Kzlu>c(!)RHVkd=Qvfq5;$pp81+bx8053$d3E zR)_sLb`evDoLIv`l6=4UoxRjztT#443HpR^^z}ks3@ObTO|h%0jI`&jrkq>*7&(r~ zf1jUKr5&@vkdnVO2|9a4s;lm^ep;ppnckVh(4EXS&UEsUphJl5TE@{=q`7|UpXWl^ z;l$j}3duroEYlaeh!wz;EaP4rf=E6%>UoVh_1v7_N93`Zv~3x9dE0u%jQqiGeL=R! z%XO9gS4zF-qn@&=IP5q&ap$N2Y`4u@?qc_tm<>ceZP1a3x*d8^IbsY@JCsqZ5TX9o zQ#y{yKAs8&QZt-uENQX_LKH@#i7r$fAG?#x^~|8p*>h3G>wGh&Z+sZZ+{?q}yp}E| z7CZHjO_(pXpb>^A#ehzgc`*4`zlPqQ$9C9IE}Q0_CQ9*J*}LF2s9E}7Uhv6?GvXFe)t%J7%f)1if=BvCow%`-9C!1e51ALZ~|=w5=dI0ze5 z7rW3QBuW5GK*wUtudz=zNz00o za~*}8eJPI|K+_FyHq(U(_F50j(8oH0zdK7rCPOj+doo@ctOO~8>{BD}3pU$PkJ@FOO}af1VDtaj6c@h^$XyCGuuO0G);jbCprpCcP>K@DK1^ zQo5)HvEaijt~59>WsmN6K0{1JZ6=(j8|p%4Qn6RXTf5$x-?O31_G}#CSLUK0*l^kS zN;t3>6>aq{R0o*QW@w%Q^(QtFx;tEj|8pic7hu`x47QRc#M6ES_>;gyr>T9=%Ygm@ z<#jv4NFuzvGI^%-6=HIY8LI&g_zUVRlSQyj;yE9U`V$}BnMPot=I7H~^>>>7KiF7* za)dDrfQN$D0O^@Zf~%aI#kJgH@i)Y+B*52>#v<{(b zw%c0iiu3L2$4x94`WZ~P`}MUWgw)T3)a19hYEyBibAQhFpY}*-Iz7aXZ~wuV9~;m* zq@W(`n)uTYrnFED)C{x-hzE0v3YIG5ENdjK@V$yN=5JPLN{3q^1csd)eQycJ6xM00 zdIMx^&~f1jZuuO*o89gZgxA!jcr|&yXY;b{^>o=9C7UvQ-(9CUV(B!*huOn9o?btB z;05Vd(-*IfMyn>7{LkzL!Wds1(a3S`d-vj!WEd@lNc#Ne3;TAbg50BKIs~_)UW)bp z0Wgql_7mO%UKb{K?GaphD0Qt+DWm_9t|!jCN|~ZZfe9N#1(iLj!}s)@OXQk`(tWJz z!D8tkso{(S&u}RKweJ5iCBqnyDPf=dy_8p!1K~bK~{O&%PoIy zh|_!O>X3xp;*5IdzRJcnEO;50yCj#6+zl>Ya!dDbIrgR$NU&N^jt*;wb_4xN0%aU9 zBxRI&UwJnmuU}GQ(?yQNI7AF(fys@z;{6*W43MCrw{bmdlukC6Qbh-h?!~GWGeIn- zHdXV8e9i!Jv(hIpfRiW&;E$xpay)6=uXpFco82uxfqOoG`_cL>0u#cGoh*=Xl8y8i z+&c~HZwsY%XW2T~`A`FV`y!!VJ3e8+_JQg~N9;CtW1z~xqI1AYAX4#~g!zt}B}?hK zZKIsIUlX1J6}!<2=|`M;ZKp2X5-!oCMW<(?$V`x<0FefKSk+uNL5)>IMUnNqAJdCB2-JQLLbyvOPn!I{W~#)Uul z>1Xa8tU{11`P=m*N0QJY&*J<#Y}q^?z^)E}PwiS2z{nY9qmm#A!*@0~%%GhJ1QxNy z^>#`8&jorgXmlJ(^cuwQCs3my8*T?pelrM%gWdL|DC6QvV#0Z?xU>9MP)&qKZ=ARp zFjIX$Fi|S3pG&S<6?2ZI)6fpb&ay2GGv(wCAPNVq<7_ghL0T#uwVvb^uFU}1R0+Pd zNQY)FyRC`U0k!J2&Fv&@@n*KsDglU(J(kD}aHh7qwD<7ET-D|@6<*>35+pRd?Ogz!vze=A&D)K?wYBMvox3{6j};C)_a>RAuTl;ItmHYOOUP$;W%2ILuotrzyoYXeO7))&U$}$8 zG!MXHAn<3Egkr}6o(4qpg5cm++?eurxfKL6HU*m#)=ksqmb%s3{&SK+AoApAq$ zPdm*F^s86_ln+wxM8yYjC++u>`^43>n-Lwwbj^mv6fVbdQe`9t4<=fy1-_qSK$@Cl z#_r(I@G@OLxjfk5QNP2b65138Ioh_g&qkEkG9OL;mr-ZKxgsUHJMkw}aSuq54da)` zf@m&349U5bd}GK`Max@&t5M&khL6_FwDjEzK4*3P5T((7$zojg#rQswVW1^2InSE2 zi=xd!Ub17Gl{q4eCt|A2Y9U1p9h7l8ieW}n?!q*}vry<)q%C;L0ZBkKwAsh(Z0NTP z)8@BSGFL6Q``Ea0BY0y(-=<fN>c(F|YRm{c*s~aN^eL6*4bAi#S|m@xi#4?f{q^ zwl(L(oM9%ZP+ycwQ5FGX+5=bYj5805pjFSmZnJ<^(r28e0cOR5vW9$mT~4#&T;g;q z;-NP;R8Tf{9k2Sb^DQA^99-wRZ}=aM4H;7wqN*$YN%hlOT=vf z$&wIW7KE>1$tPW>eb#M+cdsS< z`jT&|N}7R_6%|?C7h9iwMs0W{Y!l29!TN!j{#{1bBxem`)boyA{XRjy8Fm=MKx3u| zwhiZmT=oK|d4dmBZdO<_-AOFtf&~jQ!f2XXdI5hM`}XVndmAJ9Cwk4y0nk$oejbU& z*8&~4kp3SBR8YU_0`u_`q*tG?c&(At|6>r8uel}U8i7ueFW_78Zrz@{sGLg-vfu7* zQAC^`!;J~Uv2w})UYgmwyrNe67F9>Q~CVb z{Qq=_&V5h~G8%CsJh(*z9 zJ-Z#avju!>`PonQ^eXT^&t8a#>0^$~LRQ_;ARuAc(pZeegfV||>En2B=u5$B{RYt@ zl1N4{KkMDRWxu)MS97-Z@Z!Oy5e7k>27Z67P_L2S+;7|~mCsWxbfnAVk`(R;VdPev z&d>lJ3Wad%lXR6@e*f8ZGdx0G0UI|Tt*b-Cs7YIMa%oI`A>%!LAbL2ny_k-?e^`8g zI@DtI$i?<0{=0IIT-mxC$^$-;jHxFh{7Fn5KDHHM#Kw;91q}loqaj(4N-i-!4O7t{ za+O-!@fI$P6qLnL`zDPo{qA6kz>2{3nJj&kS0du{B$|A8yXAn*ntf}=7$){hzo~-% z+J)=Ze(jU92>=->jGKtF+vk>{_{*=R94vB1Y*SXD49p21a%=<+-CWCx{wQjxvN@IM z1n=`T30w>F0et>wMpVM>E>K-I;s9YJ{6#kyOfH#|OF}JZRPaOf6=%^AJZ&vX4cEXK zE0udlknf@GLJckn11DDftD_F$AM8DkOp@2#sd*dHcM&I#VgWz;*mi`#TR746azd5< zI`;{q4;O?)Ewh<{ z{k4$kR!BZqa-QYUX9B#V#TO`ykP@b;x$$B&31mTpZrPD~F}6Y2hV7o3x6VTO5KIRU z9H_Mqpp528qtwf@1GXD=SEDbYG4eV8&TxG%kwKDv4QgOmGU&z^$)CzF9q4<3M&G`a z@jKKjo)F00!sHS0HPTfX>gBQB6>+5Dhq+8@20K1* zo|EUVC6>GaPX&$Vu8~nE{2SEmxDD;{NDO+7tQYYF>KbV)^Y}55L}l|TQ7cY&Z=fzh zbaO;g(p|zLBcB+(Ew*kA6XW+ zbT>xbe*G2Ee~w(jit)vBur+~;)GOoR0zmQCl%&OFS)xjj@@UZ%pXp$B$$njrk1}8r{<8C)&xpL&Z)I zH@l@fK&BqU8l%RglMQW=)#}7~`?mHqMe;;7NT! zy5_RS)2_=8DBu9`ZbZJ|gZ-Fz5ItaIlRAGgmjWs-`4yCTxQ0$Lj^H3Ml&=P;MGb(( zo2WsnIz?W7$ohjyMh1o|u%<}mK6^IoVi`Zk?(ZQtFtpKIxD+(!#zD%11u9SBGtD=A z^Eo3OUy0xY@-u$2>+vUcJj)$Grg5MM3q!lAq|R+2lM6x{^^%PUf3-f~d>2y3bujk? z8AW*^GP!HzgFmAd-Hbxv-Eu}3e#*acjQ6DW$EyH$bBWGmVM8j7H)WQ0EB-< zEF+Cs)>YO?KX=KzRFKQy7AE5hZD;qofQb{}P6J_llm4^Z$U3@SxeBWRc9W$Aga^@nAK5zlFZo_ z5SZ;R^Hb$nBT!ceSti1(cSo@=*`c909_q5?}-SXnjpf7Kf~ zr7U=1d=S?-K+37^1ido=>-s%T4494|De3%T!Linycs#KKE_0p!4=N!<36BH>YT(Q2cS zO9x{g?b^=8qq-({FJLZy-Ulk-UvqOxqSZGH+ucxb`i)156V^-sytp6;g^ikcd6tZ&ZK5zVK1jqoo-9Q5+KL&~a z@<#}jB_t8>|5H-@$%FHBB61az3dt@4e1)|}_>F~yT9^`hy0Tuaqbj)Z?#7hT|3_)^ z#28;?JBMcLDX+BF-kX7V=f;_-qTa8m%(#VQTSuY4L%i&paQfYEMLjwSC=Dypv4p1Y zw?wC7=;!k^m#`>CJ0S!}Jg#5$wvFB{7jm|vrlCb-rR zgNC?_Htis%KsG}F!@r8!Mis(!wnQy@=}ZJ^x<=7eT%sUY|G2OPjXFG%bL-&zmu>tU`jLk^8)Iaz}os9`=0o@Jwbf4?$4LsSE&58aO#WLx`!oIS+G!5f!G-i=?okaWV+{Xrf zCBC@>zEEKI<$D1x)5>I76_Ic(BE#a)7uYWpFh$htQsJmuuGyaFP+mH2Ww3#ZA|Axf z3558qGwTH`hk#ow3Dser>a4cf>)Y1Su)AvYGe`m>ZfTG_yg|LCB7R4dK#KbL_oY|3 zpacdGDi1fG)EYo){eDUsJ@+zRi zg8)kNy?}YynCEH5^>L80o{h zm<_aNUy_nQl3KD%AU><76|1N2ceV-wH}v-=EAr#Td!2x0k*i;CUIUr4-Z=GdP#7QD zECwj#XP{5*a^8ax#@v^?axM~ZAT195B^i_&6m4sGvSvvZn2pcGl0T=F&AMZ-KD}Bs z&t>PWt94{YajDBp{yncdEw0(<{XCm}WZ0v-^`FS>{*e8hP*EiY0MXho`(3@*>w1&6 zOH2*bU$g<0I8i0>!7OEdvBg!N7YWHVOYkbhaDH*>&z^h~-hn2YM@y#rZvyNwztDPf zcPSeoXkDKG2ltym0Ac$zn<;HI)dX^=M2e1X`K-fVSJ)3uybZL9J!tN2y*so%w<+up zBf%1>os6jpBn|w8COH5N2+u$e>!;{V zLLB%BNV14hIA(=5AP?IQMSsG0l&Hua4bn;e+Y|h)udbRvQvQWnTOT8Lj%E{6LIc9o zYB1$=p3>(pWLJ1g6LmPMFIJo|AtYQD5s;(ZcTY~}xh<`N_yOuHIB*ZqI_(1)@RSkZ zpu9i4dScXOV+1)e6}xkVVt}XmayFd5vp;&kvT}$_$0=-1=BTs)X2BDEjh81P{GA~> zcGLQCgzrz;1v@al;bOo=C`)d$W2Yhm;%&7^z5zXL;Q8Z|X;8(+)t3AJ?iAI4KGsf| zms6b~<6TFNKQgckU!FhZCPj#V#(v7C`uZ53p6Gwqc5ES0a9`;oz>F`xlqwf^{lDAZ z$p1(^iY@#DQ$j1wvWOs-jP!py5WS}nIWr1JSAZrikJg)WBK}@YV_A`xow#_N_;vr! zUe-!@u2?B%!3#BKTG83{@-LYdwA*$wdat`Cugt!8o|Xs1gHbmElx)lH=G=atx%;{E z#}^wFA&$40*LZ%}imyB&`Vj#|0XDZ5MNJS00oFolTXG&O&d$;DICnVePP8h^R8T|m z_?ZPR#(W1P%ceY4UEdMGz0sUy>h}E=zy_-Dp(Y0QH&)I!HzmxBRF}(@1eVM}0Y!Rh zKwS@;a>W_Nm=iue{}|G08{Ng?)z-2=2pC1%FR#A7f`ch%=cKcr?B4u1>JAO0FC}@^ zMfnWsw-zR{txhLdUrN+l^Ajgtv!)D@VKB->wkmbW{aaNQW-=7Mnd?pwt4CY%dC8k$e3~|z3S&z(mPaj$QO#vDVvl!2Tf?>^y zRkOCm+hk?Gg@#*QTFd6=LG3YXuU{yJhJyQ(U3QPpGZi$-rdH|z3!90M0Qq)Yrz*s&^}Ut22|{QzYOnLR?yGVj_yRxH?aZRR63 zkg^XcbILn}5AFP?+SLzqhDDX8o6vKE9!`_!_-=7fcj2I}(_hX8)jkgIe_Nj%*1Ua$ z7v!P{(3*y>#_LCg6FDyC6oS&1MZJ#k6$^&1k2*Ib=_>{(<$;QZ@-@de%!OA3?h%wT zP|OCcx;-ik($6KPd%9je5px%PeyOi`OjL|{J_QL5z|ADKcDYkjn?nkzhlPi zMOMtmj$Vq3Iewk(b1e6C|5YO49Dc5olDOPm-{0G5n7cRkz1fUB<_oe1PCgQK-z0iq txyzU3wvBuB*TzI%P|y>cbLKzeq=>@ZyvIU2f#>5ec)I$ztaD0e0szVxp!)y- literal 0 HcmV?d00001 diff --git a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/key-shadow-2.10.png b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/key-shadow-2.10.png new file mode 100644 index 0000000000000000000000000000000000000000..c2451d31d99c113336ab86e9c02d2030e53195dd GIT binary patch literal 85512 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

BRlwwNejsSrg&3aMINK&b`3f5PF63X!r3c2Hk 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%0A_q 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 JPDHLkV1l-Iy#@dP literal 0 HcmV?d00001 diff --git a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/key-shadow-2.8.png b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/key-shadow-2.8.png new file mode 100644 index 0000000000000000000000000000000000000000..3f2637c699c444e4c4565a6a9078406e306c7610 GIT binary patch literal 99751 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 Y>o2b_Bg!710Yn1nXxu?mtJ^;L9~JEyc>n+a literal 0 HcmV?d00001 diff --git a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/key-shadow-3.0.png b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/key-shadow-3.0.png new file mode 100644 index 0000000000000000000000000000000000000000..d900bee497efb18b3f42b567cdb64f4c183ab66a GIT binary patch literal 94495 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

-d2uBqEesvdR=S*T`>DSY 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

GX0 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 QbhC(x(mhm>qIux|016MLM*si- literal 0 HcmV?d00001 diff --git a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/keyinteractive.gif b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/keyinteractive.gif new file mode 100644 index 0000000000000000000000000000000000000000..6e8a4cb2c014ad7fd81163090dc1912a069584d3 GIT binary patch literal 9549 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 Z+EPwBLTb8S9FJW+q?he@3ylKA{{s@~&}#qy literal 0 HcmV?d00001 diff --git a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/keylinked.gif b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/keylinked.gif new file mode 100644 index 0000000000000000000000000000000000000000..cafb2814ea02d75fed2ddbfedfa337c0b1be864f GIT binary patch literal 1392 zcmV-$1&{hiNk%w1VJZPA0e}Gj{{Zm+|NkNR1OW;F0RSuj0000g0Vn|g0{)DTsmtvT zqnxzbi?iOm`wxcVNS5Y_rs~SJ?hD8AOxN~}=lag~{tpZahs2`sh)gP%%%<}RjY_A~ zs`ZM^YPa03_X`e-$K-YS={|^`_I7nD%c!-#&xX9S( z_y`#(IZ0V*d5M{+xyjk-`3V{-I!an`U)E>J4;(@dyAW^yUW|_`vVXx zEc`n*e2knNTr8#R{0t4iTohex{XE@sz0HlBebXH-F8vKNem-8lg3g|~zmK0!!4I&XKL-H|o*T%(pu&R)4=j8rt|7#U5%W>J*o_{>{*BwzaU8bbBgknG zLri40@WjJk23siXRZvDuT>x>^#I;XbPFwea?3`tfT2NW?k_>%SRLN0R@t!1I)ow~t zRZ*!-J;g3dRZ{}BGAz|qOIT22#eNNS)=OGXYt_zU`&LX`PIJSw#Z*^rMY?;@Sa3K^hYcr|^jL95yo=*RepK0VW=faUbY8Ss^JLDW=Y)pz+4E^ft3`L5 z)EZ7~(A@Nt#(cZZY(%+r_on^Yc5T$d_xw(DoKEsX%jN7Y)ZFzo+{J}g2R_|TbV1vt zSEHW2Pj^7$-;~#W9T+hk=f$Vr;VegcKJ(W_1NWYHejEG#?FVO6UkZ(p5MUk!nsAj$ z&kb@QJUy9ImVW=q7tjVz0odROBBcbLH9TziQitEc2jWbu^(Rw_6^eKti7N6#+=Mp0 z_#lhW=tSd%jVx$RjtQP9BZWQ=Xh?<%4rwGFNgj!0i?E^SVw2uj!lRH_v!*nGF#k=9@scDd(JY);Z>#cz&tno^a~v=Zt^`${&%0 zTB+ut0vf6)euFmJBcqVcz~-cg#%ZZbS!IgjT~&;9%T`vz7pkaGjL4IzO!VoBjz~OO zrmJ>V+UioBs!CLig8r1Mu9TWsDzA~Qn9EHsthD|MvHpdatg$MT1e3E&qUnpW2qMc8 zjEgc`jI<5{OOmhOYWt431d{8ItmpoS5WC|_TF$21VyY3m8Ng~$q7CR9E~_HYD-o0s z`b#Ll81S1d!T$>EC&CFi+$_Tbhi9q5_ev}$#1CT}u)-Q@9IVG5i`+5AE`Z#t$(nMU zF3S^p2&u&!pbV7DFQcj`%^Q=PFVE|$yr;|^=!}%j3me@p(8IzUsL=Hw-7VBNJK3<* zAUGYd(lHQC71sn=oi)>rYCVwGUxQsN+E=TcGTB?RU5nWdVBI#(mr~uX+;hX7tIsUx zjTYY@&|S0Ne+ynK;!hh+lj4iJoekg+5dIE}+!48@3yNjJP?|0uAukgbY-?{N?;?9)v z%j2D}^UqsfIQF}0pF8w#d!MHCr-NTQ`NxX?r1|HoA1nKxGv7G+<8vRq`|rb#ll+h4 zA2R<&052i>@rio*v){=8SS0<8&VZS)-S-4oJ_rsFdGV9r1hG*;{~1Mr%t4@qIG8{N zE=_}pv*3q7NC*#3kAk5S;R8unKNFr1g@I7v{9xEM7rxMU-IL+1cvv(Yrbvi8qT$he zm?a{PkbteQ;R2P293yTHhhVvqOck*d#4AdvZ(7`954(t@+JVuFB^=|BzGy}tWigFv yEF&9fw8l5SD2{RjlN;*@A~fQWOmy7iM!4ulj$|*87il3OPtw0bT8jt;002ALe!cnt literal 0 HcmV?d00001 diff --git a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/keyproved.gif b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/keyproved.gif new file mode 100644 index 0000000000000000000000000000000000000000..aa3353f1d0d5c3f95be2816a38dcf4ac3468ff39 GIT binary patch literal 2085 zcmV+=2-^2YNk%w1VKM#7JFD{DxLYdeU` zs~gA*?Ar%S+$xM$Tzq_&oV+~O-24m|Ej{g4t$f{His7Z3JaNWwIjN^mpDk!o8jAE|QJq1P zI)%8xXr!l9AEuBBsj60lD1biN+HL{Zb0x4kavB1y2(yilo^Tt&YulbJgo0qZi02Hw z9q=l$nFBBfxp`Omh`GTpBEvyoW>9=cr4hy!2q!A3L~;elg)lGCJi&5gk5D{E5N(L` z6q2V=6H-lu^#j*~WMYB6fOBn=TZk&qEy#Co-U(<2C*DQ40N{a;dqK_|kn=CZjW<_X z27B~B*t37!?x(w%?@SqqKLZhbAE<2-s^@bajr;QEt-d+9%^XLg%~P$M>`>Q7*K*H z<^doc<-7Dc^EuDQG||`VB+wP3u&q;$(B0IxmK5Fk|`w* z`+0CDo>TEjgKQrZYG_u7?nRjfaJplOo^;-X0i%t6WvLBgxufZ(oU(c8rzQHSP^FK? zVCt!;QmQJcH?#^ss9pl9qoA!$YR^uqvihm5qK+Eqo?{6+{_I(udI}b=z1AdaU{xWT z(yxg+g>16K=4vKV!g8`~n>X3&NsL6;6ly5p0s&K@{;bO`Nq3rJ?qnXhTS~o2TqI^H z50=&+mAGcPS@A#<`>Lm-4Zjy9J3KO(_FL8D_f+T&mWIt0B%APL^K0N50Pfl3M?H2 zzD^enbq|_OeSo4sI2%?Z71gD+)m(S&2rVvvwga`V(6d6MIELjlCQj9nLE(kv`U@AF5(GH0;v^(LgxnscVkdNMw<)>0s~_|I2Rer02S(!KEO zO8__h^&^mRX!+~^J_z)$HN^u(`aD;xz7(%{l-OVV*ylS@)Ng>ZVIBjk*F6e4AX^d) z-UIPx!3#DpfCc!0l)Vz|NVV zhX(jzoJMg#5)jcSL|hy4ZrA`O-p>+A1i}6iT>?ca{;Y>)X`&N{#(^yU#w%X@q7Xg6 zMY)vGjF$UA8qb)uH8KK7G4zWZ=O{2S9zZd7TtpQyP)E`9(T^8eA!PJ;i9f<`ihHEZ zAPEV^CkA)p=r=yI35?Byr#Qcf8grhLfYGFYI+q!WTE25LOnN6gnR&^1`j4EWH0Ls2 zsZVycbCLf1CtCt4P*ipRq1Ei7I{x=LP;Nf-84fLILLcYGi5e7(42|N(I*LxFfRu&& zBxpFdF}{(Gv83))sVzw7(nhk>q}@cR5w$h4n92gCC&lFxb?OS9V$-KHy#P>yioy-@ zP@^`@p)QFUQ8Y638bVDb4x=j7sZOJ+!&GWfHE`6Oe#@g@%|KYidN;3P)rwlBYPwEH zLSb5T99(@WT+6zFv!b$CO%`0G4QPsX~b8mAcnovpVS0%Rf8!%O@ zLN7O2%Hm~zRLiVe<-*x(_7rPX3~gseOIoRz_Ovy{noM=FTG6)FwaX!0T&3CAsoA!$ zlFhBtYPDO8mddw8(`9hO{jr0?+G3Z-t%k0M`wd<;*BZ(7>@|8hU2ho6y3g=scG*^4 z=w8>m-)%^EQCr+$miKGoLhm?|S>A4mcNxn3u6*4)-ugCo2+BRhH22Hj{^sky!7YM- z-$mfM+H=7SKCnT;so(}nc*0hFuQ_K(;S4j_!Vng$hCdAAdk*-*+6%FXGaOxe&f~-^ zo^Xm)+>I8?xWU_CaExb+V+WVZ#?i>JkLT+O2{+@%L>_8>c?^sqD_NN}7IKo83}tFI zxxu`MvX!$rWh%qs%3MybleyewFN1l^_M94-&kW@5qIu0-rLCLa4Cgq@dCqjMvz_mZ P=RE6q&wSoM0RR9yAolVK literal 0 HcmV?d00001 diff --git a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/questionIcon.png b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/questionIcon.png new file mode 100644 index 0000000000000000000000000000000000000000..b6ccccc5b22ff99d170129dc78be410501a2eff0 GIT binary patch literal 10837 zcmW++c|6nqAK&J_kICF~pSeQBYAQvVqm(0Jxyw~bCdcLqAy;x=DRPwQkTPxVIYJ@6 z$hne`nRA-$_woB@+hgz7`}KOi9`EDzd_7;U6nk4s0bVIy5C|l2!OG$?2m}UxfUXE;8H{BgP)p0b?AYM8`UOqKWGWUz{;u+Ma z{}~JA6n{6-h*PN24JA*iq>cO5N6)Q~%0n;r_(hf4R5H1rQO%*7X%T-TpS=;#)BAk4 z>Yw13l9qenSHpH&qyPLHOd2ftXLq$^)5*!HWIt73^M(5YIw>w%a8(AMh`!kNH1;Ha zllba6yc21LJrcg^g%8F@KMa#bb_~tv#lVPfyGhMINCdG!orw)}60YmzKQ{L&+qGJd z1IqC4*#o!Xb^oIfioo?^P6+w0~e6Wh_X zz3sN(__REfDi?U!PwP>rcGMPEo%iku`sRF?mUWku|H?Oe^?RZWSIdnBNjX9JM{KWRe}kHg;K6axlmYlL@7qdUWlDgJXIKz^)2H72n>Wv3-xF!M z*ORk6O$Nr2XV}s*++w?@Dr)D=ZXDOoS{V9{7Bvt6-Dt_ZP;j)@rNFyjZfs#ke4>n| z8?)TTXV(zZ!Rje9b44i;juO70m*TH-=`mNS#y6xu_1f}M>-={d62Em55xKo{&o4>{ z<+$k?1Uoh6hMe^8P~|(}eo2iGhcVgw#1m)%S{V9^&NdVPiI`C`3ab<~6p?1)fr*V_ z=e)PovL2E%y5+52PO&TE z#5K#(>Z;a~5^x;J*(*`}Qv7mwShCxT$FP}Vl&Yh#Kb#WWwt~(GwY5y7xAlpERY}gOx0+fx1`7V)MLzb)t?#0t@l?IHXPJ-c z!8o-a_~)i|jg9jL)O*hE3*HlLTbuugoGsv0UOWf7x+Bfk*R-VY+pxXygW>Y3vs07T zOdkqJf8%9yZ+BiKjmsj&%X}Z~2WdH*O*rG1QOLNc&#@6oqh2bb%PRQ4qH?I)jnrWL zQ&P?>yXdi>Jb5SSLMOZ}?kbCZRd-fl)u~@2V}I-nZCvG2yL4XH z)SS9SqoiE*5ewWa)R$UC;JGSVU86req{=(r9j0+gWYpVh+!N2%5KndIjcxqlN15fG zb%m@i8mOWqT&179s%`YL94FOv9K~?7$oZbo-4E1bnn2L<{-F`R;#tdh4%q_0R1yTD0BsvEQ_xC^PTQEg~WK=gTDzg$#xx6|b zgt#&)D@6`RMPi!S?nsUZ?PCZyi5VUw%lBh;nI&|1JUxDw;VaPw-`$hlgyLBsY3Rh5 zPYh!EXY-N3q8`3v%;)mgX^gfPJik}EZ>YjesBA*L8NC+jH*>kSzGpiVlt*%7k7Jr6 zajtlSz68c&Bk;kri9qrFO3Sv!A?MSlvdNk-A%o*L9wzl#waW4T%{SljdTL2{XF*p;B>qY_xASB9|Hfq{PU+a1rY3Ho3#ZaLqZioKw+F!|H@f?XQ0$8 zUhl7}(!)DFzxMa7a_h|hBn1QnTq=m6`m5y>8RN{(t38;W$sT8CNKLr+R)79(WH_3k z7|J2o01Z3|2n@#u2!CEjmfeCZXwP-dmm&%Z7;ycs2srGUpY!f;A}VO`-;lr$jLRvo#5fKf)4NJc=&$nrVS3xzXC@4 zHLs%k3kZbR8Rw&)aE9Pi;%0y0PRA4E{z*l)`6KEQtW>>Chi?H(r^KGC*pL`?2if9` zcK8)X$5L}|d4v`Q6EEC3fK>ijutk!m`ebvM;RmumjKO9`M@KOdS+UgBIZ0S|VAd1E z1lNR-WX6Lwex$~QQ*)$T444}jE}jd!#~LM` z`Ff>hkj*g&O!HNY!J){_jv%>r97;3PsH2SMQ=1&vPqv#c3A>kKp zs>G#2oK();=O5JMGNMc3@~F!5B`JJksHktkGc_+iS;TnwL;8d_Cw%H(U#5zdq7a0$ zw~e7?hWCp2D7%TOb6uijqEZ2>*sAR!MrU4j2$ht=-o5!jx#`gIbx}h`)54DjVuXaH zV>5njO)1|c_!FO0_FaYIuf{!C7gR*fML|RMX0@WTHLbjNJyaAmxihC*OsL zV3i$P=&+>pU!k(bE{UT0i2Vwp3Ai)WOY3^#u zPO*V8ACB%k`p#ZoQzOTf{zp9Gd7ka|f88Ck>m(xyi8Rp^%IYo>22Ro6KFmFK>Zs}rsSd>?c5s_A)(?jt9jrj(S6u=B|D%DDcZU5MWZq6(q8(f=L$>IKzP7gV5xND{FKZT|5@R&tq$qX(rnao|Dt)7pU zC$+4h!lk216*s2NaY*0X``Ou1Rs!1L?!SAaDU~v$5}UqGREtmiZB*ajrZWtO#~b}~ zgbL5-!=W`Z7b$SPuAb`EI@F+w__GasDGb*YN5J{7K71woJLBi)Cmd(rJ7j%2Q2z15 zLlH$(n~zTM&Q00XnV_R@*@p`~6oQ*dc<$R=Vv^p&JhI?Uzbj(blRq`8mDt)S(ROqH zL$j6?U3?Y}IL7YBe~QeszB(AkoKpK$Z@#ykr6hEdmad8hg6H`w`1!Ew7R=9PznmUe zIo`xjZ|(Nq=y=G_^9)-7e=Yt~Xl~jQIMUCej{T*letMbn_m9`zwAQ^hO2poo%VIy9 zLHjTtV(Afz(13t{m{rW3LY(>J&u6cH3+`#R^57#6ECHR((an0ehOqqM5p6Pj*F>IZ zVngcdqo6LeiKL!msgNikexFPO?a3icE%`d4($8N_Cqn*(WDC$z$Ji74>Y*YsdZ`mQ z$po+pD+>*0f5MQA9GLjUZ#@Glg<;(7S+5$Mk&T78d@mh!lbtJ0PB#4NbRpq9;M1VV z)XcAZV3V^OFC)4r_%Gyq)_%$udnbi+4^x>xh6=iA-pk^6s+OaRqYOJ~lW0ZTSiUM=2W&_3!5@W=OUu$W#2f+t-WY_SX{-2I2FCIDg$_ zZb3}?-epj4h66Fkf*FU|ggnv5RFyM3BqPo_9z6Gl#JJesM#!4e7H@w1kjyxjfeRlW zy<*2Z=n_jcJ$(@VNV*U=#KSF!Fgk9EyC9F1r@7klP8|!Lo)0@cA@#b}dhr0#3fT`S z3A7=3ad7kmft6S|4!96G5xfJl8HqWPem+JF03yjfiypD#debTSw^pr+1$Sieu4#!c z_JKKV1ORAB zrv4$KtIA4?fz@0FJdC2Vaa8=vh6ZV#wI3bLL)Ttr^nGJTJW2ToKd1&PA^!#u7mm?4 zep7wW?;%sJ0y{(Y8A5I`>CMLG8=*Nk=E|#AuOyYuLaFz`gPkI+8s?Oq(A-qYtQVv8 zdHv4)!?(JI{yv}d^P3dp)IFw#{KmIi1CyA4>*8uhw4U=gyViS(mqgf*$jIqGF+>oJ zn4H=d{Qg5#)xP@6#WJ^R<_sV61&)3@_AQSO`d)8Of+$m(%~m*rjXlq39C;{>6c*1A zlDeSsy2-czHHTV^f!!C<^SSR2T1b>Mpag>t!edK##ix`QIUv6oVLe)g=sVxb15{P1 zu8ti?iO;zQg?Gb1zk?@q@y7Z~ux1D`8vofA=c0cnuVyLOa9WQ+}k^faS4O2*q+%@KQ1T zt>)9k=Gyo^8OcMX#=R7lmh)uN!8qyO5wxj#XU47z_~1jy^7xaVQ#-oX%I>n8+`UdB zXZ$%1w^l~_E~W3@VU-L}eE2_$>fPKB0FH{SAkFnVdz2f_U`SAy-Z=0SA;7%Evo*`B zs^NvNsQ8O7w@N)5xy1LInCn9MP|q_#LOoIuETN}FL~6?OnFv`cv!)7|TM;bThg_e% zz4^NrH}1xTHh@&GS2*kVvF6hnyHix>jCaFN-IXG+MD%EC?5_$aZ|N_-VPA@CD_u7c zO_>&c9;sw#({eHMb6k)ONzDLMk-0c03k5tpaY2zji&nYU>IftT3XAn1{-bv67fybv z)r^+0V@Wcu2RT7++xtpiRKt=lP*1`2XkH&(79bPtmsgNi1&BxIFJ~FEVJe5k{SK-( zVSyph(Q9b4Lfq0HiqjiTkn1495KZZ!JyNFalCI!uM)!3^zcOvwUBFR#HK!Gbu^-46 zSdx@=gXA*$QzLK%;<>stD=*A)LU5)6*eR`?aQ7G=jU9OBa3gB)bcF&~5&2Zm?*Mj8 z2ts^g!Nee{Vi%hVxj=?RZU0cfF8t(s)AZT1@pqi)8c{;4T(h&$_1JN9%FvUg)m@K= zMBbPkp{KdlC3VM+)YjUbr4@03qVn*P%`mPshVx+Nizf#D_9GQ4(^#zUf)KBbfCRnK zq}dj=)UX`>SktjWFKT}BfDN~B?`NYnn&LO?1og@ExY~i$+F-i2b(V=T&HzjB$^XIZ zS42)OoH_ExfgQ3y%#;MlWMjRC$r-aNe+tu#RT?Uz*TFuLP^>j|6qki`aL12h*n3u3q+eZfN(YAT!c*Ny=PZ?9s6z?17V)K6i9cYq_rRhJ}}mRF;&uZy;gF1bJQF~B)NyKct5uc5qg7E zwUP3OZ#i4+`UU3e%lt*W1LLA^tyKAjh-Tx)uGd0R#d<~DzUpqD6P51O@59qw{%Lr%S zEaH=;I2e@#XePoMA|c=>>4=@Bu+LpNXpF(M1;A>xGg$vD+HuR{Po z*Jc>q$bZ`2#sPbl`CGRx-^a$>hXev4iQo8lQrjuIwLBOB0BKVr<2jA)|DCN8)#8@Q z=owVPZso1yhuM$BXDEOB=81X~Ve}z>C)|~XVPf0-XZw+X@n=fEPlBtdw ze{*IXdfW)y1C`{~1@N(I3d7`pn8CPXej@w7VfKMLovbycQBf2h=W#=@bTWTiw=|;p zy=?OSYkrz^EFu;VlhTnrF;bwwrP(vvi;M0+#J;Fe6Xi@WEJA^5ET^wE|UDV@K9 zjT4kQ_|GUyp&656!C1-eB^ENWKGb_33azbhztI~9p*v!Yg(c%s@@%WC7XJ_|Xp515 zoN1Obq0l9l)OC6CGr_pEb->zvK6)id(fnMO=it4SIuXBu2+-|0 zGKa-$^t_Rg(c>#)RO#&AjOCwWlne&utG~B`U-!?TkS`;n8>&!)9~iliK4sUo!xl+51m|H4GJwKxmSHUV{&>5s)_qUrcZ7WkiGCEK1xr# zcTO+-@y5bHg=+bHQVI8)3>SNGjX6{z`RXZ5ulGX#-0XFoOgJ3TlZ|Yqp83C6YU@)@ zoMItPLD4BP?>(z7$TLSAO^TU&b~U!85O^A6VjAzaPcX_3CJlCF)xhd*O=1&MDa2xi!jW|hFl)~+4lw|X4axgG?Q`^qDoy2! zfdMlYOL}W~_T=NH2M-6;{Py7uwVDSmdB+&{M;yz{FvIJxJGUS+k4(6d<4n2Jw8#a_ zZnNLzgW^{OSY^O&bY|bA2!&G>MS?V{Rp&%IKRp55nz0((GL~cS)7B~ZQyG6!XCc;O zeQuO}HHV|D-^36+pYdARZy#od9Dz?RLFj7P$muSJtJ~ql0j78NokUBxEcaAGtFRmL zR*oX{erSeMxzXGBaBjcb_i_vl4)5OMr>Gc&U5p{UBN{=-JOT@gFa#etx7;FQ*exyd z>Xmj01;v_=EsGhX$gR>L^bTo4Rn^0I3|VM)%|%c4~PtD{qKpg_!v|m((Uf9D( zSMn3zbjDZjij_Kry*67zxLyT~uZ@^ukLQOTL_rdH4R;gC9MTGUM3> z(MjQT%CyC29tQM|yMz^4!e)_5+h5N5M+3921H}Q5XD4k3w`6Bn*7C#`j2K#y7N)Kw zbJh^VATf;XyatWH{}MUK1)tmMvMiW(q{|c`kGW{{SP$S0(Vx@o32;!9edJm5vf8l> zf20z24-%4T#1bE6m$|`d;WK-Epgmy#9y2|c5jRrVFVsuf*hQ^0kqsv%Ee#!R??HYJLw&q-@M;_H2rJlIrkZ@^YIJPz zvbNmbjY=!p;;!qpLd!#w|Da0)!L7N-&?AcoD+o(a$?riDS3LE67R6F#1Z2*nayDEi z1^f`IXH;mUvE+Nhdkbdf*WjSe3$fB{AbLm3_@{Fa_Z3y~eEKeIz;Hi*<7YPV;^3B` z9UwEqHatB&dYVbydLPWEMn`&gyF4Cf!dC205AEIUodQ(N%}e3@jnFm@MZNHuxv|kJ z8AtF{cNMbapKiSdA7xz6IiEtf>NXcQGq#l@(9^rs`LHRR#Sx5bqLL7aSJG4p-)6bs6 zlBFdXW0vpT`h?@SPWY^Q%b5kTub10A!~${SLOBN4dpcsn;ZQZc`zL^Vhp-v>J5OM) zIFRy%ob^XQ!cCW9#kB1SAlmz2^v`7%FZ0YK)>}DNpQ9l?SNXG$ZruSP@b8?b{#$1B z&hAoYeoRhI2TFlPJsnuglV|p5ivTp({0|XY<%hw^z*t4j!^H zkV!+V?t5beZ(Zah^=L^^1b@Q^B16G~p)kgy5mtge$l_=Y+%nZlnI-^00(v0Zi5x2T z(AVI>^WE=@p!;H+SrOeJ;rC}_WHda_KI9Kh0Hc%6*^mgiHT9^$=X3Ii_P37#cJ2z; z_2ZPOS&}iteOi!y)$1Ke<1C~~I2?!#1Fn)b4)7QM7fsw%Uw3?6bu(PT4%>ArvAtXK zhA$|&!^-di$!gRUKnPFc;sNubGdBi)H8Kz%?!$wU7z&Ha4Fx;=1|BnjDv9)`mu?Ho z&AYU>#NS!ElVCihfIasel-YU>b868}bNS(rw1x?H2933tl&M zUSv)?#r#wy0!;L?bRgWE5cxK8$jkEz)?oHnd3vdr*K4I>%(9Y)UNG-+toOu`iQS6y zpqmi!K$!uEt+?{Z9}d^A8zsh_<(2cj&3t^qw54Lk)xkkl1#8tIJwzinopAOt>ppW3 z7yv0G77vUx&l)KFc{^_t6r`=mk#w6Dvfy^TTd78qe%3^8XW92O#UrR9=8jq+u0U7q zjg)`zw1w~DgyMCzUw_`Q!cha?zkQ(y&13TT6p^7J^&Cu9nVS$PCChK4WViH|qLfQz zT^;93#AMD;bwn(fX(-%Y@Z+67XLOvX87YlU5 zTt%00D&e32|=P{r2I%4;|5;?=c;xrbC1xquTstXjs4v< zIZ?4d|9*q@60yLi8@H6R;A__viAn@8FR_@p_ky}}Z5R+LtPp|oaW|!DKSRGw99K+X z6r592Bt{e9+*@2#w~e_&8aQms`%S+3Z%xGkdGS<|WYVF!TIDR_Isue5dwgc;%lZL? zu@R=R_8y2{HE%zCQgy1S{p1Lu8t2*o(;dq{p)Ky5tXn!tgfogsbWAMlczX3bu{lwSRwJdS_+BCD+0`Q-qX zPZ9eoJop(}D=lfCcQ|A{RT()zIxbAt&hv@u#bRr+;TKP8$z$ejxBa=y+tiNPz^uAL z#?d2+XaXFYC{2$qhFre>B&frRZM!?A`gC z{3B0Aw0EeQ=Qlu)>Cp_KMbG14X#>hMSZ6Bk$4?xil}hojrhK?^y8)YlrY(|Wvn^uu z3vq&)@s_YzafHnof!HBYdg{VF^iPO5jB)(M<=nDL=G!ZeIab60T*u|0mcm#t1j{;d z>Y7g|Vags9JyY)yKAa%_C#73+Tf8cOb1r9PeRy3O{RwLzj1}K5)24;MP%&b>l4>=r zmkq%ss}5zlF=eKNw}*)kP{MI5-bsz2Res4Gb-!7Ndu$3XJ3sFMRg}z9ul2m>-=KA< z2|IyTcs1;ztJk>PUVw;A1PVK966=wuL5=?O4Gco1ruFqri0K-m?N2e`U!cs^sP{GMyc-X*F)tlN^3*5wetmaVIbgQMn5 zhv;#vq7j(G@NaP%o45MZ%i2$#d(?_}z+m20`%Cmos&)>IPM0)4|LSK#Yk1UO-r+0H ziS$vQ`Yq};Z=4<#4iFzV+Af;SWM7Ecn=y`>!ag$spEJ}EPt`Hk=jD-%mn1|}H&q6H zZI4^Le$!I7E~WanyA&}JQ+A5}B>*&VBWroULh%&LlxPW|f5BiD1%6n|APDgV&8en* z+IuB`0%)6J#>G7Gq?^O^7UitjsCtRFTpfU}o(@U4b@*B~iWxlnY9~*MZiB%Xtlhsr zF_u0hBB&Oct389ke?iS`-#oZ*N@r~|FG00jouEJ9IjL7|pbwk3MBh<#`fpC9?X{PS z>p(V{bzWUeYHgEw_d3*GOs-URLNSZ+-=lhu3PNmlcZZscd-e8XzKw6j@lT4UJl+)I z6gTvck-2<>hBVIhmD<{GJ;EQY1%<|{OT(6L#5SE-sQLV6m|ZL-iSNXRepMn*gVV^n ze?XhgdC`wi8ROvl4V3Xtu%}%1;AUQlYPmmx5EGc;O)J{4=i$td{2=NQon+jJQQvB| zikZHU@2YsPBxGa$HQhnX=a>mK2u3bj+bh$%zMN0~qwUUjDz-oe%Bo4FSZ>cKSwCkn zUht@YdV38(urF)(x$?+ZlDqE2E0mq4!kbU2;Sv+FjwwT(D?8)4RxG+5)c(AmO}<`} z_{<1Pc1XC{g1NfVCwHS;mczm^0;9KB)VFoktW{@!JnEU)NY7c7Z&>CS`;%njco#zE zm2YvwoO?5j$B*Z~05=~~QqtjC`Stg?;RflG64PI%+IyZjv*_+ndvSN!i4M8U*|ZO` zzRKZOufE7oqM3DcxTaJHt+z$O-J)Lgho1|#;n}ZLHU#@DM8_{jFoScD(n2|T;*zdc zESM0Ck%PEL*RU}!z?d8q2<u#23N3gyYPi zQ#8x-$WYsIPA~XPNMCorLwV=FmFWJ`V4bA(>!9_|4u|}pgD)&c4t&3&(e=#_E`vt5 z9X@MtsQ9ExOWx;gAOl|DH0~!RLsrR9L6_&}CObPafVdrDNj2#`i z6b#Q|HK3dzg1-Xtg^a*27Sm-StQ?d@=?d&Ug79o4d?pfQcjn_@ewEzK%vbGeOn~)u z8yg+HEzEE0ke2vnMc@r~ZjssF^a=%w`wD60T(q z)-Hf(x`B{G!WwKEYOTY^zJE`9=rUyQ^~m+hz1H|l{gWAot!q?rShc7#a^O);*+uAZJzu8i zZHwk&>g~Ri2lpb5%}i8R*ps=@Kd7vYr9rZ=H-UN_NMLYIZz?sIf%A86LJGsc*^tNF zd0Os|mL(tLXO8IFrnD6W*(M942|sG3+$Gb`FNNI~40*oyHofNskA(3uM44SiM3C2A z8)dh2n);e^s-=vdS55NCQQ#?Z7uuS&#!Qw?0Z9&OqbGgI3aVGhCu{&Vv4II5bw8|j za8u~!$y{@l8O7NbFFgTwpLK*UM3e8?uo~L05FantnUreVkdl{M75XA9@~JzE^DX0- z(5VGFg5RWp(A)IG6c3kJQRnSS?ZH&Nf_(AT^z-4?MaunE9q{}#xjOQ(33i8L8g;Z< zYxxwZW9^e#T3d+k>#@j3Frf|91qjeGSe&bXh{RwFj|iz==WgOBZ^<|tXKQ*9yuTd3 zfU(-y?*7XioVPGGtw|s6!F12efxAt3=Tq8%EUv1i_JIWAx}Dc2a`z8(Ktas)2MtM- zb&g1Ap!UQMv`H>YaU@LQ2rYzXUa~UD^9}D{pKz%e<`O~+6SH9a| a!pV(wskzg+r literal 0 HcmV?d00001 diff --git a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/scriptAppLogo.png b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/scriptAppLogo.png new file mode 100644 index 0000000000000000000000000000000000000000..6dc71edf6516b98f85e91839ba0b3f0677152af7 GIT binary patch literal 1115 zcmV-h1f=_kP)CrbxcUKf)-(f<;kI9^vcyE3~Q_=s2$j^n0O^Cn2k#z%>j;ZxxL!r7JC z3gT-E-KNxKg|jQOb-+GiW%z+KadcCvVM~GMh_5Z|0B$K8UAe6S-X&IqY2ZcDqLmsp z1$;xS4Euo%rGqQ4Nzx}ey0-8Ju)J_~Wp)?vA+aL-M%t8`D4bmxO^{~)zYr_KE?{-x z?8>ZLTR6q&Md&uAmKDyf%+>;*6Z@<19lj~myrrc7^T&vlVJmQL;q1!n2H+iHML14c ztnxHdIN8UAMMyg3(H_rF>+K%bmanSc2Xgkk%G{#uD`2rNFBUx3*vj z7P%L&1if`7!>=-0f<>mA6U=A7!4iyMN$mjt;SSQ<(jG)DV4-czUO+mI{xKGymmVYj zrjsj%jamu3iF*gVw1l*a_y@59xQp1R2Y}CrJ^aJKKf#MHnsgJeo!G-ah7-RgolpAC|2Of8-!)~UD~CfhH!435$E@5MGT}QJA zSW`H=GFVIeVyf}8V7-g3ujjbqrUz%d-#($jp`0~P7^D@ zF5vdE(Urp#@HKfRehcmNvWD~;u>yF-+#)aue98PD{sCZPg}fS*HNZbO@$0>3IeP)# z2X3nsP~$fsE$qzo@Q>icuQ!$h-{Zuu_coH2iVxsV-}c%j{JjB5k|arzBuSDaNs^@A z8q^N30IdVAs2xIe?IiXxPIJT4!}18ZEP=2D!V(BeAS{7cxId0r(+=Rr+94!Kk|arz hBuSDaNs^?hP6K+u5KJ&`-thnc002ovPDHLkV1mV%1=;`r literal 0 HcmV?d00001 diff --git a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/scriptAppLogo.svg b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/scriptAppLogo.svg new file mode 100644 index 00000000000..df877d201d2 --- /dev/null +++ b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/scriptAppLogo.svg @@ -0,0 +1,72 @@ + + + + + + + + + + image/svg+xml + + + + + + + >_ + + diff --git a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/toolbar/README b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/toolbar/README new file mode 100644 index 00000000000..c3d9f3a840f --- /dev/null +++ b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/toolbar/README @@ -0,0 +1,3 @@ +Icon Images README + +Most modern icons are based on the Tango Theme, which is in the public domain. Images should be in high resolution, preferably 256x256 pixels. diff --git a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/toolbar/abandon.png b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/toolbar/abandon.png new file mode 100644 index 0000000000000000000000000000000000000000..66c83b2616bc24dcc8506bcd5081c11cfc9f3d31 GIT binary patch literal 33495 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 Yddn?BG&vQ~of-gT1$FsaS@Y2U14pb|C;$Ke literal 0 HcmV?d00001 diff --git a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/toolbar/autoModeStart.png b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/toolbar/autoModeStart.png new file mode 100644 index 0000000000000000000000000000000000000000..34bca9d799e92a1ea275c8f0bdd3fe9e9827ef79 GIT binary patch literal 8064 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 literal 0 HcmV?d00001 diff --git a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/toolbar/ce.png b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/toolbar/ce.png new file mode 100644 index 0000000000000000000000000000000000000000..de66a0732983686372f7dfcd3129b5b4b5e6e0d4 GIT binary patch literal 24858 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%0AjAUomi3_T>GFPaxd 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 MCN{>EhVD`S1H<*!U;qFB literal 0 HcmV?d00001 diff --git a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/toolbar/config.png b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/toolbar/config.png new file mode 100644 index 0000000000000000000000000000000000000000..8f65f2c53b9c596178bc999c39e8b0239908f539 GIT binary patch literal 34200 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 X+}?e539Il&1|aZs^>bP0l+XkKp$Aff literal 0 HcmV?d00001 diff --git a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/toolbar/decProcArrow.png b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/toolbar/decProcArrow.png new file mode 100644 index 0000000000000000000000000000000000000000..08be1f6f8f47708cda4a75f68f33e8d0c23556a7 GIT binary patch literal 171 zcmeAS@N?(olHy`uVBq!ia0vp^96&6jA5L~c#`DCC7XMsm# zF#`jCD^TS$#?0K@Q$RuX5>H=O_A9KsoMx<@VSgQgLKdDbjv*CulK=ewZ_jLZpe4L4 zgi*%8$iUzMuh*e4py+(T=}-Ru|1UW+fhT8@Mwo)OQ<||*S001eW=U@MrE|9c&0z3! L^>bP0l+XkK#DO!# literal 0 HcmV?d00001 diff --git a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/toolbar/edit.png b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/toolbar/edit.png new file mode 100644 index 0000000000000000000000000000000000000000..90ace114cfc2c14e42f116836ee0891ec7febd41 GIT binary patch literal 16499 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ⅆ}%QII6J}| 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 ADF6Tf literal 0 HcmV?d00001 diff --git a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/toolbar/expandGoals.png b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/toolbar/expandGoals.png new file mode 100644 index 0000000000000000000000000000000000000000..f3315494a85afed42267101e02b841e04b8f8c11 GIT binary patch literal 8457 zcmbVybySp3)c>37g9)I*_rpUx zQ~`hm&{9=04qiCOf7$3{63`#MJ;RWrfkUjRqKXSrqKje~CyT@Pu@abcrHXYm@b42S z-+bRA#r@ohmclT-f}E_UXi`cwT9J%fmFixck_(Xv&SS}?o2yQ0O=bz~BZbvYrHAUE%M;zba9-7qi37Y?oh2^re-TI+oF}0%DpzzIx z8P6T2H?q=hZK7BV{waVSus6oPoBbF^cw=FjZLugkhRoLY?;GVi2I8Swm2j%(Ao12s z8uF45c7O)n8BxNxC5SY&Yd3|$;M%OCFQ_*qu1kX+C|=fad&`nd#5aTUa69Y=eVBn7 zU7m4iq>klpV?cW$VOwOF6E#7>fajX^%g!#{TSo?j!fQVB@WM$UD3IQm)EzzxBK;Oy z4kA7CSR@bD@gFlhuQUKvl+7y~E%HT|&A?a?bO^$anO1P?2<{E4bf4y+nnD+8Ss^av z6U6*;!3w}Q4f9~;IEJ?x5<=!}vhfLJVDLGcJX48r0AL2qfMhDwiogacXtALy_m5`g zhQF%b+T<&@aVKcu`l}o9=M^o%@zQOxG}jdv$TxG}>08q@RHYPrBJQ|1r_cWhs#rS2 z+~Vfy5okyjt1yzD59^GGg_oa0dq0rZ8m7qn zo&GjMhroBIE@s1``FE3<4G4?wiLjBD22E<|CrLfDPa!}DWj~3-eNB^_wH}T4l;N3% zF#I;I`QRR-T{|r^@jnba4A+F zFY6Vj%5vgC&4C{T@=BDy$ z!xR1=pPO_EEkEthkU2rVX$k1mA`3BbyVIGh<;&}$+;szvdbH==a*tDg4_h8@nMIz! zKRhy*5KfT4bx~g9LXg%|CWOtqf*{KCW=SutYUsThqzpIdHmFg#AMQ0_qEdu6(Y=-H zNpwiSeIpE%t2$MF&4GA0)f%KY03!MdyczBFXQEl01Poz=thfV?lo79`e92!YB1#d7 zpI0#;7+fjHDR1(&V6e9298NsfXs1sg@$AFl=+^GbIr8Ktin|tuf(n$uNXR=tnos-(G=8f-fEwTyEgpi(I95Ai`yz=@P>qtvEppzQ6-DI$Uf1 zB5;I&zb@>YP26O=;rU;KdgC$cQVsX_)&t7r3q;-y6l4$-YJxqr7Gt=pJr+oSuYvW1aLq7$)4KIbW$4hU9xwc_Z#0CunwBH zUjWxx21KA7Ul9o%8F!BNjWMTVo1GQZk zzofr?<#Kf8;KiS`JVsWo{86-DPArJy>~;NteMvCI3Qy-b-{iyfwP~>rN=^aYB7wxi zJ!ymyvmvb+dSAWe1C^$?+JI8_?$f5%I1eW{T26mmTU?EEJE4fk`u`LQqlG7hqu_n{KW_Dl3DCrM zxdh?4H*5ySOdu3BqF+#_FAGzjGfPdkbTNEH*&l9W_52lQH^9$-r>8%Ny%VcsV9O5E zWh*bay7)1k@1Gt0%~X|;H#h3fw|uXE@sWuzZ$a1uJJJOBZqWK--2pVp{Cg#@f2WIe zgTVYb;kiO%-ujl@Qmz#Bs{~Df(WbVqJWg?Q)QL;=zmu$wcktT*JPY}UEDF4tI@3di z?!50G7=(3mqsKul51#-omg+;OM9fqaX8enU^`E%=Q|mY>?ge;uWxANORNMEM>_OXi zlVh{sZeQ5}`MG+~7zG@G`1lT|?*ORhZkLA&#hviC!Y#Z>-+q_Nhi5?V#PL180~ zaAx+BtqtbDBJ1Nn#?EqM>-NqkZ^e0dBgOj3m-IC|nL1$! zyevX2lT>jgm?dK0${H;bP4ne(SeXtM8_FkPfMp`xp6e{2hG!MkY?PO5eBe#*-VRAi z!F+bM3F8#;Z^!E^-m-znc4&iKLbSFzu1qOV-@J_`|h3gLcT-Sh8RPmgAZ2wd5K5m;f}|B=E0A zK*%hb)GH1b;hc4(W?Z4c(gL_IwDoRzj8s{RB_4}5ee~Q&!Rs;A%28aNMzL1)b|?N! zhUD`WaV1au)x@@5|CFh$_w^pT@BM^b0FFRxdGNbixPNsKvI}cAVL&fZH@<)d$=BwL zlJlB5sAi6LiLN!PsANqFq;)78aiM=J7wESf3T zbURXh6$5>(hJT||;fJ{5#bhIXCd81ou%)vW2vxzdieW#wa(42Yxa_&fqb&loOhadR ziZytwea}+G&pPjCkLDlL2!;n0gnk`dS$d3VjqXD}oBIdq-tkVhjv#n02)Mb%D!=+X z#&+=Mu}>K^uW9qcZt`Jz(oCEf9k(KOf0kYe>)P|1VgjErx+&ST_r{n*A8{HBZcno< z)5MBuoKL4=a5nSxuoqor3-MoTzYBfLA;35PJRG$}4YvxfBSoK^^v0)!6Ft!GCy%um zJsMp{%cT}7)n|}jyJ(qP`dU4f^wF#~%m&Fn(5tH@LyO%l5jSyxzKrw)xAuD@~9kEi%Dq=6&`f& z2K4Y%f@g6OMP9pIKg!~>@#*bq=SVY%n~&unV%nu4)Y1~nPpz?}KwmwR_EMO9g=%>< zD#!4_y}@W!Gu-{hIHA~n=i+N+OTViEwFet}A2d^)j)Js2GfGkP@tcBLw`$53+*Rdp z;~WgWOy{{VQVIs|$h^fJEL6w%Oyp)@qGrz)(c7%y!pAb_;+$xmHnI%&hb+v7mGfM< z{JyM^Uo>S8j_DnA-?J^S^qUq{ski1-fv+MskP)InUYPmdKD=U5}O7m2LG-%>m$uk(syDX93zp8frW9uP=h|~zc+Bf$wK}>Y* zyvBrPJC?JURXx~oZDiZNr2TpA?u+_%wx1(Xjv-IW^~OU~F!Y`0@#Hd7Swo~pqUsRc z*&vDfk$(hA=`@&q<`4!h{MEMa+))}jmY9+j+3SG|o$moK;sB6(^(g-j-Kz-g%$vL@ z5RVqII2R@RWnLN?*@;@17R%3y>BN|tGaY6A0!=LLye6l@y$$9tJv9|vWJ2M}_p#fS z$@%T{pUrUgU@;6M@6F?b4DTdSeCi2>V*|^GT?Sv-?9A}#f#fmy+vA5VlrabIZQ@rY zou2W>e4dbKBO)F-$Z#Jh$IN7F(G7%>%9iL><^lrMZ~pjh-!$>39*lTUZ#|=kijp5H zln6VYm0ao5ZpW6L&Rl!SL2}`a)EB}?n$wqVoULQcP(>W_zWevY>J(7Q1)bd)rCs`@ zi%hL2w8*TTy?+SOk5{g6Gu_L;t6TUdxFiliB`}A+m!pbpMe(QR{2q6M>kmLuwI}k= zqX*P$Nl0)k5qbz_kU~LS01IMs@*nOT&fHULxKN;{Y3TbT!5}@fj_~Ef7zy}5pS&%# zSRV>Zaul5Z8y=3=49GkuV(IL{4%&H&4?1m0C(iRI(dr>PXq_C<`db!*$b~~K_61c; znu5>bPI3&&xp780#$?uj;@{w|9mubNoaK7S}2Y9<*UtLm4|FGXSCKnwW;~DIIIKMUE3K8%2I-)Xq6mJnL7zvtSY8AA zu@{lzV21D6k46$3M7V2d`K1*yp*5mFe}8{EBW(M6?FpR44;m7%?sVTqMQ|cB`Q(%E zK}OiM(Zr`RE(dSRkG}ARL0`jU4$jvf)`5wR-1n~{WN^A`_%2O=G^{n{ZJ|aj!Hz1n zYg+8NEQ3Ma-e#vX-`pPCxt&;DHTb+++Sf}Swsb35PZ`Izay@BI!y@8h=+yd8!QYvI zdBe6dhA#V{T#ufRk^VZ2!<%)fMyxoO`_9YZ_yL0Qc8u-1uFm*2V-A9Fnm_oFX09Ec ze7!2b99~bN#pE^zA$Y-J<883g?B|f#!{&`Ip#ay-n$Ny4jY*9B9NE-uI<*lQVdSj#Om3Nzs3A;Aw4TmPvI0Z<5-J~ ztfqp&x!!f32-!TwQc1FVf-3NN%7_l|H0`%IhE~3An98EymY_SI1Ud^WS4n%x5nhN_ zL=Hql5sjDkn~fI3;svdE*eF@U^Dfj4?%nxj>~cPG{UQ+&Q;8Gkj8_V6#zZN>SFE=Eg%h%#3u+!t&6K+65$-q%MNvC4RM zPfE^tp^k)6nzSXBSop3va7yx%TzEq=G+uGkK<86uN2ors9pOpIrxM);M91kO4U76K z2Um|W3V-09$&?bHM9;7Fjz}XrI=;TF#(hD4Oy7=Hj=*&DuDL|r{l+w;)Hh7 zEGcDCnzwIQY&TLu$%g*yc`KiW;T}!B*C9xbG%yp@%t8L{2$<*I2Rj|Bl<(4)ESEc( zD;TByjb*%&Nlk$qIRX&xByBO3C4CIrX=e}Pj|*MNEt^Sj`*1Ljvo~^!S%y!Rn0% zZ~1^eG6k_06K+US7 zWE^NvR_M7l0!rU|B!Fj9=!(7eYgellJ7hM7N;Pa^$?O*c4%vj@Eu3WN=)GOomrZJS z=;38vb!%;i2vG_?N&H~#rJ*)0PF9RL72UHPR_k&#o0hTs}y2w|)*J=%{_QQ6C zUARTnh|{JEqtB=mTr4HS(bXe~?J!IJ!ygp115hdGg_be*UPr2{vm?-D(rsW!D?!Jz zOdljq?+DSQvXrbh1C&msMPf|tY{eq(tmV)qJ1)C_L3HdF+ZOapDP!^B5po`hr=6_%e0c)_~g7mzK3C?-0Sr9@O!i6730! z;HmGs?~Zz0E`hhCoiA3LIbZGgtl-pD9#BUokNA{Y0$6fA*OmC$xZd41_UmI#oXcp6 zzE&oQzP8ku42ni9;bZq57e>wd!DKOZ3D+)<2J~ybW!w+SL(PjqbRDW#;@2YT<5hLZ z$Z(Y!YfH@OqPDHw^sRAb!LvXtOtXtrb!L#T?DfIpxq1ryGWwDCG;Dwo`wCS}OWRTw z%R9HAKmQad_j<}A-wML>%bm7@yqN+wR%mke|F-o4TyA(A9?Z?cCERU;Y2@WobM~_u zuf?8!PRT`={@`s3V8%=CLGdAOGW)sVi*ib-nobL@`1Mcugq^6 z9xHPg>&yDKt38E7aTz2}u{0KJOR!`DyCDNW< zIrpU0XQ#uGIRZcTu4XNUGE?3Pj-b{LxA)ZgUnDOK0Jc4XX&xW_V2{J#;eH}xg}`fd zUO73?po$F*E5whCOZd2fas7!+aTM9V+|Gg2?jS{YzHdU67n%Y2IJxVRy7bh^5%^RU zA#3f^6{hnAZqW`zrCl_ znx8~ms((8M+M7cZ__8jvHgj8o*7Fhi5sB{?`XC6YlCy7-Z-pBOj940L{bCIFZjSF+ z%jEge^;T2Rys%Spv+e*cB(_hou3ze6A0$%{al1;a7_zNhfB$h@Sk&0e^IUvX{N#p! zvSY%nYACS+0QhVNid+m7(1rXVvFb(oWOe848dennye1J)#z<0v@yA6v-{&| zO&~u~n`d^lbCt^W<>5ddREjv4sBzRge>M2aucxBKpfNQ19_2~ONtcavM8gtyg3-py zF0y>oiYsA}KxRKj+b`VqFDZmG#zpR4LBV?IXp7D0A!*sB>~8QHxXOOq+Q3Wl)6``>eCoxhbkOzb&)f5E7TR}|T z240a+FxzGOkpxJ?)Zo-=9pGmgF9sL!;92=W+`a6w<%nhW3~?g)fQ%GStAP2hFGqgN zZx-jm#z;ykWUUDL@}3@V2faM=a-79Co+>eMWg{Wb3zWjfC0zG-Abax$m>_>}txBgg z4NcH1>VDk)7g@kRzG`)PoC>}(w%eSS_);bKTiyKr(=2Mv$NmE|wL;&}ea8XsO*xx@ zFRs|b(mr5LcMz)f=_pR#Htf&rbnDx4;avU=2av33Wx6F`67}M$)~I3vC_;#G9Z8@k zN{o4+J@f;ns6qOSKE@-PKOrhJ&oiD77HPMNZTsY#Ht0FZWx^Bvm=^1cn3T8i?m|J3 zc4^}aGq6IX#miRcLPZmq2|$%Vu}*>1Z=y8@a^TOT7~4UuO=X*F?3MLxpgY@rAc%2v zdIJ363&629F-2_fo-TjJEgCag=#n9=5rp^0ML+dr(G}kTRi_J2`Pff%5>_fwVhBK#E*SkUN{$+Xcs(%M3JS7?JP`>GY@JRvcCv!1L91wRx?< z5`cPnTMvhH6>r{IZ}kK?ukJ#U3H|bjO{UjtT;H3v9WC$_4FmQG1SC%9t=mp;b0zK+ z0HF5H`~Ld|qo|QXdGs@d!+u20Ell6E&TU(CkeJsWgBgpH*7z?I!B7L{hS@hRshRE_J84z_5T zc%x9x-0U^1wA&sKpo82$iZf4mxkCSp}`1ybY zw^awUFyJeBWc<&pb9RqTkf*N^`RmEU5Ta`cCJRIPhJ--K7-XPJA>VL&Skjke*<9Q2 z;XCaD1NusCu-kSg3)_9n&qd2J1?LVYJ7y+7Qh##z6|C^cGc-+Vu-Ki(M`x^t8t%Z+ z{DNGUsyk3BkjcCjT$Dwn2U@O_fBw1t6Lo~cli-@1Tce%~>5-OnBV=nEO{x&P36a&a z>P~Xk7osH5AbC8-^b>Pe?*@aujXIg;!be_v%xf-k;s={gY+}WgIAOp0un4b4TE2)^ za1EpN*Aeu%dGb@SwInk*)sGa4por7@MCu4=GgKr?MfWZ}AmP7U07Wr!z^oExz$qnPWVCWM7_F`_)xZvWNti&jz=- nxTojADm`+rVzc(fA9+h4gG}co8N>g7nGRZNda5X-;kbwxaEDr^7%fTye^rwst0Jinpa`L(M2@U8yex%>!jfkd2F#2(;IFTa;T24r+&-gx zdg*JsJm>M~yX@g1dFOvQXPZ>g{b~ z>u#%fu_H8}QPNOS>T~P0)UZ@>i1O%xV0Jhc`o@eP2^F3%uU&(f`W= z&xGX!g~S+;C(`**!meyjKo3<|i2d7>sAZ+!3@SkfWp&{DXetUO2|&z5!mhFrO6)xV z)>a=-e>0qaWYHCl``S`h8mGe!op+1#Lxq#emdXniULSpA0q<@uTCTe@nOR$#U53|< z5`kQScOF`9J@0s)ew&lQS^<*;IA#|?2A@}y__ooO+Wb3bo$kT! zNOv?jE#L6!7UPTg@Vq-!kMiV5-<8yZW4hO31ZUDZURYbBox$4eDovWgg)|N<#=>`< zdGR43XLWJ%rB=o(k!11dn&JCPr&yJOre_hzq&Wb&FX!M3lJ}@aUIP{>-%ujy8y2&! zN6*?puCsmLwFI6|vCe~w8b!VsJ8E==M^S>yz+hTT{qbe{J+=4EaW(q$>2a{(K}|U? zMK?KpFSh6ztc!H-X%D<%hd6;)9o=u@euG+T2 zpZVFEKfVFhip5$l{Mn8tES@!ms{TAO~|LBej`0r(zLPpakt5-~;J@ zYX-VzLnE$l6Y1Bx_)}sf(PaE*TpDiH2l2Hn3-i!cC;)Cx)hx=j_&JX&eD}SZIod+Z z*Ox)x2JXmwi7!a$3;V)hI54~}CZF`{8*K$st#@u4t9 zv7dlH+}G^zR_^q=Nzbb>^v~bK01SFCX>(T9#h=GRjsJpkbfEO;H`?_xzNc$qd5uT< zmaqQPR?yIi^W+0$vEfIEaoBQCXLgoqGXx5Q9Y15Sb>y^jD-jM8%zH0&XBQZ*XoeBv5S-bVAjG6@8z`3ll+8~Vz<;$_zdsFneHg1? z{Xenh_MVp1q*s5y+0eu8uGas`R(846NRz96%^=q36Oz0nNEa}^fhW8N_`UC}>0#rl zk)k5~PwxouosV^LXaVrGG>5!t5zd0BiS+y~K{-QiF2%AinrZ8a<8j+$3IW9rw&Z(_ zA-*$3(piQW?B}b3$JlZick2uQ$7f?-VHcMB+wbYeEe#V^l+V1+nhV!3JyA3AI!P>! zd2$O zU7E&dh4iDBU$~&HI#Pa(^Uum*mA)~6rJuFP@vOn5S35eF3Jj}{cEpU>mc(Rmjhs#~ zPQHC@3+ub(Es_2?`=jR;O-1Lso&|xx;4@+%!MbMw^&{l~c6pzpmn{pw2nFjX@vulX zH{Kt0XVKqKtA2X>mrZVqNcf{+YKETfhn!cT&sVh$bG4~G`dq8$Y7iy&V->1nL*;KM zJy}w4vc+Jd!S%daKoqfURteQ_kY<_PFR`0Vm&1GouV<5>Gc@A~k2+j<15U6}7LaE3 z7)LZl!H@Prlcr+(77X3xJ-*rdr)cjmE zXH@dfCVe5}f(DO6iA=}7z6r`z`*3ifarrI@xC;n$t1~g3KwiR7ClfQ6+XR!x#>60ny(1<6sbv@qj7hlmf?=u2kzmkb@ zddJ|lpJwl6ZffPJQo_jK??5TaaPU%T5imJO8-sJLHh(NlB=Us=)RJdm$iSO2C_-)0 z=?*U-#_RiV8h#QW2DCxg>g&1jtYaGnB{<_G0jQ85yfP9U{HNLaeJQ;mep__*=kFT6 zg1dG~F}-Y+jW|bAPu*Iw4|`}HZ2si|IZoIA`4WS{Ed~O!oKZsT%2q9bYBKhQf~8fBeCNZCGxpkJ(JxL{xiXE`rb3o0bSgL}*rCsm z0x4ek(W>39F6}+OQ0r!Ic-R`RVYlZ+@a!iTLn9#y@-U~}lkIhtsP?d}laggNW;UG# zlji^EGoApUApMN5%KbWUzAnXtrDhSOON}o|pBFyk)msjAzUhC5+I4w0J^5vOWk}@_ zn{7l_^fm&&H}Y8lk~#_91*@_bJ;;*Yo2%UFiWy%}hdh%+Dj|kEHp29cZ8~WAx@Xz^ zR`?Mq)r`JS-Map!-MpBq_XsocXl|K$!@h@^fn7VoJ5%t&BKpV2t=$%>e@0T-`ghBo z4tu-Be)o@Gats101;;3`72Zs{tu~Cy`TQmYyi@SzeuI+YJ2^&OMMsB8Ea{jB@oNiQ zREJ0!=2W9s|2kmFeB~v{D)f6}n4Nt$F-E^-=PZ3f9B;JD3t^z&17A6Mm-^+5-?Du4 zQws)a&EF|@+EaQ&sf_Oi6OVUah6c)5yD>cq6N-`VG%1Cl6tC$D(I=jnxbBBL5yOOy z-lXg~fTip&cl=mf8UP@=GKbMVxC??cob8RxDxlGM*=o2VTyNj7l|i{)lk_f=&AoDp zQByto3r$BJhaD*Qh${|m4zRSP=r_H>*mhXIAEHJY&|>yiFnbE-dB1*!p@rG$YC3#U z|1Y?3X(G?%#gOZLJP&jGNza=RLhivXU)RpR1Fm7-nXiD+R48NNq2_>f8)}|CqsPde zSXs}3-Hzik)Ty3r;z&#uBC$Ke>dh6Eey^dhUy_*WnJ%ntPTd~7euLaopXf9&{oaq) z^juhr+9Smk)QFBxEAu%P%q}B0*>ZfTVn1#LjA`Fk3U(bPhX2)@B6QmxJV4xqkIN@0 z6#|$t0s=bsKl0?6Q|T=Wc!C$^XH*tKi-39euml~HRdt#AZ0*R<4gKM-(?Lxe6nXC- z)vi{|#jo$7)vn8e|2$FjAsbvDA33hLQUVpkqrFkC|G0UqoKtS!`4F+bG=JP){;^PX z&~>?nq4hQ&tN|O0s|a*|CIiQkmtZEiJBrSm>jVlwi9?%jXuv~I1jyP$djDPo&U?V$L)H+vfPoGPmrwO zfC*3(e2N<^Rg^;=bhjv#(F)`w3unX3dqEBkzYgjI>z`^Yrhd*LQ3=MGQ#xY*{B$68 zD=64d9<&KcXqkZq271*(yS6_yQYU@AX+aP6Qs`Q2JT~0QrwG#tmd3+`A#4>_?Q&^k zz_Hl+VsTC`Ii>z7I4%>YF6Ifb+W!t1>egdpTAZ%mkfW=pfv^ zaIJi*egC$4o@KddHJ>L5NY%C#2#y`|e0U2S`32?>d;jcL_nA7-o(<>845FaJPT3tQ z`zt9_#)R&p4LwjKDKKwTGoVCp_i{7%9SHjcPHG)g=tN>RHbmCXM>B^enD+-31C|o2 zwy;-U2G5@A{@hcA_-iJ8;aV16$7z<4oOd(-xIkLPfgEtm6FJvzb!C~?R9j@kOMDL{ zi629@=0fJvj=n8bh|_}uICJMQGGfZIrP@gF>rT|~ME50YGVatRn29;KMfa$8DS8LXHPCEmFEEo-&~k+D4biIpB9Lj|OVFw~`0W<7;27_(C| zk!-?GcL9x~ADT{i%Yur)SQUY{hbBF}7N5FbvaJ=KcQ-iFMJDq90@c zfD<{FA?77(b~;mQCZ;rf`h(Si!o2+3UOb{qNk2@pA9OZZFq< zKX4_~q025q89=UaV#Ez59&E8WqK4uur-J`q_?$vTvtCSzatvl$H;Nr=r*Qm1gqY~| zCkL6!Oj1C51+f5N?5cc1Mtg))iYRG(q(fEAVW)b_^lkLhq}9C#nkuajPEO-3ex~3y zH~Sdqb+&#Z_l-bGXLY1j4oLhi8He}%;;WZ)(-NMTiWvBO1NAZy!Z$9aZ$#Bq*Oa}} zUL}(DYFzg=@xfMzD9}u~1NAk>;`;!vYF->=aGb@G)f0+K8jemIsFNMEHuJCw(8X~0 zuhM_JNeTWwEQ%w@y?-?OVnG17GH-wr9g#Nt`LV^VbPYgbS)73hKl{mWL<f= zb(z<^YDxV)WO<+D?WMV_<5~%>{sUc$3fViETW9+!$U}%=!ydE?KPc^RPNfx^(}ine zKO@Ik;G5ZH(naPTY!zb{TuG-TyhfYx7fP&&@j)wl80tJZE!XO;z7)j_D912Twmdz< zGH`#QDW_P)vXXQ-7>x>?!_!0XkQBOsLW#*hcz(r=CB@~0-1YH$4pBCVg?S^tVE2dc z?|PQB({}_0AA||6WL>%?vSDt{5DrUiKIpq_Zjb3NgDBa1!-zRt-G;RLi7}Ai59$EG zHCybHWaJK-`a2jMDMG?Dy+7YiTFa@`Is2+6HGd}Ug7K50WN5Aw=Gg;`CjF(b7ACDC z1Lq18+06-#9|4iMSp+nderG9A?Jg&H(<(z^v|WWlIaBb=$oKS1rG*QBvjU>~|H&$V z&(?&00R!ep0oCFaV8mS#apsp)X zLFAC7SUirF^%r!@!ad{zqiNfwis|C}XuneYnuw(^AF({BWO6lDpk+GfXVbs?&#T);O4w6_7ZUrMx^iunlu`0d0)%{3budKN%jJ_Jb7dB0iuAp(qvIzPGCe^Xma+`oh(-bY zA&!@#D}9E1R6UfJ9fj5R#uk5r!<;@J=RS#iaFA>-ORJ5XGC_YMfz^uDKuB*&bW)ZW z1d-XwiEkkwhhl?TuoShHNCHh6)TAH!OmyK8PBUe0ZScm0{pdXt-BCh+rt!n$y(2msLWpWoY2 zu}#82{fg7FvaI2H%^mv7<7DMaUc+Qh^{Rsmz3e}$X?7UqIjBcD0oBTi?#sdT`_48) zEu)8uTZ2JR+=}C#lWUW#>%S%6iQxycvo6Qh|D`r0s~cv#clz=U zQkZ$vzF^B$a_#Y*WdjSgd;f@EPHpXT1Qb#2Ml3TFh5|_`Ln`x+!n$~>D*bGW>EI|q zd$uolSPz@$Z}?5(X^Y2WzJvqpoThoUrB|$bz*?_fl90;HA@WiCN^dR@5vsFOeu+F% z6ggZ($y=r+?9xi?r+MN5+#7?COjtxLb+>v-8^w@TRKor5`B?Zn_o+dk{EuOb3_Vg!j@myq{jpOnTY* zoBp1r?^VAeVJ;|Y&-?#o!r#yfNS+TOZe=o8nh+tsk=X{HxpELDs_k04R1Pw9kOdL< z$iEhpF5a-jzUpC|&I&VP3lhFLqCNZo*Z6$+0U$`7xG zoRfUlywtf@&Sf@d5P)kEht=Ja`TqMlliuG@4&qon+B{coiwe`9ue?lFgGEF&nSiIecdgmh zaKSKZ{VwL{Uf|z5A24djhd5nU&=g5NATTMpkqkk7odwR|V8VRVA}hjKvTloyM_l$F zBuUgiI)iE}D_&TCG9+tfGmwJ}koGeMJ=a90kGz1281s#_7yuaj#FBM2bVxE7XKNrJ zA{!8syHWj4U()D**o`RqVPw<#v8ww)?Y1l9QAXo}ltI!GVcNBqN_koeLi_@*NQ z^WP8K;aESUfU)R9h2+f}2<0e!e|={#0;_Qdy>hQm1BqR#_xT1akqmI1#iCp+=mhqd z-C7OF#f?tu6b(Cb)9iZ@Tnw;Am*@0^Yd&i5CG-rhtHX?qQ7`LTk#*KibHiv>m(I46 z-{%EGQ|V)|qi-=go|?X`mViGEZH#~x+0CaE0zTeytl{OAQ`w98Eooifc9 zEgSK@jlc+-gXRp!MbOpC9f{GY|V|i5gL#~9g*@Q{M2|qX{R&@1%PEFO@*e7oNt73 zefi!C$X=9272qxjZD;GZrTfq!5DtK=Jjz##c;cfYC6Xr^zCjS*tJYLUQcv}+y5FV~ zX9p5&+y40V+X7_1u~qo6L*%O1M;paPzeI1oBToRX!HwjDJ7TV_dw~cnjGGkB6`K4Y z*-{w>7LLUm8i0JXHq*huuQOkG``EhPC_5x1Xx0J20w*3XQ-@+rDlwX;FYWvid=$*^ zvl?n?7ZY3Vfz^}SY6P?|=$U<@;>Z*~Q+;JDKo;PAV)FTd7N4GeCDNYpWVg%l>rDBh zvQ6`F?mI0e;0goHWi^`w6tpNp$bo)yo0~~g1Wl{$5ja(MiJO(i)cDmyYaYHK(TXJK zCT&hV(x4z05}q5ILZk)4oeIrg{E!MaThHSg#|iE#BIBEe@J$11E+I8`U5cV4-#m9CH-)yG(J{y9heWOf2)jJLL`ugnht!-%QeA= z0aRfA(<5I)=x&uM5}R36U&6y3(v`(dKroY-dkcPZOy(ZfPlpxvO z%(GxUb`5xLG#|3tg>Prny@2a)>`36-&zDECjzz?%zdjX~qv)%X;qSv*7A=|Sba+R* z9Caw5Ze}7PRtM6!Ti)Yo2QnH>#C{nC{j zYaEbWuzG5VfA}<1vtiw+zU(&+4O}@T)5F*12va!w0A0-nb!pNhpvZ--k5*4~JatPrvU~nKaM7 zbX)s(=QoY~w0f{MHI_vX7#KEhx93j~ngDZUI;*0*dL5RbWImsqk=*?e^rFP=#P z=C4cmbkDZ;LHVVvAWB2=AOesw#EZ2)N%TRSSy|9Q=-YG-LFZemCQ!s2+#{(x zZQqjEZ=VaS*#5dZ(uJ1JMNO9RLuI$cGyLD?Hr``JR^{&)f?yy;* z7BL}A5(5lr$2V)gi@+L{U6r^}E+(!4Em8jV{C1zox0qR#9qo0q2e4YL8k?rC93C19 z!rx=*`f6pN+z`u1-bY}h=DU0eemLv9@J0cQ8K`U5?hr#x3bduRqTtGT; z-ktrAy^n_n!k_kO{;*JYns6}ijA=@tv;CIDQ*6S|E7kMPaN=gTKF7itF!Y%jD``^I zX99LecEvn*8fe2FFgsqnVFsP$AVx!X9`LsOjzc9*^n=c{0`*w`{CXp|*d{Gw(G&AU zkIZIWymRi4^b(&a-E-S!*lpeJ79fgonfq*Y5wULEWTyu*q9D(4*hLgPtkVeUPUnwm zxyP2rD>Cx8eWj&XWqQ5G;CryBu5$*M1N-zO<3&Wpblu49V9Zb9Kj8pG5b;*9t4o2#0w$}WepV6mIb(f{-wOOXu!PbWzSa?=A(*p&?; z{?yqMJtRQ+SDJ)`?2noxcnj54S37V2WW?B&d<<1P8>M{}jsovcO;49}Bcs~;T6MLe zZa!DjPCYaHfdQ!@@m10oZz$}e44_X)ZA)oMAUy|oy`rAT&by*vwp<#OttS2tXmTCW zIgcVzJ?H;RviHC>rl*x4OX zhFTdUhZES~_5LR_9oL#;6s<=gW`C?&$QyXj8tkf>Cp>UKjo(}cCp9S;72_Fjh&fcV zgz=L4G3UF(eXjHAFhd>dcN!5Z*HKPmXZooSK5s#OXT)IxGcW(0 z9PN&6ng6+MT)x!I=3{1R6Lc-M`>8uIgKxq7WdvBLcGMWl>>%9fLN;~WV;RiDg{nN3 zZQ6w8CS~#!+ro8$^SK?co=;q%*roK`3#!VZ!A*p}XLdf__o9%pVa)dZC;9nGZl**&~p2ekF~w;xcSkJ<@68l z1uI9$Y0rQUJ6piADB9gM&t+%+!~^`;;_`Q%Ai9gOW8K2?NKokGz?$8}2cK93W?y{;$Z6MbE z=hrVUqEsWVk9LuRE2-v`b|3{mP&|guP2_bUv24J|T`gVLRil|W&*mU*_eTvxIGm3V zJ~RU~El;e#{68h%m#iG1EnKXTv7aP@bpr0gV=Z zv7O~TIoummAR3#WXUn~W> znxWZ4o6p>nZr2cQP;*3A6# znZMkn_})rQxCc~0*k;uPj;u+ETo7~ueb_*TlMomgo~L?_$XhuM;HOulI-q;YJ_V{% zV;a7`DUH7TUkzreS@(p1f9HVI_IohC`=O58xI8?r?B>UVJzA(9i9!ruV*)^BC7^q~ z=Wl^II_<1y!<;Q6ZjL9&+y8*{boSh?H9RC1hqMz$Vt3SrVr5_fFa%xPsSJF;lxzXi zkuX<0Gnj{-*VD9b=s6ZV602i0(Pct#H+ZwMr79KM&s>DC_=PTR|!D0@_>UPS3thZtE z#`6sOBAI7#t-GD1d=*#zv46$I?J^eHvKEJg;9fAQXbeFijtqcaTW7bo}br62nFstZ#^{ugC>fA zvS9+pLsV zJBsk1?S*Ra@2`R6sa`8CMQ9)*q2DnQP(*D@q2TY9rXiQZ5*rXh532Y+BBeFMfDfZx zmMwl;8G+cowT-B(G2bmLgqolNHH#GWz>Q8+9~g3RBjgD&fsB%Jw({`b35a~OqbJ59 z;AS{5F+!9(y_NzQBc|owNE0dGSk_Z?P+8pM1UqgOcuKxgJ~6BmFEIe%_TJ@nOE`1T z2>%JRT#(G*_Hed&q-tp7rjX>vG~Aj8Q<30i+;grCDOIio7%`aHs>pF)#Y&qq9AD-P z7J+0;a&s{um_V7J4sTx%+yrZ1XZu%0eU{^khCFT3{$7uBYE$xGw7vJ{ zbLI)ie*H6-t;2?e-@8-N|A3qondjHyT4CZT+Z`q~eLm&N@npIrwBAT~jDWY;gD0?9e%@+0b<&eJyp z`L!xuVU8Gi8-356mFeDvwf@|B5bXT?(H6<6L()MM#=vSy#&>~}-y$>jZ(WtjA3)X z&zx5GKYqjU1>~+PQKwdVgHivPydihA;@<0vkwPv}e5KaXO#-kFK!0#ihO`8v2myVag*(rX(>+r^KvFn&GI;#II|(YNnT%DrO&17*P5zUyuR^SypD*P(zA9kvt|Q zBTcT#Pmyx;dVLBgMj5*>6kGLnlCz>0y3IwQNT3gChZbsBU%tzky$0OIOF1gu z$F{TUMgd5e+l^y=igcGuWj=6>_X+|3f$&c7h334P*wXA&?CKyHp|dIxfQ@IS84OG~5~~v-Lm1F$-I}3F z#r0MS@j&aNYkaP6a2uiLZ!Uq8y;C3qQg{bg11|vpo41h+khUvws&F~-UBHyA?4J#C z;zY*0xgF^yV@f#-6^I(J=59Mlp8-_iuo9BNff5Nu@#vl?gvfr#8H?s5X#!D154Gk@ z{yjXemNlMMxhiUoVX1r=oF61nh=5I*GowKSZ&2X)*4Q2Kws}x1V!7xEF@v=)N%Ax=ebY^25@{4%%jXGX^D|u_#O!dG+`RGR5L=)P35{>&3qQC?&T)5{Y z*Ns=)06**C-B9*6IUn0;z3<%E0R8DLVwq(%=1;B4DqqAleOp%7o+4Q4r>QfnQOavd@ zBY^5<+#p7HP#3O{y`C_(oobBYQ078uWwo zLNIX~0Le7BX|^gG6{s8cBFbG$yl^ zEtOKM)Pg$u`o6w)R6YZP6#Xf&S58g!t!!)o$SYh!E7b(=ID!(Kv7WD_89LVKYodhh z4RZeu%&fg)3=3z6rQ-155pR#Skntn42v4s~gdAI1$v!UoGKfDonOpy>tY5SZXm6Lj z>89;XZ03~Df5!;#+5<#N$W`~#U0Y^cvE3tsmZWx4Pfi!9iSS=4Rh=lEA_+On7kc3i z$Ox1@N=0qPt|~(A-qcq#wmi1W3!Biz(d}xTRYG!^x;%W z#x!`e<%yE0*V9YQ9v@vowV2HPNkU}-p$GH+42S{zY}n@UqgGx!CnMafl(C(=2_AHw z&x1KN2!c$~Ux6$eBtQQe^($y9i1Unljk;GDQg<`#o`NNAu@X@{TSSizdDA($7Q@9O z;;uVwAfHenW-ZZ_au`Xv;aN@V?UkQ zR`V%^Am)W&Z8E@L3B}{@EP%Jt*Sh3id zGBrmsN)bUIW=+;5VGHafQyy%$&KIiSCo<_r%##EeHyR4PgVDPThaZPq>vDiZP5Lqj z&x4H2ry@L|rEqq$!=AI_wtsrJXC%(Aopb%5sqNR0iOBC4+SHCTi)McAGegG}sBaVh zykQ|qA%kmo-`8l6QiM^z{JgB<0IB8Zx^w-|a}LFhn)w!apILlDP0}0qkCjdzy61m6 zA0^Wf382uvV}uySGclvJ0^n{Z(it9_`33NMXI`Fw+mv+vRU@A6b3BgIA@3Z+r(_p{ z%B9y4#G0=b_=wL6s&tR4@ObhxhzZ>^}q(CkI0M1 zC5VhMHtnf2ruh=i!b&oXcT2{sE(AU22NblMa~^pnOPBWp-tb#k3t))@sO=l=zW+DV z;iN&RWhNT?b2Vc7Q1J==!2tSq{E_TtC~I`S*^(|xOLjw>cR~c-$gWNzQ;}RAWpdv= z?`%Aty@LP<}O9k&u*tgIFRm$sO* zv&gA-9cTE3!Xs`bD|@4zzjN^iQ#X`#eu)_^O}oG!TW-I>xC_hpv{_?9$drLW@iuzn zpraeeK}m{>Co)07C{swkv0=3wCAT$1aijs_Pq&$l;V-?0T1o_rlq)p z(Jo*w{a&-iG+-PBG^ohur9p#W0Ai7yBF1;cldM&b&arSHxelfm z&r>>i3UVlmq$nt^Y?)!TtfqO32@eN8$Jc`x@dN#77oviKM~0Su&QDxB@s85L^4~xm zon!L3JCeEypJ)p79fqp?BI?SF!|(Hr2{#BRwcDZ_tu26>EI_&+*6sB=_9QC~vw+U` zNzh)7-{crG)QGl*gV{*iw^H~jCpzQq=%b(f|J>RHaG_(;0-6Qd010_=SpePnFnr-*`?c?0I_(w9D$ z7;&&44^o#_Z`ExlBiSZppWN01=G$Qx!_B4EGzk>=DC~-_cwVtbyn)YJ)RpAq7eq3) z2IM`GuH*g!(St6i1Ne^ zf?UkIiQ-;*>yPM8o6tm=u`T3`O_&z7n75#SA=a-tUhPJHA*vcZBHz9(p+7zT&mmxc zCYGGtbezY3_7)2vu^djP|D8j!^}VFt5gew(<>XiTKp(bJF%I6S{vclWt26KE6M0tz z7z{o=-~9l7-0f)m$EyBrEPV1Sv^%q!+{q20B!{aiLm(};@^D_S3z}78P#{^+!Db>p z$+~G`_fVnnQgv=l5j3;yJK5~JD_Vq`Q>)v>PnBULX6G+iI%B-~9d6cIGA9~LY7?}F)e^;L?XJoF5qWP$7?gHM|=r*9YIeE4XuM~`h&_gvD@CBz_4 zRrrFG{hz0jGZ8pNe-V8>p8fG7g(u&)={mCgnm5)9m9Bn@F3LBxPYQ2>ZJ?R_Bv-fpW~K9ecI8t1-2$d6($lcCCIk6G5V`NTS3J0CWgjI2u&! zoJbIAmTh5?Dpvqh9nzz2bSgY@riY@Dq!AU_OrD-M^v*x;sy^?;1_y@;PFA*DMX#$p zy=GCZ-YB6;eEVM%qhI*aHK2RSBhw*rQ|ln`_(i@2?On!;^}H`}Q8p(18hK_r3v}X5YyO3#L*5QJhF4`ZZ7#=d0S1oMCMNRJ37Ai%+QJH= zz~Wbf^1CjPvb=8L*TbnH3ar>fLJQrKyAB! z21V0s=u%C;dKXK${{DUITdGk9pV`~D9$(v^(18Ct()UM!i;x1$$y^fOTuZXTpY4__d@bio1U$RU(sbMJCt-Me$9R zj%lXb>)(kisR}(*HaspoZre`>z{8aA6}3#t)O4F09Tdy2FhT=3oPH_k#U3E5q1N`_ z5D&e+Z&Q?|8=t~8rMo?zEBj{@T=!JN&W&b+M6X!blta3ni&yu(iq#Hbr$~>o$KdO) zLQlxH9F8cJ$Jb$x0^1LPyWs%%+JJEkgD-;%0oMsi_PqsByx)#(8^mG>dalee_1le~ zC*TxMi4t$3`kdf=HR1b_yy2bydx>v5Vl_!ibq88Lcsk;`r+N!_CEZR~9ZH1m%<{0e zVZeF|2@*kzUo?S0o*Y6(AK|a`*~@o&sLTt(Z6~;ye|-foi7Ye#@Yrq#&+CD^v4!Oq zv_+Paf6J-+_1>9oWlvM5X>KxFvB*5-%@W{*@&noR>`d(`Oh>NR$JH-HA~!;69yuK` zVTt#)Zx05tS92=k&vnc?M+epoola-(2PHH9jz}z=Tt|lGXHPeq-{l;L#kB;#?0wWt zyiY*#DBQ^ue>fu09VO|It!5(ob5RyPDXY>+|w;l&?(-yuEo6 z8&KnPXHBs4aJ-%@c4tR>=jBq6ilCcy?h`c@D(9shYFkydlE^G{xs)F*58>IuYFo^iLhpI0h}OSR zB@Wme&rV)hUbSVN;#Al(=Dog)*TYn5HFqP}UQS zh}II-=~QT1IVw2gpYd+ew^s8O`2(8A47 z{IWb&*1^DfF z8LC1tNQf5geTvEi1#}!o0ABCQ+6yI*Y6)^S_lpuaTcF*KCZ**p>qrVg{s>X5^8`#- zyvj)WCK8fqz4{|1Zsp*>Q1@%Yt=LI{P{7A!uH-o86P5B(u`(`zBGEH>* zYA_1#0Y?ES`NYAGVfGZh@tuTNo8Rfv8J<$jkT)gaGf^UCH6Bnf<~N4CrN?Jl!m7^GW-Ma)f*1roUvpNKv^t1mq zs?3pRmj_vnH$&b9eTLWrICZ5(KV;qzFU!_F?-=*f8S~wTSGl(|@+%hNZIO%L;@ghT z02W4P&!PYG8AH~4tXs3MzTa54j<#L@^fqurk868S-1BJ-ONgaVGdj{Q6HeA)=lq|{ zS^#)n*hBBnsJpqnX8j`*)pXPSOjED?h%}UkS8#apc=*k4Av2>7O}gAP3!VI>**$w- zlZU($4m&kW?lktKnAaorrlP88u}^U7D&>sd6ud(N6#G7d&nqypNH>VUZv@)ax~fPC z(TwEXVjj+Eq`=$qd|xD$rM6zZd1bkMc%|D~h#{K!&GCN3tE6LgQ2AEdfs^-5IQ96m zMGOG&uAZyi*H{Nb3H@B|{`5lkP+hi9D)>iyIakS0R#3XO2jj^5Hhiqa#KiXI2AnIY zON`HSZ@Krr?T9xe`JWspX3ZeJxL^gA`4;y;&wpj+^Kxn)09W#8#$lQM`I3rrp4Ppd zKGiLnStV`95p3K~SrRsq>hu93g(oOz?-njsI+2InW<{p+S(cW_JfV}ht8D0Xv+pK2 z*hLrPZUkY~>)!q$E* zb8FAZsGGyZ6}#nh{)2e?Ev0plR9aE{+`Z`~0S1Qbkhe9(NG80@UO2@3kHt?cc$iD* z#erYn|Wtouu!tu$^@aeTJfQHaj}RI;%46vI;!!*PGX?(cll;FGZ=>00c! zc~_vb!w2j-W&rHHxi1GHEVD*U9-!+?{ECb{{8U8zN;%*p6hSh6EaD@_{jHw*TAKHT@#L;dw+W7L-{O~ z0E5%~47PjkR($BRe-LnF)4hFscXqw}{!P0}&r8umGetin{^iO8hV99&3=urX=j^|+ zYlU5Ct;RvSm(LxJ$Ju=OQ7hqbdivL$zJJrrn4mO8$O zp+ZCaqql19(@p>NesbHcoo*FT`oZ`0wb=88{2w$#r|ywjuu9wg--8*)HtI0wczJ#c zRpJuJUG(P4f&;U9^&c(WdS2UCru$#kYv*s}RZ-k}uXU$dT%93&L0|!E!|{MgRnDwq+1kTGWymN0~(Zl^*Q?`67fBek6J~vly zW4o6x>nU9}DOcc@^GprkN}SdDSL{};|FPi4t?Lt`H@*CRBf3aFYs%r*$2VSVS_G{#45cUjzRiESZ^!4z6Z4?eNpEM*AOE!>up> z-&S9Ux^>Gc#VFJ&a>YqIC*TPYHX7GW|84LH*d*IBPkl<7?Cd)m-^H)Fe?zmvv4FO#sHm BSO@?B literal 0 HcmV?d00001 diff --git a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/toolbar/go-previous.png b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/toolbar/go-previous.png new file mode 100644 index 0000000000000000000000000000000000000000..5bc71361b2739eaf147d18c5328692f686c53356 GIT binary patch literal 17733 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 gfw(}q{Xc)f+FVdQ&MBb@0CtD5{Qv*} literal 0 HcmV?d00001 diff --git a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/toolbar/goalBack.png b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/toolbar/goalBack.png new file mode 100644 index 0000000000000000000000000000000000000000..d9fdb938d5043a9ee7038acfe37c2bab3a2afb41 GIT binary patch literal 17218 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 literal 0 HcmV?d00001 diff --git a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/toolbar/heatmap.png b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/toolbar/heatmap.png new file mode 100644 index 0000000000000000000000000000000000000000..f3d50139f93d358a60af287c3d54e0ef75883ca6 GIT binary patch literal 900 zcmeAS@N?(olHy`uVBq!ia0y~yU<5K5893O0R7}x|G$6&6|H(?D8gCb z5n0T@AblK!8O3M*Z2=0hmw5WRvOnUG77$fmx3apDfq_}j)5S5Q;?~>ihJH+r3~d)B znR;j3xV32djlMUWFEaNwaCS}&SNO;>?aMcl8yh-h4zO`~3-!7!5h$=}G;~vb^XVR= z+3EQwkNe5+ZW3AWspvpA*x(G+x^8&}L9zXkcJqV&Fh#U=zbs zMM$JZUoZV{3fnf_qXlgTe~DWM4fb{DmA literal 0 HcmV?d00001 diff --git a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/toolbar/help.png b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/toolbar/help.png new file mode 100644 index 0000000000000000000000000000000000000000..36fc1d94c264bbd30757668a5ae9e6cb03f91c3b GIT binary patch literal 31781 zcmb??Wm6no*Y(U`gS!QH_awMOa0%`j+}&aDAb}7Ft_gwQ5Zng|uEE`PaChFh?yBb> zyj7>V`%71y-DmffwbqVNSCzv=dxHi505BEgWi$W)Ap8*s03*Y1M()3@0RU=%f{dh= zZ}w5UPqy)2KmL~-k0tM8dtIj)Z(U4!^R_poD^=#!2XzB0YDg*i3=co44{J0sC(juQKDtj5QwHsOd(HX4wF^b zy>SqJ-lm{S_K0N2U}nLEX2}yd-ziE*{o9E$XGa-8gP0)Zqe}D$mTC|~-b31uCsvB= zP89I|?450y9tz1_vna^J-|}PiaBgp8=i&86{k3fmGLjCR0a)k4{E<>rK}T;ur`Q;I7iLw!ERmYD;^f-7O#^n<{81#m-GFTUs7 zXQQRrQ3fdi>_Qp9o;pz*tTM7F^m-bKtX>{2!WB|B1O_+Y;U|(YHjw*m#zl{NMXpwA z&_WbaQ@@zD_ae4`$MZ)o(9^@nL4=r-P$Nt7Qc7>ty_ufV{!v*_KZ!ROvH#sy9%ZjwRwgsT9jhOtdokU+uxjp^O(7#UN3EKD(o=IIMlRw; zvcUCB2?$ z+T8ThtMMh$;k4@w6V&W6yt*vZz4XJ6R`s+z-B3LeoQ_GmA(yt9qjql&)YG;E1ly-? zKUNraR6Bu69~!=2e)`?kY&DvB+Sh7x^#lZ-RJ+U};eoH(}0Q3&~`dH*W`~0atL2a9{y2S+iRT01q;9fWJQZeXoYIjUP=n9GKzQ5;s04GLfSUbsGf-VrLg9B z#&Z5*Q&QyBp6x^bAM=5AdYY>&9=^-=oQoeLt6or+`45~pD!7Oc?f5AfOnD? z^jmJ_#xoILbh)~`91IGzeNFwyuKumNz8OCeh~?lShc@UgeI2t7x26D`VbV#TuvFkj zq&0R;u;#KGHa6S;ZaYejNY6tUn3g)gl$sFuNkrwlC+mCarNmQKAfzYha%o~GE)0>& zTT09muO((uy$#KjYy&Mym$<3T?9VseIIW1h-S{3DH?S1=8!6eU(ODG>lj-#vBoG&P zv(mqXTxq-#hFxhIQy)pYbvmgzYp& zp#1zk?cY~6>QSWbGL-ST*$ia0?I)4AVikYn59e!%9kEm{3z07cxOc~If6VsC zuMOxHy);~D(Q^Hw860@R^f6WgcVI((TAls$@u(K7@u*t;RJh|z?RQX+ypsJl~1i0`Ixk|u7V-iEv2fLuw2D}nPM?#|dGsG4Hp~nsbt2k*i zgXB#O(ZmJgU-`wBIg^j65+l?An%}Hyw77ig#u-vhUZnP{n!hWR3(OUftd%ReM;=(U zA{Lt74!ZCA5}({pzIR-jjBT?!#Mh`JLw;$xo-+;fzp(dt8C*&QzyrSyuiA%`ANhGR zzi0qNV-8T8*Rwa}t)C>#el{jeha_ zhBm9a26}))6P9^k}qC4Y`Ke~23K(psfU7TwxRe;9+^AtNCJQychQCBBeP@ab$L=WcA= zw0l0CtK&93C3yeXw|L!24lqjAc@a&p-8zxoAuUnuf8gl&a!D4xGMon?A|Kwael$Y~ zZrnpOS@6Lt4!HZ`#JPVjeuLdzcv)rhJ30XbCe$;(se5DOzTs%p;`m;NJu^n@VmaH@ z`%Z@cW&Ad`>n4clDfb--O@5xh!+cus+YB$^xr+uG$UBD)veV2352}JYCF9JY3wp6f zxw*}otW@IFcHQk%G7{CTd)=k8<+1liV$(6@)vzy+JCF&AH|n+`ElD0T4Fe)09&##I zc8r5Azxi>j-$rgEbJ+GJBc=btyXD8Var22iXCUx-uA~?ye(mk|@61UO;IVc&)8{)D z_!*?jf{$Z%|Kr2*^We3uGkeqDPTPw2BAH#6RSo8_grD=xozez%8eyv_XQ4$1nK+$h z6}DekW?p|th*+ClBYkPIo(VK+cNb$F!lOY?;FMbmIR6#08n%M6SicGYnvDGYVyGq( zX!BU8eMCNJm-HByZ!s zOpxPsTAxo0tU_w#LMLdyL=OOIJ?NyyZ!K!8%)8FevO8s%Y`-aCNFLoDKs?kveNG59-bEi|yXbq%1oWo@*zu|XjQ%)sz8vsn zaR=Flem`_;m^e70dD?7zJcBu*aPwNLu*HBz%IkH*!~p9E9LTH)5f6w2Zj*^I6R%oH zVG>|?dUDijB{^+9ky0*0zjKt85nK_C5bnGefN!G7S)=)VHkUL}bT zWO0)bh05eW;@W*B@{;rszVYku58d=!6vqR%ol%i@LFDBAbkFUz%Ez7``=jFP&iqb> zn)#)5-ioY?n4beKv_kydE;iM@8tAEjRp@*C)~UD~-Rf(C)r)`MBrB89<5`cK$=_Px z#BRabrB7S}k|4sS)Ij`QX$ zw|iN9zBQ^h3R`O-^$3Y#AV0VrWPf``DArU*=a)-J8TtE*RBFio@(-25D>(s~_W30l z?ChAU4I3TJ=_qF6&3#*pT;5vZtM9fp21cSJsG~v`J@Cta$BVO`;UYFC&G;GV3G2EL z?TQ#)dXP?Q6`oD1u+^9!Tfj)8Xor)MMeoY0MB{%l)o}@~evA1fKJhZJ%l{0UT)RS? z@V(rP{Umk`hQNNwTdb_T-Vm?GA$RtB(=~KE$f<7SQnf=YDca3KT}tRTAXQm!s{PU> zaL*DW0RBgg!U_YP>}e=ZXEIClGRDik*X`e~^j_?p() zU)6BZSn>Vn#9{9)*g1GW7(namWtJAcMzP@!JqQ88P(CGz^ef(NO${1 z+RSQ%K4s_d+9>a#SnllKiKUA^p6?VM^p7DwQ?#7M>reMbRGKDxE4~h|{??BlclmVV zU9YR!yg|3Cue4)VSd!?0Wq5dJ?X>Ug7OqTqEB{1FjK5;Mr@;9&aV&9sEUs;^@`3u? zSrUhL!Vf9MqHmh@X$GYVnu%Vg<|X$nX_Ecrfh=)jVU23G{nAy*%IcxrW%gYtm@8cv zdwVwT4Iy2)I3vL+;ZHD)Z~rg!^HJf|FBe+pQ1R5U4MXgX&o1IuXdn7`&uSkc^xPtF zIL98G`%&#S3LEKSln~3sxG-rfnxv5r1&VY3i7Vt8>L1H}m%ASCkSRI2cqjRrKP%1m zoz1Y%;*4`~K@EFwYdc|(w549NFOfyfW*hapq|X~UX&f?!a#RLtUS(|=k(Uk&G>E5dfix%2%dJU@cTM3mT_IdA{VDA*VJ}kiPdoYpw&=4sRGi?~ z$ly1A3gBBO_D4p@D?=1MsBPlUj*@A&~UYruGi<=ihU}L^0QhGg)V}9* zW;Um9ms>vX34CR_P^A12+S23&mO$<}-aNc~fEE%6-}FKz<5E=C@Jx7i zGJ1UFR^X;ScSEwW_JMtV1H;=FAy1(gp<`^3O|nX1FAPIWM;B0zxCo~DyaTo>X~TU- zXE2BGShK%ivZ2MUgKR(>jRdk zhuLzZN*F41X6RI0oW2axz5P`&9f4ZKhD^?Fs%z5+|EP2S9>jXlc6j0eDT=3RY8 z{yl|y_M)(l4#mqOl}kTHC)0zJ(S};yV%+J=)9poj5b+2uNMu)7+K=Dv{AmTyeZmxjgE zy3@-F%R8zpS>~OR+wkDC?p+g@{nvKNm3;`t1RWgYw6N^lb*()EcSq0s6xr7y{3@|7 zFL$y}`{yfI|BQTsjn;iuO#C<4!pNZRRoJ(jOiuNBJvMF^-a>?75&oK5ETDI=~da2Qz&ARLl**xhXJ7%mYeEK{>iU77`4q zIyqgh03!sW&Z`20b=D?-(1V#O0cTwRStRghA*+dICmN6uEhg!N%;h-)#{TueVLu*7 z30ZL*YIhnY15FOoY?yMVhL-TneuU{(r6SRvyr_haj~65}wP(AD-}+@quVYAWAroL9-q$=Boxw_NO+l~7@vs9>6n|)QSkPLQY)9PrN+5S zMI0vlizR)iJ|eYRrq^_N0cOg6ZjY1MoaJI88#iiOAT8M@0#_8V%6m(Ib$<|3mj~!+IFLD zOxH?9Bd?6)-QFt`e~gOKG%bSK^91?_5uQ9BF+TH{00Tzd9~<+p30}cf=-iW~P1HW! z@3z-%+yuhT>_ z8qit?(6#O-ama1XnpwIaDl-~t;WxLy<7605t)y7}XHZ#mw8jNvg9rQGcNFfBskAxV zucs2WuX-cU#uHy`PRChtGK%WPzJG$t_Z1xTYqLnGPqW145JB<$FT|1+PgsA=kg!37 zXDcl{`MLq9tWZAa6eN2_d=(p@(Aj~-OY?cwm>z-qc*m$>i0_GSDRUjTq{De%A&cl768_*XyDC z6OL|XdTyIOfh<&Jmr1wW;!!5K2?p_SD)FR80kwr#(5I5<%jAyhHSh{qy=h%|^4qypEp&aHor zx3>3UdX(6%fe|7nrkk^;6X(uasZ4H?n$q(HyRp{~wF|y?&2ve(Yhp6hMb497I*;Ko zq2{Z79Oqs(AamAtl?WktEX5B+he`q*sL06(w2zir#j9eiUn^^e8$ndqC{Uqx-K>tl9aCaQE!c z|M&ObLXqo6zzbu3DQZ%6uFA(>e$TqZnmx-Xp`X7E3Q#^QQdyk0Aq#3k-(V+NpAq|r z#wXi62!8zo-~tCr-3bT~$${h0UT~n;pohgn-jVXE?)VnPk>6Flb&F`z+G(gRYZM`A z#e9OE*zrHgTo8-Lur_-$fZ2y{q{PM@Bd`6C(r-4XqA*0>S&-~dUDedO?IGGzg5CcK z?1AC%jQ`<5I$sdl$>WIsPUufbg>|?SEeG#ClTailV2+S{aMXwW%o8V9*Ph&2_6J1y z<&|Plb?)%KyF)!R_w)lhR#;0;;nXDsOi?dp$UqcZ1z25lUkI^3 z>xld!U>d*LA`|yRHND#^`(=P~jYu?F)}OcHF8H$3KOX0l@pkkHUO8apTT5+c`@vDJ zla?4fd2EHZ<0bvC%a?M8mhGU~n_{)&zdpe)XyocFlB}R*CRO1zpUe3ELllHyD}B7t zFZH&Wf=C|L(`Gky_P4?HrAc^h2!k>hG3lYcBNDXE&tLw2zF-mf0ru#9%K|s3O#u!~ z&NAi6y|+5--Um}2SIJ2 zpj>EPyAS57&kEAe4XPB#GK1Xt`3XTF@F<=@j(OGV>YPiAR6=EPCmRRa;+3`KL|D=e zXd+^WSU2?PAm%q}P#+_xE{aa{*?N6k>>1d#r9LC3MJBupZ+hCaFsJb`MeH!gc{`dJ z76~U`Y*i_&)yj~0KK~#Ee{(}9`GjFLD4-FOe(kCRy}3Z>P%PYP1%Bf7E3Ea4w{|B{ z*(=24ZV&%C-_eP%bn9dGiM=fFWbJuW6~D<7DfbbZn*+?puzqboIoFfpZnJLAdb#7f%#Z`ZCVcr>srWF@jG@WAoyc&V(4 zHX3rR^T*uSpDna8i=_l_ic%~Uwb8y?p%&ut^jbtA24l0Q8oFdM-Wp%KZRR8=^!*cZ!VQH$xi zRhdoC*Mj1Zsv-U)-UCW#w&5v99CmRcXq|F5vETA;VeS=2X_w99lAt{ykUUwBk>d_*Re*J~c5 zc;%g8YcQNRT&?d+JoQ46cJ+k6E9HN%llqu?cdeEV?OhJQ*N!{K((c|-I>C%6MBmApxlZLZC_hYbU^AV!&O>e+MUwr(I$hg84T6xiT5l~fo z&M6xDcmlBDb)m!Ss#eF8(AxB6BY8!Sw-Z?VrgX>qUqe3Abv@xeB}otEea!~zx53*R zs^DFp{~7lkkXo^1^j)EsD$KRX2F{ASUS+8K##M8|1iwtyuy@=YGx5Crraku9ycDF7 z?oU~XAdYTJc$ec({O3V+G0|BMD;%fS61kKu`F9nIbWe)ARw_dw+=nv%vkktiYqMJ` z5&Uq@PzaO2T7I&;+(&Ya@Bpg4qmWX(Iq*p(J;eg z+2?0^D?o3BRnV0{DWLzVH6P;|^=2?x;X7h@$Q#dFh*nO`yTA|AuC+0}%&B!UWIBFl znf$;GRrgz90?OasI=8zCTB{{f6<}oC>Vp4I3&08okG-=`D0RZtDCx-Wat4cIH_$^( z5&0cPf#7rdvhmC076te`furulS$^yzScUqtBuFm-?8SvY)Hxq>6a&weM{n9QQaV>|Ep$7+)ydC<($ zlK{Twfls^xd+qJp<{aMh0=d=Q_z|pXVtGC>Pz6oh94Slb?+&tDrn8^1SDFaU^)Q?t z<7A*SYFEgBwT6bNKhHzOnn`iFh?EQ702H-->^qiecdC^-yqTF;-L#L(HI6IRd6t!N zY{?%0blR6WrCd`JXXyCN`>-?ayt z1V=d%tlOY)*jSMiq_mv0FDav`AC$Y|z^bN*--|eAB*?8B8M5L^TL%4VCG;C?^I!hs zjEd32qDc)ZS`%?mJp&N*j@%-hQk)%t=lX-`C0PD8Qu!nKooBp4cz<@FQ5khATBC)P z5a}?=F-8O4yS!JPP>bn$_kHIs`L;Z13gxXxqDUu|oZ9tyIj5d4v(UGC5(zgbmm$pp z1JYgBwy}+`lA(NQ~n~mF@FeJ;pufQ==y#e)>}rOzw30SF?(IvN7X*-#78aCOofqf6nbvTG~vRHBu-8w23|wMM6p z_$lHZcDRo6`{xIw;M534jUUQL-9^}COliY3$>*~za9*_uMwMnFfUavS5{<%u@~Hq* z`=m8F_JB(*v8*c^Q6xx)8pwnxarU#{$3reg;dpZj8MB`7?hN@ppG>Nz`u>*XKy^W8 zpb!VZAd~r+md0?N^regz_O$V{%EGl*OiLE1=L7h>5$5Inb0O3MC3aQ<)cJOP+CzP8 zhnRiplOhQ^Wu0^6zk3zJi~BYuPy}EKHnhS=<V*krV|kTX4I10p=yb9*OTXAp{_G{0}?>SSFW6A5OZt9)P9EssI=7&N%Qajh-=l23N762|62`jS>@i_ z=3Zk{zh3W`tp?kpMBMnGzDhOidh2{pyART*0l+ zzjI_aHd}cPUD@=~(ZH)9Ir<;kU_kI>sVh-@?c4Pi)xx`^utOh`Z!A&Ok7T86c=C)G z07PA`yztmRO^Inpu0i<@E=A%!-vDI&*P>IMDS!{G8dQi-V*t(Urm5EbIetKeO(W6-ve)xpS#hihRqstHEfRK`uBux3uPDyy zAohm4=BLuNEsh?wdD*U#nb{8oe?EcIIC7j|z~1+|QLyr08F;6q{mujcJLww_zVuJG z-q;R5H8?rR{8Tv9HxV%yfd@ePZj|0U=^K9CZ&A;#)ELl3b5O|9Q4$tn~%~d45nyTCApK_-u^O; zgLZDnX10~hbPJ|gzCBOjTlN35S&jsd@bZKINcGxQwsz$^kE&Cttoa`pfJ_y-KCOY? zjy$>6oGzP5dssxy2(o(Y*6TzwJ+4rzQCKHyPKG{vh5$CTnlpYl-nqc)E!Iw~jU*|> z@a8%3QJ>X&b>0@wkus^Z z1%iJb*zWl|wpgRWnSGY*zWoRl1jYma*qJ!I(twBR%aOD=P2aR77iI5+Z6DH42Mn1f z^o^q?tFAJ&aXl3ufPTzfs0QGv1vrQ?ua23IGr95}1L544JVpitzt-p}KbCjosiqPz zqTiMKj$=SqXy$oaQ-^I}$LsTO>69GG$-w zZBKNF{4C4i;*Ob~`-6QO%|3 zE~%uc`=6qNL=?f2gmToq__0(ZEQTHlu)Ni4rQkqerf7wBtvOUrUwT^|mJA4S+I%Y| z{WOy987zLUQ7s*;UC|VLj3=0FaS*0rsxyY&$A<6D>WbBOyeV6#;a1*S$ndQrNNy~@ zqJ8dIg2>agGjaS{S09+`LZH*lnMI}ty8G)VAkMrH2j(ny;8I*Igi|5r>{-%6$-3?N z8bpug(tAMw#Mv6lci(3v>9N-5lcN|7F78Dzda8$_y%tqHEIIs<`y{{t7S$eMTt}|7 zf0fKr!tg*Aot}`e#b{*wT}LD?DrSisZ~Ut;bsYQGhMq*PtF84IXZBpSc34rqQEMrx z_IE21@?C7?p{#8kz>6Uk-9Q8mpujhe#C!D6dRi-$CngLQa1@Ea`CCgTi?6UPdELr< zK#CrhN7QDLA`^$b2|c(k%N6x6hbD5Tyu7L}j@$0V=J5~5v0izh=p;{~jPgN#j`M$4 zn7<~)Q$8@JHHzuum&jlYMtJ4#UoFDxQYJ~Dp(&QO73%a%A&%uN%2NWEEmc4UpAWq0 zh5$c4LCl$X1tA6rVJ#Ki)HX(vfTVD~}5-8}P$y`*Z zG61YdK@H9C-7{rb=XaMbVAMelz>8R*(u2|@YW~h{>t2T6P%~QufZv$ZiN@m-dt8NZ z;sq2*4^eU5a;15qvQwTjLV^~*kAvRv9xb03;pqsJOUGEx{tO3i>>gJ<93MR~gR`A^_BR<_75TFJRltc^fc`eZ3B0V-QGG(pC)$Gks4)zS@s~RjcIc6i1W)X2WqO1A3=C+tnT-ZC~F#ruf8Nf7-v3XWv zLM}EF{bIg%JVkroTz`gU(Q$)qQ|)xhawOpU=2Y(;Ht|?itkIZ^%_-P=MmG$a8CD&g zq8w38pZuAWgihi)#l7q}$YdKg3Xz=C&B({1_3DRe*|=|7URWOMXU)(_Wvr^UV6?f4 zOfKUr<{!k>-a8^%T7}&7$_S2*&t@XWtcvHTfKSQ3Vuy$5duJyh;Dwf-_4)9B#zWvE zhUWeNsN96p=!RbJVtp0}EyNW8P~3Tu<0SScDKiSL+juZO$nla;3^!LO0X(r2HtJ_< zecLP0rGE-KJB7M%G0JFi8m{UwCia_(!J#({PGesD=Iof4g17c%8qSL`SBTjNa;P+) z`^){m7jMclZVp&`q%!Dyh2#k1pf=5ByR8`GNRL#RU{^1KhDx9fEBy$#5at;|mU07L zvhmaH@i+MdS?Zra%!kJRlM|urUt8QQepgvd#F-7Tr4P?>@tnU$%y*ENtb6hPO;N#! z{vB5Jk0B}A5S!xeBj5=`UoZq!lJ&d$)XoKC3rgR_BwrXV*%JXYbW619^t!`P`znQcQnM|&Z_ppyYE8EdYQ$D7Zv!qVRQE64r*P?I8J@9=dAYBK z7HB_rHHZ*!;yIpf4ySC-*uj7$?|K@WOnVqMGvGMJ_Blt~+&H9|wd%Aso<0}JWy znLCA_#UFWUf1k4JEWKVRZ#>KQdfo16J!!>v`)`l!>6;tC2w)8X`MlB?B!cdlirALO zSD3dHga$bJ+tpT9rT2pl#I$U-$b*~Md|X9mcItnOxa=ZJj$nYbEJfwF)g701#be2l zHoUbHR7_hx_zRAYuyj&Z71?|^)lMR?FZ_)tf$B`qQ#7Y*ejfNpz5dnG8kX>0I1k{t zn$7|o_2xv^OIleeG-_(G)HlxMI2-bwsiEIN2=@DDQ6YgH|7l`UG`?Vxow?lS@>nbo zc`e`*cJ6{7#&ZuTRyDa`&0I6$)r@N{&gNu9qXM-O#DZ~%0OEXK|Mh5|gI-Op7yS^R%j85Uv12>nb1gq~3>{XrRv`&uOwh+!_SnYY6~rI(6);o+ zcUcjS_n+UGh%HBKuJ}#k8_2nV!d+*vuC zsuRO5YK4yctEu+f7cRhEdW_g0S{HV_Pf`+UIb`W)H-gp2dQg1VMd>;I(%^FnzTPRO z^I~pmxGu^>oV62IkEy(cFwS}~LfLfzj!VW_H^qGb^p_6lO8ai9jc`}HpR>cF`~K{I zdW0Qo$bD`v`11m28cUh-hg_HEQ;wYEUl1sTD|d8OQ3Wnj(;;>SAAsjay?!2$=lbq2 zzrbSVCRg8S%^&jRdzbA4jGj1vtEjbNB7=}e0iqE<0UO@lX2#QbAGjD@3Bn}<+B7B> z-%J$|CEj*76@IPtTHj))5XScW(DD*TJSM@*Y2M!5XYjGmR$0pKg%x*4TbqSFlQ>wI z9?;fy8iBXZs-f-`FT`;N?DDK6*@LJEQfK>`yPhK`i#L&E>0Z^%U>m^LE~Q&#BD8qZ=SJKnaVa(~66p6FBc2vRXK>-FAy@ z(71Z^c#%L>nSljZeQ!E=9X-Q-QX#Du()@*I!-5@u)jW~7XJPMyNoni$e z=4uXg3u^NGDk8UOy}v{*u`@wh|Gv&qlJ>(7K+tEvB^;>S9;Me|8h{fe5xp}tMBHjP@o`i4+r}(D0GoELGNPHW zf#QftIjaUiQEpSC(<6v$+veYI4Ee_epy%x)6~T{h@Lmw@FF__AtvN?2!508H(lhKJ zl4I(4z_sOuLh#sni@nm2q>h4?VX|;XL2^SDhw)4Ibz|858ms6(2p&`|{q2w!B0m=& zRG(U!{OlR#Oy*_AQR~djPitux!9;N_2 zpG|qLw6|pVEo!XF7O*9I6u^Z^wI|!T>OI>iD&zL2mBeC;KrfC}mIlIbdT&oCk<}z2 zRn6T!r4FdBy2fbxh|$59%p4dN3uuK`ahY}8BGUmXl$mGjS##tj#!~-1E4Aegj zX~D@|RiB!#C23&a+0D4wkb-CSU@O**)np&&8{~cS69NH52^OzYk?do#na2A0BB{=j zhYTmB8ti4yF|J~cF4*~y4Sqm#ju5#mOyixJr2gB+TvCUG2}X2}t+XFF(DaP7T?R7SiLa;5 z9Nd|ld{@q-Se%o6VkV!&WJLMoNagR;Q)oHRdyKsg?qSP{0RrGWV!tipG9xRh+!l(jVsShd99Q6%Mym zG(xA6beJ=aShnl6`RA?~22gI39Vw=sNHG>0vEHe(JcGsW((18NkMfYO_Z^%nG-2}e zZ64?36Lnr#)M_j!pD6Mf{#<`!5uwsaWBT#Y+z_lxDb59*L4c`#fGGrPs^KUwTG(b( zq$`qHU)5ggzx%5WQcR7NM5i5lHB`Iu#l&=7lkwhJX7_co2<9~5!MU?7Zk$P#9#Lv~ zU`EC)>u;tMr$q$>|3X=hsK>^fN(v)-e647cof^Z~aB-uCzJGZF@N16UiKUm=_MaFHQsf8;8D>O0uEq5NtSoAVg@`+Lgi;wQI5m{XOUXuL1vY*GrHu> z_C=tihgp>(#o<2?CJNj&>SZ_~+Y|^WH@9>#;&ld5GiK8N>V=m3-+%0r;>{ehfQk*p zS~~2L5?hc@NjvZT@h4F1mHhE%K3A+`_NAeHeCiS_*z+5BXE`!&LQ#@}CL6GDd z1R+o_+Sw%NoV~n#z~8!u8+_<6w7k_64p~zR`xbhsiU7@gOf0&Di!HPFCG#%13DO%Z z2XdM8n=6_-zCP$8yQ*_R2RVwodXQG&Qy+6>sC0ZqI;CKM8}%a3QaphrZnXl2JiTSQ zSUqpu57ozQQmlh)HKfYRW)r~LJ|2=s4#)mFCR&=p1p#7G8%_gU)zO8etfU4w%gyjEy*KpqXPVcb30jsMHG0c{X${&?h1O=ZoW+cTA?kpOE<9FdHueKHwMc zP500pDio^BW2+PN&Z6e5RLuvQr}OGWJbzz+N@uqn<;PIBm4<#u)1-al$2#sIe;mnZ z1obYf9pqLxFqSpGACWvhIgGHz5(hC;ltnv*Pi%P)SrU*(YnAJ042j$w7q2ra#T@iM21C{ca4ae`Ub_t7?y_8&5+@K>8$-{HV` zPOI?}No~t@RZ3za+9z82%OL8?_&tXXJ%%9w_@o~p6s_PV!OkNN670pWbHi{wPg>5M zim^O|^7MEom7SBytD-MQ+ZmE=v$IxnhF($PKyarbHbjuFcc{3XZGF4IgvwvLj*j|p z{bvqQk+Okq`12G?G_)mrl7ArYicG7Bm0OnO$BKi^r>90BOHu56x`Aeas-XlNZd%1o z(}~OrT@I1dt3}H_$S~85-ak=FHMDsq4J!Y>dG*=q&o;Fl$xY_;x~^UjV-+|M$4ph4 z(Aj{G_`C@S%5M>gDRp-t`R#mNQuwtV+u}LLavTVgDTJ%pz~xIJYR8>n4BPLn$L3Q*e>@S~Ejy2>D?T2=ScUMzNc>y%lDoFQ26qv zA6|NIEN`$R&<7+FVc&Xy=Yb(JmoJiSfL6Qqx2klSYI)De!>3nGwM@_eQ)z(`T{7+a zk6m9iFtGC_F-psHs61oM+~rP4s>P-b5xHiJ*0ZwtTe1P0HVHXH>Se80Y}>_*@7 z1Y-E@cd3#5QwLHZHn}=e%$mGKkH8^8{0*1?j^7BoY|kk0ynhm~v#d!E=*-i|x8eSh zeeC5JP=SymN|z0*6z-t{Y+dxUTZjo@C6*YbF4d<3_}hfZJW~#zqg0qYW8MOSIhzS(d|lB-sPQ869=ZM!Gb_oWl{R-8 zdJSrx0Z6@mqQPWsyVSB<4A0Tu6t8E-kx5FJkzAZ~41&aeX@Nq<&5#$ey$1GPwnTk* zzD$AcYo4j=1eH?EHVLOBW4^A)GcRSU3`ZR}qq<*jM!22>K%LAoDHj(J1Rt1x0KX(z z;6MuLhIq^%dwPR<`0Qf18n|K#`7s8Y}0yOS;grwel-jy=B;Zx8t?QziXt)hJwAJz_*nndcMjbzw_IVDXI;V+6;wKs}lyhx!xc=$PDHOtLu z_zG!i+N7ErX*&`eqHk7xEuy>_p=mn3Nu%_=eag6id@E!WzCH(Uy%r{-#M4pdO_RKX7X95FxiNoU->uL zoSpw4^)afLuM)y{Z5w5^FD7le@r&2Dc(K&DT~vHvXFXAr?^daQb2KtMv8_>IH!%7E zEeH;5P^{m5=YqgEoOJtQ7PK6SA2i>xN5s-R_kUUd0>@5M1!oNLi{pZIiyY$hKDL&0 zSU@Bbv&$NhN3H*~TbTW=x0&r{<_hY*AWpMMQ*1HH0*f**05Q9d{j2H;?(tL3R2iW9 z2X)3lWN1GJ80?x4Z~4ZaJVWG`I)dk zpFVzj4{6$(PrbMKV<_K=llPu-P?%ZSlMb2P$=;I6?jl~b4@(U4+M@l_p%-7^BSi6Y0Ts{0u+w()<4f(K;^0 z7W)8edvCw{XfMSJ`#N#pWS>V~l)8(5&MiC^)VlZcS^8iTh*O$)`_Bfaf*73i(k($D z4zT)?&8OTN%qZ8EMJV&8W4?@nqVA(1RbUvI5Dy^ABaZ;T(phLe<(EzeN>NiGw~Y<+ zbN{_~2IMSAg)DmOuTXERay1G?SZWAjF@kF@n&qmUQt6%Hm&s(@??J77Q1Ju*;AjrR zX=}Y9n=|p-U8iueYuK(nZ|&L5IV`BP8-8GW1>*g9ofqJ4^l=K|vOo-Ay*SUaajw~m z6So1WJqRydN^!z(5E)Y+C|Y>Wb^$5IBH(}+kj+ITfTXh z9C9ICu!blckVdrrE;+c0S*y>7U))rW8vE@%)fEXiukg!cGSdC{VjSTu>Jh;|Fp|Sa zItKz^WGsirUOA3a-6PyJx^2&THq14u7Dug1R!rvrWm+b}`>E;^r4Cl~#2MUJSx@!_ zP$`Lb_kfd;;2P9Y3=cr6S)n(`>V-&_)s4~hiERF5kQbQ$JNi`wqAb(>>T@RnYU?kS z?`!>;w5&c6ATh(h()re=uteNIh2k&|@!G=9|HXW)h~V!V&f4es6Z8(H@zlnu}>Kadny;dd}tBw1{ zlPiHbQbF2^L8Y2P2AMAE#~4v;lMgnM2NX{c5=wglW%iNrp9u&^?>y>_c<4cOWy+^H zf;F^$0gy<)qIwhhyy|#qR>K77)+vc5es&A`hp|4xzc%Er8v)jHkC=2a19C}ULXk{5EF=H1(zU($$86i zSm{?`ijZjU8)Ibr8-b`y0DS}DUxf^KPdxdq;wvIl(^wf;2*~n!ol;&Yo&ONL0_idpqE6c^AAE zmIPc6fW@`W3vlD@uc8HK+C<97geeodsv{Z%qR1;Tw4=C3A6xa(ZO0w($@03TMX-paPT1j)G7`V@onTVjUTWM zL>M4m{=y6|mZQDQh3?O;|-UUiloF(4^ z^Lr<~__II~ZK$r#f-D5;LZcj)pUa!*JS%Otfk|n5Xk~&F<1Rp(7?8>2a5jP$FmtN? zY=gqOS|L%EW<(u+APzoVE8H0AVE`>UL0i0i0E5zZSleqa)oQBPb}*`{!4Wg=w{_CwTo!>*^ap zTD=pnEp+7s8UG@nAy>kr6rJ7SUxQShilaKfB8jfK^qm`RAo!_JSr$!EvOB7?B1FvM zM;-(T`zb%GE>>(Iu=`1n^8gIPaLdZt?tb4;2K^&h&e}sYXLhbh=><3`!nJ`Y;coMW z_tgBu{GnV^nTqtN{OO)y=*0wbo4y>ZF731G*LPvihd6^Z&L z)TUX|8KVfxw)(pVy7U_T!3>8st#a`(YC6UIH<>WB$%7I>*gFLLZ|iR+XG)yqe9?LV z&PA|sVtUmyAPR}<()ol3CVN%A*?KS+Fn4;i7v`QiJE+>feA33=xTHuY0+BW`)rBir3+iwgLhI2un!0&s5Vth=#wL%2WS$@}48#3+2ZZ{6f{3GAeHnxY6XOLq z;hd+>dIB0RifseA1%kTzAq__6AU#b*^YGwT|m!c}$$)N}(ng@t9;kt-<5eeYP2^mY2(AOd<6Qn2}fC20k z!;YNlK{mqqXA9;`6%I`ZD4{KQNJWuiG?pvqd{beKSa&n!Zv8C=Jw+1&V8JYFZmV*I zML$KlB%NEXX*+cdf`*0{?OIdtE(c)Uv1xA97&o8Frn7maF;!tMiWJ2IFbUm5D1Q~{ zbQZ@t`>=3sxc3)UmL)5SbcZGY0JyMu znw|iFNc3JDC%!=OVYwm96h#p>IGV*+CXcdY9JxG2&Xi77bjd>wcmA+3yz4DxqTC-> z_Oy9@<=u^70~FJM0*~BsY1I$#tHG~x%eA1@xc!>cQG^g+@w{j+@;%ns&mEWVNSP5U z$`qg|1aRQtJBOM!-qi}A#R;PnIINhv1d*l9)A0C*wTQ3WzHHx)w(;gaFWbvPW^i4rx2^@SWp64-hEsnSeXN5C8zYar8_i zMN69ly!7`8$lmCjWU4@~U=CJ``RtSJOr{F3?62w-39X(@~nY#(2(JSpOfLCEuB z$gchE(VhvIT8UW!Hw1-|$>NMCo=fPBO(Dwysa?S79xx6paApjg0Psa7t+| zpU0j<(!+v*Wc89+03xz`G`kt4ptroR(+72e2s)$G<4xQ@D;CX+l<$q!vq)#8I{+g# z)`}uUTLaj-b#tz1<6SQSxZVk)0QMZ}#2?&nQD}Y37tKT}5koqgk3a(K;g1uU=lqcB ziUe9_)?g%^L*8_c!)50AaOd}qYwtWCPndLY)(5}>HN*8u?3RwQwze-xd);g5fd zGP{m6Cy(&VL$Q_w9Z4o)SQ7CL!0tmGs`|XNb?fHhyxbARe=*25Fa!VqzuR-nIvy${ zwo-{0mbFaB&I27qtls%6-ci`$WaWZ-tY~QfplV|e#8zZq|xSShLpP)+1v!4!}tod@^NDd|iR<=yV!FG1As`^WF zPC?j0b=(@s4bau8IJy#X18Xjr6+suUV}Cow($f5YptZWCXbXTqkv#zW@x&*^@NK(} zM$%VRp2W(<(H2ldDORWIL;WJ4^;5%-TQ5VJc?Y9Gi2HF?>nHl-@JwsxpnWZhoqVbb z6dx;w2EbbD9kMOYk+*nJ%XCykd=bd=YRo)|hi)79`v6%f`pZEH@B{_^fDPNR?*s;i z(}7O=5#{QY^RZ*UunwR%nG9qF}iDKW1W8Vef zphR@goXtXBfC8Kp63Ud9L#Y~#iu_s~Y-Iz7kAI!wii>6kN+-(DXa+m>os>%@^zldv zeWd6Fkc%6S#e|1aM#i#uanEtQ>&iu;^<8=KTzvNz+mUwmLq}Lq)I&2IV}#d3VZZyD z4-GSYa3*TohCsGNL8|ofoHA zd@R~NKE3By$lEAO#@$=lVdbU0Co!lvTHlPtwnS3sBSoD80Kkcd?`#LaJ*Yq_#gi`` zilnb17022Y^K3;v3AJ;>uq760uBz3p9o=#|MVR_}g4M2A$AZfxGoWX!zgw2V~%tU7sn+70YI$Ea>@vf}ST|Is{Wz{+f-gqDs*Tz?O%^fakU!!Jy*( zW6h51K?uYP2}vYj$A%{2x>fUaCHTs~NCwaCP#*RpVX7E1F$55!KNW+9M>BYSm-4=5 zL0Q_=fO#{gDC45A^SJd0t}Yj>2n#b744Q!nFA{`x;at2G5yZg9UTK9c|~x@Ae!=zv?YO3QQG4CWHXm z9{JLKD5e2hUO1r5Q)sB(c$w@4gv7XZVCY|-5k^PAdg$`|1yDXrJ8)ISO@Q04D1~cB zcCN3Q68O#0t;=L$MK?jgdfK}WigfDs5AG=jF%<7u9nJa!z?R<~Qq^aVjj5u`L`?xe zKpqssp5Jj4on7JI7XSb^T($s}<<>0~omJHjEM3Q|%Nn;;P?3rvziPFdANv$C7DQ(7 zt2AHFVv_-nOX`MshF@X*nQNqAub-;ZyT66LRd`_%d1%t<^XeoRlT%QXmnCq`%6VE% ze$vrBglD%Omi1B@OU06jBLKtri5NDP$>7m#dN06FDpGOWc-evwvvGeJ?-G-m4^*Q_ zVTqvfE&>jI`3qxhgVjIEk>G4zv0&H0J3nlI`C5N=y+HJ6D4!l;RDpgH+s{9x7&5*) z2I)srCd7Ug4GbY6S2PTjJS|9!VVFF0ImVWWf-biNz0KlfJ7jv(` z@)xZy5VC`V4#2`%<+2Ot_5p?8G@#h?{~%;RT|IUU8Sn?S{*DFbIF3sqkfiJq2z8Qs z%GXO2uP1OnTVe5-f%R9kDB=rC-lX{XGx8~41bDIa(a+0nL6Ky_2mk;m==a62W9_|o zMX|{th)k=k@NNlO1;DRE#E`HZ%7>^Y9fitq45Aj;V1dIbVJsjEnu2u~^%l!#QkLLa zb8mjlUyBQL7TkrbKlnCNlY$+mYdf|MtS6!?mnmCJrwUVrU%qrUrc_0H`|tKQ+R=Kv zOD;=+p<>E}62NFK@lycY6Ah+m;-ROapJ(`icPzC}oK-|>%=>s2^Rxq{P&&n+ldPDe zTMu>@0+de^#GRRl`9G(h`OowC8L(4^$n0>P^#_xOFTjt);pUf}0+Efxe0uWbbQf6v zA%x)FYZof(<}0>}pFXu0`Mh+D{(z0436lvWfYVRjF#R2YQ1bkXxtX^^Ra&8(BlMW}$l3|hW~2o>*(p{_d5r*N3*9@<6T!Q)Idgd-07hB&zU;nTo zhI}=$(hxE!1kiTb*rSv{x|Q+3V>{8Ockj}PdU_u^yX0fP2*o|vyt$|cf9Pa#2mnB?D)~bI{Ebd(c>&#h(H1&m2Em`-atW+g zQt@%JWwt{Fq7S=>y>msJuq2|$XQ4>ynr__;Y8p9(*v_pSEqcZdMx;z zywpiU`D=5piHg=6j6S9gRLHBTr+;7h83jJQK?pv!aXCofjcL4e^$p`kzuu|px2{}e z>W7h(MvzG(fRhh>b_|qCYXLp|!}zaXzZzNZl6m#ma3z2Lwa{8`p>+V(O|^c9TW#hB zny6RE#FLOXIC<+NPX*+d%vfU4lysLIb+xOH&BSCsXw& zuMB9IQi1nfyExKV1%T5%L-_GyJ5~8R)3NlwMpPR~CY1o%AK5$tkS~kj{ez?U;Um$# zfbwJp`vZ#N3}upF8>2t)7pITBb#8 zE#Sz>9z5`<^5wqm7|DDuvf_v`=>*WWb@LcN{y_{MO=ofM{nCD+Q1a1@D=>SSwH=d( zqzv=0^FCE#jzF|z!B}ucA8WNJ#5(bVUX@;+&sY+R#So9jeLt~SO!*0#UTg{vhb+l3 z;1R*^TpJ61TL~4?OJvA%Xh7@lB?xmzh8K#)hAQ0l&I`3E3JIkY|NP+ZFgz+h@=FN* z!NSJmO3_aR4kB%pEk9@)KJr%pxR(Y#`}-q!Zf6^=UELJvbnjFmhEIL)3jEE#{RTPH zw0en9I7jVT1B38W1yJiQ0w_Sex8QmS2=mttwe=DZ=LbMIb#8u2nb&jKL|~oHOF{qt z_P#tij_OYLtKQTtwRTy%ZP}7+8Dz`y2Hs*E+ZmW(NFX=^GXn{e$xH$XNoM8+XC`kZ zyz>Hi zUdFKc2{Du*zd#fd0w{F-#h#OP>n|d_f(OP6naOCtSMO|v#iac+Up$VT?L*sf<}?52 zdUET3m3GG&VD>`*0C;Inln^{3E)GW`_@Dpx9Rx#?x1v;INp&&q{Y*0$5f^V9fyG1y2!xUYE@}C;FWLzllw?MS>83@RUr0{K;fua^DlJ z>E%V3vfT;IM2_DF#N}7;w1bU&T#+5trk548KufjE zarws9CHU39+@)a%FbZ*#IXLRZ_ntly|Edrtu;?5owA#Y5qy-jx4T9zAHdI@P`bjRE zYzM~jiS3}rfTv~=rH&$V2+=C$XRi?xz%ovA|_BPVN(#ir{?4QKx&M|oW zp;#eF%9eomi6SZa1y2w%$uAOT$#PO}L9U#Lm*vA2B*lRa^=vsteDD6vs4U47?7J2Z zAQFk9h(dw)&i3NvBkkC;^G5BR z5&#@fQ(l0FzOWuY`OR_4GV=*Rev*;Qi=SboWT2o;yqLdKIl=){5-&#HL~WvtI3dXK zmWk&rfoxKc5X8#B;iK*`eAF|R3@_pvmW&pJLck>&iL^l^2x57lFZ?#@AP`XlHN=;A z0}S#Dye1-CmMRi%prBl8(FJWpdq=O|zyS|_wk0F-1HfzVT*5o2x>!~1d3k{+G=FU?>{x+KD{E6631#$Mb>TZd-UqMrwqKNR z+~+%f|1Z56tw0CNxc~rwtFx`YxN&C%fc12FAQ-}>E91C*YZHt{jp+)?nl;sx!R7Iz zYtShP17wqVh}S{J<(DBfM4^PZk2v#%V!`N#P72D20KJ?<1VqO|Q3wR&R~Hsg&c4Jf z3N!skN&Hw71@b5Q5z466ZU6uq$Vo&&RFYgAB7k0&-n?X$D(ins>q2~P>oUeZt4A;t z!9V`=05128Na{F{r+fGB{dW!B%^2o12}r|YGJF?6pSb+w`93`Jd*+i<$`GBcFW=sZ zwM|ut3at->2$E4^%R;DS6cSW)LXaS7xdiI~wYkZMeNH5Kg5)_^RbP&Kccl?_ zitxW*KZWC`ncw*9vzUzE&3FYmAuBup02se?I9R-F$4P+u5=36;nTtbM-@GW6)z<>b z3*a~o>zbe8WIQ&gTT{B z9v5N!FKVvnm>9nF9`6>GDW4iVxZc zaqFgK$g`yy28@RA!Z#TWxcSB!oa-8e)8iKsj&i%%_w%LXWrY%g`}s1V0NoGm@(b!! zh=5=}Mbmp6L|`H;Zz*k`4>)ti`A6 zstWPQJ?k@i$p-)&lhb(kM}NTBg!E+}j^Od0gL{9S(F$}%RtW$A)EDk~%V+oA3UD=a zx!dDIXa6K_-Mn1GtwBz5MVn1VY`CEsXF5jUo(?drcfx|pcmZ-2T{$5r^d7|d0I<9Z z7U3D$eK~Ic;$`V>k`v-8)gX)+I;>D4er#G)$4@hJ25j;Ry#j5nKi|G)fgRu4vmSXe zLwl*U{=rZLkN)gUoW3B5WC7qroiFg$y}d{0~=2*w|c)^WCFxdIE`dv=_iza5mXf2!U)UzzPGB?dW=z2#D>u zus>nBxK1Es(xy`HKu!&acvdbPpe=m7T!q!wN`BeAfC|^2kl3(Z-}&MO*fn{UY&`YS z3Hz;MU&5<9}Q?2zs@NOr5^dD`^ z?_Lt%zMg}7kE!fxR*{tg007R;w)Vn?U3QMcCc2zN3Eumt9~)LKMrEnWvDg`201yiY zHa6FyZ^Vt^agVGQz(K~+GZTUg;zHOD%X+640bxJ12nabZF9dWw`_QBx1P~EZG9i94 zd`Lxpl4!$vby~^wZ)qsU-+!((N6X z@JYjhbVNW(YT-01RG=-sSO=xy16{{17ZY7rM?~`gllbvsy~rwin@Vmxr$ zN@liSu>Fd)G4zkP@QoksgJYaAz8}XO>CIdE0NP(t>qyKxvRVMp{t_u``1Bi5j=K+l z;9X3QH-MAvL)dw919H`j%dU8_b6CBs5>|@|=ex%MLRo8tY`)r1KuCOgT7?LJEdtDt zfa+682$T+ok|@xS8;c-WEFz}@@e}nT^3(VEmB=r?l_-w;`E_vMaQF7*xMOo;%KcQs z*a?>p-+c0Qbo5?jRS%d5**Wotzh`~*BWsY|0svs5v&~!5xa$xB{t7@2T{q_R;6m>> zcHUGEi#gS<$a?`i8W)ygQAGjHc8tUZ6pmM#0Ucv@zuk;L)ev z#QSHN5BsDv+Ougd)CtWr-c%{^B~+R~Iw(NzGmg;q!;^{a+rz;dd{v zs-q-I?(RL1ok>42=7j(NfUC2uqo`rmC^+0MuIn8f$AH6wZEKe#z8cI%YA*l)^7C@A zwPg{8$2}Nwc#_0b849q)0TiKAivYEFV!JuX_6xlMi4f4uIeIM+^dtL#kV>R_GLVW2 zG!b9w8K@&ao%CC~yb@o(YYmF>1kPjH2nNG=?Abrz^`jrL!yv+a{mQ|;|EbH$W&!g? z006+*dH7^e;|?ppZK2BvfUbTAu1D+3y3x4(73P!s~anDv2OxXzMx_#z{&?mJp?30 zAo0Ic1nB++zlmKC?}UsHDD1nC{LIz@Fv&sR?h{QgLxD7jPq$4){>sukeDe!y(b7<^ zNz+o1|MBPE!HWk^G4J(r;Qpg`|D)doexPBEbB}o^0KgC6T>aJ8i-Hr&!Eu67g$W_J z)H90V2_LqsUMwFDXv}sMjk*^@+ zzT~a~wVm{iWsnPC#s0!cnzZqf6vR)qXOUm#nE^sjQeeY_x39wXHH%?1==YYpKM=-a z&mP6gZ=PXVegLoMF0AkBYQD1@du^*cF@e&m-znJ_60Nd9r##eT?pi1qNp^E#E#I!Gn@Bgw5umABJ zB2j580vzVNfzNmB|2OHEKIRGLtpETq5y)-Y`m&Lj)&r=g>j;3JfpN6=Ij>BX$U{yl}RxT|^*We`F(?Q4)T}cF_i95LmeB`|WBz_n|AlY5^{;InLNJB(> zRx(`rKtTK_HSy`?WywznYRmKScXzg8V{=W4yFY68B#8;P4||_FghR(JAj;T5LdX$+ zWa5s&wjcV^T5*;jUE$9vG~D(xOPHH}8Iay01b`bGtMQZXd=`r<#b3-x&kLXtjS?I^ zbp@}!bqUkHkbtOiBEU)v3LYC)yhD)eBRS8QPEIDpKm0$uPy7LwyP1#of{@57t_#0=V4~|!{F^@pPywAn*I9{cK>!Q@j85knIK8fYMdeL3 z;JDS|MpM&%y!l=S7FCy`p|*so$=nM7Vs92U*O%j_8>%to3u0(2IUx$V%u*-?0_3EHOx6XKRQ^jbgi8; z7;h3U5D12G_(Uf{5n%0#YU)`5eMA5N?70@KZ>qxDrYiUYAqTa^Zk|<_b(l{AAR#%UOi*9F_1inm`Q^`YKrR?qQd5Sw4s zU&gAJTKvkAJZx^R#m43uSS|WHr{YaJ*Exc{KRbx7zEQdM-oRx4*RCG_#bF3Z&Ht(q z0+ujj4d#mgs3JV8zGD6TtE^?qpXWG3C97j=jtLLnyA==I-U_qPsQVFNb=NU4fw#{N z;KPf<2!6w8%_AXy}5>DhvitW&pNGQ$DH>*Q_|(y74MgvD$?YhwjAudKzA zD)C(!eIVY{P&k6;51hqQFT4#ea}*zdkjPc<@VUQnoO$tlyhyF3S6iPI$aVqHZ1q|7 zMRl9X3zy&eG-ov5%vuS@aoD_ODgN!NJ5XC*I4dFm0KPyNr!QZ{2N#ENp?5rSznS@h zkDSG4g$C40#km4Gf~R{+P|d&~$^T;$MgxcCi%YO}StVB0FVJOJF4^r9%@2;c@z}49 z;H?ijWw$*V4xDg)eBcrH;QOOc=h|zT7kd(Oifk4DHRM(^ZvcRtoB~V5mWRJ<$jSeT z+zLyJ?f5TW+K$~@m%*UpTn z8D0gf*k3S30JF(}`kErFSyq9ThH}M$IQ0U(S9myZ%y`E5~qNwD(%!kGm{xmZzGf*b0}&{$iX zdcYPPL2pmrRTrLo?rpTae@W&wUVwmj1LHmadiDKh4k9xPFP_ip1YtG`fLfxbD-ed_ zWuK|bU$Xg0gV7>*n~+9<-G+zn-GaaPOfxKIeO*+o2Yt-tMc1Gc9Rri-8l1${iD?i5 zsSw0Fr^5ru`ow~e?ndz-{Hi!ql;onmrU>=bMOa#0gwg_Anw@3_p8TOu1ix)-$5Su7 z1*h99+ddizw!1t3_|ML+qx|?@nOJnSB9IlHK(-3NjFF!&GnP8#DV)@cKjXzGYm0mLR0D zF}XQrloi=fQ@{InU(c{lP z9SXUF(2vYomYdU@K(+~hUc^_uY}u+WY{;u_`EQ)T#Jo!Y0A{lZf3{@>9=hiyEKxbG zoEqqT@O^-5L+-5iM`v-aWEMTi8m&# z78A@y1MD_4@@!`0SuM!3nNg5uL1}>vWren^Ai5Yl#rv+h@bvFa;ElJ=BN&u_LzhIt zqn?42KN$b`5B#U6^(M2HHgo0$WRn1BCAe;$fwicvpse+tZjbX?%%x*cW+;TLXAUE$#$ztFV{X?o&a8b>mr_e z?E?&7om3<~AtW4h55DL)`TXT{~Cd3p;MW!ivlv zYt5FElMUz_apBdY7xD6;vltz7DO!90h=v0L(}Sn}ef;CsPGV+V_L=B+){rgNSk?*v z0E5YtV=7(qm%FVcOYY+gCVDUdChWOZ+`452?%vUimc|Mgl)h7-0@-qsW*gBc!P%}+ zyxjIN4!+ZlsT9dQ03gw@(>K=noAJ~Cc_0!A@=x-7*o2WTZEt4{6ma(${Ths9QEFKM~^PHXX^ zyTBP$4MkgM&&8*2Zp3FcF2jZ!s*z{2s9B7OY&mIqV%itN$%{kyf5$s<>+K{j1Wvq5rQ`!w20a?6_U++{7U z`y6Mol&jiFg8`)_`PjLk0iV990qd4m!Ir^DH`#OowZiLwsfoO7>ZO zF-SNt>YwP|KX!iKp@?5Xd_X(t^s@YV^f_zDrt3!wLA4N7O&hh#vX?Vj3}rX&-fAmf z@j0U-uZ zP9e(v;06Th0eWJXuep^#s zP3tam-hwS0XHflc00+R9YsHey-oA{?h@DOw)-rmF&8(9j$d2nojM*e} zx@}UH*E10bc)I-)T}Pc44!#}oJNei7 zl*p86eJ12jcR9Lj){(6O!2iybMc3M|QhVgpu3eO0yJoYcpn8+RWKH|ELZi_Do6Uj( zyA8`0m!n}}DHc~2p}M>PWrevYE6PQ2zE!oyp!MWOqXd&vevG-i7<2hBJ*olt* zF?3vUz~%A5>kY_#Lr>j0LcXD(d+@!f{tu5&51o;Fcn31mvmppGB^q;rY!m=MUu&JI zMc1QkqC9`$hB|vq>t=I)#mxq@tvdCgNDYqTV97DTYB9r}n}eG2B9s;9p}4??BD)QR z`BoI#t+3~sVcOmm~{K#aC$I!)di0~ zfIuLGKp>>_nyn^8L%yMqd-$Yh@YJ!Xfe-umVki?qTT!!?oWXnsAyI$WJJ}v|4W9E1o;vMwT;hl2RYP*R zEFFOmFF(_Qk(HL8M>Y!pF!O}Y*rF>eBi9eJ-e|E|^6NIQu$9)YGTKU;OgVW=!5Pwj zSRk7~qQP)rx;N}~T=b21o%i&$OOUDyPRi;KX^zbIs^qDo7PXfRrQ}WBT%cPCCUa2l6(HXyrWk!qLVk@gJGZ$7@ znQbNI21{-cXSCQ2rW`wGFc%n%7CUD!+QAu2i02tNgTV@LvAzgENF;WfjYRz*(J+aI zz0pX}O(G#rG#vDhNWdKldfj2)#AwhpH0&Sm8jA#{X{!zu(`xI*WJ#Cw>C~s|C8%ke zb(oI=z(Y?VU`^{)pUH2kvUKM&0qbIxBeqZQzK96fb0C=Pl0#;cjF1%8iQd?F#EoI_q zHS?*;)j;loRuTbLnHn!3Q(?$N98%S*6^(g;Ye@ijWMa`Xwdz`ku9oDP5`U&V0lh;3 zR=HMT&`TuJ6^MCA50)Cc==45PIbU|YxmFi}~8~DWIx&Ua07EzEZPsamL>N{1tnS@^X0P(!q_@CAG zI@wj?x&Y_{J!Ae;0CR^-;{4f;`{kf_K0rKwo#Zo&>jIGd&@=9z$-4kbZPZx*>zD&v7l2O& gX8SI{b<7j|e<+6%+oLT%KL7v#07*qoM6N<$g4`$>CIA2c literal 0 HcmV?d00001 diff --git a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/toolbar/jml.png b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/toolbar/jml.png new file mode 100644 index 0000000000000000000000000000000000000000..09fe8efbfb60ef99a1e45b92a1a144f1410be88c GIT binary patch literal 1374 zcmV-k1)=(hP)F0@F@e$;rvt zmw*3fuYX+>DOw~F(G0^_qm=SnES8r=veBvd*C9=W-vrzdl8aPX2+3b)(6A`*!d>$-l-Znv*?xm;wRoE?qk4^Z9m|rn%qg zuygaqbwUHdWq!YZ&tr9g#QGI=>sKK4RRBijbG_#p*XDPHXJSUfV8jQvui0YByeYhtq!@lHau=CJ}-EkI?-qx|Lqu#>keFV)Pa`^&WYclcQhImYoW!Le#b-ZryWE-rlFq0b> zJD?%dkJ3D&V9(st)r=nFK?EM23EhifpIZ4XIV8cl0N! z*Q|o?r=E(F;2#6e135r0U;_q#;F$N1k(x94y-TC%~bv>q{n0V9~rxW>~;54R#zl z%$ePLi3JBn=TB~VheJo&xY8H~VD7BpG^N7H-;z3=fdI&`HI* zVrI{50V4u6)6l0$l_pZ9iO6>%^0bJ2A|ii@NUMkhMI>?Rqia; g#m(gv)#(!d4^ko(yRC?kDF6Tf07*qoM6N<$f-fb9-v9sr literal 0 HcmV?d00001 diff --git a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/toolbar/junit_logo.png b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/toolbar/junit_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..5364677c487c20da9a4ee2b11dc5a467ad8ddfda GIT binary patch literal 1208 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 P7fOJi4;J$j9d_$q@udxW literal 0 HcmV?d00001 diff --git a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/toolbar/mgt.png b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/toolbar/mgt.png new file mode 100644 index 0000000000000000000000000000000000000000..ff2b994af0f4d22c7b3658eab51287cc34069f0c GIT binary patch literal 5381 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 literal 0 HcmV?d00001 diff --git a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/toolbar/minus.png b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/toolbar/minus.png new file mode 100644 index 0000000000000000000000000000000000000000..9228592fee925a222944635f2c2a2df7710fd755 GIT binary patch literal 2837 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 literal 0 HcmV?d00001 diff --git a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/toolbar/oneStepSimplifier.svg b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/toolbar/oneStepSimplifier.svg new file mode 100644 index 00000000000..fb4a02a0055 --- /dev/null +++ b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/toolbar/oneStepSimplifier.svg @@ -0,0 +1,738 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + Jakub Steiner + + + http://jimmac.musichall.cz + + + + + go + next + right + arrow + pointer + > + + + + + + + + + + + + + + + + + + + + + + diff --git a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/toolbar/open.png b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/toolbar/open.png new file mode 100644 index 0000000000000000000000000000000000000000..aa697e0212d27e6d79881d1e3cece3c51f516846 GIT binary patch literal 14175 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 U(ejCr|C{r^mVsuq1~UAA03Hw=s{jB1 literal 0 HcmV?d00001 diff --git a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/toolbar/properties.png b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/toolbar/properties.png new file mode 100644 index 0000000000000000000000000000000000000000..6538022e5181b42e60a948dcc749f69e5443b801 GIT binary patch literal 12522 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 literal 0 HcmV?d00001 diff --git a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/toolbar/pruneProof.png b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/toolbar/pruneProof.png new file mode 100644 index 0000000000000000000000000000000000000000..d6837ad07022089b0a2bbd30ef1150dfb95fe90c GIT binary patch literal 29042 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ℜZx3@2e8Ek3%Fp@&B{^N-%SQkZ35i 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 literal 0 HcmV?d00001 diff --git a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/toolbar/saveFile.png b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/toolbar/saveFile.png new file mode 100644 index 0000000000000000000000000000000000000000..cd5a73772423d56ccafe1612e10e201959ae64ed GIT binary patch literal 26285 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 z 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 literal 0 HcmV?d00001 diff --git a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/toolbar/stop.png b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/toolbar/stop.png new file mode 100644 index 0000000000000000000000000000000000000000..ef81e9be0be700eaf789c24475003cbaadc9fc9e GIT binary patch literal 23714 zcmXtgWmuG5*Y?aXzyL~zNDnC?B_SXoAtl`%64Kq>rIeJUNVgyz1B!HagVNnGFu-@+ z&wG5o;Lo*cuYIoE6Ro23?kV6C82sv-AXNsLDWNf_VmHQ(9yw{>AfSP_(*Un;n<0}C%n4tfunz=b(y^zEZ9eIq%{T><`C-xv%`aw;9JxAJW_o>Pk zXVUN!_G^$9wnLaQSi5{usygjzx%J~`siuQ}v8Ayx1kkRt(b~63;`kf6E_IDQ6@`7( zFZH7@^Ja5dl|e$St-4XY8F(1F>0jZQ@};&FSDoQxUDwA!#8#$JfXWzz_+g`vW7XnV zQJTY6&&h>_fkHClgT5hSqF;HWA!<%82Afjv$dk_m&g%CL_RQ$0t>~V82yjWV6RNDL zpFtZPjTYBi+|cb4WCJnMf@va0(T=tLBSr-fdmio7|A&mo#@!Qrz+IKa!XvHxJGp}D znv&s&^^#49HW#i0@$SKqrGw9cEOf^AFDeW6;R%IMs53u?2?(o+po67zwRDo?tnp{V zz=q>TZaZ@aCw#d>?N;>ia?lspZHB$N;mCm%$9TNzhS=12Fl|hWdu_%O%;VPhV&!Zz z)M>BW=qT9+&dE8t*YeXEYlk2!T);VST{aMML7pX((z)@UT}KNUt*$ULGu`)@F)_&z zj{&Y5jS@!1i@fzq+hc^r(h@zCZw(ElMunA0AyfojYb(Cq9$x7`da3E_YVs-{zQa)c znpVrfkSurbtG>iX1&BeR()G=~+tIw_E5g4?3NN{t9{ZDdki&!$+@CH3DmrES)sMo} ze*~*-J~^9+hVEvN@3pju6Z2_8wcI_E({q?eu(<`egTK6}xosTox+-6bT%FZ56j689 z0%-sY*dT3&*+-{$yMosSdiwenX99e?Gr3gHOi;m?S((#^F(FUsYNo4V;R*5#vW$ea zFzfOt_;Pl3EeskuL>Ak5g*E3wvgD!#{g6cxMCkxJ6}@3S;dgK8Jua5aKytQEITf!S zL=(k{-Q=A!wUcUfcfO@soC%tLsEFzudQv*CyC)C^>-Zm6RsPx#g<_uTl9oE;ZxsJ}H1_ua0e1j1fp{%5wP@17`nQa?{`ka3;1;=AByRuX7woMJq%&bk`s#Eo zMj0Js!_LQ|E}>UaS({8fIr)Q|lz7njaq!IIUu~;0Sk@0n0{!D7D6mLLyN~9RRFkYi z0EsD1-!XIvAA>B+L|3{KRO*#=ffAwn#0Yxrnb(@x-!I1_Z&EWx==E@%*No>--n2dm zvMK;=^gU9oDNpULm?Vt6@wJYZ@jfj~8`zkoTUQJeBZ|b+wyIHj`Sj}xtFqTbFN3p1 z;|9KzWPZ(IYON73X>Vsy9@2#*$1vi@ehmMIo+h~rZfZxZDRE{wL!8jOivPBDQv8qf z=QqAP*0#z0#>Un9fm?IjOw<8Q*z|lsRNP$Zqc3bbqb+`6@4r-1rY}^3E-u8J0br9e zqGYD7{=pqk9OpWC4%1VIW@+FzIU~pza-1)6jxfqTdWx~T)S5QZ!2hysgV4|2z2$Pf z_cJ%LkG_XWWAWP4HL%kqHt0`Fth&1F?#~~vpN)g?1bwP89Z1>%S}EQU`;RTwU z{pekao}=h0qZKqQlnHzbtuKDaExdtnSR#>N{ zek!%P$M{>FtSqSkORTa{o4z+b2d6%}hdUluUS?_cNF}cY>7w`3zK} ztW%-TvbOfS-kdjXjrFJbv7G!2j~B_xJeS=>)6Z-yN~E4-5rUKL$zy`_PlI@L($mEg z7$AwVr-a`|d-tw9WQvOuA>esJ6;B+_RZMz+r?E4T4P|j9$PX8#dlL=i)GlV*FrFUo z!r|?Y#MDtfEbYI>3_men?f;K&v28~m__-=1+XRb9(lZvWsEkE`I3Te44rVh;7 zEzaRt&dqiOQ3?O;?dt1LcMgu(YoXNF1q8>JhH!95D}%i61AG=2$#Zfr5XMGSEZsDQ zxXienJXMb^p_57=(Weqv0gyJ0?4$DPV)J7opM^#5u0LcHNrAvgMm_$;i#Tj`7fCa> z*P49JqB|X)m-mznWFkr`dEy1z$@;f(iPTx1>5&zwVupl(su%Ej@7BgALJf2UD*;C- zN#4kKWZZKz_J2d=do{K=-gW+;T~>h$ayI@hn&yq(mkvryw*^9IaV0qRGVQ*=oNB1o z%&W}^wTEWt@&jDGirB!txgUD3R924NNn?zwOcnGz3R^gIQU>G@BqWAEw7Hzj-m_=MUMY)_N<=+F~IM}^j~lM_G6=> z-{C@gMs7@N4GUE3?4ZF=!QO^TO!RZ_ac{M2XYA;4G4pc^2I_ZbKJ{=~NT46Nh#x3s z_zy`02`4BwNN7z9{8IHlCt05UgU;9|c6#!A=;9iUBxw-Lv)^Feq8I+wL>p>1<>{x{ z2?i`3VE06sHV9E#x>H~#4OJQ*qV*OTQhr}r?gRC@-*h?wQCgv!OXGAt9i#26hBnD& zAK^vr0-jR8yqpy{E**t&yq78Hm!46a%dgDT`avKR8~vNvcx{rl9Qp4u+*Sy#>3qoQ*n5- z)kcPRHvavQz#Q*Q_d(sLmJDiB9R00H%NsWD%>j+OTS39k$RFHoT_&fAj!9vh_$HbR zziQx+$;mxJqc!)tza64pMgKnAg4oo6!<13YL2_yo-~BcP;K{D0KCHW!O54|Id3D;aay&!7H24NsL4x4^nV18dbc@Q4U2XyN~auc56t zp)a`#r#F?_;IJuy9!Q?Qabi1xyZhw*P-S{7(>(AWkA2cF7H~q_PsA+msix39`>Oyy z-KJOGh@-2^8M_a@R$l(Q*cF=fpU?_8^!*IFCUn%CeW`0t@76{{pE=p7Xm-XB1_g%q zu%j@%f`#6U&B@+1Z`Xb+j%?@j`^Jr=@e0}yk3R10H_3@*10x^gEnC?k)3uO_P82VxDqWh1Rtw+FCL z^EbM6T-N9u&UXci!8;x0qojgZ%*$w@LWL_UdH(8WOLNbHBQV-7FQwxNF+(L=FHD)F0@N3DCZQ)Q}t#}1ATIt z(8XR;D&Wnacv3nA730`kiLZ}FN!90u2n-ogvVQiLHqv=uBsl;7xD0;!N))6l?n{ab zYMt8sHly_)o67x}ANwIVF)Mg@v=%&Z%bv#WeaECju;5I%N@$U=iMN54)kA(;79+m3 z9lNNAR#FL0xHV2B`TN2?cJ>5$rfJ6K!Lvr#jVRKh61u4`AU{Q>N5^2Qy$cmvHv3kO zANI>a9-_zIEdrrm`ulhv`(P&K?afOIUiC1ws_IRctk6n9Yi<|!*)}gj5sR0! zZ{ki(PZN|@Td}9W+iS%2=ppd7E$pUE50*LkrmPZj@Qvrr&@U>8oqBPkp8iMSZOR7w zxz5k$qXJBi$AP0uC6rF?pHPEfGXDLhw{4p%ec#K}kKPO;{n~x|rIt7PV8L5ka8Gf_ z-u`~{t58%Z4betG1Kfm(58!$)r@}yfPlOn6Dna_wyWeEP8eB(Adv7o8 z#EwNI&*C4BJcr^r(DmFMPiztcUu%Q2-d5gTFqwQsdF-(s1<6CF-1kWoXtSB#2o#e9 zldfrU2GgwIqEo<9(FR|F9x;~tj?EYImGGqDKMxoFj69{7v*bFr;wA4nYK}ynp89=V zuR~-msDPYs@*o3o?6is_^Hmo-jy`M`y|;UFEVA4(@Amb_>U~uZ%0v}(fbrxo z=_6y3Djun5r25+rvL?k^5H~jYq0gTP>^^ptQ}UF@V7;1}og~I47_^iFs_&>E(0B!E zCHVMSLT%BlzQzs+2V?ZWxiY!?uskoc@5LRNuMOw$z1o=>E>epFti(>7`}e2tv_&BP6noW@l2FAGcgbrzrY4OUb*Mf%FlJh<_XJhYnrWd8$~~Y~ zV_+p5!G1+Q86dAc#pxkbog_dleiY7q9~d9qmVZtce1If>xPDA&L`v2KEu$K-H&>a5wMYwBy5cS zE$Ztk#Gv)asRa8an?yGWK>Vn-3ImoHt zQ+oQJSJ%AF0r$EgnNk}YaxI^!=+k9Y1m3(~zY(k(9xiyQ(Mqm40GfqE>0>TfVDO|tN<)O;3V!6Yx(ZSL*II;BzUMT8W*@K=(i6qG) z0h@n;BOsNqspm+N4**^~pRR}MJ*U9(5>do~#QsU`ynU6GTLL< zbRc;8>}xHzWHi5y09?lH?=GtXi$rRfJ!E1d8gLqFpT1W}k{c_lQ)WUd4lpY{0wN8y z{A32`@UPq^&8dCiNmvp{uu~u@wAV zfe%R80K^LdotH@VSx2U1Q+`r5H%yILriGgJt~^h&$VI`kSOhKj)?;;|qMi z4>!#q#fk1?LR{h(^H%QXQybbQGSIkGSQ^DZW9s{2xgN8rci&syEl6i(5(I^@@QCq+ ze|h0_@2S+;QodHK6dgnlIjOGUZEQrm&t4f@e!pMzzRmjBNXveyL|@>IFk7-%>ACn9 zbRO;B*dKi;;R}*HwKFyrX2_>O21sGW3wtGgz~tO?t!N8m^Rk5Y z!^1)B5S+{OffyFDifU;-pOTr^7;b_;&WY*@&kt-RyD! zB{>u-62@Bn)twEnDIUd`2+cFL5?N^3=5z9Ti-er_#TlReSVu{v8W~sbHMSR`%O%Nf zm_eQs%()71UJGDmI6LrYm~`xg#bSS*%24Sm)A5S1TLz^0X*MQo-KsJ#rt68)4nLDR7) zvQ_;)DoJX+JM0^beU>$tUo2UkTi~_s0q#bXzP>B(u3c)u>>z4`L9M}r@g!tb!j)#p z(!C-Ty0#8vzzL%QgWrENrpgXe+&QrW4=3NL(`$8ZP@C z$;K4eOre7fP51x9HqeSD6D-ka8Aw)vR>zAVh^zWu1&hjOhs?8oN=|0@`t>3WX6S_H z=Bnv_arRUigdcP@qotM+#|}|f*CX)i5-r(JW?qj`{Q*MZWO4Ro^Oa9h`1*dFuL+KQ z*%WleXY00KwC*f()M6Wr!Zy$;a+QZxBp{YDKsKfcE=S^j;41b+D-qTjZv*OPl|CWQ zMTIWF%BDY1SKxIu_VuOZ?78Vwjlv2Bey=@-ay+t7{W&Dh^45U3?%DR zL|iQ%J-Im*p$-y4%n)|~idjhgtYR2z@@k>4*4yo93U1eqlF-z;eu54{?;)sWq00XR zo2lMT0tAy)j(2GPpJ2<3$uxG3?Ug@sNC>zT0#E^W-P&|CL`~O5w%p(vflz(Jk7E-1 zouKjHV0bd61TE1gmp%Pec%kkdukil*FUWD zv;{Ig&WT7bgU?vjsW9}s^7@o!x3B2&k;u{)mC~o>XeTAne^uLVtO?&LN$5XKEhS}U zj_t=AIkx0=UQca9YI<+>iQP@4@c`28yu5t<7lXy3_AeJb5Fc;`kWtTH#eEt_&Y8n( zXvT1n3g3RUL~M{r^Vr)zDYp#Z4bl0?F5We@xYEB5GJi=(P}a@KDcX+p%pzYk0)ZTc*BMJNnwjuBo>e*; z5`r(|S^f=M>RvCoLZ_~+Gd6{Iy_L*GsjYTOi(RP5uGqiU1PpfNfS#G`^$`?kM6VAm z%}qK;LI-^j4+LrMl@KYjWG&udAf=;__{L!G`=5J%VFsK&39l;lD3NvRJ# zBsvBpyE!N{>DP56758iyKQIy<2Y7xXNubEpPSO$J0jY** z&(uirBgHCheL(x}XoYrBl8F87hW^U0ZsFBZHl-E2RQJ$OZvrmVRMJnocyhF%-SK`b~2NUlT%V zTEB6R*(p_T`S%5A>0VH*_Kl(n*BbCQq{VyRoLbgZP?I|^9e7R)skE~3H*Lg$1lxDJ z-y`^p6<=ysp9e@Jc$Sp*S4Z0Dq0Kv`k2?malY*&owFtmGOrJm3DLfcth#;ry%Kn`< z)^q!u52N!2)(-~)ie#=1UF;iIQ{+7aIugehM0n%qg%=Jh~#)o&NS<1ov*<MaDvx1{0GE_kA*Gmt5{lI0yr;+Fd{OlkB18_33f6FqW z(P289r27L8Cq(J?(c}GJJ4rNi8wGLRqXdV1*@%zRlULem3V zWBL?fwET(DikI6OIkYey*M5MuRXNS)taDS3t@%@)x<(2=x z3ow2;#A|VlmCl4<;7Bwk3RLM6d#JD8w0dw^_xz30KASoL0O2y{ntd4c^+)e7Q;bX; zYPZpSHZn@64tvL&6%ty zo_`=vSpwTDigQ4GO2Sa)=2Gw8U8dc-Y-o?7_D);KFW4>(y?T0fe2#rB&h{&Ex__BQ zylUOC%eY61u}eLEnsBw36Z#0`hM=Y&-G9dvE%*@P(5bKiT?o~&7iO1aWX#@c?{k~; zK@1Fv=g%JZwA0wZ!%YjV-uyH=d>!BDmNk=f{NG4^EnJQ1Gk(dWA~BSgp8`!w z#XH0a(r?YCjM3BoMAN;v@XQF&;UzQNjCL4!?Fk*wfq0xvAuTO6`=UG3I=cUTrfLCK zM*wUwFbNd9<@Lq!=ew#dbSgFIAMViT-sR1m&KLdQa-HbVzG8B zL#r*_so2qA7oJbxXm8TI(wJQO;&t$&k0n z)FZV@y_lt>k`eJ4J@J#&GsG^_1O^x(kKPrnOXI@($Tv$of+}8!yOVfn4`mKf)eF71DZ3Dej3MNh(%x8;@7Fw1rzSz1VeUkW&l_{VwpmtuD zGyyF%2DW2QswO4w&Y{Q59Id3Z*Mnc12>X%g05_jFsCHWdW~G3#r}2+b+H?qyQe}k) zo^_T@!Y_l3ANbNK(^NA(R8u~QZ=O|%fD1FX^YOd&^~sM8IhNQ9x}jU!AMfrE%qM;H z=Tbg^aPoH*o+(@g*D~h-3vSbw4et?3XBKhj9`3dO&6J+EfKi#_iJJ*0i zb55NXNY3^TR-@2EpG;404yNfl@TNG@RsU_KPW6lQ+Fp4q zI~d&qEi@&GQw$Y>!6bS&&qy#<2O>TTJaR2~YIJJpsR(!kQSA8D!!;)W;?BK*8j#?^ z(agv1PmoVlrZZL*JFZ;W??gdq0vCcdPgyLo2+?R)=K7qhijOSm=rS05E~jteDf`nr z%`kqNN3b1rZUmw*OYIjg(r)urnZ&MN^VrVnecfdvca#PrbmDgIR^zq(A&E9i+=yJd z>DK211U0m)*7QoBbe}HND&+|YgUeq)lpJwcL%jrtoZdwOTudE4?@VhGd0nU~bT`_V zrTx!f$c#~9UC8{57L(ZBd=f7ra|@xmHYLn0ae+ny;I>EU=r%7 z{QTY?JUx#I+LZ2mk=7~Y+hSpF(Dxq`-&P0O_vyLJF2j5`G#q}au~fF=hD8$@y1{Tr zUEq~fL4%{!(EwXU({54BPym}bw>e44-{?P4*8SCA@6APNBz>wMfEY0=Q9AG6T!Hak z!gW(I|Ar&l;ce0e=ZIMcgIvn;!N4CL{dd03Y&&c@i(=eW_449z9rn;HcX>vsmDD{a$wWiq*I*E{XYbJQ1=uFsYw2W)A}D7NL&N z*uK$J4EWANlC(N9F3=- zK$}<=Ax^2Il6B@sw&)U$(KQ_%D$rNl!86zGc#r1s;6f{_;y%r%=_JrPv+a!Zyvqe! z9JGMF!VZ^>Y7>rj1I13aMTf-IszDWuRP@dR9D%`CW}GybVVy5><=ds6VRJ7{&b}%`Yu~Jg53x7)(8dI? znS$a{0B3l3+~u(T#+@DP|}%9$;&OVe>pq2F$-hLmsKPtS4;8yf?6<3nR4f% zwQg{*r^GIccptc;AxzS+9ffaw$Nu+@f{q%Sn-X__Whw@C36Bn15KqJpH&7oKi!mvWp$`+>Bi&mgjt5B=H$h zmcx-UXQ&{@v0XvwhcCQCjXzcv(oc) z-<;c~-7-Xfb9?R-jVfzN?K3Vi=9_d($IUm=!haC2nzUpJ?2l%ut{a zW5*i)MG06~J9`{h&4_DLP)24C!!39|DBw3H>4Y`5XSexnROPOn;`^S-)e8MGBRzrz zrOX9ozRjabR_}J7vriuxPRN!I(F5`xtiX%!Lk5BE-(VWQ(Z{Y#rKp`<)R+)F=bU_? zbY{e6R4W!j|79v$;&4)SE^}**%Efp5wi1Wt29B+iA?@aJHGT!AZ6V!oThbsG)Ay@i zrU@bL6xd}4!fC&NVQ!w}b5qmclTQLX_sG#?dxsGVU7A*-W%=Pi!%5>LSl~aRKfHms z|I>79BmWPY&TXAWFKIt#;pB&8Ii6;k@C!AE%Px&oen5G|)FY{hGDz`BXT5n_R@Qi0 z|Fys`HwTey`5HxZQwQ9~u26OZXX$y5iYUoup+e}}(tYx0oxEImzE7EfQJ*~1W&<|G z%Y)(z+PJ}zvszbI?^-rQq1s@a)NY!(`b;!-Vg89ss>liph^B_%Jk$0Wf>bwf&|>dv}c_Y4?xDc5qH2NetZg8KYw-$B!>D!F<-(P@Uzu zzUbZE5dOhLv1G-hoE)Y%eA1SdMexRNTK$d@0})jdwY{Jxw86p-!4p*>lZsE^Mx3gZ z-nkUzZy)TGmHWT9F1w)(Re@k3LB!Eq=YR1gPokCA{I7KF!C&9fY2iSai;A8IjLT*` z$=Llkk(^vmRGcU;)ux@Vj9A*hY`g|HZIW6qa!$VM_tDyG4T>7oPD0fI5IBG2u-r1e z0xp@Yu6i~#&3)H#t@_xnyca8ONL7v=yv@X%){$(N`rpc|PTbJcm$nWgOG+y!5{Jj8 z8=dPZk?^A(R`5h5q}9x+1!~%n~FNf(ill5d#)3;;;%a9NH8wh!W@W zMu6`->MqUOTHoNQ!)Nz%Qr*YQWg^cjkcoPFvO5Wgq<3N2VHuIZ7iWbkQQcxFw2aGr zcf3byfVw-w+bwpaf*8RL7qTlnGI#*D0`hNa0t->JSiM#pg(@pvFGwXNoHf)Ie~^#f z<{yG3LS7KVPgky!bQ^)~>gt`nX*=(~I0P=`m{zRFPrTX8V(hb-!FjiD^A2Y@B)Fnw zxnzG($#?zYvSZ`jYUvA=Kg=6-cf})HH!6F2-~c zqB)}*Q&haDyqqENLdBk}(~e!7(-1pcw(V|N;V6jdiomcgqLR!|g8`x?r!`UCFyD~r z{`A~-7@bRlsXVN-#%Nmkrhe}{@+}W?TA=OUxqv{j*W=y!#_X@SX!|gmW{`}x&qp;D zsejqW40Rs%r_r)8j83P2VEHUZhD zxvq1q#!x@r_t1mcO;w^xL`}^S+1pL=d6Jx2wYIv;sl_@3QJo`T-|(Xmcdcd$;?9^e zmFA0uX?ro9E=j94t*PgREuPu_0(rVgLUd5l{Z{{tM$*IxjQscSfuwbKnBn1+rr*Pz zWV)PDpi{9`#@liN2H0zzDm*nv9+tm9TX$GHh%MFmQbL;;X&5FP8S<4eWjXJwWbkX| z&&U}`2FjQJfw=%*V$;^wn7-;X%bw>owgl-P%gxS>V*w}C)aKBIe6=MtMC90~8Rx`f zJ?nZ8X<2SCjP1c*^5<2l4-;RG2vzX*SV3124;bztD+La5T00YtAP8n<%ujaug_`9c z!%pmtL!W{5rHZ-I7_bz;9==_;63cL9j4f-QU3I%K73& zuP(gI~1e0=@Fe}#kxla@aUbmA`Y|3}E)anhIyAG*Nr z@L`ceTd>x?$spi;!y!JhD_Q_e(=>aPZ$gM2+Z$-Q*)Gxrax{YryYmRo5tIcc3Eldu ztojfkg<^XOf!f1DPU#ab`9}3BnzX%^nTD?@+>@?mUa*9gHQOa?scoSWBbTeH`|YQn zKY?E10uu$r=3zAz6_9H!wR_jKepn<~!| zqo_n#=Li@?AIsC!L$1P1` zk>AmBkQ6)@kx!M; z*!NtM0pwBJf$+Ha{m+@<=n&)kvTv|2`-9OL#+=I7&<_MVha5r*Ac+KyVC}a24~lt| z!G{U64)->rC~P}19F;4_&Bx7^i|=G7b=*0!3~%+{y*s_qRApG4>pK9tNk#2cHKb5B z&5op|AR*(pUaTdzWNu{2=#M7ZwXMukk!yqLp+H`XLh+#P%oQo0Y1U=uM*I0+SK1x5 zeN-S%D~Hxgj@il#e~=dZ*5l6A77-NfK|fxi7_f<=-V^V)L zvz|QEuyv`SDe7@6lOWf3q{g0Yy-v+2n;9L#@PHZxZsoSG^$WBd4!$h?zIK;9pdElkfpoizp`pC1*BS#yxC zV!z`>2mA$=yMLfzZe+@{;g=FW0D}r~=$C-3m67;v*-#s)qeD3x)S|zduK(nF{MiMS zCoTB9vPjoyhbY5g94oAr@nqPM6j*ckp&=B&=B-96{_m3`!}4??P*v}1d9-cuk@P}s zGEgmni$ULCioeL$Z@^yCn<($Rj(2k#n^~^B`=ZBB$#g0ZC|Oil`N7j6I#~6qAaCg7 zAkYOy*lZm?9y;PUG>!MtY~KL09c!$T^X7Yt;WN7Vf zKpUTl$qmi__q)$yfonkyG~NVb7;`T0wY!(X*#5|1|xue`XCW@}Or}t5Rxp*CYGye`b{vmOKN2M|&gw~3m zU!Nq>UtC|N*m-XJ=oNg}Hy#woN>wQ(?s$A~l!}F`^lz6JWD&!-&U*tDTk6!WT^kSQ zx~^ssD_l%YOi479HJ~g(!QV4o*1EGQYvg)!~NhAYuJr`-HJXb?*ZVR7{(0zO%cDNI#p6IDKH^5BKaNE(^WUJDVk?7Oc zqlJT~4#i42sF?X`!#&>{)7!xF%q06~hRccc?*tzPjY)f<#0qP|s}dsKwfWq_A8+?1 z&h9e2FU#f1uWd_umOIQbg&#JQQx8?mN){|K zXo1%ZgNTYKl-?Y_v-<-X5tiTTO^;fqGVjsf-;ukIm(>basz>s;G_QaCFh=EbG37Nt zJMIjnF4tR_dhEYgrCcoB{-(~i?c94^Y3^W`x@&p3m|!zro|;)Jet#n?EOEtX zVWw*ODWwI)^C-mb`y#7=)?X$*{(hDitBoD?#3z$d9g%oc`Hu~Y@>iO>>;h*ezbWsK zqp=*|KU6necqdPuX589Yn(AAlf&Xy^HXq>b*pb?-*2iWCo{|-rK^%I}vPvA$kI=!k zCX?|Q$$pu_!oaAEoyy|(UlC+Ew|RzxttG}|;G?H$r*iv9;12#h;x@(9Us!af?=Qe< zesp8M@n^!Ujwel8RYqBG(l4(V5mQPNe+2kdR{Jj3Ca(U}{I>aS z7#%DjhOhI{KW+AA+H7oM@_XwCCUi(Q&|T=4f>(V)V~t0L@$%k0ZU@Gt-Qoh;&FDW} zbT(spt|bL>6Tm?_HluS}Vd*SyZogtcb$u6owQ01M%-tz4oMrJ3)M(Y}2}sX*>RoM7BrOck(0cN>UQ_EGQ!PD4JgWcXddwCQdrB>hcnn#w}+lQ9Q$FK+QDXWX18OizGO#eI)IAy=zSouR^046~8rqPu66u#}?=#L~99lWTkM1Cq&=KI>7HM#y z%t7KjSR$R0lHr@blBlP-7bfOM)Ofs`w73MHqMow{>7ys2_0@l!DQZ8J9OTog;7o9d zI`CL!T;E|lFo-4P*W+aFKXPh*?vS^F{)YQBehF*i-~Xg0vm{Z{Y$s+qzGMkxO85_} zET0gs%r4m}L5T&q>-6VH*)8g_L|nqcJgDAWH+ot}_2@gicmeTo+QT3vjAlIBzp46- ziMIep5Dn`6{*NEcy{rf3&zG7?&~EkO3o&C7=+7Y3^T8LdMMt_u19QT6ck@Rr!Y0E} zCXyi1f6i)kbxRMOXx*3?ZqeJeHf4$c-;aCc3*35G3AMTy)B)*XE?PMeA6C?hVUIVx zB(Xu%tj&MJr9wKdJD4-?efCj!BI1vufex`Kt@C+Aa2An-&#! zJW<(`Iqv26YHYyrnMh=K-i47YNmY<$XXaG)sIt<(((3Jvxo69Nxw?vq^1Hkt;5{Ls zbSg@z32*OEMxCbM0V7s9xG2xtl4f$?DN7=L6yT&7n7T`C=TtE9a4|NtJSpsYw-_il==ffJ*Z>-fbV>-{{RkKKB#jm4kG-C^>i6(yc?ogH#3MX7o+TLtjDiUa zOGqRis*W6hH^Q3H3VdZuAdtV)wWtp0svPq>X>ir;Eu(@spNcdzDKu1eq-qKTqEwXw zeis0EqnWi+^djB1)r?*)DJoZpQ6KWxi2&Ix1vxHNFW`58#`ijd@d>d)#BNnTo;_X^ z^T`D;ihYaf{7j_)&qIw^o{C;ml9Ng29es<0QHvQ#q1xsjGe#?(oq1kgj7?7$-uV~v zl7Ehi+tGA?2Lj!$<@j@98vJ;rrVH(E5yCL%)`hywjJ*j4(a|e(qH~Y6H!0qNW>a|}NuX0+1l#~?5NU#aimcY}) zMr*j~(O4R^Fb_o2uU~|xT@t#`i>2UsU0}FVz;9Qxi3_W%3ybUHjCr!^>-autb*Mh; zwYS=Tk&Bfy*iMTyctmLtj{pB^xXOU2x@bELBg_aALpKZzEg>ikLw6X2(jwiRLyICM zgMc&$3W$J$gmg$r3y72hf^>{Shs?Wt@4bKb*STk(yYD*t?6cOoH+nJaM7TapNX%kJN>tVns%@Vbm^Gw4-m{giIn{HV=GNQ=yMK+`9RNxNBRuv*40eoPGL+ z*$r%{=)Z>f8pJ?B3XDIaLuQS(2LjRV^8FoqQQ}$4Jb+Cx;#wklBtJ>Mxa7*csgfXXiXTIEVTB^&@P&9EyHEc0CP})Urk;legy);5 zut{7$Aqw-PzC0iaep-c)MqEJwA^bT}wNWXAi=BeC^(2}4I6*`sO z(ZQ$%IK)k^^Cu7!muP5cjSkT!YhqqY`^272RoGLHo#g!97H02(Q>iaUV&_?ltS{l- z4jNLA(6Sio0bO$Pk76WFnB5Atkl{vQ5eUid4y`#z1ezuhVh?fwT z#Ns?TqeLtwA=M|E!e;lQLxp~EH3^py7>%Tv6(<6%$B>FV6DO*AaJ8UZBr&5Bm zOAJJL7mr2bBU9xHzu3B=#W8AkQaKgGm0f~f_e2qW|HL@+1b&NYxe24TovP_%)|y>- z770W0zSR4c(b44JBu>PYRkHiToNS=lYV&+yZ=KLdX~KFcG$6$dW+K)_VM^YoVqu*^ z*T$D^msf{lyKOD;hJ0!BAs5oVy}N%IE@8huO~gIhDg$sclJvNnk(t`;b-o5U2~%!v zi3EV~Z{HT?$A%@NEM=&T(ncS6Da`N>TDCCN@hifUVTFNv19I5-*u|nO;al4Pv&9EbzZ}odWp5PtPDW@J%ngPn!cadM~K~4e}*k!AUf=U{|n!glgpCxfAJv- z`lX7it*wA}gcKPvEl5*S zyV_Ey54yTq*1S^>&@D3005RC*9(pg4vpqU%UgG4B8VW{ELdo{%?@A`9dU|m6rg>RK z^)xcV%^}YE@VM3{?B}kMB^kl(@oW2~7gs>i`OoY63Ag_l0B?^uI~ETl*}|2IGUyjN zY%Cr5e7n1!7%Miu8b$#3I&O+mnm`@`t?mu8#DjJI^82M`tn9pO38`86yg7RGFBdjg zEWbI}Q=8RlVBuQ4f0L6^on4D}JBZK)*0fA*=N@N-J;De-UMLtQfuKr)pLBB=km1k6 z6z{mtsFYP1WM0_)m2Ob@Cn-RT*Ts!&ElTjlIbT1jpU>*(TdGE$=-5_+i?kpG{P>#I z(2bv;odpbo!=!SMbSb|0`Z+Cqz%F5;sk1Pw?$)x`z6L{!RsF>N!J8-EO;KLH62c-Xf=GfvDYXLo zW^=PPV2Z&Mb7*LGDGIDf79gu8sYDj;GrA~$CJ3MIYmt+SDTyol9eYe5K4SVgppc&} z_GCmmUt+%Og*yRFSo!@#>QjbmC$+9i5rgE#j{$8GfE6b`j8sno3m50oBkf{hKH$$` zuE&bp)fAEkM;Jy%&I)dD9sVuApT_IFXXa_ok!o6Kb0i@#6B7Un#b0!yBK6Jd7bzh= z@lS7)@ba#Q?#ghJ1zJTONnD-~=TRFV*RzM5;42c31+D+W)ni z)e3*K`2qUi-(y^x(;v7>TAr(j&PJ@|9Yn0jISy2(d1^pR;HRoZFM;;c1L33%!qACy zp!{*}T{#%6s)a(Q{pUEzf8`Gjrx-*(!^Te^Rm9aag(&`tY$UcNfo=-g6YmB)h{Ik|wsh%2jEA?!ZSBK1wC^YbUs;9PBC0|8!KY3vKCE?^J zV<06t<8ATur`#gv)3D};qvCx(280I zbZ`f&Gb2nxsh~D0L}sEaBQAPO=&2Qtxo+%!d(A&@TFWZC|V+o}L*oG`X%M|!^T6)PtV`tz&xV0YHo zZ;59G^gG^~kUcr1cenYrIzVdoKla!R33Ig{HZYV3 zS*O~VxBFNZrq9gGYzA<*t?c>AR@uWZCI#3frIAe1FbnUS^2QE~$u=_((Bd*~4IakA zP5rZ0lb)zQ&01MDYvrqFxT;{=!OHT6R_w&5oe7zdaVm;h?+M<1vYB=h0@B4aR65jN(+IOIy4K>hPH0sgEDPpfS>voy_69 zXilRP9Q2UcIZog~8{A@87#- zZ?I6MPqEXT*3JkY7f5%e0ffkZ^VC+NU)XuS`Te=>qHDq4E?J{T0N zyXd``GM;D%gpv#&c5oig&rBBN6_YGsp)uQKagdo+)C~`u^>FocJOs#<%0g4EuScFV z?q@kl7lo`9l4Sfh6+QqMa4X?Kv}tCv~zSK$>4@)BPmd&Tk1rPi%Bh*${~ISjZiH*aYRdi~zisBh3-c6qWYykd2joe`*5|&Z zDMpjTtJy4{|Iy?DZh?UP^W6iTdI%nnTp(A}IBgdx%I3UfN8`_gQVTumca9oq@tSY$ zTH;5AMSQl#i+K)?bA@$q4AY&2S4_P%cIq$?3X6fZu;T!^ZhViEm4&buDJ_(pQeQzK zSQnK5IJ7ajo5oyh-Ek8OGqet~jk0D_(78v@dl3w?3T3-!91?Q@17;ThwRLc*(?Snr z4WXyMev=5Kjky^$RY9g!XfkXhwN*7)zrddPI@H)S?Wpl=9-FqmKObiwSte9H_1!NkOmT4ud9>8^j0w`Z*{X$$ zNAAtcE7tAKX-lDJysN7xhvUUVTkRj;30{4DIw2sHYoPLx+gAxF@dC>D71E`kz|+^O z#iHqT<8B3yckSN9%q+0O-f{Z{Xse1vPW#fdJ%IDtH8eiu`bR>OAS;l{CZDfhf8u;e z5$MI5Lb?DRp8p-M=j@b8BnYpJi}X6zhD#qSPs6NCzhoZxeE2qF0uYiM%p$z9#UI^c z+d~FIamf41%=0lm@2E{ni(OlNLJfKmKz?)}CZ(7kNNS*oFG%uTUkh%vy5qT^gGRUV z-meOFafq8s;gAY3o~}*6Q(N_YBQJLFmW4(_p`?pn)x8S(I@p2&d@yleWM(C!lzT2* zhvGXP$G=8Z!Y7C84cDZfjjq{E{HVl-o)y|Wei@neSGs*?kFQcS9G3s0yDepaJ~C@* zlEn8cqYM@Y+PAT5!h$IDtq29+O{`G!81eF)u z{=-$QVnIrhc?10VQ=fQ2ye^!0Vq$(AYpAgn-!9A=Ok(vnm~j{xFU;oH@Fvy=Nnu}dP z#fqKCj0+9F?%BffY7JetqPa%MrjJfXfI@1~0=lR$kb!&LIm=QC1n0^kFMeB{e~p5+ zHPeb}Wr`>XS=gh)8_nYH+PY&S#-C=rrhtB^{d@gH`k1MuMB-XQ1leVKA8JPBJ+kGD z_xoUlHdmQH(HN%1slGH8${O40kFz0bxn~LiE(eE4zW-RTJcy%3^4jo#Jr+Kc`hG>r z%!wT4w|(IG3v_@ebES*SBiO?)?He~C{BwjvhtO;44QT^KCTw-Wz+_v>283McSmTzA zaVXjPy( z$7{Oc0~qZA(Z|IBR#&p38$&wqlw8@K?wFXSLmFJAVdr7(Q_J+6oxwjjOd(T731Fia z^C725J0hLDKAX=scK0Gp*W)_?k2lWL!i5gF6JUFmKq4OX2ubkKoX@P?iSv4$lq0n~ z9<1%CfsvM!?k@&%Avac(z!E0!?lw}<3>Q4HUKpt%Q?7(X{+N_f4Q~x;`i+xLyxsjT z4~NHGF)V4-nVXxla>+_Z`%T@k&8>9z*7qQcv>B<@>m& z^X-2isEwL?)Gq-C#A{`-lMaJ)Q=#x)mc>hB+%0Zu91`xwpAiY}VHh}W<&eXF9 zB7!?)5impkNI`bg@27k`Z%l~pss*Fcg9I4jNd?t(UO-DI;KtN+%U|`sTWr4P&Cimk z?`vRUci>g5b=;Zz5}bloboLvua@|n~mET7@Rx0GzT0gZGP;IgpPjdjD3xbQFsEZfX zR)=r1t?TAyRne{=@iL9FWTM84;{LY+i=S&2I~hV^eF`nBYs=e-g}uZEI9%!_a7y0W zjHM&F)D4@n8-9&G=XZ34gzjNS^iMinaV$^pz3wh@?0knTKd0)qbO&z~K_fqZudX$g zbiZFO25*K6rF3?&wv^FC4e-BtRGFMX+_C29;6(PP1GKC)Cf_IWW>O~LLb}wT;1_oU z`CJ}FM6-W_IbIPhH@y_v9;+nSz4<-B7;4eeIT>iJ#t9)Zf(cU3Yxu-USL~3;2hq~6 zF1cc$^9cxc=Q?R>Ynt_d&^@!aUE>?hvq^E&*{Cweu<7tPHV zf6n|B$-hOpqz6>=IJo)t8^N;IeyR_-h6H3}%{=Ux*{|B$#UK5C(`bmL_|S3<8%CNT zmUhqm{AaHQNbu<8yzRM%Qsr=H;@Z-XuGbQEkV-agrd(hoUry8h@#8X9#>L(9y$Lg` z`Zk=N_PhCE-eKQo2A8F4t061_|~O%)5WtNUJO>WyOM)KQ`a`rf8{)+)aTaWxvQ7VHMw_bdUiDsCYlpc)Ku#7zUKFuNS|`$ z!V6jCsu|sy{C-Vx=+1X2J~@Y)NZBqi>C52$&N44(#4s&k8zYOb2;+%xp1x%tHx6K?p? z##|(&)w4`bRW^x%srnK6hUVloLC8~X2odOAc?|4IPMLAKzj`kVfN$XZV< zTA3eQ$o*MM=b5yn5~O*SeZ!X_o5kxeQ62KmFn6p92GI=9yvz`Po8xdcB=CzpIO(0t z;XwO$58rYVJIkM?)iBctFxL9%jkx$S3~=$p@9i>I_sVGQ+~k~+staMm+C~;A;(bpE zs|cbQ?Yx*?O4km$mwf?n7fM4``uS7YtWKb;{q~7oNJo-=99rb|tX4RY`9DoG+H(HA zE1d{|HoTEU?zQ%do&twYW2M+OE=eMg#6x6zQb)ab>)EmTViylTRy7?;M7da*fGa*? zKkdwW{eCpPUvac(_K2?N70CNJ!*fMHXY~BL%e!wC-__4X^aoNhyW WpEE|MUY3Z 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 aEpum!UeJ0IE$$u_0BES_BC3>a!v6zE%b+3v literal 0 HcmV?d00001 diff --git a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/toolbar/toolbox.png b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/toolbar/toolbox.png new file mode 100644 index 0000000000000000000000000000000000000000..0605c1d9d0d1983c165f5ff7159cb5d205902914 GIT binary patch literal 13275 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 literal 0 HcmV?d00001 diff --git a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/toolbar/uml.png b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/images/toolbar/uml.png new file mode 100644 index 0000000000000000000000000000000000000000..0dfdeb7fe094dc5b2e93d974213d3a926d889585 GIT binary patch literal 993 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�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 literal 0 HcmV?d00001 diff --git a/key.ui.fx/src/main/resources/fonts/Font Awesome 6 Free-Solid-900.otf b/key.ui.fx/src/main/resources/fonts/Font Awesome 6 Free-Solid-900.otf new file mode 100644 index 0000000000000000000000000000000000000000..a95c71e05dde6fde919b9513a96b73ad03377657 GIT binary patch literal 1031648 zcmeF(4V+AM|M>rNH}^AVc7^0-&01#NlO!Y|A(bS#BnctPT}VPgLXwb>BuPS&B*|Tp zBuSDaNsC=cXl8bHc9ZzOKJPPibzRr@`u`sP-{bLrJbwSjKlR$rbI;Ds%{gbrerC^c zTj$QVX?|_IW@?ce+O&zZdF=U0n)c=f&8~lc+dJ-TW#0B)Q_U_srD>m*+;&I32EE!J zAE9Y_hNfvf?;F%>=ucz%*_!6+scFj__wU=QPqkd%t9)%0HR{g->jsbdJfEueA2ecA z5@`0(h^Cd4jnRl1ow+SZ_ejKMAKPn z>DpzhE4^@UxLJ`FFw_^dyjFcK`{#IRn=xMB#^+wm`7&3>|JyFH%owj}mtuTm$zT7t zhG|+yO?}iX!!##pF23Uyy&s>qO^-#|71|`OLG#Hy(pY8AYWU-NTK?mXTUi*nbji6b z?n4W{Cby(F(O<(6tp*><>h_8W@4J4b5*{m$s=j zH(p8G`pacpS|6<_z1*$U)_+LbuXWX@q#e-e>UX7`p+$|hX=keI@6vbJ@Bu>xN3OZ1 zLEZZGuWC}iQG=@^)!PgiJR;Kak-o!+4C)&h>0G2~q*2|5b?evYKVrnt;Z5t+>&In! zITxvW-;hC_`}d7J(zkc+@W>!`n#j;0LmwX4YglB!eO&qQz9S;N2KR}K7&2twg9Ap? zRac+B_C{@n_3c~djv)gF^!b+?Z4^;oh{zZI+gEQGGW5}51McrXB2xXn8j-(#`(GXT z>s8f}$Dno5`f9_p;o1Ogh&Gs=;u`*Ipw-pt^WRlk6FxTL^Q-wvb*+tDW(01jJ;L`5 z=kq~~5pCqZTuW1yG~zlM%JtOH`fDTjZzz{eV4lD!@0b=L@z$;!#INFL%7_7TxJCK2S>sOEHKiBhrTdvmj|9r3N{AD}v?BW%L)N4jn$IW-CXMlWe^2)36xz&8BriB~u zBIx%t(6Y5ES~V@oW~k0xtHtxV4y&>rJ4RFOTCJOQx7J;|N9&>8tMz2X_vQigVO92% zhcrNYfR#UxjW<{x>_f7njbP)AWaEzF!H?F)Xk)dn>V+D`2gZI||`wp;s5+oOH1?bW`}_Gw>g`?asM1KQWT27aR*(!SLW zYu{-{wC}Z}+7DWuc1%02o#1bm0_~*sqjpOBNjt6mtew%$YUi|Hw3rsx5?Y~_BBhuiAO-H|>JT&%k_{Z74${tvyYewW@&zgzFF-=p`?@6~(iz4YGteR?0guij6; zU+=FE&>zqr)CcN=^uhWNeW?DBK1?63kI*01N9vF0qx47h(fSyDtp1ojPJdhQCsC^e6Sn`V@Vt{**pVe_EfeKcmmkpVepTv-H{ebNU?pd3~-vPoJ;9pfAv0)EDY6 z>5KH2^~L%t`V#$BeX0JMzD$2zU#_pvSL$!*tMoVZ)%sid8vSj3t^SU_PJdTlufL~n z(BIcL>YMb<`WAhw{(-(t|4`qqf28lwKh}5ZpXj^vPxamUXZjxfbA7M=g}zV!Qs1wC zr6170)(`66=!f)g^~3si`Vswm{iyzfo~IwvkLxG&e7!(FssE^-(tpxV>p$yf^t1Xo z{TDr^$Mu9>s3-LzJ*5}xCHk-WdHpy2g8sXHQNLtphHe;!X;_BKa2p=OYxoSm5io*A z$gqvDkzr&SrHspr(ncAhtZ}(f&bY!TZ&WZU8kLMJqp}e(vW+T6j#1U9W<-r#qqO+#<2s|calO&PXlb-EZZKLK zHyUk>wnjVSCZoM^vvG@YtI@%@&FE;{Zgev4FghD|8eNQk7+sCKjBdu=Mt9>Lqla;? z(bMQ<^fvA@`WStUe#ZSqe`A30fbpO)&=_P4Hij5Ojfae3#&BbV@vt${c*GcGJZg+K z#u#Ib$Bc2tjA_Qx#&qKuV}|jpG1Hi3%r>4g<`~Z# zbB%e%eB%XUf$^fT(0IvMWV~!FHeNB77_S;jjn|B2#_Ps%V}-HOc*9s_ylJd9-ZIu0 zZyRfkcZ_w$yT*FsJ!6CMzOm8RWNbFJ7+Z}GjBUn;#&+W)V~6pvvD5g(*kycb>^43# z_86ZVdyOxQea4r@e&Z|Sfbq3)(D=qUWPEEJHoh~C7~dO5jUS9WzHX~-X zS;fpTtD4o!sF`b4H*1(R&01z{vyNHUtY_9YuQVH&SD6jXtIbB{HD+V8iP_Y=)@){8 zXErylH(Qu3%~s|OW^40CvyIu-Y-iqNwl{A!Z!vE*JD9hb9nIU#PUanEXY)?8i}??; zt9h5%&Ai*}Zr)?|Fz+>cn!U{4=6z-#v#;6Dyx;6^4lo}uA2bJ=gUrF^5Ob*ckU7j8 zZjLY?HbtPus`->T&3xLNZa!nq zFrPJNnzPK==5yv8^LcZwInSJLzF;meUo;n*FPV$Xm(9iIE9MgORdcENnz_t;-CS<2 zFjtyyn5)b;&DG{x<{I;DbFKM~xz2poTyMT-ZZO|BH=3Kw&E^(!tNDSs&HT{ZZhmC$ zFh4eTnxB}v%umhT=4a*}^K)~r`GvX9{L-ogu$5tDTBWSZtkPB)tE_doRnEG? zDsNS=Dq5ASEUU5=v9hfyR*qHGs%Ay4T&ucO!>Vc3vT9p(th!b`tG;!m)xf&SYG_?; zHL|X;8e2`Qrq;DqGwV95xplqO!fI)?vTm?iTQ^#5thQD=>n5wcb+dJgb*t6Ey3Oin z-EMWV?yx#rcUoPne^_0uyR2^3-Bx$&9;=6SuhrA)W%ai1v-((lt$x=1R)1@N^?>!D zHP9Ml4Yr0@L#>CbVb*YKg!QmB(t5-iWj$(*w#Havt;ei!*5lTAYl1b=dcvAyJ!wt0 zrdU(0r>tq#)7EtB8Ec01tToe`WzDvpv*uXOTXU^>)_m&)Yk~Emwa|LWT4cR!Ew)~< zmRPS^ORd+eW!CG~a%+XP(t5*MWxZ*ww%)SVSZ`Zvt#_<-*1Ohv>pg3O^}e;y+GK6E zwpd%O53Ft0ht_uMBWs8Cv9;6s#M)(jYVEc@v-Vh@TYIf9tbNv()_&_N>wxvObMJnNWs+&W?9TLsoh>qqO9^^Gye^;1?+Unru8_-igcXToq{&X&PA?SsqytSs7Us zSr^$5*^*r)duH~m?Ah7RWzWfeK6_sF#_UbmJF1kbl9glTSUDAPs^`?mshLwN=Y^av za(>D=opUzlTuwZvFsCHv*PP#SE>_)AZE>~r)xM~qQ$!n?$dVwurWhc8lH}?H;`++9P^zv~RRubXatBbWC(ybbNF| zbYk?0=%nbA(dVLbqc22Xj4qD85?vB~HM%tVT69_T_2}~Gis;Jd8_~7VEzzyf9np`Y zJEK2De~O-t7DvxVf6vu&ja)0&mFvs(=LT|vx#8T*+)}xh<(AGZlUp{oQf_wcmAS2R z+vMJo+adRk+|Ido=KdqMPww-%b93j_XjtRwTA^AMYE`UNwN}SGcU~y3R9?%xk$IEz z=I1TQTamXaZ(ZJdd7JXS$UBtxLtg%|D~~lh*5X*FWA_~!aBR@AF~^=f_Uy42k1ap8 z=Gf+AJC5x-_SLbkk9~9O?6JaQsbd$9`;J#RUg!9A$DcaB@c4VjHy!`@`0nF-kDomL z^NFkzZ=G0sqA0&v{*C$V@(1P*&wnI;O#aOLIr*>UznQ-#|DF6j`A73l=Kq|3Hve3K zSx~K@UcqezT?>X3j3}5=@NB_?f|m+jDOg$XR>8WM5i?`1m^)_2E{m0pm5G&&T@foE zs}QRgs}#$MHIKE3wT|5wYZL1byF1oBc2BHFtY7T@SpV37*x=Z>*!bAw*p%4R*i*4- zv8Q7*VzXk;$L7Z7#pcHr#a@XmiLH*UjqQzn89N%wi=Bw&#|mO6W9MSYSWzq$D~?@^ zd*a@BARdg{@yvLscA)!}0Ip zN8=~sXX2@Nas1c#`S`_zp710BiL6AOM59EL#7&9zi4KX5iF*Gv z2ZbLNeq8uz;TMHp7JgrtS6EPZw(z&Y-wQ7#-N}q(m1NaqwPeF&qh!-$v*h*3mdP8E zHzwOAZ%%edc1+%p?3%nMd2h0JvR`sga#(Ui^3mjD$;XoulT(sUCubz*BK9#Ebahdqq83Fhi)IzQRJ5e% z^`f_n))j3i+EVm!(Pu?_i}n|NQ*^lKNYM{P$BOccek}UA=v>k7DLrMSGE$eNu1Hl* zWv8m8YNqO^u1PgZwM?~6wN158-IlsNb!Vz;sz<7Ks$c5C)S%Rm)QHr`)T61fsmD{3 zQctDkrd~|Fl6pP0BK2nKz0{`E*3`Dtj?}Ky7pX5(U!}fIeUth&bu@J%bux7-bvBhq zoljjT){2A0c5#{F%Zsy$s}$ELu2o#G_`2d2#WxhUExxt*j^ZxGcNgDVJfL`B@zCPo z#g7$FD4tY2wRn2*oZ`8~3yT*QFDYJD{6_KW;`PNFi?A_ZJ^7 zK3ROG_?P0s;-cc>;@?V)5>JW0Bv2A836*4&lqxA(Qm&*@NzIaaCCy9Pm)ug)vE+`D z9wqmc3@UlF8B~wafmCP&ouw+lkHznVe6qNi@a_Q0~=F$=7%sGF~nQQ(zXRa@E z=4OA-ng5+?HRj9~b7oI0BRyxn{4aCnZ05`@ zWzO6-)-gS2?wOu5KM)(rocW2r%$c8w&5S*lo-;3uEoRRAYHUqxTWnwKAamy9={fU{ zu~>S}ToTuqGy9k`hvFIOIdeIgGe?*+S542EYsKrwuZdq5Z^4|oUA!Z6<}UHO<2~aK z$0x?0lsWUPKj+K~<8R2Ed2M_{d|UkUKj+Lx;(77YGG{Jf&iq?KOIQhi;_^gx;;KaB zL<{E3w=!qGojLPE%$XnkmpSvyKj+LZB^D=MPptcoIrE3inLkPFW6u2DpL6DTqDbb< zMxpP|IrBAzU1iSPTjtD@WzM`cJ!k&>FLUPe|2AiCz?`|sf0;9PXU^PYhB$@4{Jm^0U5&fI`G^EJ$w zTNOQ5^m5VCqLoGO6s<4XSoA^BCqk6ospB$dK9h>6Idke#aez5< z>GYhrI&_b;wOqHGiQEI=FBfMXI@&oqWI0? zcZ=U=&iny$<{ibK7VnWc^Ebu$GG|W6ocTO+X4ju{<}h>SGBRh*{U7JdV@f8JOp-bC zwtt;7UpmH-FnP?J<cOm8VsnRCz+> zt1H*7T&r^V%Kpl(%0^aE*4eE5tixG*vp&h%ob_(j+N|YSuV*dGnw2#@Yg*P5S&wG* z&gzBZQ&Q#i8>5ED`D{Zf|tge@kGVr6&F|RR%EDdG`GJP2r zGJeyvjPn_(j6}vS87FZ8u7Dp z(u~E7FRN?Kc$Q|+boq?6k})x3LdN46oW+pA@%|Y+@(j*4;Nu_~kkOxyy)y2~=$g?b zqjSdX8MkHJnsHM`n~WPW8fILTQIoG$4F4X!5I!INRee|ZoA4LmP2pF=OTsUQ7lvO5 z&ksKnemXoQJU+}VgdYk&815auJgdMkkwtut> z>=QQgIQuhu8y`QQt+a(U+nel-_WSmG_ImqWd!7A`z1Du)USqGaSK2G=<@W3LYxb-5 zVtbK2%bsCRw=W1GTLU5QmVmr)pCplkKM!F!d=5%!kxpN!WI9w{wp6Y#}+RgE)~uQ+u>l?ANGdbVJmEeHT$A{f$d)+ zdqBdD+2`yt_G$Z+ebUaikK1|nQTvE}*gj+*v=7+(?S1y%a1VPo?X-8;+ruO6t>N12 z5gWqu*&EifC#;rzVR^WVz0_W!_Jpu)FRz zLR-^&*ZR=9&|3De)uC0Pm9md54J`>R4lR;BZGLEOXijK$XeN8xboREXp~>uV6GP)= zpHq9>$k2$;u+Y%ZVD`BOLjBp}`hOa+s{L@*XS7d#U@9X!Pzo6jDb7d#p~5k_&~6KuwSrGuy?R$ zut%_auv@TeuuHIWuv4&OutV^cVEbUZV4Gm;V5?w@VDn(JVAEjZV54BeV1r=&VBKKt zV9j9lU^G}YSS1(5X)(6%F)&|xDRtHuERtA;_mIamumIM|D76ldt76j%8<_6{jW(Q^lW(1}O zrUj-3CI==3CI-d_#s$U(Mh8X(Mg~R%h6RQO1_uTP9tiXg^b7O}^bYh4^aykhbPIG1 zbP040bP99~bO_uMXdh@7XcK51XccG?XdY-5Xc}l7XcTA|Xb`9$s2ivqs2Qjphz6<# zssti|tU$#;`9Qfq*+A()sX#`+4g>@KfH&X{SOHGT@?Z2{@SpdW_*4F*KjDx0&-u^z zPy0{#Px|xy$NhQ!qy8iQ!~R45gZ=~l{r-Laz5YG^-Tqzvo&FvE?fz~4t^Upajs6Y( z_5OAKwf;5!)&5ofmHy@aW&Wl9CH}?!MgE2U1^)T|x&Ar++5VaS8UE@1Y5uAH$^J?H ziT?5aasILX(f(2Xk^T|>Vg8~1!Ty2%2mJm0{rr9Wz5PA?J^bDM-TYnsUHqN>o%|jB z9sIZW+xy%3+xT1iTlrh~oBNyjoBA938~Gdh8~E${>-uZ^Yx=AEqyDP?D*lK+%U{u7 z-e1mN)?eCR%Aeu4{XxIq@AbR=mf!GezKgyKzVp5kU&@#CC44d8Io}!IY2PW|NngJ2 zxG&Fl)OW;p*muZx(09PM-?z`V*SE*F+qcWN)3?L7-M7uR)wkKV(YL|3-nY)T*0;vD z+PBKL(zo2V%(v9H#JAYD$hXk9z&GDF*Eh#E+c(oU!#CYG%{SFI**D2I(Kp^V&NtRK z+BeEK(l^34%s13G*f-GkfUm!=pRbRvx38zKhp)S@o3E>{i?6eCN{Z_vU$zdXIPy zdk=XJdJlN_d-r+wdiQvDdv|$vdUtrYd$)PFdN+GFdN+92d)Il_de?YYdslf^dY5~b zd6#;Zco%yYc^7&Yc;|cPdgpj&duMuQc&B@(d8c|Odnb7(ddGXmdB=K3dq;UkdPjJN zd53xjdk1ri^Y-!f_V)Dl@OJlh^LF)i@pkrh@^TT?8#gmr>8CyP3PGyRo~GyP>;*yS}@w zySBTgySh8-uIjGhj<~bj72W0C<=kc6rQN068E)GhboIIZ)7cubx-2@Rjb{rx!t4grrd{_u9@y7t`V-uuJNvsu5qrRtbwYoq5Rj` zb;>=IYwpc;bmek0xQ~-r_lsQXX*6ql0qcAiYj*_yjdl-o&EdLtyLPjN>hoV!?n#tm z)YrR~xt6+?uol(w+ANQ73ju5ZXcoHKyV`NPBVBo{`>4CN ztE{`c>ojY=9P7}{H5#slocLgKZ^yZourBjCl79*REoQBrv)c0*Ct7n{W3Bd9JI-Nf z!1)4=t&QA|X*|l2YF%-}{b23C{&DU8uV4Cq{R`&)|J6-coO@}mQJ1oo5OY)>{c=85 zlx4F~IjgiX=dEmzk;|A3mDC^35()A-vjFE0XSguqQ-93O&iKbiYYu1DOjn;-jX0O( zsE8Sfxl+`e^L4lu=bBCC6UJfMp}yw)alY4B|HtQQ-u>S%^Y`bR-8-E#eg8N9=#A3fWN@al#lJP+);#>{1EsYx{5u7gYvuS?49aU2 zw2E3KEsKBMAR=cx=Wu>>wZ9y}UF$E$a9_#k@Uyj7(&wc<$N8x9;bCkDNj3oIU!-d80=;OY|o>E0i-qIqTEU`JClBk25OgXCo9aQm7Apcl_Q-tw>bZWh;_h?mW-ShW{q}Dy}V@g#S)!)u4wLN%uS5n(VZAX>b z9_qbcmD(o5(y8q+Je}Gm>Tfoc+BOfTQ`?8X10=P5ct=@M+lRkBB(;4WO{ccc=yX>y zj!CE1;n;M_>YZwp^0;)$kEc@}pH6u~I^~J!l%GhaJSm;>lj)Qvr&FGiPI+oN<)_jq zPfMr#bUNkf>6D*Ir#vH_GVer7$}`g`&q}8}JDu`#>6GWBQ+__3^4xUF^U^8LPpAAs zI^_lFlwVAzyfB^eOX-vsrBi-6o$}&z%CDqTUXo7vRVVqmV<}dt<*zvzY8@|2r@spW4t@h{3a|K89lMz#FSbjqup%rN6y>D2Ny>6G72r@S_u@;m92 z*QHZ_H=Xi&C*wm#e*Yk;;zcPDVS%&FR$gE$NiErc?ePo$|JH%IdaN z%G=W^f0RynM>^$?(<$#vr~FAe6G`RQ~o@i^4@gHYWt~__oY+* zGM)1Nbjn|)Q$CPR`RjDb2h%Bk`M2qm52sW9E}im`bjshSQ$Ctb`G<7M zdFhmorBgnhPWePS<@|KY1?iMerc?ego${%4%0H!3KAlea=XA zW9gLR>68=clnc`-C(|hxrBhC&Q!aKg)%`6=r69;} z!#Y%{eMtu@v4Kf!ViH@J#4b!?Hzu(Mlh}(%?87AXV-g22iG!HLAxvT$lQ@h?oPkN4 ziAh`vllU@B;?kJJWiW}$ViI4DNn8$-_zFzo@|eUGFo`Q-5?8_`&cYoJL2U=p{)ByNRC zd;=zNYfR!BF^St?61T-9Zih*H6DDzcOyZj{iEqIqz7>n8e*MiSNcF?v6=(4<>OBOyYYniF;xa_rfIZjY)hT zCUGB3;=Y)~{V<8|$0Y8LNjw0P_yJ7f2Qi5UViFI+Bp!@OJOq{iHBnn zkH92;7?XG;Ch;Sf#G^2YAH^gdjY&KPlXxs9@ne|8<1mRI$0Qz)Njw3Qcp@h86PU!4 zFo~bUB%X{(JOz_@Dkkw$n8edCiJ!(Ko{mZU3?}gmOyXxTiDzOG&%z|0jY<3*Ch;6h z;^#4m=VB7i!z7-MN&EsP@d8ZZ7cq$!ViLcENxTS?_+?Du#hApeU=lCEBz_f>cqu0F zYna5#Fo|EsBwmh5yaJPWB_{D3n8d3viQmK|UX4lo7AEl;OyajOiPvHhzk^A<4wLv@ zOyc#J#P4AeZ@?seACq_^Ch;ar;?0=ETQG^YViJFVNxTh{_(M$M?U=+LVG{4aB>ota zcqb=4llV9$@d-@gd`#j3OyZN6 z#6MyZpTZ>m36uCVCh^ah#Ah&x&tejv!zBI%lQ@P+9LFS1U=kN%5+^ZIn1zP=g1U_9B(^Y#U6{mfOkxiv zu@{rrhe_pOyVmsiPe5X;;S%;8)6b)jY-@HllU4;;>MW7O)!a@ zViI4AN!$#R_&QAD=9t9SV-mN(ByNdG+zONU22A4Cn8Y_?61Tx5Zi`9W4wLvMOyc&K z#5ZFS--1beD<*LVOyb)xi92Ev-;PP#36uB^OybU%#CKv6cfln72PSb>OyavRiMwGE z-;GJ!9h3MTOyVAx#P?zn_rxUbg-P5SllVSN;y#$feKCpqVG`euN!%ZkcmO8x1DM1Q zViFI;Bp!rGJQ$OB2qy7ROyY+yiHBhl562`Pfl2%@Cho*Fo`E&5n)CcpE11hnU3MF^NCIB;J8Z{4pl+PE6uYFo}0z5`T(Gyc?7FGfd(=n8crB z67R(%{sNPDA13jan8f=riNC@mK7dL5H74;vOyX}ai4S2Ce~U?c7?b!rOyVP$#NT5Q zAH^j80h2fnllT}W@o`Mz6PU#Ln8XE`#3wO{f5ap{g-QGqCh=)Z;-4{z&tMXt#Uwt5 zN&E{YaSW3iHk9bOE8Il#UwtDN&Fio@dZrc-!X|VViI4% zq--*%#5yLifl15-Om!i3dCN)c!X$QM5_>R-y_m#4OkzJKaR8Gzh)Ep6B(^b$!sHF^O|AiK}B0*T5vMiAh`wlejh}aUD$Jx|qcEFp2A95?_f) zto9WWUxi8B5R>?7OyWkE#MfXFH^wAxf=S#IllWRp;%1n{*I^Pj$0WWUleh&YaZ60% zR+z*$U=p{+B)$=oxD6(8TTJ41n8Y_>61T@Bz8RDF7EIz>F^M~165obN+!2%bc1+?< zn8bHr5_iTVz7vzU3nuYDFp0Zj65oYM+zpfXZcO6tn8f#B68FF)z890YCnj+(Oyb^{ z#P?wm_rWCYi%HxMllXp2;{KS#12Bmnz$AVUlXxH|@gPj%!I;EDFo}m^5CMNMLOyb#?#Lr<8 z&%q>q9+P-3Ch;`x}wFJKZcz$AVVlXxK}@k^M*i!g~_#w1>hN&E^X@e)kpS22l~ zViLcGNxTe`_;pO;<(R}PFo{=U62E~-yb6=}O-$m|n8a^k60gA|ejAf`Ehh0hn8fQa ziQmN}UXMxq9wzYyOyc)3i8o>rZ^9(rj7hu&lXxp8@dud1+c1eg#3bI1N&FEe@eWMl zk1>gNViJFXNxTb__)|>c-I&CmVG{4bB>o(ecrPaL7nsERFp0m!B;Jon{1qng0Zihr zF^Lai5`Tk9d<5+A`N{vMO~C?@d_n8bOQ#K$m+k7E*_z$DJcBrd=t zK8Z>EBPQ`FOyZv~iBDq^|BOj|29x+KCh<8;;$JX{W0=HoOyUG4aUmvg5|g+HlQ@M* zT#QLvf=T=Vp;@>cdFJKb?j!AqGllT%QWs5;2)-j0jY(VulQ;*HxGE-bHB90tCUGt%adk}M z8kod2F^Owo64%Bgu7gQj7n8UiCUJdC;wv$U)&4=^t1yWhViI4CN!$pN_!>;&#+bxS zFo~OD5?_l++zgZWI!xl`n8ep(61Tu4Ziz|U3X}K-Oybs<#5ZCRx4|TCi%HxLllUe~ z;`W%tH)9gtf=PTUCUFN$;@dEZJ7N;wj!E1JllTry;?9`FcVZHE!6g0%CUI9x;=3@3 zyI~UFjY-@cllUG?;vSg9_hJ(F#3b&8N!%Ng_&!YHKA6OPF^T(O65o$W+#i#804DJR zn8Xia5)Z^A9)w9e7?XGiCh<^A;)gJahhY*A$0QzsN&GM-@kmVKM=*&;VG=)zNjw^p zcnl`-SWM!_Fp0-u5zaKZ!{^8IyPlCh=5E;-@f)r(qI5 zjY&KmllU1-;u)C4&tekK#3Y`DNjw{q_&H4CIhe%HV-nBBB%X&!JRg(z1x(@vn8Yt) z5--FgehHI!5hn4=n8b@QiC@7aUV=&dDkkw#OybutiI-s#zm7?~9FuqjChoJMn!zA8-N&G%0@kUJIO_;=+ zF^RWe5^u#M{s5DB8z%9Gn8e#Li9f<5-hoN{F(&a&OyW;4iFaWVe~L-G8|#)fbxdLdli0*0wlInLMwj|#bviEA1>s}?xS+9}wx{a`2RkED*7a?{0N`&>NQpc}EB2ID{ z)>9^V;Aef;d_oz(mI+4G##d-2&DozyW{*_)j7ml=0BspF`slyXwrq6+(?O1)2! z!}D9EjyKA&oYe6~IqcUe^?SY?_9K-#?k9)+NTrTX&0#-MspC^~*pF0dE}ip&lfIhq z3nz7KYtBzj>R8yE(@yF*+MKga>bTgPb51(b(k zWwqO>)bSrt-ANrU7-e0mTvs!)K2@wjEy{C3Wjw~nYmZ7jSEH4j3_oMUN&VduWq(nr z=WR5{Nxk+(t2wEk7o)6umHJsE%5ztxe%^}Kby7bkMe8}KpDm*72P*Y*VYG>pI^HXK zy_5PqdbEX;kBV!lluL4lzl~|t5I!ZQuCtdVkceQuG$8qj&Y1GaWdG) zqpvy{rx}+zspAZyuQ{n>6r#(Vj5Ca{I~nT!t8GK-_=D&QCw2TmbfuHpKcjCriPvH> zeqr3=Wb9ZE?R6y4z@`{c)%)X#-cbvaT$TSR|wGNO#?F_8M1HL4y1srPfE#ZE3> zXQStx)X$^Q-Q=VY>NYBY2*doy0`WcJduTC5|L`a7al1t<0Uxmp#S#8ok={ijw( zC;b^s%VV45@mRI17`YCW#&zehZdF<%MqVrOTHV zP8zQhc`KZ>dl*+aX+0R%IcdC3=Sr17}&*w0ihbz6s=G#+~%`>jf2yXT#t ze9U@0#n;G_*^RG*VJgmH|MHk5G^@tC!T z7@u|0hBNYbkG;t95sW;KRT}H)*cvD85ys6<+9<{yPTHf4yPUMqj9)ov>NdZ2(#A4= z> zPTDNSx16-ujBnFgzV|uCA}8&6P0MF{sF9&lD^s)`8A`Qo!RZSc#uxe9KAax(ayys&w{^7D<3qStoVeaO`rrg5_+}Sa~P&PR0sO z>iCsdMJJu@8>{4`j;oHbzEtYxrdaxc%zdsp0SOS`ui)^ zmO5~m?Tj7iZp^+P>+Ym~$#{>GzMrv&lYWq~C-vj<-!R_qq^b4M-$}jZ#|Aj5zk^~A z&|ogl>s@Rpjl=nj>OPTP$jEkzO~y&aDNe?7jB4AEF^5rY8`4V{r#Z=XQR|e{xkfRy zZP+I2T%*`bV*k)DFskc%9$#cs+lkcA!?AfzI?s*Rd?!O^WM7Ic;xgZ#(0p9;#|f&C)vl2;}a~ul2NS}Qt#Ww z3Y_$x7*9H>^Q>Y&(m5`F6=RH8XU4URMNaBiu~^DU9h)9g-$TXbdVx1Z4zEyWpKa<33I2rdb)^t)olf-LL zT`u2;u|8dm`!ilcP4QsH>xgZretwRtbKuc(S{I~Q zjOy{+#queP>M``d?2GZ9)EDzQ6@QpU;pZ9E_dkKzzVRn%Dt?i1hLbvuEk4sp9p4t8 zMeMuA%Z%##$ymZT*U8|1$LBd2YJIBfC1WWg`*8dv{2JpMv>Lz8xW-8xLmz+J$z)x` z*U~#&W)fn4vo=b^Fm?uS|F*U(Oj4kLU%za6;cQV!O+)5qzd6nBsbaXP^ zjJMN0EayTAo)0QBgYhAH1ZOgia#F|XCLX2HELR@mq>g7wJmF-h>tTOVspEGO)1Ac6 zU@|K(&TtaX#Ismlk#V+@I%qnIhi#X-*-~S<|Z~ei8ot_YwD99e0~h+n&r}jNcL4*=)vG;H38R#E(wubuw|<$!x)R z)=B-{orn`Vlscw1q4opzOLZJ=f_+eB^1Mr2bTZp&TA@L#SCiL_LS8=$gZLK4icad7 z*TO6(brQ2MVr<}~zr}bB@i^4`W`$kpZhR*r`;==jJl4QPU>~Okkqmz0cDap21nd2F6c2dWrCObHpY=>k=C-VtL zwyVmV#K`tlnNKoyC$^88=O*uUQpcetdlB2%e2S5Ety0IHCi@fX&z#Pv))DK&oXw~n zKbbtwlaCOO&z#Gs9s?OI7}aCozN_~Ylj^>5AI%pT)qPU?$xDptHb@=cnS6$3;3bSR zX%1e>sIHf7Wv*acY`Qm<*rcbwEQn#m2ck>#ryw-EcE`8MM=+K$y_ zcQ~ozER#E()Nz){&uK50QP=U6lRCCCd62$i`6kAr^aK8o@i?8pA2FVEl6`}HugJ&p zj~UBQE&MfOZ6~cXV;!o`@-vKAI%(>%>{mrsvpmMw$Vt15@fuIzFc8Wn#Um<5Y@Rr$w*hdl**|>q8x1QpCC_T8DXl6|pXgc--pvk0Ks( z5$nzx%=iKEI4t&qqEF~E%(gFLT@|setZ|I{Xg?m$_>Ge~PNL|rlg0KgI^txpPZqHb zi&%%&WX5Ao>ez^)d?$5mMA45<>ez^)pPj6y8P7SXVk>X?R9dna{FL+UmsbxcF*b|-aAL+TE?lj~f@*wx8e z!N|5u^;Z67S%=YwAU0qe(Q_cGinr8y& zXWoC%d^vCh;;$mS$^!2*XubxRgY-2BZv@b9##)5jUVwpd(ai1j0OA`Ea$BGcjExAF zSTNp0$n^mX&{i{-8({lu`edrG0PNZ`?-ED!_tq(nDL8c>o1b7tBWFUOp0k zh!-NtKN5X^*lmIBPKVJq47N!g_F7Qs=%>Sg9FGwFbhtC( z6A*T_AkRRU3ZQ@FM1(yo$Vmv%?hFZ*33RE0ND-2tjv;uSJ}HHwU;8 z@c_bw79{BU@I4mfG=%8$!|U)&4Z_V9B){i#3$hO37Zzj~;SUyAr#p;(W5@`?e=W%A zB>uQcf%meDMyCSrM-h!41>Q#@8apWPUSZMLQGxdgi^fh0yjNH>c2*#b!=4I+bz#w% zt-#+aipBv7yq`ui4piWEG0~W-zTz|8oBLup$+dx$o2USWnPN#2L;lON4Qsk_hE}hF89wUvqadWz_vKicu0Z7(=fvt z3Op~1CZ_`L+Y?QoA%N#!(FD2$c&-#psS3OgPc)(L0WyjZo}fU2pE*N;VAmDUj~E2I zu4p<-fxo2@P3J1`_Y$J1LV@i7q6z&C@OMX|>2d|0(?!$O3jEE6Xu3&(zdaMsR~V#U zhLGRE^Gh~D&=0`&e9?sV0<8E0h(BP*K_5++d(<&)AI_E3cQy~Kwo39e=Y%ijlnvvXyUs4j&zKtrau(;yIIlHtU%&kgrH#n zfAIe=1p*Hjm`WM!TTL899RU9BR~$rt0Bq+G2SGysiNy%r3MA0a2R#aG0~80n3Z!j9 z*g=5=%671e0{-BCR|OJ}BkZQY>j>f?`W4`{0|9-LL9lI#gJ^4j#Bzk)6-eW-hXQGj zBFs@Bfwnm~P=S4iiGza_c(1B}-pb%T<>FwG0qrSy~*NtibD8;$V$}>bry7?saHe(8j^( z3M>zcg9!!Fb|I`+;QguM;N=R6P7Yp)IM3;qBD_|C*W<*&xeAI_56(k;9`fh1b05q{ zdCCzkP+;F8;^18hB*!7VTY-Hy2$luqFC(4vDgUJN}{972p+ zfWJ)=%`OH0epNKP75Ez!(cD3SjN=h@Rv_&HgxwYRyCBibWrEykfWBY{VDL9WqB%o> zzhx55;7tI3za*N$D*^tlNi=6Ekb$w-+*g6WpApRi6!_aM(VVBi-=&D=Aqr%SM(9&u zdy9Y#fk6gnwt2V$e*-3(`S-<0pNjBA1zy_{upuz`+aUqHo59~$isl;>cuhex&r{%a zXVHAK0{f{E&9^DAZAUaOR3MGp@NNatFgBa-QQ+@V1ax)=`!*8In9Bh6sVABrRN%ec zqWLie-hU#RA6Fm^blALDf$brp`Dq2x_aR)SK;kom&nU3nPe9jakiq@Q<#-;10v^*0 z9s#h=9np-j01)JS0h<7WzjYMNuPcxoiIC~wO|%KO;W`EOqam6x#sJcq5Q5GDGBOcv zQQ&VOMDtb!62Bt+R)I9`)7=UrxZXc2@b^>#dOd@`vl7tj8LZ!l=D!s9n-+0smxAJr zhma>=#lJ(G=b&>DB7eY&e~XvfnKPeWaB*^;ineriMuXdWQgMMXR4AUOsh+5#Y%g788G zk{E+6^$MgzMrgTBf#^Pj4=C_=5u#}yZ-!k?HP~xo3Q*%nF1j&+8=TcL3RpyRsAh zPQI)Q{*u|7I_CSk;)hfWIBb$(y26#PNVTOfljA0Xd;?z5}ie7(_}h_&ZYC{e7b<9VAI`2*lahIYOx({ zHp~?d(~Go`KBCWQ7ww}X!XtW!q2d^Ey6}szm@RG-4~iw?Me(M1UwkTdiSNZ;(Ik#& zE-eEGd*x}x+A-P*+8NqZtp>+-Ezs`L9@C!F)@YlwtvH(NC+(p2m+sIz>)CpqK2kqU zAE!^!%k?__a{YRJp?;_SI1bW!U4L8uP~WNlp#Q2LGBh0g*VRZja*Uxyv2lWNs&S@q zp;2Q*jO&aA#skLl#;e9gW1F!H$7o?LlujI^l`03}sKw*uWLY7n%UL);@eUl)_#6(V z+=+uMTgDP`H9F>}j<5ef%@YM=u3pE@j^kt|0 zosG_^opU-DcRsQ6#LiPXPwRY5=i56!(0Os^rJYxHey8&{oey;Wr;D>oYL`A;a=VP^ za$J{lyZF0=y3Fb_yUV;Tw{&^3%jzy4cKN!??k)$qYF)iudv_hsbx7CJuH(C&-?gl3 zr0eBfZ|Qn(*T=iQ&~;tctzEzDdbsP~-JIRJb<6Hn*zKfl7j!G@7VH-5c0;%M-5%)n zXtyW2J=N`*Zm)OS*llaK&%1ru?T-{kO6Qccl=PH7Dg9FhrR1lKNI5m-oRqSZ%9Lp- z@s!yqb5rg}xi96>ltn2^QkJExOnD{cjg<8%@1$%_`8?&@l%G=$rE002QoE(5r)H(* zqz+CkP92weX6nVMiPURTZ%w@?^?}q!Q=dp(milt)Td5mUKTZ8M^_SGcX+~P-v_5Hr z(~8nc(oRS_J?)&dOVY~If@#y!W~9wcTab2J+MQ|lq&=GUV%n=|@1%W@_G#LVwC~e? zPdn7z=G+Pz=*qVC6ZKdt-e-OuVirF&WTS>3Pe{!sUo-CyngX7>%s@j;Jm zJ-+DiU61`e8hae-aipj0*|ldz&#azg`T-)=ro_l&W_53&8o!&J)Ej=rJQ2L1UQ_?4;Uy@##K0W=4^ttH^ z(;rML(0g?6lX{=g``q4BdYAW}-uuelxA%Us z_sZUH_TJe0liuI;{;l_s%#NAqnOT|rG6!ZBW{$`ln>i`-!p!o_Naj_Uw`M++`DEr( zna^jg%6uhrP3F4HjhXLfew6u1=FZIBnZIN<_mO=%^-1s3zt7Mk*J~n$?_Jr(7*%xP5XV1=_mwjLM;_Q{#uVufRy)AoJ_7B;OeYL)w`}XcTpl?y% zk$q3;JE`x5eJlG$`Zn~vuJ84IZ}0nP-)H-->ARut`+c|d-QM?`zCZVE?x*$Z(l5JT zX}|IP&g(a|Usb;=`(58}UcY<$J<)G@zt#QT>9@7tSN(qI_e;OO`=|9E(7&|*`2Lgn zpVR-M{?qz5^uMwH9sM8d|78EC`>*K#djEI&Z|=XX|Bw9-<>)zPjyI=kPHIl?oB=t* za!PZK&l#69DQ8MfdCs()M9$SY3v%wtS(@`o&ib5#v4;(Y_)PZLW zoI0>-V07RW1Lq7}Fz~*CPYir^;Hv{a9=LPh_XB?(`0K#G26+ed9+WevXwZp+&L4E) zpi2kU45}YAXV9&K?isXb(DQ>{8uZ$r4+s4;=)j=AbM;(zZkOC%xmme6xkGbD)@6=J)AG*F^XFCO zMe?r7yFPDT-rafk=RK0QDDRoPRe7)Hy`A?_-sgE=-3S z$jBk54mo2;`H<+4>xSGigi+s=dR{CD?t@pj>`^>k`ci4AisB37-&;dj9 zhaNxl-Ob$yKmOacjZ1k{G zhMhI+!eQ0JW(=D*?EYa-4_iCzqhVhU`!!$Yr{@pKACh01KR*Ag{EPFa<%jd*`B&!8 z&wnI;Y5ptuoAS5k@6T^3a20eaNGa%BFuY)N!MK8R3N9`P6od*A1=kiVEO@A3X~9bc z>k8g0_@dzZf*%WhEjU#0ccHhiS7C19@WK-dCl+2*7$}@pIJ5Bj!aE8V6|O2=TezWc zW8wC~uL^%C{JpScxES7Lc)#JphL0M4%J2(@PaR%0yl!}O_{`yRhTk;&j^U3Ce}4F? z;cpJ#Jp7B{dxjqvexyh%@)q?h>RpsqG^*&hq6tM)i-JY*qPa!47TsBNU(rKF&ljyK zdadZKqK}KV7kyo{tLSjiKO;Jf$R1HL;*=3*kGOb5Y{Y^Q3r9RT;-wL9jo3V5`-tyG z>>bfG;_qTtakt{$#aYEU#lwn67LP7Isd#+x8O4)}{l(SAk>ZBp*~N2;7Zl%Jyr}r8 z;^oC_i#Hc$l3S8rGNxo)$+;y{N-9dCCD)Y9FL}7+ znUWVv)|9+mvbAJ;$@eAuOPWfKjMPSU9hp5cZ)D-f(vf3FP8fOS$csk?Mg~VFMqWE| z!N}W3J~DF2$md3`8M$HPrja{F{y4I6ETh@DCem3QT;|0jVc{=%BV@B&Kp%Ts$taJ zQMZh`Yt+4?7L8gu>a|fDM{OImbJX{v4vac9T8#FN?moKj=wYLe8GYvHDWfY!Pal2x z=o?1gHG0wL#iN&vUN!oi(OX9E7`=CN(-_y7USkH2DIRmenDJxI8B;!H`j{DGt{rph zn7hYp9P{0nzmDm6Ot-+y@{qsQ9}kA>s{OI*xWB3*918iPzF4rTF2A5~c+rUBl98pM zKrH5qSNrP%(-ZzstSTDt#{*H7;x8+&2vk;82dCA9YU{$0>Csp`F{6HF!>n0>XxLXD ztcX|pViA9Npwgcx55%p%zKUotQXNjj0>1L^o2kQPeH4%wN{lQQmTIMfDv4Va(Ow^WzXZqs7SS%4h*(-e2;ZQ}euF6*% zs0fz(Ln;kLi`Jlid=c}NCCbW@7uEin0NSB85<(C{>!?SpRCO8fSKxjHst`_8RaYN< zY5$}(vDP1}X?tWwFq()RU4OJi)Jh5YBk^z~l&GtU`P74v050t|h($u^1}pbSqBfGO zjEcv?{z$Ak7zkBZKa2(IYE&BXtM|vtt9^y}cx}Y`c`P1A%c`oy!)U*_FXBgoTPa+e zm=#rJP=DH82VyecO)&4qxR^|>&M0}OeaILQ_>MySe#QljF zx-O)st;}B?w<5uKc{o^S#lxYvFBGh*j@O~%%EI9qAOEREpOyP-K^(qlI1XAYM~|u> zlvn$sadb#1QCnxltNeBGIH){=s+EQP(Teg=ct#*v9#$P!i-HAIyfTbDIWdw5g?v#^ zI_idu!d6MjqlsC8ASjw?vm(gd&`=QuG4XXE6qw;hYgPt9v{7GqFj|iOn#ski427e1 zWhy~_d^Z%11u80|{X7`jK@K-8+WMR|z`iV*Z;P~%6jIDQ|C2WtbNU|k>{O_bwRQ7G*gv^Bw4HTurZ zeuh60ir4cfto2s~QFD9T)nib^eg677L;@jnSupCWMF*An>#9Qj3JlDkFYd3gic%J? zN7n?)1L(^bIwlegfKk-Nj~Yq#1yh(6wTF9opw4O#46T^2HdvR4#lSW|6Y*G}oLOM3 zI*Q>A+ME>(skRQ-v}sY2#S732RcIk!U0`NB8AmxPK!dSZIGRj~CCaUY00^?$7cLK= zkNsr{5DYU9Pzv~lMJHSyH7Y~l@)~QLm4{~pE80b^AAw@3!>#0nF;`x5bW%HZe)J7R zigpserH!5Pko1>F!(i~-g-73Ldo;>p&=(J*%<+b!$8uFPoItmNzhFEbHOf^I#uE=R zd-PlRRD|OeyND@H!TGknye-LMd+MhsT%advE++I8pAH&l1pN!&+v)OsVvAj*)aij^gT z)_iBvnW~aiyZ_#@UjCow+rR!lx2ysr6Dvdhs@5D;d6IYPs=}=|tcj`hGEvtS1$%3~ zPz<3g0j^RGIV4=if8&Wd@K{I+nApn0wPpS|2EDq(Y-8814!k>79tuVjllLk1jWG>w zo4_<(9j@p3Otlblh$KL)<%u$M30N2!tRhf1BM=HlS_h?Sk-*Fdm^E^#3`C<1?Z<4p zwrQWNxJvtkWHD5l)dH14Pz@?r7r=Z07Fijri=hRZZY9**#w8Eb&F7D7TTh6$!3fOr*VyBbJR)d9#a zfjHU`GcOoCMij?cH1To1mavH<U~q5F0`E5%hYvzBUZeBmfaC4pFgH zRPjMl#6Sr|k4j=;GZAT*q}rr59Gjj9#_?+_#AF?><#E7&AhOh=tykenTP(5Kgk|95OmWnWq!4UntOS`dXjHLy zWdLFgsbeBn!&(QpfmdFFnL=syMxOiCZOvp>{KPaqlqdjebBN{jRf=+bX**k zj!+y;7hxnP_Bfa!!zP^eyrW%mn-JVC5vmqAGzhgV&61({8alqE*@vTXKTI3eB{*&C zHRK{+IXGXOB_vS;#zK$t(@U8^DIa8m+(3+=FN@OVevde19trdeV-iWXFYYI-#h zuwt3K-;OZqG3Qw1V7=LrEEOBFqF~2})K#??WQV;!DK-C(#=5R@Xyhgh~$)6jGoPY9KpX1ChxwgpnDngBoM^y0W_*r7Kyc z7?@M6zCzQ+;+%sLUtkD>!4JJWgKaYqLnY}(n zGOns;7nQr-j;ao~s!ERWwge3@eP{_7bmQ~KS%|QW78WZe}v%9ga z`z%`!6R2%~WHGfS(7>Z6W|twxQ=^Xwq+J6YW%-O(!#d5HXR_IB+lI1^wj&@r8B}GP zO!2ZvbwdJrslOr=fZ%J5Dtjj45thJEEw3quNeadgRJ9_C7GipX%^h|%h_Ems)s=%O z1uJ0&k6X5JC08-$A721xyt+O1}q(iPeC@*#L(=h+y=9FS9WK=GRE1 z!3TqFjn%5?I4F^L4~y~$!XXS{NEtQ*hmH`4@)V?4YMZ@M&0KA22AUNDgK7a41DUB% zHGzt$y4x&TS);jZafbStiold>TODjNv3M$HQssv^g2lKPjJhz#f?A@Or2P>Le<*5T za_C5wyI@pTuS!a-+y~0!VckTNE25>#@RC{xgfU*M z!7Km?4BXY4w{3Ma`Dk+Gp`8L+7C_$ob2YEof>5;`G80mx*)rzYQB`<54(7q5-I88Z zR1u&RYkLqp)k@;ZS^BGL=r}ZscsvaAw$dN1i|L6lBxG(-A+y~Bt zXE=rtSYM}NwSnp?jPZ!S0>fKrTs)Im1RB8b0)>K~`9nNYMIq2P_@M5zP86#B!8Rjm zQ30b@5qMpV#o3sr#?b#DSFD6VtuM18n4Hi*k;JT7;ChiDjLNk^&V&bcTg=MC5=xV- zP??o52ZE&~U{+#roDG5YcuR6YMWC$;qnb2Si~Q#?(DTs`aZ_9NV~f4`&?L(8iD49D zmW|OAi3MZH5ZXQX3(979K6sFwBTsI)gQD5D)SO^Zs}g0CQ;53Y_u9`X#*n0?Kr+?( zAsB?!u{`BijMjRRCm!gN$t={+Y@eq}NJNghuW||`?6n>Y5@rb2LQmW3O9R_gAY4Qf zkg8Rz-d-tU5mAYW%tq{0F@B}b@0-#MLCi zB8DB|XRQ0`X)Ik;wLac`-3ODivW=@HVEd;@uGebwwbtvfuI#T!SXQTUh|L(}35Z^m{+Y=Km2e*M z*R$nTS#Ke1Ho&gW6S{w9kTsSH*oMn6hExwkDj-szdm!&us0ieUK*+kqGOwES)TI*g zRBTp|R|-|spQs3iRZIzjD#oKZ6s&}K3N&GpG?vcFtF5(DOFH2R*f!5vQz)cgJdR}$ zUOS3ZD>J4wEkluH!?oQqN@TK9%3$ZNOt63(k2jPB>tKF^*pDTS1mv4~G$%?~uV(NN z-vK*k-kp@2V2a>*&?04sQ!Jt42N9?t@iJa}F6Y&4n3W(zKoMqv2txr%5677AsEC>f z!&NMSL0*sBvAVLLYV|S*m1->$`9xS_L>0k!Y68|OBX^2YI#i^MF0n#}iL?UT%bJU@ z_=d@}HjEmmY%w3RHqJ_(Wif`;Ef!GLLnu!%NdD!CXah&v%!P;6g_8ML zH-VKn#=Ah&a)K`@Z>ig%Ab6Xd9s%{Y$%&96lspOIf~1TUT{LqI)@_lMnnY(LCor2< zY{A~r6p~`TrKu#Z)Jy;kjdu@#5F%`&R;$sFTG_Z9sYPdjb0lXRR;BDXYgA>J#FNrL zSOsRH*2_d4cw96Fqk>;8-`XrOX&I~!Vkwtxv5GUm&~8oIAbmAK`z$>cb3N?X0q6rt z1rFFt`un+F#rB**?jV_0$3z$hJBX zu7DsJx3=?Ov43W;HaIJgyoBka-6d2)jQW7h{4BEti#y=+kyu^09`Y6pXAw+67>MZN zP|UJ1D5eTx!s;1%!%E|x#j;sB*6<-6AEj(rDQ!BGory|D=8E^jJPd6H_WwHk$M%b0 zq;(E$oiMF@c(l-0d}d3k>DZia<|ef*$@&{ zEevOh#VTu8n^{SbiPbqQ$uM(CwhLQdEE7*_3g2#fSgF_66D%ZvDTXjepg16sOt4sj zO2Kv$3{-U&JO?5y|G~x?<{oxj?I=>oY!kNX01Q8&P*oT!A*w*}02U`9AY;t~Yx!k(7#pVWgiQo$@N)C{Fed7Pys4l9v$%Q$ z3@8L$1rIBx%exj}uc-hpgbZhGK>*w5_3l>PI;qAf{Q(0GwmGZqF{#HAEFHA2siW{P zEVN#-{%kWyevw75Ogj~Ie%?mdpeBA~8OQvnhKj90s(3v#To|%yu}GOLf;|MPZH}cz zA4` z;Kdg0VRqy7MJz#Ere(G!$6$T47K`hZaS=3&d1$5&)@hcDc^KJNAJADmX#uk2Fl-Dg zuWlXsYGWna2SeCug)L61i|jS|_HkG-d1hwHO|IBmyE2bXf<%YL;IvrsId!3SC@KZH z?b7N|mOslOkAqgiGwo$d^bv#;8*6j%AWLpccg{iJe3C>ny;NgH}_HG;1)*W??E`bTtpNOj~MQT=zxs{6X zjHx1Maw~zw9XqBp2b*6cf1qRtbswVXG?oIe&p>UDw4$&YRjU|`%vKn*R~Rf~9?MPG zreaG@mK@b~jpcJ}HeqpvHx^=NB9nr(ih?ogSF@lRypW~s72CF$kKc#B0C>wDlMVI; z!8R0(m16>|XJEr0?`ve61f&ux0&z`6AlIod7M!U%o4q2;d&QK1uA*GL zN=z0Yaj-!wLV|N(YnBoQ14r%Y^{H*XZJRS-tiZe-Z>R~@2f_WKtTR~qfvqQNVTXhu z)@rD*7FDdiZ8tygh;MgUfXx=k1l8TvJr%`J;?aV^Z@mEnKLBCb=779)5VV!tY8Q!y zSr*0&5r8QLl5rRmh(#CmN9`A4o@8A?#}3Ydbmb3O*R5s>wXDd~QN1#gM#17?fZ@f4 zSgb~g@X+^dmZ4-8OaW|5grx1Ss$xl&>6b;2O01P(i4p?=v=&WrNLz$NfSRZwgwYhm zR=V+^%otAZKGf&c8-`Hs>4c41u<5!^-qy#m~23TZFr*;K;`m|b}VPg$qgc8vX^i86a12Cf33X}pC)<&3?-R%toX)+p+V zUuaE%>d}xN>#A+BwujY?_I0ol_~-2f4`0$=&5B}D;B6o0X`6LY%>1w(S=0{GudfUW zv(*k!FeA{94@w$XKpmDZvCO25b*LTIIAJ12#URyTY{SHYy)3v1Zi~gbx^nCfs`J$a z%Q#RxN12Fxpwb|=w^G^Y}G)q zFt{fKL+qM_r3gkNwopN4@;9);1eR4BVf9}vQQ5YTWHYI1ptja46Fg?H5UDgP=2Eu6 z7po6K+o-Ej8x^r04VHz4CMA(rd|&-Mj`h3>FhgaZRO=U9y7KB7VAq(OEOu^-Rkn12 z?z5!?dl4a7=++(M*vXGt;!6>%7-EYTm?9XXJ^f24~bWw;k%G9Ew z$*raQ=?0s*Sf*hv&K4}&HU+{>?gIv^s|Py^HB|A&IsPyMGYa^#zX4oX*;|q{g?_>= zYPCkHzTHssp;}zw&1GnIsG9gXgSXbJwhvcgo~#U1fxfLjJ{~>l(jHXpu`Q!fB9hRMDnE;K33Lx|@am8b59;+Y-xUY7=Afy}Yw9 zRD+R;&ljrIzP~`Y7PMbg4Z$IZPYP61KxX5;i_n?z4MB*<40-|9VKzou)X04uYUOk7 z)7nG_dkipTA~PjrLsU=~s?gI>8Q28=vs2Ql>i;As$tMP>HE9NhAOQm?zUyE$PZ0|% z0}xJPth*+ur@{|0FV;}!uL^^kLw4bkV_e}w8b~?VVgj<55kw#P?fn%>b4HPU4Ilwq za`h>tT@_o&Iat*OtLMFvSQ1Lx%$2%+AsrORbaT_qZU~5k~=!Fe2Y%4L4M#r&kjw`FZ zRGqDn%ZwzZj2BoQ1QSW_oMJ<}5@5oX_5oD_oVsELXc(fJS{GF7#2_~HB^fwah2mnC zR%$mN#we@hZH);95Vgt7>n^WA@Yj8um!52x<)i zdj_D@gOcz|80cULPI+Xg5hdE8->e4;i^^}FZOG+#<3Lx zGj&~snv-}92X;9~R@LCzmUS4cLs<)9D#3Jx3C0dVLcBx(MFt(sdq;Q$1NsvdMl1yr z^Hnug{9!?{v>R5ZeY~O<2`Dj!jTF#$Ake5->u|@)DP~19K2|O)0>hAkQVjaRYA0xr z58P1Q)~_LYTXSSs|o)10_#Mq48}rimq>E!7?_^5f0zxZiaMFt5yux{96%PRN0F=8 z%u@w@6+;v4*Q)pUtgRU3%D{v^X=fHvI!FZ;0(eCb6RvHP;l0ZsQ;-y<(*R5cSU70+ zU8nt2o7qTdzSdryWES|i4!j0UfnCsnK$X2G2m9p$n3kZgSW5`CL1pK%M0xv5ff&@S zIQqugr=YZIbOg8&tIepDzt*0#TP;Ye1_XF5jC(8stp@Ca%dWPVT0C0S4$~y28l~bv z!Ne!8n6;I~IjLV*S0G@P<ntRgAH=PaK;SwLTF zT-^b;0ip9sbL#{DO;XR5`U+YfKpX^yiAJ#irKi$CLi?|goc@~P>#U0{KaTm_Q%A#y>kGNOdC+-&yz*)~j;$iWK zcvL(l9v6$m6XHp+7!G`%5=+I?VwreGEEms;=fv~k1+hY`gkzso;wAC2ctyM_R*Tof z8u7Y#1I~WdigjYWcuQ;$Z;N-tyJDl*B;FJ6i_PK#@uB!gY!M%et>P271llG(6Q7Ii z;tR1u>=a*$uf*5l8#oI3R(vOR!(Gr1VvqO{PJ@0D`^3-U7qMUbDt;5ci$BBx(Fj*U z2jKwkkT@(_#Gm3X@wfO#91;I&qzO&abj{GDW@--235P>&&7*m>4q8X8lh#@5qIK1} zX(?K&mZo*rdT2ehbhsqyrS;Y_wLV&wmaX;G`f2^O9JnbOs14F`wZU*#G(_`hL$zUA zzE+?WYQwc6Z3J8xm1rZiQf-trS{nm*M#pN$X~)B<(TUng+R54}+F0#WZJc(RHeQ>c zoeoz=6SXt7N!nT3+1h079PM1~Jnek#0&R+Rp>~mWF&rUXqFt(8runrptz4_n0$Qb3 zrB!P|Z5mu8g|u3&P77-hZMqiKVp?2FXfw2WZKl?s&C)K{uF$U3W@}f$mC`lZwc2&s z9PN7T2JJ>|u6C0)Pn!=1OE+t`Xt!#&X$!U6wL7#s;c)40?H=u3?LO^(?E&pU?IG=9 z?Gf!!xM6x+TckaqJ*h3$mS|6DOSPx9W!f{^a_w0-XnJ0IL0h4%)Lzt9X)kFnYp-ap zYOA%^;JE2^?G5csZLPLWTd%#PZP4D<-qGIGHo~FPd)oWjX6*y*L+vANi}o=bJbj{l zs%_Ig(>~X>YhP$Pw4K_Q+E;M<^o_Pl`&Ro-+pT@C{h;m9e$@8D71TcMXYChlzxJ#4 zoA$f*hju_~)SBQRs#!av9oAa3KefNKzqNm~Big?@PW;g|UDpj=>LwgWIdzxr);+pc z@1S?oJHeq;7rm?AO;6EN^)$V^-b3%Hr|TJdFTJ;(srS*d;Bcz1-cRqZ=ja3Uf%+gl zS04-)R6}&1K2#s3=j#P}p*~zM(nsjUdI?-pmFlDP(fSzu82wndsXAUiK|fJHNk3UX zMIWo53b$3K>ErbY`sw-^`b7OqII%iQKU<%ypQE3vpQoR%U!YIXFVrv6FV?5(m*|)3 zm+5}J434fU^nhNeSLxMyP@ksP=pnrp&ac9HM4zrl^_U*l6Z#CjUZ1Hq=(FGy>k9o! zeYSp;ezks$eyx6;J_l~HZqRSk=ju1<^Yr=p0{v$F7X4QJHaO3^UB3ekwC>XH*6-2p z)$h~q*B{Uy)F09x)*sOy)gRLz*B9we=uhg4^(FdK`cnOAeHol>E!UsbpVOb$U(i?R zEAFf2k^bK&$^^X3ozER(#zo);iZ`ME1 zKh!_cx9A`1TlG)$PxWp3XZq**cKr)|2V8i4seh$^t$(BM(!bTe(|7CN!>QLE{YQPT z{*%5>|5^V<->?6w|EB-0|Dhky8}%mrpx&$>(hut``k(q=`rrCL`VswKgA4&jV7g%# z(l8B&;WS)^+wj0OSO=q{(aGp+bb*twZbpicYNQ$6jUGl%xD3lMdKtZqOrwvHWn>$D zjebUdxDgv*3^WEAxyE24&lqC(;8tvyk#7_jg~o8B$QS`9Vj5fv?#~8;N z#~H`N@z{yRNyf>>DaKg1ARA|#W{fu`7^fR&7!%=;Y?5)7akeqpILA2GIL|oWxWJeK zCuJ8I7aLQJON>j6%M8C!W|SKhM!={vs*GwQXiS3>vyf41)EQwoGn;NijhGQP62=Up z-k51L7_*GajVp{RjoHRk#?{6(#j`=O=GRG&RB1}Wo$6sHr_Gb zg~PQ?#(T#5#%ALK<3r;kV~g>zvDNs*_!N%VJ~KWywi{m&#vjH3qtR$G4jRqIA>**oV*F|RW&CaY zV;nL5gZDTY98dc90$6@U64#BD>0NG6k;R(qwnpL-v&EGDG%~ zy=A8CBeP_->?`}p{xU}nkOSc!E>{kgd2)#K$)R$X%$EhSP!5OFxDm2gmdKH^RF0CP zDJP1eYetd(^#EF*HdjLMjd%Y>XE z>*Y+@AZN+T1B0rW}16_B7MY46~Qn+srikm|14F+1Km` zw|_b20CS)@$jmhdn|bCC(`ODfhne|ifmvt{H;c>>W-(j?jx{519{}kC=~|kC~5~i{R?;NprEe#C*zJYCdf)GoLY+ zo6nlhna`Uqm@CYc=8NVk^Ck0T^A+<|bG7-JxdyHg-!R`a*P83h_2ygV2J>z69rInd zOWb6>XTEQ4Ha{>wG(R%8m>-*4%}>lv&28pq=I7>i^9yr_xzqg8{L1{={07byzcs%z zcbngvKbU*WAI-hyPv$=JXY&_xzxk{AoB6wp!pACnTjImasINU!GWjpCQqOk=; zry)H3U7|@#i7vT_C;%7JRTV@*xWNwJij$ww1~b1Sx?&yC)$b9_Sw(c?=|uBq5Zw&i zdMwdBaBKVE8AK1GjE^F}$ImBPd@|8fE~2OL?q}19oIHE6*?~V(Jc2*I6jk@fDyW#Ip&+nIzI?DIw ze4<}bPClZu>1iB^0(Y-}qD}uALiA6Ngn;{7cF8K?hSeNK!f_4>R|gUvxD@WRlth=y zNOT)TBIP|2-Tfqb%p=irGl^dNNc4eQ-0U!kegjGL&mb{SkQjut!AQ%4`&S>_84uk^ zqTqQF!+{a&NsKHZF$S)Mk4Jtd{X^oEYe|fUJJ{3TCNXghi8KEsan@`Slkv`TkbYi& z5*Ndb?j^sGD4Ry2d^w50-6X2gNmRqFb8Un~WHpKC9uo2WBrYFI;%e04x?Lo0oCS~b zx8YQT#|ZisynExl_k0iU$H&9#H=cb8;nO#gcp*e$CEPQ=yqCnQc+Z;`kywwizB`G; zrkhB7AW3{wMq&$|*^0Vtdx6B~aLK!4IEkHSllbZz61(uO-3v+lumGO(;R1Ib+T=IX z567&DmaB1^?r_rdGI)F6O`69`TBj+bb*sS1w+BhfIGMDp9i;WU1YW6|NXvbYG+#bW zxjhdj*Y+Z9#Gg2|7S9~>8EMDQA?>8Gq>aV(_^U`e{Rz@0hDbYW8flX=;8}YWX;V(c zNgNYMyZC0(E;)s?N-@G}5NuMB0p*IQ;|fyAnFuRU1gV_BPVy9Kp$~pTK*1 zCTTb4k#<`b((Xtj?XK;l-FqQv_y0`VLx3KUyFZh`BqBW$Kyh!@!^KqgI?wy#56DcR*)QLw)pMc*^6r`V3jng6@ zBYnzMIPLK>(#uLnuXu#?;5gE2=8+!Bz{!|MOT0k(EaWl!EYhzz0jCk-{c{hHesdgN zlW!*d-fpDdzaA$6A|JGkzNjbZOYrQ|xVOBV^yg0^eZ||PuX>jBS3BTjmWxS$y*s?T z<2~yy#ff^?lfJ1RPU~1m`sQ!p89I~nkGqn-Z7k_u%pv{j(WHNSAL&0NNdK`D=|4?_ z7i7F+{~nyEcLnJO7LnfUC%vTro|QFVALQoK#1~ zX_;h9ID?Eccf;fTJ7k=HJ{cFC0PpVCk#Xr?WR!I!qhb}jiU;5o{wa8p{}HF|2{OVS zG9oEtOrMLB_wFYnj&dcC@5~Qz`rZ{}%$f|()}4S&WL$}RvmYems*lLH=6W)&n?uH2 zlxd!ijQLe$ENCX;f{L>3|dEZYSfpjyO%OjEomn;}nNSaZ1b^WW4khPI{O{#_H~5 zy!Ij)ub)N6o3E3xZVMT2p?%**UEUo8+=P?g#*(pV0#156hKvt;;54`1WNcYR##SeQ zJU>}TI2i^fwN1qdZ9n7Gm)~&;8+4;D&~`iK3{Yac> zgZ|sQ9A3bm#))Z5aDvVgI4$IHGJfq0&)UF&E&$rC@h38HT%Q5=>;_za8-K4PtQ_i*r_BJttI&c>bn@%OO7MC^b?ZLpsk-p{xA4Q zuB;*X;&PI!_L6)V?fS~+Bv(%)`PzDtYvz!A{WOwqxB=AT%^OIrMVZ#+kX(=VZ!k%2 zL_2Rn+1@`)^20MoZb4nPmXq9$Hs6WgeEBoUuRkOCZ3^%X$=xW!9<C$s7RnKfQA>rjvB=aLz(By+|DG8;l<&bpY)D^cHTP_H?i$h=`5nR6S+ zoHv%t1z(YQ3(9lbDl!*tCG(CIWZr!%nfIYg59AYMb21;BPv+x~k@>^`G8Y?UE&RS_MkaWOxi*8$^(fCfXt#~ek+}(Vc>fYIKRBMu z4;Pd9Ioe?Rb!2{ly6-%N%r7q|6LX{aZD%rfA0l(l>tya7O6JcRnZFhTo5{pc)8;{x zv1J{ZN1WuKkIA83O%9`s9OiytWMEjb2NkYn&tatv8T4&UG8 z7NdXQt}9&(HxL5?wJk>i+JavXaTIgVRIj^me;9GCn{4nN*e zaTPf#&nHI}uB&&GW7?hMsKN8K?~o&W4ml!y$T58dIbtV}Bi;}Ak{mP6Bu72!HFGC9 zF0UuYRW5Q|>mkP+y#I!q$+7Tka@>6>Iqv_291s0Mj>n%N$CFQzV+r29tRp#|8AFa| zQU4cEmzCqlu_}igFQX2tXAzvmljBX4VSP3^HlSTL;(hOrCC7)z>m%f`1@HM7b=mqG zIX){Q$CsUe)#UhQGC6jAN{-!ba{RED96v4~$4|$RV_zdVeuo`G4Xqvcd`{8>s4$l;EEyyW;dgPe3LIdS}vQ%{f+$KW~5d&%h>LQdC9f||Mb7h3rt?oF=M=nu z%3tKX_fC{y4NIV)cvXZ1(qoQ5)mE+J>#dE|^_lQX)SoHO>5b5rW?sQA9Y!ZEhcIpkb- zJ2~IFi=6MEjW&Kp&i9`p=Lg7Z>tS+k`;DBRA0g+CRpi`>cYld?`sNODev7*7-bv0q zuaI-^ZRGq3W!d)+Ie&ePoWG;22Yw`HODZ}4sw3w=E6MrqKjhL3a^W~@mw7I^oL7;{ z^%=Rm8RY6Xh+I9wjWS0H@Qw;O0KcRCs*K1L7y*`s%>nXk9I{$aUlwa#IVraU7D{987Kp;5mWZoxJ2u8AI;0 ztI6HtZgQtTL+(EBk$ZrL+_`u*?^$ve3UU|cll$0Oa-UF2?vodgd)zc~kN<$&XPidv zGq;iZtU8!RZYKA+CzJa^gWOZUBzM_Ja#t6Udpg3I&yjo9JaS((o!r-aLGC%o^M>ol zJ$Eg+=if)}n}?G7whD4D{E*ytPA2zVc>bP#T51&$vADoUw*HXU`(f1zX57td9GMW zp6l)*&yDAj=Qcch`p1;Oo!T3q?YAxiI)#P>6kk?aA-mX``q_%{-J!g{_{K4CM zH+ctCk~jAS^7>NATNozqhz;Z|!S6>+An%wn$$QKc@*Y=2-cx#!_taeSj-N^12@jEX zBGM-wAn&z0gglo7$@}!5nD>J@{sqP)5yEAH+eT9?Y&pY`@!4f-O`D?pDrWs_7&vaaR+(7zKy)Q zjwkQ;Cy;l~7v%jB_5HOQ@B?{&dy%{cGRfQ6Ox_lh>#xn^{pTX`9$87=f4`v)S^;&y zF`ylsU8#e&oH}&sOdYxesYACu^#9m<7cjf3^5B0@l9@RPnF*H!7(@^Ofd&;bWBpY8 ztQG4e)7AoFE$t)!0z)*dwGuByG~tdM?gU{1t^U2VYN(eE;svWz6ty`)Di={uL2wX4 zLK2dkNluvk{eJJ-d(N533??f5`~9Eh$+ORzy>9P%Z|hy}de_=rbNPGUqq463Df67y zW?cu3WL@*$lXboDqO9x1FV4DN`pT^9Wv|P+aN%{m>Q!0SF@KnKz2?@e>vaQJ*X!$9 z*Bh2(T?=PtUH$iFU4OD6>-w{oWL?L75O*hK-h4~e^_F*MU4Kd4zoP%QF3h_A`uDT0 z(~iu#7A?xU-nKRCT6|5`^|za|u6MpG>ss>Jtn0mR&AQ%qY}R!Kzh|D9buHa5>st2w ztn1v%v##@i<&uN5uFFr#x~_Ul)^#=I2k*(cRy>q-UH|5+>!ZhHT^~Cm>-yy1W?i46 zjn4qjzf$k>H)UNzk7ZpqeK+g+@|vvc=EJkDuYE7;`u8_wUAOF$b$t_De4F3z^fr!K zm1i#<>YJBmZ&{Y*+4=poJpbn+)t-+&Ik2cR&z{GN{I>quy!vZ9n%OCp|HDvUdwp*G z_@asUJ8zpkcx`7(fv=O_pmOv=k>0WXR@2Myvh2Ef)jZqJj^~X%__JE6-$*S}DJOr- zWd4g~S!H`)XG-P1EFNT3D-G&cgluzcGPPbA>Fexntnc3b&Q&9G?QxHxzC7O`INgqX zUH|H8CH~$w_KR|N?FmZ$plK&4A3gwFU+$qGNiVT46Xp&Jq8By{HdN5IZBOo zpAfhHnXenXmL9hB_U8Gwd;8k+?5#b65N9`0{F?q6FQyF-SbtHT?e5FnYK*3APBgW& zeEV3-*N(N^&&$;E%X<508R~$TpiGkv1rY-O=>bi|%FR71Ap2uO3mS6)<=1avlLP)L zTs<~y#vWMM)Vn}f!OXPbMf6xE^L!IX$#!2lI);J)hC37VWiN|{K3b~*+vF`T&8;6T z4>ns)$*EeFV_!CW3Lt*Fwwjh@+$JLz448hJd?`YMU5^<)4eZYAUtI@Nzh(;qJ45-? z0)?O=>se8Swc2HAe1UG@-B8Pc?e}4_7 z)CL{$ymmV&36{%iZG(fPhWhMLUd!djOx*A{z~qqa<~*;S!dav1c$l#SA?gPG^F$f<&Y@zMLi}L(&AsIn!G?V-dm*krf zk9^eBmfi)G5AlNL9m9D%X(``st*lK{ZC47X`?5iMS!X}*8a*q>-reNp{?(s2H_x79 zuk`_0P65pG-d={czxWv;D|;@l@-4O1pFD#S-MwhME$Xf=t37FfibG!)4OB$MI=gMF z)!v&RP^K8i)}9roqE^MwuxZ3d9Iq zW_zYTN$nm8?CecRbM!}m0&^%`yB##m`mgV!%Y`}EiT{(7oz5UiQ>0<9=Hn@3(Z zwZ|Zgl-F%}4*(OAk6fl&a0G##=YhT8qikQt{~Upyt>(l0q0x3B?Y91OA9H*+Es%RK z5;}$U>24gAx4nn*ZLe1kWV55un4sq>XHOP?16rBP^EELG`?oJEjk_9mG1&5NG#>?0 zQ<2|HLdcN*vX@HzuKHc=RA9I$p_!t23$q@;?){T@(&Xxwfq~}$$KwlbLovML%fL}4 zVYL-YsO#VPrYl}oWSLAg^T&R+fc!_FdwC`O0ElII4foJ=sx_9(vwcs!n6AxAeT^kk zO+iVZVNEoa|6c$m!pm&>UC-m`*6)(c;BEf5Ku0E}VNRhcvgEhGj&2*MeV zWbYC;$Jp8>O6)O%Ws`w0r-GtR;4hj?s0^g;g_K2)(WHGLxqmmf;&mWPK&O(>7uJ`L z%-B`y_>j)VAstDnkD-g%TP?3Jh{cI^bN!Hx#vvt&CuX)XP|L(nKctgtW|pR5OJ*^k zm|lDx-Lf1wq=Rah6;)OtQ3hOU!>30WMfK(58kOMVflqT*F>zsUHmDF&SQ>2$6wy)0 z>Yq#U*Gh$W{Q1CXE*5wT5WdO3Y%>2B6mes&h#(a&iwA#d*!q*+Sc|%}P^r)DtoN;% zcS7}~ld2~~^%HJ7>6U8cmg-G6Rc|r7ddo>SolvcGH$IO!6{{WX@tnW}rV<5evyipN zEKnqo0}X;a*G36Le=A+hUkD>_fGf3F3X|aRkXo60gNz)uE=l#AGiMks-SX_rn$yV( zU=L`2P@`~cPH{?+1IjrFGL%Vrbfy}cxftF73hx0mmq5>wubw4# z4)Nt2`JVN&gnuJOaP18=`Z*+#pC1JnLrm)T3BbcrhX-A~SH!+o<1+iVq27s{IRROd za^{ot#dISuoweHe*Hq5GhRK;`@eGvub|&^`o_!s?n0>v5#!H$f)K5j$m>cFdi)Vn% zj+A5WlcI+xSa?*s6c zP1i#SpE>m6JikDNm(2?lEObQK(X^1?8iTtrVUeGBQ#|-nnfby13zYos!9nrd+xzi6 zUyEg$9ZDUhe$cJO2)##4&HP`zlIFnwTSZH!432e6c3bdzuXL)= zQ2vCt-5n(5n{9GW;H1=_47yA{40{zDNPSYe%bU>KfV z?A5d4{R|SSrynLsN4ZttMwVsor~jp(d=&ed4jKTEZxM+ZLx@#UTII3C0F+qp#cE@g z1-C67{V*<_B^@tsQTq&EwHhj&DwAsV`#M*G!b!70A@7Seno57MI)941G%utoEy{<< zD9?|Tak95d=$~UtgbLB{Z96Z#WFbOx)N@@TYd1$eNAa#7KMuf_W}#wXLljW@r#$)> z@^!vua7AMV4D|QE2AAK!rZOx8m!oR-hU~85^6X%lGjBj8_Vi%R-;m2^$lCSLZXFVN zqkVLlQjw}P_8FKvS>Ax@xYiZnOb1KuMVd>w-Uu8qYV;v>9aoodZ37h}CocOOp?k zqs{MMmR*GULt#SAQETX0a2c2W_B9>|pTmn%+E>in&6(bLdx8PbGM~e$ZxZgWiG$bJ z_)06Y5Cpfc^yNa7VRYcKX&+&j3rq6mCqwt5-iCgd=MVe|Eb#OGBjAG{J0-`O6|+dHxjH#s-e%ntm5O8U9|hXb}hC@;(MI?tX>`xKoAAkU_ksFfXZ?Xi^f z@6=bBT$5@FN&*c7@>NjdI{*m3> zX5%ka#=gRmva%T7HIXvNbyofgjXUZAI_jc){(v;xyMOf(F#dqU`=w2-G4{Wq z>GZ2sD=S<7sIHpFJ6|*DMY-aSab3`xFgnu7x^OO-dI$~*0;F-=KNwT6plGYCs%w}d zF|e@=4x2K8%HZR<_%Y0!F$PZ()580OXb9zV`;QPxB_>2LB&L+JxglpEa&ucna7;R> zgrGKsH$b}=6uw!ip47`a8a06368@l4HP9cb!fsgz?Cb)Ark@fkp|_rr#3?m!8bFQ) zwSB3E1U=yJmrC8kE=>+aDaOik6QHCLh5Cd?3YDrmyiD32Se@%ESt?}4^h)hvl~`~F z{o{fOZ_t?tN!vR@e`9rpZG|3C!98B1hj1ifr&aRNt-=|_JBCmPq-ad*EzNKpE?c6` zNG$1N8Z49MFruw2v7<@sxJ7u9>dudwd#*#<=voL9h*=%v&Zf)sgK=16K)Y!{&H_S; zo~9>ByDw}*b32C<+k^(9t8XM0b5JWittgprs?WR)z#kmlcZHN|~N z_3x7Uq3XbZmhEFsR+}8w8uTKHfd^|bWaMu9eK<_#VUthzh^6SmjwAoMsMD@!F_0;- zLG>C-s`NAFzi>`~SpdE*y4K*+nsS^vlY(()4Cu4{JzqYnN0B*1ouUbm?QQYHWBQ#x zNY}V>uhnec)hSO4cOoZG0qT7XL}HRnQ2;47MFDi&rYH&SeZhrBD2Vw1?!sz~Y+zTHn@F(E8?PBMvp)WyvnZ!~d zo9I;tAL6`wzdKY1qB*ku*mnQ+fcv=z+WAm8jY9Q?Gh$Cj?LMT<@gO5wPDvYu6QYbd z+A;)fHfGmvba92sV5FnCYUbfWy)(~lU=4U@o*&1WQd>$le~W3@VoEmpI{DA=u`8Q) zW?>-K=K}0hhV2XF;YWn!aEJ3_9vm0MMhPf2rP26~B-x<{COdR8Z8g7R4J`+{w}jsSx@1giS9RjA^IbAEU2NSt=f9^lHj~ zY8fsgjLgVNNoT$sEhNS@KU4NtGSAMdxfk{3rK8bO0g7Eu(qGwiKtjHM3#!@=TU@)Z zidUAuCYxZT*ZX6gaSEi)uM5R@Zjd_XC*Bkf{)Ao~t$f(BM5*6}le`KOH0}x}z+R@y z*AAk_c#z~pX-DgfFBa=8+x46O`W(K50W_zI7IjCLyx+lD)mS3$)cc~E%Zg%mqSTVj zYA6DfXR;_OjlQV%YxMT32X}mnBHtMUq~7=63jYi1OTw!!shV3RBzXZvcl5mI70&)R z=>G?UE4~RDYqh&z|8tBqYBA7vB-R))q@4!3`%gVJX|s=$qbaryS1JTX3Da9JZ!5EH3l$;~Nwkul) zN48<#;>v;_{1cb4{TE3pM$YIkW+*x6&B%Vp?s{zZ$EM~C4E(jseiYU#S{bv|t}MjXs5MkAyHaVwKH`qtluV)~hM1s|ZrUY%F}H*1hEdgTbXYCArUh9k zU=ev}%17Eugh!fU)1nBm#gclO$nlBslU8c!09$YTp5yYC2H-ab8K9|4>#Op%w_!;z z4fuLCGoF!Jx(Q&7C*vI&ek>X6%Okholkw2($xp%T$={mi*iS!T)t=u`Tiss&iG5k+UUx-v@m#`QsHF4+`ahBM+6qHS!yS%Z$WYWsBEi5>q_(+k|Pe~y3jUk|W zGq|>YiV)B)LDcOZ;XU~_!R{3C=oUuy$$^2!#qpD8xK}`fhHti8Gz`C`$RHbG6C%Qe zQj#Ih3~GnXuwi(h(iG^Bi}X-sfUKtGXcN+35nezXJ#+#Fk6zB2vd== zVM%7&KWYEL=MJG*j7UWBON6E z2^u(Ol|q%6r;;!bL#_-Au&-PW5;iKP<=gH#je4t|kNiG()}GkPi)r}w53ZxhBKZXQ zw@y14Lw-4Y6axN-mG43H%G zM?duwJHVKJ(OG*$U$8n^fbxQ(W`5f}m;&`BjXoX0pBveNPv%eh^ZceRSm&!>xREHr z?PpT8NS=}BpSk{8O8R$p`1JFOERyG1b77vnexr8r8#`w?ne)T4=?h}B-EZv?a%L+zl*%CQM#ma73-)q z9YtVC5mkaoeSBy43s*%%%@o$w{^3x-<5a9O_mTg)V9!_Q*$M2eqE3{KE~Ue8*W@vd zKX;2IZkbFu1d~EoaxiD)@$8Re>dz=V*<736Pfo{cn`Z~9U<_1{B%r7nJu}G+*n+sc zZC~4bbwnutgqHG00WQC1S@uCf9%vNm@0uAm)iUNg_OnLFYKq%W{PSLYIoS&p#P7Im z8w=goZ8eTob1n^Cycv+|KCH=<0}Ag*e}Yj1_5X|P>?MJotmpc){Sih`bE~Cw$$osX zWLFWOd(t$NW`3a3@=dQ>R3YraApk=RvH{dj{$hpzfSUmLRyxQ zznNkC5@=USy!S<5b{|8^<_Z0w3_I(MmEh5k90>l@ndBR`_y?vwFHIUVI_fhTOXm5@ zDr5%W9O)`IJJ>1f{$BOvo!Gwd6K_yP_p6TYtfA)-vOGH(Vz-^(bLI z$gYcMJ+TI2RW1g9N<(Jvr2M;v$2fbA?Qb#bFo_Z(gJcDqWX}B1w%d)?Vg$w&rOX+fbjPrh>3a|8C{gi*o$DIQM zS5gb0*s*Iry_i#e1Wd^t`G_zOK2Dj4PYDb~*gBr+@Qi~sHqdc-4X_b}Taj)5J)IBR zCFkxnomyp*oL)xEOl1zk-e$uQB$ze>|9rTl$bGgr3$k1UnXQ=%LDqk}e@CM~vwZ@$)de&ay6z!C zn%gr-qwCFf11be|8+r{F@ouj{v!naHYPAm)ZvEH=*jJ-EJ5XT(hv?h~0{a7R*J`t_ zKE7;!u$4Vq3>@O0b7Gx&uq7jsbiU9`Q!I}hTybvYT6#Flv5gEYlJDT7?ZTh!A(9%l z5K2G~b^MvH3%ko4l5Dd@&Sw3l>%`uSV4F>~)tADCB89m9rA`XMZU}E0K)6uZ>t+;D zs=WrL9<-C~^73QD17}k{>%k-`oJ_mE>cczP*i=rFJF5q=(&u)lB;6YiyNw4)UZ}MA z(TIXs%?ra;-wppU7X!o={c1rp6>6AJ2LYKZHed<|DwB25-53-JDrqwY+=Lzudhv_# zzq8ZvV0ANwQA-POM+`>gpDf$GV1%}RT$W+k>Ymo0tY;PtHG+-^_cR`DS>bGGJ$joO z&{W$?W0?@eabH^pY=lc&zjR)Dyi+pS3*itqC!FLUjJS{UQGoJToW>bZ;qBmUPfmDQ zg#)+j@Dlq9zr?<7xWM(br|!!UL>%Mi5VG3pkp+%S21bl2Cd1b8yznPud?J+4J9>K? zUqyP}gkZKEjK7^j6A^C_u34p5kd-DljAfEN6qZpK zL4n-dhVs7>JEpRag|-;8GpPe4=#|tCv#BF*8d7mCOFN4mm|zmUE=5J<$J^xJivv;& z$tDvlaZ)Sl1khodg;5)}8AdJq)>bxY2GqR3K>%Ay2`?NFgNosWPb6kHn>2$uWir8^ z{3gI384N<$vXwR|yr%Fi9QEzI?aHufHaqa{fr0lj$%(uT;wcuPV@cI0gL>RKIl?t5 zQ>nio&kyhSJjC1MP){tCQ43%vG+O8ewaX;-VV+&h@|qU19}aoqZWbL2104;n_(;*f ztYE%qi)3Mb#W_4J*j+yadOpjqLrR_Z{)+~iE&2hH$SIi`d%*}~w{?hhq$~l5OhEn8B zaGA6K)#zijJq5rEo#NI(lZhbX4);K>^79T>bVI%G`$Dzy1%NtesPCVAz2H*_#UW9q z34cqk&30uY1;t_!R@W>)e!P<{6&FK~M-|t-S>2p>1!{GeKc>sYgul8XTpNd#=X0)M z@nwrIfHHUW_Ti#h8?o1xNp)oU^UkCEqY4jAw$JbAt}mJTr0deoHPE?*I6Dq5#+4r6 zke#q=+W#g9tx4Ilf|ykZ%_}Jl;aX`he&k>zQ6Q)p;#`3>#wZ=sX~}CWVZjv~5)7bj zVN^vus0OkU#;&-68LNjs#F*;?C&oQJ4xNdym6xp?!(0HmUc)wVg~f5g!Xfn93ap;SZRnw8}6n4Ulx_&FzY5b@C ztN+Q@t6{qQx+v3>zm|4ab>?k1)&S&xg(Edid}7^V#X9_Hvd#=5gc*}RJO!F%$|Yea ziyGC&-La64X{uqBOiQ^DoWv6d*wiWIX5x?o5kMRq-yAS%2d=b7p>dU&K#G}(=cpF_*`Y@U5|=_r!&Gw}Hj)emr1!y^-w>hepkx{|eKTPDtahF-k#%BwEE zvRb*adew)nzylZI`T57ff;uD|xC%*9L?w?R=`u@G(nUz(l1ihi6JMfK`Y#ZD)w!3Q zbJ5vy5VA5aAN?}GXRp*0@x&~zIc6OXlDw$I*#)Kz<};^D{LE@|wBLR7*A*P*NnLWa z{&-QjPrQ3%HWiL)G*GR)yaW8Uol5DpFGYvMTEujZ_UO^;7@^M^DUDOyZorJh0MU<| zrs{Aeu*_QuPTq`=<-^uzs`NGlX7PRbGyjxxO!k%GQvk1dkw(j<7BvmVvSX7~0|zf96|t)?Z|f5&p5steUuK|%h%WDx4rPy^0==?F3Yhoo{%qzq12 z>r`g7S_$P}r!enmA|Cvif{sn3ceWQz*D9+z8`qyWkuu6R_>_c`(IwALbz07jWj&k! zYs}Cq!kaWjX^EML+d@O$EEB`ms>@D8d^`nHtz5;aT)6CpKO?{UI2IAz#}R1Hi(`St z9|Y!xKa1B0=5e2MzVIi#CXXn3UVK*A`ALoVIyG8ZT;E?m_2k1kDu#8K!@4(y_5ELG zSPLE2ty+&qJFMaW)i~BK5#0SbZhCoGGQ&vt=*AxM6-?4(uaQ#C)yi(+USiWhU)#Ii&xhWR?Nw0Ns zlH`SBJIL{GQX~(sIUCOQjWtm?lF;wb{lz>jloovgS$LZw6_SK$KtG@GuyV!~?s@hK zN9M4z1%k_{Io%hR*EC7mqA>twt~Emc>>)1J4p6tlWoEV@Y2@%cBIx=Q9t-KB^$5fHA+msT!E)^;02!VtY{HPsqQak&tr0@hjxwUW{_;#w?i#2!TlnDe@y zxHr`wckzNRofR2iGGvU&N4$ERq@s z+ya;X;+mqS(HCGle$wkW5fQcWM_&0pvaDqid^~v8?y0Yo6@8gZpro@Df>x<&JhfQC zJKD8j1&3X`qZPcP%{*pGr$#H?PvmV!p&r}55mEPe>)eh~56KeSlPM6pim_bZqN2v3 zLdi18>fBDFOvKud0deBk9=p3V^aL>Haei&R?qs)1!p_i3Zoa9N@VC3#7 z8m_N9p9c$%^vuOaQ{4z3>K;pH4%F0gy&{slB43TOH#eWN=49od-7CF{n(2fjVC7H6hGjH@uLW7q#4a0kh^<*M@07>C2RJ%m zc^Xao{M1b4tBg5zgR|hxXu-bq)LqE4w-sF*;ahesCKGcMVpDBV@Y1R zxzLV8rQ1ZpfsGdnO0rW-m%%kL__Q8ze7sY|3z#zvIt5~Q0S9)5DbDd#mY>~z<+<1d zdh}eO;s1%B0+YNam-m3}>cN>pYuk=zJK%OZii;WAgnjow1?4Za6mOCw&qDS&9mfeOcA6|5E0DdtIXg37|xNg3DIbfy1D(g&P zJBi1$m(zm`l65&CuAl>JHlapvvyPr(^BO`NLm-8d;xz#nEn95(5(gTuy&iFWY!*c?Cb za`O7%`r%J}T{njp?4H9J)DfIA?>ypO$a&8ZVdL*a)p7)UT@!Voz#D5>;Cnbc{NVfKs+hKamT zQ6*Gt30r-R`4Jw#0$OlvsNJJS%yN`0Cp<_l%RVJwj>8N}mPz=C>{OLHw8ksh#Cd)c}kU{fhQdzM9=W^CDX6OuxDS2i#jfMha`V=By z4FCTkQb~D6Lnh;he0vFJ+=LQQLRX=)kY?~i9+Mu8gga}7RGL0ScZK>Yk|AWN4C;&0 z=~l_j_JM<+hV;^BnOP}i#jJ;H-Z8AeRm3ka;H!A+>W{!CvNHK$xx`r)Db8+h+rnQ?5V@pj|ViF1nOw zt#&Ep3f08Y1CFMI3RmifYP!Rhp-lTz64jJ%mxHLkg|Q$#nixd8jj1L_*fq$Bf=v#4 zH)1gIjn+m*(p@0^=~NTzVO5jOM-rCU0v2L+E{)SgWEaM%hF&mFjK4K?hq5nVd|Qy2 zcms4dx(W-<6qm48(A?)<87fr$Fg!qXar_j>M75I3d%!N&QyLRco462c&ut<%%#6|D zMOdbR3d&z?-s^(j|u4d-oQDQd{Qh2R(BUcw8-&Rhe53{weqQ;|zGLv)hF24By0S=euK_)%;0stU^Vq zSGx1Ie_;@9|8HC`HXW#4P%#SCIeTJo1XNRv(O;1Sp9I@d#xbhz%y7)UL=G5b_yJ;+ zh7PMCIQ4ee;}&KjK4>Wrr^f`X?;U?}oMpC5vjP7121IJx2N^b$lGNO$ zv}~Z=ti2b*f((XhI3hZtul?PCGq{2~lHCE!*J6}H1U>#+sfLy4?c5CI!!*$|cxj84X3#8X&0Kx6+&5p$&o;b6`9}_x_ zJgKS$4IelU+StI8QKC#TXJpw2pm$iquCtS;uMyI2kaH4NQtp)Sq12 zLDbViEZP!y*AF>y0i=AS^=2&lqPQ!MP^@yg4W}1TKB~LEpi|wHWVNi-4(kQ5?HC%a z-tGmd8C=U~>G1q0dp`1InTDzke$>`?env5q41NvH-KhO6$HTsu17k_~o`P@zxGCBv zyDO3+HknTdEz<@8v+l9Lf1pDAh-X}udyL(m;46-}zXCkQ#)DWClg?HRru3^&Tx}086080xZ=}g zLY z-6Ae5(N&m2iAit}n+Q(HGAV7!GSbTox@#EE-aQcEo^}@&SEu>Ypl`+{Hw;*5#Q#2U zDRNW0{qjT%Lu+|G3sVP<2&;!`zcF1v60uaVtlfK%aG`9Un5y6pkRFFI)Y`Bx_FTXj zqb^1ie3=G^9@=QZPNzrMKDd+t!?E(NaqYQe>I5CUfHY!_*DloR@w;ua|T28gaKk)EY~o^M_cL#owu~v7|NE z1SknKuH$7NpYgrw6}*`B^;==wgj z>?fcTI&xrPJC_~5e-$*mLn3`GsYMAt&AMuIQXYciZvpg73LLLCk<@dB!spC9)>ty{ zx`pj$c7V#s_JILOZ#q^hYBub&*tfX3gHzQvcRxnGgWhrLT1fj;oS zxsbsXKW{@5>8Acifp?hwuB+`gQhoN`-cVpzjxI*#T5+{p5d6WyWuMNzrfXyVT* zU!q||rkYziKt*Sjn>*RyT{3d?yyH5Lqs*j1713-+&xClQTRV=kM=C*X9tu9x@5kXT zHV{R6DMF20Q;}1-we!}ASt5$Ez>*TS168c3&{iwQb=*2t)`?RA%>|0oAHA&Rt(z!v zxb)!wMl-Vy5e8L6?(ydL3h%vNk@QfA+H3NF?h%Y zmFqfIG0t7vd3!&7d?7A;#0yJL55b{ni4qZ+YXhn~dscXH?tQ)9&Dsp{YY88iGGaCE z@AxQO_bFiCUmV8-xsNmO9x)qz5P{=L7iaO*CjARUu7)hD^m1>U2L* zzpIE5m6AmCA-|AK=r|&BiY$UOCFCFlut;Di{S-Juq5_|*EA?ZfHHb@1wS>#HqO-LJ zE9_DaAxXQ?L!KW^I3ts>lXIM5*y6c|u<*mIB2oi>;fObVr%5UNs}bo486>eY30OvYhn;#TRq zpjx>AgofD}D_e^=se>>p`ZX8`!886QNnT;&M|o;iFQ`;6V1BT8SPxG3E(1V~7_=d@ zv_?~EF2iM%8cOyv4P92PTm~LuEonnhL{MoXtSuU%jUWhrlMI`}hN5F@RxhhmIfo-y zsa-*}A;#hWY={AbjULVAFSO|G5zKp(6xi4+pff0oeZpGS6N7pjzz_zCoW~E4JH9w< z**%B z-JcurLk-CNm`4R~%jc=L#|>|khP+1w6mA*x|3q(%d5m8FOZo=xGQwWI7 z(Hc$1qqY4v7KP2DozII>yGh;JP-=Lp_8XfTpD4}ou%5#lAG3>TO+#91SVZd!?}VXM@?M>92h+D30}cbGd)0{w*h6h3ZS8H6po6A=pO& z{C;oWP{gGJanjUU3@%&axu|lL%Jj9h8Yuijcs4?3v*)QtvZEEvL&HT^8Dz22j%;T$ z`)_AG$FliiN<3fbB&ZYa>5yA?b3zDBEVkilbqP?)GBplyXfsi7mGM0ht0oGO$E z?^d;y5WJSltq_>l@V-kuc4J)3le;i`5vx?u?T9vrhp=4MF&EIPKk2mxyyPiGq3uY< zm%TP3;UBS&y8}mEpey#Oi=&iMsiKy{dIg)9PN)RIWM7?>7}ZPQJF@p59{Bgk5Y0lg z7@J?oY8j?@j2UbU(Q=9_C65PWFA%U>A`ayjinB)u!WK9$L6L8R{_IEr3jRX?m@#;5 z2WoVb05JmZ| z>g69x?z|?i{Y+Xt;Nv?>bFhDFZ(rpcI67=BH*QGTMx5No#mp!?434jnm-~r;2h@}K zk70Ozwo2m^RY3tLz?8TxWT8h4MQreay}irg7#EkX9Fy6G2$xh#9%clx@G!>_;^bkrOq)_? znG}dq$89k3@OFIO{JqFJD8)HQ4Fyp7R3_mQvn@lf_?O72G@g}bZ&Na~4N;6R zAzu00Kg?ATY)J$2Kh$dVIb2RI%df4i{(GFHYP3Jrra2lv)0Lrbucp2Lu%G1BJO1#7 zuLDvf|ihX?mB_n$ZD?f)0)5X%8m=5Ep8N^ zp~R=DwVYQ_7Lg=M0U9V8>m0J~Nr1myMfh0A(PQS}U_1kzh8X1eKQky=SOksGCX5le z`!h%Mhnk*ofjDF@OrYz=HZ++FQq18P~^zVJ29TmIk8sM$+p)VG9z@Lg>R?ktCse~9}i-IcLUMl4s# z8Xqj6(=otu39T7|AY+!mqBnB)S>E=FL39B&5oiDSfWMpdUHwS&rZR7uZofZ0fM7wiH7Rt7P;(a zwbgd=e)-`P_#-<7dS>r8;{D?t+)ssueULMXg1ozv%_n0^5?A#H)0~LrM6rfJNu+G$ zi(D4xx3e;%d{S7C8&QNE3{=koOX?L&EMGmYf_@|^@-<8yRjxnJ^Q)}4ap08pWz0FU zmR1vUE{pMe6T6xI3=Axf7K^RXJQm{E$I@e3H%^Emn7D3C$0PyL_&FP`aDSKg@v)b2G*Y1gfM5f66KTq&K#d8m-ii{5ZDzZ=sH>4l8cI*u(l#j9u)+f3$XL^d$l05Lowr+->{l7QHve#1@2}2x2sG@6GxVq z)kf!Shl-~T4$6zqQyx+xwBHLe{U)f- zdI4o9-X9Cpa(+b(Zq9=(7$Rvs}t*z+URWn2f=$<5i%z%0p4$o=&46__{A_h**P zJ{jRN2g}C&WMzN6EW0Jo-tz0(CEV1C8l+WR3~6^!fe z`fvA&&*~two=q{Me9?y}bX^~QR4&D^cS7)ppXAyKY&*6A*)mUY|2W9QDbC}!YQc2V z?(dEz8hh0Di^zxM%6Ran7i%moP5H3m(*w(%YgCVi4pUl>3v_?{I@aS7Zh29mxrGcD zxk25^7H?-plGaZssIKADcMK!+FwL9TsMOmENqxWtMH0fs2fO0XNe4=R48?yR#DbRK znrM^O-!AWr>)sZ5k=U9(i-;5CDI!j8aI69ppoM3F2BT}*rhlq$ma~d(om|T#hN=S% z*nJcb0T$&C8y*O-s&8+RC?GE+N)JvzkRVdpXbDNHRBuR?(=B?o<)#HluYmY8oe!*4eTuH4};$P2Uy-T4>J1C$hme{Vs)7M8$q@}n* zOp+JWzAp~`UoB<+3ft(s%g?#w+-l|A>Nyvki!XFE;%A>jFGc6%;a>v3;`o;;WwD-01%}@RckFpkT zwlp@-`M4J~=RF7JeRB27(9rMrGIP&@+hCe9Nwr5-EWt#+;&nyVSdt0tXK=AK^GBb1 zx%l-bz0PJYxGmMn_FDZivTVR49qxPT#i@go6@8gZphQi-JLW~wjdLaak%D|MJn!8y z=`!iKHjek?`fmKK($0(_!s!v>5!$q=b6P}MpX86xQR&L3%rNP2bZ2`q$uU{km{&g4 z^lr|n{-?>qXLSy(!;iKDPn~@eP9+}77BN3tok}x zF{Yy&YzD%@zz&Q0lV0Zy?K6vd)CAg{nQn2IZ1iMWj({TO)F-rmjHpi0E7P1w*NPJ^ zqNJI9xz~#!Jg>j@8Sa1NM>V7kNhqXzaI~G45<}6 zAFF9U>v5U%P5J~OEMaepDK6Hv?65E(A~$>jz`1mu-|m!mue6BC^mg&bFr#Qpd$E5z zLZgH}Gk@e?0)DT-!=7}PCT*9jV*=woUz&Q7rgUCa&|9(V%l+}i=2qHG3>)E5;UlCu zJ>u>_8g)!OK#7|TfvJqU23K39W%jDRN@&vB41h}ya{WO;e=Yefou)MWY`tvtEXu~Vbd4+n1l8> z)@_+gIglt%R-1L;jKX}SxW?jmLnx`Bu+KsdO{MDIUH}yKSs0$QO52)V36!R>Sacg! z84!SZox%yDAdshbVIi1I%Q}5bBx-3aCK76R+G+ujeMPrSVv@{<2gMGLxZxIxvZq{a#%&&|78DMx-K&rLumJHzXs9dIgR!|7 zY~O~mOR~T_zzDLwRDAdNc!eZY6te|h0l=3O1%i4EYW^g4EmuVYZ)3STevatYL;i*Gt%AJeYf1r!zHpGsb7O~h! zRdn-J?p&N!9EZci#SRy0uv|lw8(-!z)RwKiav^d!(l~KbZQM55D&h)()UWF%dsibo z*}z!dj5y3;jc~1HO8U1pbdi9Dae!T|!y8xi#fWLh?Cb8PvdmS%k(*4*lPw57V&W~bv`d|`)A*Ln)TpL6X~KL z5{y?8DJ3N@n_`m@i2?<%HV zL6?vC>I} zg$y<-aA^@NX9%!b`auiz`!NCW=C_@WA zMA(y;IHaH0D6tpCWu{Y#LcuP3c=WcwWA3Dc`NU!O?FdBrv8momQmb**PTbiC1OW?e z#Z?M|#TFJf?5+aHj$!_a2Ok2sWVy|)W>o&9!4}KMExZ;tJ$%yliF_KH>f9d)LBno( z`LgUCRrb4)u5ZO>hJM0yjFjGl7dOwouFieCyIN($7$`JYENz0tra7BnlrNIRJrvtx z*E1xo?3V4ZJG^e=H{2fkj(mKwOX(j<(~D!r?J4aD-Dg}!0pGyM(1ODhgtoV1drD5&T|6cf%)q+n_rR+ruWy* zVi3x}RtR8Fs`&a2CWV8GMZky@2q>oM^arxa*O;Ip4rZ|TMIEf%Y7E1pCKq17#Mi6# z8(eXIwetUvH=;#~eMK7`{mh#VN&+U!h_a+yMc7HQj1E)-zeBEkphy8qJ_u6cpn)^A zB~u$Vg&}#Fi#9OUZVfge{Vw2Ku4tU^XL42|bwujH0Y$96a?VNiOD_at2RZE6mrJp} z5#C1h8|`|Q#|Nd#ByZXAVHz3BqArUZT4_HMh}X#D+#Q1~C(gT-Q-D-*S&i3#(T4v( zpEr*2R_Aw%PazN=Hz)oS`!dfTR?vp&k?jg_`%{hfud4I!@X7{Aam}y2WIgusQ`V~#K~mVQLAHq4V>5km zy01Gh6DRpN(+6-HgPi^Vwt&WiKXn;~o+z?lks>Rd&5WVN`9fL)L}Nl$CZEn^S*Zl< zClX`X&EZ4+BB2tgyhD~vV%3frW(BbS!x`CcjWaHSGyW0_g3IIW|4mejr~cl zv$;XzgP}1LdCszoQw&ETCR)e@!>1k9>9XexbUqW(7153N_+B4dK6-zhPAP*&m+E+E}i{!6(c%UcVZK%0G|F zd^bu|P0F$e@eQS<68c5WV-rq}lm( zcN2mDh~|aJ5d=gGAvT}Fm-qj3KaSK-Q)RK41tm)7O%N5`7nE&o_=zmC3Lg&0n$(9w zy~2ls&OE(w2>jH%8GyCngtPuE6pI7@EGj453}7BK;yt454=h$*CXjZsp6Rt^{YE}X zgrH6CtL80v6WsJbx~?ClDqk0V{Z^;Gd8yG#@x|E_swbUPJ%L^7&V?Nx*YG|-Wi+go z>QOU1$bzo2!+CY(!|jmtGdBI8Iby=PxdWvZdctF+9!;encPv#-?|k`G20;^g6wU@z zEkj4avMVuRW1@8o2|uZL3>yR(ViY5pfwqj}sOiC<)yfXX_;n=$vl^S9G<~PpG^Tg2 z7Z3#rCE|r7FBl1TP9M*{O5?71D?7Pk8Z#h$ww+rQijVqsvk|iiF63++&BqNIabnJM zdY+<#rOPDw+@pcg@!E{Y8cSlIb5%yVOJm)S9#_lzlU}F0G_2*3KeFn7vpJIFbB~4{ z>{et&U+JKrprmtzf|kNW!;JwL;lh|*xD~=<0hr;!zMaP@O*h>FrTnTA9K@|~ zEUx16)4jt~-4|IPV5$^84#5<>b63eb9*nu=q?>GYIs5WqZ4}lAd&l&PHKO=wluX=G zj&IvzhCr?TQ?}ZouuJ$Uwx7+jzD6uD&e)8{cq0NzMDf;;Z?V^TgKVi>#);>3jP56F zZs+;+W~^7-hphSOTCIAp0A2zTv%eCPA5*EkYgjKRKU(a*_Cb;t%HR>qitgay>J9f1 ze_~0sQ6pR(`^Jfjr&*|!YZh!YRb=zdeArXE@?b$g+|$R>*P&Cq_K&q-+QW(TT+S{q zsFsb;U#)NE`=R8M5Ogrg7z#gpVHQ^A>qUWPMI45kHvXv``;%T%!?y8LWn6|2k6K6{xf(`D7OmsdYb=A4Mm$mWVouFOU0 z$#&l8>~=)u->9yknoSe5X)zudOIiX!i4?deoV?4=a_yKxLL@4 zR*dg{;sSoRj>e&fd;7i*E%%Vb**Pe;3oZgvu`AV36R|Y~w!Jn70ooRC#QZjJ7Mn{R z>{MM8pWu{}+hp=hJn$=fllOJC)$qq3_r3t<9yicmsqIH5o-o8fna5!TCuDQ1x6Z76 z;H4mLwqKGE)`(ztANgZ93(SF%@xgE|j!iY_cw7zq*jzcHOU-BKlb1-lPOHbxz^n9{ z|6Cwm;D%U!o7sPnJc2eKp)^~D2iR)C%PS*Z=XGBE;q*tw`Jh3N;|m~e$$NtlUc)be z&)3sUekf~Ju03&gFLU#OsjrvoR2V}R&dY(*F2QcosfQU11Xsh(*>S@*6OKp@*kMZW zGQS(9<-?649@~=c`Nw?nLf^bKcFE3zbYQ?2Xx*hqr5!#QF4Le9RfQvB7Sgvum`i8| zNua;9nXXy## zUPCIsUAcOuMNx7jq5VNMgSJ=xaFE{=TWvHuEYbIh9Y4N~%uKkH+cG=lbuK~$n7wJc zSZc5v5ff)Dfi?gi$`?o>bgZch;O~ zaqX*6AJOATL@W)u9nBns^79!$}9tC}0)K#+0_Rm;QxD%aw@)?-}+cdFaaF7)s8)|o!J~>2?;J8l?p=rxc4uSI>J~_m~jU~ilmpOiksQly*quhy44lRmO z36%1aL+JeO`i<-;0iqH<(XY}fv@J_R2$Q}m^zKN3b@w-P2f7@Fnx*J|`|s{r24hb5^@{~ZF?vTl`acp=+N zwr&kQ2hBN&Tm3}QSm>$yk%UAzskz7L&aIpO+3=MG-J3a{!p>;b?)NM@0?W44gG}aSLz>#z|Nv(S|Ws-xWX?xB&11 zX1eq%_TvC!P~zQ}09Ga^S=7fSb|Ms%6BRsQjZY*r2{Sk&0{!@4YVWHmFRcQ#J6vZw zJKS*8H49(bCK@P`&&+kUk7H+y-JeyTdmJj_nLDBQyJn%Cic;)iS(hE4C9!v{_Gy@d zm4H-Ry8SohG}tzzF0@ccM`G@5wi6(n)f9ngO-@TmIn8*iQuqt$NaB^4Y;sPUQVg_u zl63s0np0z`6)+6Ouo`2b&OA&6kgkqMWn;)aFP> zMI#2Et8`+Ka^`QUvIkmJw(~(!*$z3>-xzYh)5N(%EtB-g&Z_KMi^@*ZTrr=ZJU2K@ z)gjoMR4b0D>;t93J|Hrj3tskhsRk1lpb_CyKVHA<9;`ixa&(pxCfkKDc%Win7o^ zk}zLsgimI42&j{a@d-vdfMvEyoM_n*iZ?zWMr@!2{2dU9wM+s%H3;a@NddoX0w)Rj zq)}XBuu^M1^AvS0DgLPgI|{#;2>9Vl@e@6XE_$YaN)@==g=mc!vy(0uC~y&2(W=(8 zF1k+e13IQ{V0C>MIDQ_l1h7aZ(97sB>{6q{0`vlYsT!meUHA@y=Atjb4=%woBPzm5 zG<(N3uo(D+*pZTE5>SHDVKg|5xQL|js1ttRT}09N=-W&U=h(34ZGzPt1{FTdnUc9+ zF9jaZu7D4{R$kGO<^b^!EdZZSs{jeX0@LxL3bXkYqasZ*XEj1z!B-aH6s z9yn<^Hfa5#N*ppgaaC9V+}WAO1u4ayDm#8Ue#g5wPHTS5>3QQyA{qDp#NK||YKc*t)^T_bc-jR!I$6(G4SmWO1-&uyNjGjz3OwBNt!0d zIIsGDq;*b&dPneHe4uoj2<2>CZ8<@4EJw63Ky`^K4#+)T=aV_h}=$6yVhJzNJ`zq#>62vIg zSei2#ID|B$7OM?LjEMQ07&TmzEC{E_6kWMRFmw{-@vXfiaeV1?hH70y$|51`5a?5B*Q_$g`!=Tj|y5av0kR)V^?N!MvMKkld5sHL&w8)B2ZI~xiaA6W(7bmLZFh`LoGzR)E?7ssjm>!Yv`!$ z3@^vcUcwBX|9SuI%U~}zCG)48gJT&S7sO&|(|8^3Qu<=Qbc5`UxsTzX0Cxxot~3h7 zCL8i96AxU-6I02!gjTMaN-(o&w1c=wXFy1TnY~a0BLuU2i&Lv8DasJ(Ndy!2R!=IN z$v##%Lpe}*hQe8T_5$yE`N+uh(~UjDL9wS&lFbn%2 z2Pa*9%V3a?5K-TGD2SYW(lKDD^H6#qNxolS&qsbt-;qU<9!bKntBRUAUtqU8f70uG z!~)jJpZv(z$SRZMufOHO)IrLMzDy=iqGo_bEkbMa?7MO7yCL7C2-=Z>MGPMt_&31L zp;W%U^?~R;P5H7aP&j!+Q#@R**_03HS++{@X)2gyU7G2HhojrnRzTF!R*`&~3W981 zur&Z%#lK=02HcMuVF6{?AGn|n$ZeL-qC6k{n0F3$G?InE36MOJ-4zDT%8sj$gyx#j zg`+$HUegciQVxstWULv_>47LkDyDw;tUIZs@l``|2ge(#s~s0;3`evG}lb5o108Eii_` zyoL8|H-*7mcG22qh-R|DQ7T2gUvY2%goH4e#aVG19Q|}` z=E?|yf<}@r;f7!kWF305=#c8ex<6f8v?_Nhc{0*N&G7PjKkX{qxj zFbo9XDyuqO+C0TT+b)jJ3Ai$0+isCY54_M>qcuK#B-s9>*Ks3l9*Y>hmqZp5Zk`_( z)c<7v>T#Vp&VX;q-KABO$4RpoG_KyUayhs(#ze*#nkpx*icFJt%_yu~^Vv_90=X(< ztz+2TiE3-Fvq_oMJb@_jac?a=GTdM}&}4svbA3CGN*(C%ha4A%AjxQXPl@V9d(`4V zMzoxgHcIbNhCZVV2#Q5iFAaIMrRpJ(vxOfMK@#c4k$QF9XSZ>vZ)H1}+QrJBjLIxl zJbmJxU|iDUdYL z9RA|qAcS0lp$MLW4878BO?b`Di>TLoLhnBsi>(jwtS(7}kWL?i#j-zw{)_wy-M%$iJFQ~P5*v9*E3MW;B z5v^fs`8Q?$l`WDuPM8;SC~tn}i5+V?Z|+z%f8H9_&dpW2R0ji?m9*6dJ%#QFw4~Y)Cs@hsxe{6RQscZ%838amfyG zKTMlx#dHv;%kK_dv6>|k$7 ztTXj_Ey5;+8!7UXp+9~cf`+^h!*wOS%z=pfOjAVTATZzdB&EZhcZ}o(K%9LI(i787w%G65;BVFxPzfwP~5z6B(Avk}U?ZD}tnO%AE!vw8sSkKT?smv9amj*zp5 z6eEKgI85OXv64#M7iSOaa4Jm< zMR$0SC?U`3Vgoj>fvndNKBv z=lAyS186RrjU504m@4AlSGnJujBCd7M+e6b!7-JibDWI$QY+&G0ZE}1S+|{ zyg5T7lH&}BR=d1KTsd#!e3O3?mBhrG)_7R-qkWR>lx`jMrE~eNDE{qK$PEcadD=B0 zqQG_}0MzZowtxr5l$Z$|KJbmvF#)u4?X|FEj9@@vPMimcCY?N2rA~rZqZ1yn;HuAE zHE)+47^RPBN0?(IjK@*5U~<*s7>Gh%WkO>mDWfrzS72SesOBoRRK~T26v%v9gaswXvt-}=d8;ONqBf^F4OD){ItjMd z5DSR44D7vyU#1{$_sH?eeg0`S2QIp(u~%a+Q1q7=uNPlT;)Ck04~Es?Ns}ydt(j6i zD?U`MtYVoM66MD-JUE2)Rab6jq0SFJxV=6LaGuw*qCS@{K3=QQ1vAxIS=Q1`eXqKG zGQAN#miJ(Y@H45|rjgkZpmFJO)@HT*E5JQ+JQ3U5mlvO@+C{T&NqCC>wSRS>;irTW z6RVDb^+EEdv3uFz;8c}}@9EB%;XncorUFzd(}@$+10t!oMo^{9i5@WJvLh`f-9sso z;54mDjE}>(p!O)}Y9@=67d|K3gWc$gHxD{rjshF4W8#deKidMn2h;<5#!fi*QH3ce zoN&|h-UUmY$s1vIGCV7D40+rTBd-1l0P#!@ldiKMQNA?ct`g=IEDW3saJp}|2WI#H zXC!?%VWxqE=^ZEbHhDo)*tN6=CZDJU(}GYA&)AG0aGg+`1fBsIuYpB(6Z+}FK|&9f z>Hk2Jd)ji=CXkGohcB~8iRzrz3rI<(Y_Pm2l@^U=Q#>%5jz7g`46`&ivq_3&HZKFq zQiW9bdDdDgXdGL%Ql4>3Nw}f9R0MPEPk!k|>YgQP9@gl?X`l*qnTiqMfW>e|IC5ZE zm}pr8wm@!RIBOTfW5_N_2|1;#D`9Mpfp4|Z-7)egbx=&E&oU1T(*oV|8sv6XuK`{) z!frzD!80#V1Mal~e4uu0j28^cp5~|9s>}{113e~v#5KI0LrH(8?8p(ryi@?-{h~CJ z`^93{>JmP+9?Tr@(R;wb;D8C}03pH23jP`gCVNaVJVwdr6Q_X@MsT7O$Lq#7X#KE*;Sj`ZEg+f{i;nfPs&k(!;hUp)U zDMkh#kxU#FS^*3vgoBq7Qc;A`C@@^mBV`9c%;xgWgkf1eFidTdVmOd5+=&dq^T>_# zWj87X7tFFgT0smis9qy-3D1!zi-KRuA<|4aL?o91u9vApo@G9<9AK-xLg+&J%EzljCN9Nqq!)_Rsp!@`ohhY#G8;GoOOe-g{1h1yeVy-r~WZGPpnK&G_5z zeCIun)NuafkPP#dv?ltBz1&8B>A|1X%KJM`?yT=Wa-?R^Z+?Gjv_VVdm*Y~S zmB#hA+J1x`hd@RAPJEPmdN5v*blu`0aIszq|1eeD_iA{N>&{T$&Aya&B*W~85D z9SXahPcg{#@>jQ?O94eEu23G88|4TNq^<%TMG|B(e6q`x}=p0y-m7+p+jh#8jqhiV)|A_>7BRn$&B=qUP}dv#^DB3G!LT5-U|2Ah81_fyBA&s!TEReN=ELq~$)fwk#Y=NBLsH9_ z^Sb|S5)}ZZJ6ukLWe_Fo&Ha_Hf;XW5WW2%af;VEd2FHL0-?S`l8P3$a0aLKmQg}m~ zBL+K#4gq8EQ))xH7dnL32g`6rVqJs|(a7-A8ukthK*CobP7kWX2NNZjH@~@dXv}Wh z-=b%Bh83sapDB2g3-4aUAlv>uP86$TRM2S>-$5&bNk*k;sSfD~A{M&x86c$+p2Qp? zj1zNa2Y7`?c#)HvSgj01zjA(qor?J_kNNGiotfXzvf;(r+4&7XP{}N`exCv4|Hgsa zCO7={X?PWp2nK@X3kJP_;=e=Rt{%Vv$3*;F5hAcPm0mk1ALUMnXB2z&`=416){}cx z-gd1UDA^2|mU}{ALJguln4$Rs4w{AxB&kf20<}>l-7vW>?A=WmVfmfzt>vF_&WtGN zZ}@U3?59s)Z-dNlYpeCBs8*Re+yeK9rx*7KLhCuTbj*xq(Ob^-1RVm7+}hCo8B3C) z4rM4f!_*!u*l=w25i`6z8Kh}LA_ayIuJ{nc7_mp(s8e}5A8IcYaVw@1X$^@9Qe z9|sKe0YHxnpuPHyzxd5u&3=O|Z8L}A&7MY@k#g_Sh%=7zx*M6S& zHtz(2cx~p7=iT${XYalC+H0-7_S$Q&ty2^D(Xw$c+Amwnp}A$p%KJp=;BLphjf1!p z2ie2~!saui{!)vuj6oa4X&rg!%7_RJ^jZ*R2quWC{8^rNj~KO{m@p-b)~&$Jt35p* zN4XkhcbcaEq94S)N;JoVe+Md;^Zx0A_<-*Zm7zvrk05phx6%qb&~}+_Wiwsbuth-U zizGnAaIi~Fe;E@p52|Kkjn zbW3cbHYn{-7H+_k|BFGVxlkWt{IGBR5@z@02 zE|mssN)VPVT9*fW#L!UDD&^TJ~>$Uxs@x=UKHqm1*|;h zvZ3%qYHRx(zg*>^oo%yvQdhvJpV>F@^JoFZ@o%sfm#R0IORD$kE>nD^tvDVed7+-k zPu-3j)!5T-x?8H-?fOpJo?^RPc-wB56D4!(6kI_rH(qA-^6s%ybnl3W6RC;Rr1t9q z^kv2_YQk@slRgk?27&@br*L2}jt|JzZl^IC4<(2vEFc`olI~Fr-GsVYF5y9v7jg+I zV_KrciaIUP7_jh?I4}sv#{vC32m?|faFnxNHWOlm7BlX{O7R(i_8oh@uf%+i3~`Q{51p?K_^y1UM@8ziVZ`1J!-ly z(-kG!QJINhoUro8B`Z69*BNSlnUtk2A@&_+x?+3N6mMk*&-poVp9zupLtQc^(J-x= zJk5GTi#oZoHQ24=ONMM%TT0J-jy=s6&UM}SGwOu%UXT^u8`PwMu+tJEEL}U{_|jVDHDaPvd9yI)e-JL zR|7!LuzxBd0PyGR_~iMYaMCZkDV7qUA2KJLzB&G-`qMEj6B$awn;b)Znn(*_gTspTo_LX?%+0mi!2gMcAvZX*-mTJQO z5nu5VU?|&IS5fUQ3Y297V&L}wpzc&x0;Zwb_#kzxg&pc;+jPI)v8XFmt6^ne9x6@V z@sh#b*j4_y$jfh3$Gp?7JZS0$wfy`)Aolz;)~ZnNNMT!{<$TP6dN{?!x;`A(cSnD6!NO?=qtw#tBPHg8qIpwCxlqqus201@v!~hH zR62J{hg`__&jHr#Z=etMV?39U&6Hibhv{Xp5_o$5?#G$B9^Dj|hGp}Z3RRaypw6G# z;phtUDY4|c#~OVF!cLbSsMpLu)$4R72)72lwd2d%UnCmfRCvb+$pGIUDu*= z&TJ(dhYQriYaZoHHq;G8wa?;X<~rv-kU@!J@?8j?szESj$^xeAEztMQ9?uE*vA?)88*_k`7C&v42?Kt z5-~#=j2{%l*E2G}K+JDXaou}^bJ+flS*$a=qV$nz>D_K9myI9%#JvZH#?PB~NB14@ z9OCEyrE%c*L6R4q%>4Y=q3TGpB^}m|Fod8zB*&$n^x8azllPS~Tp%Von>5tp(-UdK z*~QuqvWx}oC&9BP78n_I?|YPnakzaHx%FlO>3SqCSA$9MBd5u-7zW_jG3K=N1LIm4 z4TY2*Dx=goCs=0I@WxM~u$ZIU{wREsn;4{ItIhPi??5G_Ev7KCBQhzaC`%1m5%@`dqfr_E#%}*@V<*iE`P8nyXeBC(qGe?v zlZ)!;xUgBf`iei3pw07ta2T=GpjaNB=R{!8m4r8#ff;{LpN~m~IPXA(MN->0Hx|TO zs$}q%sRi;4|MmB;k|Z2jiJ01Gyht+CduJU)vP*4*HHttG;&qK8;nGlW!$=#CNzUG< zDFs&QW1nnYVXwlTO$&k0Ih^FeK$lMG$ipEX(?d!jvraHgg0Vl!*d$e*RS$fo8D|4>@7>3V!~netP=mq}{He|G*8JL*}J^Frmg<9^zvkE6K!E zEh^M^Uq^SsT`s(iWT*6h;oTJmY971|Gi-dt|CQ(73D)&>pR!d{kf~bbMu{wYZcEGI z5+ZO`Uk74R%i}YuGoo!4B27I`4MvZ5+4kS#;YbZl-G01{7u}wsXNQO8H-r zIe};;6ZslpDas^eCm`L#nX;cXSJuO~!Hm&P%CsH+T^coOq+$Iz?PYfTyX*=Kyzc|_ zhfCMI4h2*z9NyptM9jl6rxyAmvd4enxry%}X!)m|vYBU>65*BYLmb%NH}FB6OF4bO z;lb4(yYPYwSQBRSKO^`==LHvBc-{pAl?w(gJogi4f%`bNb~qUquKf5JAN>fJJ%Sc9 z-Z+bD_)B-<%;TC203%d`k4%F*0uU+hk!Q%mh{_eyLEKeS7vY7t+-i##AY zkS<;>=rdFShB35&p{{jZEgEKnJWSLYgj+m9FO9hVjCyPfJSfw2hiX4OjE&>IUVmgDf}ikRmPVa*1)*%7ed@1IBsvwrkg_Tf~d-5p+#0y zPG&RlS5*6t2&L&zzdpb!XE_V7RWDPwY%8d9F)(1-@o9(uJX;g1*@vwud}y}N|2Xr+ zRyy|g)4CRcl=^bUNi5aHQi!GVDR5-RlCVF_mPf)dGko;iEKB~cWB^>a!un0I3&ps_ zVXWl5oS)*H`u6aDe1Co7i~)*?Ghm>aIw+1{qL2#fF2q1{-eauE*L#adL15x35k#WO zM4ZmriCBvc=b4bjP3R@4(Vys0voS2VJ9D&a)96Hkz5z!FxA2tev{M;^l>kMiEZ)g{ z1Ks(PUbATNwP?v>wR1#$K~h6YYKU@B7UiIfKw>c_Z0&gHKEsm6THETLX=OgU-yh8)_AX)KhGr&{m*Yu5fBgWn?!_Q%)sRpr919CC8cO(3_4~q{$*^ad*FheT5hy6*fagp!?K4l|%xbp0Vb`TPd(PT<( z7|aNrPg}QYPr6ec@T_;f8Sds37_y6>b~{@wu6GUNN&$LQG+ngsGQ1Z8Z{mptC<>CA zJd*p4ETPOTYu0=Pz(n)2hsDtIV@Z+ynQxvmf=cyAj^`hV?88DVvX%i6ZAFY^Ik!Fo z06C%t#J9myL$A27L|%)7aV$-jbiTh(F}5N2qQm$@gSh*>9l+%KY26pwdZ4fUG60=E ztSmi<@7YV%Q{^v`>%Z8Fp2uhcqR|RCoF+BuVQ9u zg03Ld5E@OgqXFp&x;H>yoI4%9-gBsoDnV{3unW@&!I`gv(y*>bwfXJ+OSz`%E~gys zj9?tDS5Vy1mj9p@ zZ?s;VXD?72&<28uG|T>$fjkoKNN0DSWvmHqj!}NZ5BKG*S&Kp(cG?3&sxHeXzCfWD zD2<&ataby0_d;~fn`NlOgqZf(4iFi@Fi<%W2!(XG>%Bk|T&iO?T>?kAXcQgI4%UW| zVA;pQpaX?N)bPbgNS%q>W5e@SM;$f1a~aup$>N6w(?6{QBCg8VizdpeIWe>hb}bec zTD5_kKL5njf%cx87SWhLshUr%jK;0f1q+4F|BY!@AklKZPUoJ38ICOtjBSHmuucRr z=7Ki#CFh}Vn*!l)OF-pYdU}_kl{%0UUvOdZ5zGbDf?}HV)KFIa7yGT;S=UoFc2HrJ zWqJBZI&IiM)N>O%n@@IO7?H@StFmtKyvemmU6f0;**WPw9#hm-T1KK$7$hRf7IqQf zT}L?L0C-YP$o;0CJXYHZ{idFjZHK=0TqvRAlRat)>=C;81-`>6pIH-e%EGkRJS>xt z7-~v<HD>eeQewrmaaIESWJeA5-cQlJM%epgkw96m79w?=McZ!2=Q$B68*ucv zq;KHpCDfl^D}c|Fy1(xP(HT;UU#1RPBYA0uBnH5Nxq_Ya-a# zP*-&;#TRjhV9{>cFtXi%u(F8aLu|+-FzR5X&zbg9q-SmhVm=}eX1X27+kOa9X;_dV z32_|up$8}x7Xdx^b?r1o644bQG`R){K)Dh~F|uVDC^HZyw*#?aT6 zPdvQ9(fc7-%l?SvV+tKWP1I*CK2p_yUADltMGFoc`%L-ILYCg7Y{nqMm(M^OtFDYg zyzhr-J3bL}&zKw^;f5cSpCXVdVgHdKF=3afaQ6$F;sVV)nRg`X&I)-=XRdd!OlWF;;k(-dbimZBA#xtS8a+ddQKl z8L!AXo#V;tsSMrFccPdFNnVIeehR3}vlp7TxVU~=_u`(@6DIv`}WJRQ^8dsm_h~P*xHD=N)X90fwK(i*J)ZPT{Qh z1o|l2!`wbGa(cE-%0|{DN!{R*RK1*1ZH3^DUI+emn?SG^5B_D|r%d}$yJLh-A!gBY z)~t=~-jUECMjdb-)W(VyMPyx1R2|FPxlUJp!A%V^S`mt6x}skv?JOd_Vj>_bEDIdy zWLHYRlq1akSegO+@inKiZ<2uKc>O>u7^ndHz>6}zYv8P?f{GnRszjPRXzetFeJh_xjpu(C>TCS?nlqt- zijogs13sm~fTi6dZM@h~_wj`7xc()?9gudp#~~}PVDR5po9$0}oj>^HmU|p{^J<#$ z{)hWs+j8pzHP!BM_&l37SvSq`bYyjAimTo7N9|Mg0bl+}gzxQiHGp{-Y=)=%c5X>X z>LjcAltj#|*g;2JmnBDcH)Hc_9ENURUQKmbm*f0xRAjQgEza|+`5+y+y++kwOlD^x zio}=@z3yM>N_AE!1C{gxD9G7tZ}64eUT6S-Ux_ZAy)H~blz@g96pzO;>r)jwSV-jb z!|M?M+2IG_>daO(Sx=z?gN@@%w8H(M$kPdnQi@oV2U2N(*iHx!n4zgX8WzBtZFR4Z zvQUAP4C&AnRhP=Zg3jF*v!mUkU3vCrqB48=nzi4f(&6Fj=*wO`Y7R(6>BR2S7)ctM zN?L*s)rc-hNG>i!HV#Fj&yuaVErwCM-3DeeR5{Km zj2|B@g1EjJa#V+Y-=h|s&h+fJ`A_BY1yqD$y$cMgOH7Ty)G0B@FeVJ@Al_8K>6#Z) zO>6^;Iv4Eb#FiXLP6~`K?A!Fu+zdMZlB>Wts$DC_A7g$(CK%?-TYa6sp@^)9u8;z; zXz-3yDDq7f#rS~su(s`Xd-uc$-#W6CqqJ8I9{109@5TsErzZH(J&iZ9)3ZLDWDW~Q zz(b(V{9(qBvk|23QAYZa1N?0#Ljgf||`xgo$Rvb!rV)ua#e1=<@he4mNhf23UAAbxx(;xgP7_~5@=rS3Q85uddsL*1n~?u_qW?Rz{!c2}{)EDW zy{<9aM$7M^(ucI~1R8SnccG1NfE+izd5|e+;q^N`fKwc5a4w zfd4gA8$vsFQdC_DoNE03y|p!DFB&`!@RNE9;e8T0D`4RS3kLc6-GRYB0#$Q%u^Z*~ z%AW_pyxcy0Cx+u|ta$+X-zA(I9N>*gfQ^1|SNQ7mC~=dlRm^IM#_V)2{UHgx#7tC6 znzQ6W-y-PujRE>T=*kA-=+aNv!Kus7kgI84Rv>`Dx?E5o(r1Jt$FjR|mKz#BW!?rl z;3;E7n!V;hk{2>zu|0>NK1!O3>s}YT2~u4>I?tZJlu4loI=j)Np-=5OKN|DsPCWQ` zpt6`ip=6;>Cec*_83e;y@Zv8Jo*ZfbRW$_pXqC+Tcy zZ61o70BnfB=ylU_IY*SWPLBN4vP4Kw3#dI&m>0c@DUG+zYb{xmJ6tOjSrjNLWWeHf zj<#b~M+D;pmI~oNipSmK%A!C~3iMeD+vnV|S;(_`P7_e6S zq}MVCgTQ;KFBD!Yi9_U3v3;k+@hqTrd*gsUMM0{w&H{^oaj1B)H5)CMmDdHc24ev@ z;0v^m)Dt^YV3F7KGgWNg3oMRbS2S6@fpGO{>M@`|EzW>6bAl-FC%tY+3Qg2{7v(+pt z2SURBl=IkAptb7d%+DWI&i>0VtMs@zlF0U~nJ*U%q%e2&Xn({!P7PAQ!#%y9q!DG; z_-pv+LA5U97=M_rl$sAhPRuTdb1IxrkpBg?UmE0AF>+aCv6}iSwYM&^$m1R|-hWjB zM+lGV^*_tRNb~&J}jDj4e^J@j~(6B zCie|mHL~|8fR2pI-S=~WjT4d*mbL9RHkjR}dq4Ea4pm}2wnW$ZEK*-0@?zep;60VAcZ#!K7J&_FMw zsSHwZdZdNfE6@-~GL%f_wIpRVJh{7N4!whj9d9%cfM*#_Qc7PhrC%oX8IB-v%1xW;wxxMVJNAU1~e8e4QM(=-ArW*5oI>0k$5Jm^uLNnKC6#5Om?GVLIQ< zntG!qSMLoqy?}v`e!u~+(m;LA-1T|ftuiS@fmn=b1}+cdG2wjHE#pRmb)1e}UQ4cH zWR8NJ{5pz+ECH!i>oE%%YZYc&E#^*6$MzBzn|G^Kx3Iz%reO@iVf3XZ*lSImOa5UW zx(}Rxol)g~h5{>ZVH(!*n@q!4gB>M*SZp?27tb(a^o>$zmUq1DJ>=i=;r(p@C6xb* zwvuVso9ol#>C+xg2Fy#OHMOzFaUkM7` zwj`i&YgBc0p1*Uj2YI!MO>t_j_Rw_84qLn>&({rJXn96SXjkPsY#zzmX1|!v=R%#1 zkKE=aPkN|y0**iFP4le@qsq*OWS{#GDM_nWq4zZ0-4ImACz((&4M38H9qv2_F+0+a z(=u_Wfe&kh@{^xc@#cg zAG>=2XMQ61o%l@OCVC$3EBDGWxQzhU{&wtMnn(VMwi}Tk<=ehZ40ONn5d+hg?1y!H zkhdKlEWU#0_J^tBGwTM}{bLHdQ48mhvA#@i~<#_=gehco7rn zkw>P}T;4pob!$2y#qqA=Iz9M1aipCBMtU-?D4por#1Il3nu=K&J9zwn9bO^eicjd= zi%j0_e4NdbGQJ-IufyJ35g|4W%yS`l?v9sfI~Ho{lO@$K|J zT{T%1JDB`fBj`#k@CgG&yHQYJ;HaoA%#KkHVD|`o&{F)^lODPlmPsI`3JXR(>0zhX zCR<%c3GGsfVc89oFeW8RNERrG4g`VOn?T8?KuN-Fpakqpg%ZYr{ztjTgA%4d`qB<3 zF`zIOo!9(f?~V(cB-{p0kc!cPI#TA0tr}Z^1m((QAJVZN_V~Ud!Etb>SdpX}gQjDE z$7XKWef{If{-V_OkNR_m!@Lg~e?TCqFFpkaWwNLq?JcU0jLMcRkcfWZYIO`z5YFt_ zOw(wCEg6~JbjlO6avWa5<5VxG>me8)G<+q{>v9tR)9~KH5Ox-*PwxMMV zOa+93S?_a4^ch-f>N683edenT8t*s6=#Q*leziV5&{ z(#R4YU}=_gf&GuMD%)ZT3@PCMzU2FvVATg-bvYC~sIc|$?p6VaukyPU81oqB{5Dgw zlm%O?Z<<0fJKUz@F^lZ7{S}akgHHQOlV7fVKq=Y>jk{_;`tJ#u)PI0zZeO>()c<}A z-evzaUF|LG1ueUVP5pO*!0HOZI>oIvS@u+5ukB(kZM{Ah8qA*S0R9p@dX=*l51Jc= zAJT5p-M%#NP>=&{>dC?B6tKa|Wc)r*8VLx|F35g~(7Yr@SB`F$7?dm}MkGUG(79%b z@jqkp2}pMI4WEycautK%Bo&Z_^ww<_Om6C&^E|xYj*i9MV+A-M^aDX4z;zkb0C3AB zfKlAo5)l7=#EwWm2q%LfM92w3RG@(U%{B(7Ofmxx{){jEKO?q8k!3QKG`0i{5_^HH z^F&n&H5UUJw}Nr`5jdt}Y)Zn!taADC|AOQE7Ln+24>(mXrBV3|t;T;rEcy^%7D+_$ zG_yJR8#&OEq8I05ULF=o(?K+ZvnXggoMBZ-LaaFlvLNTnzY>I>#H{>OTaSBqF*n0S z{(H9&+!;;&+?PKI)#Xhd9>5va$>~5CHthg}ztpX4-N2>}+q*)z#L7Hep6{2G5sx6+ zIRsy5zATgEIEU;jpn6FgFA49AJzH)$|7hpBbBWx|l|3|nzrxt7c3FwsJl|XCSUU6G zg*FfMjr-6R;)njZoK1^xR?*z`76%j3U5x9y9vQghmVrmS0PeeQ{Ko4%kFk5Txu9jy z43_8gfiyr!&2k4D2|djtd{TPwZ=9!UfY)LarV6(i)<} z+e0p$kCke0bC(nQ7juXH z2JG?Lw_s2Fm`E8`L{Yq#DxX_C?@6mGRhd9)fOY)n6Cb0E=t02CqdmMLmk!I0fIX>= z*AnexO+*3R2Vukv%rJxvnSWuBjvvGZuK%R>+JQj^79#2G3W@ewsryPqOa5=li#x-| zVM0m7*~HFsp?}Dj`TG78X6@h=UR)2&i}AkUi_cxh*u|X&9Dst||7{o$4oVesYT|?* zF@7?9Q(c~CuVrO5MNiUbNL-F~>&T|DE`}Em>oy}>VroGI4{C?3T@Uzf492@gA7fyn z1CNgor$9&bZ{j+Xq8pHZ!2@vQ6T_6lE?-bliK$vQrows}hg423E3w{Cm!m5`)w|II zYjxJ|MK=~)TMYQG4pYoM)d-T|^aM7Iy%joE(wR`Cm?fXAFkpnOLcfD?jU#4~28%FY zGmETfHnhOjFkmO{IDm!bL(cV{0$`=3&p_{EA@_k)_E+M`9hHJM&aY5oKtq{CHGWQx2uG8DUEJWXG_Pz zZwgKD+0HQGC;v1W%YPpC&-@rAt8o*Ef=ur*yURImF1M4@#9PZ0&h}I6X!jXiXY6*V zd%8UvefTiEekto$o`1%wl>?QP18016xx)VlDqp$soHGVKQWu9v~ ze^8m>1W#YOw*s`~&P(y+(0hKqxAmZ^Iw(65jyIxJ1UOBrtWN(PwmbTj_In1W_@Xatx)~mr%Uu}3r@^f4a8yl>1K1lfQ za9#GQSiW7U)OgLXD>;0X=->geZd|z-yP=$-rNG})SN=JK$c5MdtSDTLv^?bzE9=ov z$S)JO--^ZDG5!6ESnt0dPb~O3gi_kN0iJYQo$U>+S#mgADoJp)+gn+jj&`-nTE@Who*G&< zUN&$sLbMoJ2|%C$rgV2T31Nd|>W7{-bZv+;kITnSVO1z2%74h_M$vO4ZXm3y9sljF zhP@tAL)_L#j;va(Q?|0eHM_vLM{~~}`EtkntiyURWIcP05XmNCsOo4P zIAZk(AdzGu5{^;isX2nawDPX^0S`r!*TO9zP&5D?`yomu0IZs=9rRVkOO3%rsEnB6g3e0l0;wg*pOkgAdIth%1AtfRdw@W8b z>!7Euh+fLp$N+K)`OHPuWgsbVU(!gC*+@DzpTMb9r*o4vv|pF&7L3fsG9N_Z zLovOJV<qoIYWzAR-d<2xXjZ5{OA(e*$xg{(d+ZC9N&c6pcMSHPNe7pM`L+zq<<9tp z(pr8Qp-V1cg=fpt<)%ktGi(eCKsw@82nHBHA)uHk9Njnv2->#Cj?>Pb9^^kH-Gkkd zx#|=2Wq9NHMCGe47kHpGh7FyQQTCd@HZ@6+=dW3lPYmOwLTO>p9G-PU;t^{}MuI!1 z%^Us^gA4C4qTN#8BZ3<2&E`kt>|h9{ng`f7B)B&S-00)yes#4~jQb_ir~dEymO7y{%plFV|giG z(jxx&v^XAFO748ZLb08KzdnDm?on}^ zg2;1nid0m7w-s@pH6tQ`qjcQOORTB^bqP!fR%~YF{a*RL)7I|c0rpAV*8>w(GCJ@; zAhX+U#3)Dn*R8f28#-0n%?@;t5ZiHht^}uqX=5evhagfb!~zQW9zwMd9w{6kgZEN3 z+Z4_olZ1c*0Ib-kMfL;^Br7FFR;09k=dl&bc@w~&@JmSfOwD6!KNQSc`5^z@?T5$! zCMs);Wsr_uG&7%^yXcf^>g`UzNA}?!k=A#!=HpqEJry{DT(X1cU3L)i4xhjQ*tfP zrIkqmSS-{NU|=C16+=qow9rqRQ{k!aLF0T!3@}Iv5_R$su+y`T36wC?a+cXW*kS`tdLjA~|}Kyk9$S zPCCwWgDPq55HZOc7_IaJwOTw}!6Mr1d0h?>uf7P%@v@}@|2oot`#|MN&u_k~E6@L% zZhu6dn9}hsbQ7?seiKSbQhws1h|M&y#n?%uLiu2DQBxK(d#|F4Ms#*?OZ+ie<+zy>poiGX+1h9Q297UN&cTDr#m1YkuVP zOi!CQtQh_cp+X_6sh+2UJT402s1JRlieX5Gg)VgVSCZf6*y?&ue}8pw-f3N)xW%64 zIj9-25b)@)K2%V@UvCfDf2O^BeV{3X_{+@tH_`BHoA?GLE8{0D3WIs_&nDt`T<19H ze!*7%>?;}J>OyE5QY|ROXBwT`afSm^+vXhFxyO(83R->_@EcjcS#vekMIk z=Tb1Z4_(ZesUQrMP>yE#FT?ggkAkg8A~oWMsnAi$0V`rsmkqc%_C&YMUwi)PNhqk& zxBz2Xgkf%t<>nobDzeq_xfP)|@b~n_0lLS^<_cOBA@Uu>SdZ7kJ{#DRt;U%ozb-V` z|8ypho+P4SA^k;Uiu)h?&=s7SmQ@lT6e}z0DOOgj58^F(zL7)0Da#8masaW4J^*nG z$b(d+0toqw7p^L$oROZqrIxusG2~okd5H0#bsoEhLig4VA1OXmsgFFC{7ZlzL5C=XBahMTPUP$~3#ltlhVE}@XN&B`*q-};z{_CBy z!at)oon63(ib=5c;NN7zF_AJFoKVNZgIV)L>)~~+F}#N$J=Jc7l%@`ymAQJxMQpN2 zJnp7~@^`PP-tMvKgd)}g)gJuI<)+=M(;{slpEI_)dbtxDQ*)Yq)AY>5tmrguhyFjn zg8hX$VrOv#HcXol?t|y_G9C z^h;|21|V%%E`Z@ePWGWRruGnB>8%iu1b@pR=H#!W z!?Xw{cQxSj{Y~;%+6i6EH<#K_@YeQze zlb|R^erhPHM-QwU;6eAE9r|0#`J{o;~bsC8P#&t|K;BAe9U?*^z9dSQ(+9z8$AjS2N^+DJmET&DG@tyxDA# zx&kc73=LMO3Ru&kL%!={UWOlt{O(R-FfgQb^8B^%yR2Ps4^F$Z$qJ|7cfWw&DT9I- z1s_o%3C19KpA5P@9=0imN(9*o;J|wPa|KoK}QIKASmQTpra}+H88N~3UqAb zp7qs>P;(dmy%)lqat2KvJ9IGdNWMv6hd=MVlYTwtn>R<2{5M@h6t!z!d4&Jpg#f$e zmE0Y}SDgI$0KK$i{`rr3VNCq^dWZnGesIkz56|#1u9sHLVlWo!V33VoR(_8AwD~MX^}=^AYL$Ye zZ{r1g5ql2?48}CDB!tq%1C@(GU04dRT>1$R6L^e5)A`Uas2vr$lEL?C6VAS22-qR$SHm~=l zq8uu=V>KS#OSSA#NiQ5g@sH)p{R9hFKfdgYFYRDJxGl|Th-OAJraD(6Ij=!_3=LT; z7?8MTp*l#XM0Z?Kf%7dB4T_@lQVqtcQZ9uH7;|9{e#jcJHJ0rRmDa#yVbTDX_C8&7 zYp3AL^hc<$JbQ&Y=1=vwQ+Wh>HP6)LN9O@DjJNoVjv-|*z zX<`5PNl(wG0l{4mEC(5xh5P#NZxjZ*{iw7|5Owe;jmD<25Q#gix z_7j&aqYh}Pc+}4g$bx)mmPr^T?C>Px|H}IHKwbV9nSA2^76I*LlCFD8RIQDQL6wdH z86Dq@;|rcPh}iZ?LzBf^%26uTO+}J#IwbYb$in}$%3^NpwjHNRJN=Li{}%$hV+29O zvL-Tsq)f`cA>Ub@KC%Xv50^f0@|ueegytYdbmnc@ONR^9$Dt6{GJD;gYWlDwgj^H*!LqOvq+y`x> z%VUcFsXeSUu}J4|0vPuHu+%JC#BheuB0a=Rkpg?9C`^QMC=otMeym|^ypKuL-7oOS z1a!tnq1QfTTVUvTN%tS*ppN;Xl?D5}*A2Y!jRUWvHmoJ;_YY8CHVY060g~-QEp+ni z{pAWXpyT!`k zj{bw1n2z)vW(E-OaRQU%`McMwBR;H#317qAJK?I_Tj;c)f0*ws6LTE>M<^aGO~m#X zRx--XmuL@;zI@hp{!|^=h%h+)6roJe-6yRzh#OjuBhrd917qYrXXG$CNu8rUejw7m zHD5LAGNcPXSMi3xK;#XHRg6^29C3kQPx+)nxdjN4gA2U)M($7hVu;viR72)1+irEas8QhPtms zYgYOqm1vkbHnezjG-M$u+UfL*^|L_g9zDDf4Lk4n)bOS*WQx#e2g(i!&h%e{B|(iv z%p_>JjG2U3XRD8_{h6XQw8MrL#r#L3?8qnyL#9aDS@-<4XY1j8Ir1m_yHF6Sl5q&Y z2j_q4LS@gXG&S+A{-s@~4SaxjIaQcSog<1ti62AYoXxyJgNcdJB?IG|B+XG7%}t%S zJ2FmU;rzU~r+3-BMf%T=dnlXuAtV=;_J^>zBvXl&iWDTcZnJ2xpKR`451Ddd+G$WJ za-PnGa7V?U?fY2cFQ)DduJe&WDKmI1KCUcWHg7Skg|y;pB~^nIYhp?z2HB}pON4h@ zjQVlBpPO1r6&0{pdnG_1^hIq^f=IcRF#(DGq}PUBAxXd=78R$AxiOHmZ_&2!*V?kx1&#@YG3}wWj46|nc!dOYjbBN|3 za}M$wkGiOFBf-Ls!DLHwtjCIZH+G%U6g;DkLXqPpR;<=22Lyw z^Oi}`-RAUCCTZ0DTvR*gs4(%IN_Oo0rsIou(tLhrXaJ#4-+6k*Zow9Tj;*g-kcOB( z2aoGo=Qx0?0&9UkF-CS1;@oe;yT1~o*A8`oP(F%X8L*v3gd1wZCXE;!7iNw+EWh5($1M5u zP+{9IV2z!_JH@&J0FFbzr=!4OM%;!D4k21&fj0TFNG`3>7y@I4D|X}obS&@l;A+6) ztu)7FWLp@)b}*D zgif1^@HULUUWtkmKaG@^Xty~BISZ3V?T93P_*;PlCx2$XzuA^bPCj#)l}9lV`spfa z&Ro`#OvklDoOCi7r zR&Kxmgc*H5lKf$pGPzVrM?PR_l9Rleq=2-J4)u%H zknJE#1%#P$NfL`8+r=#%RJM!TKr8a7X}VkrGzH#5&u}tOwu3?{b*4;nrjp4FvVSqW ziCZZyRp|Yx>*DMkPHNA?p27mtE7bD|<%-@jtnes?XOhL(7g<*dT7_vLRwoJO20^>9 z6y$WCib7$SB$yjeiwe;onA3^dS!uPKu7(o03Xuoi6*IBy&QH zl3DVBN(!yaEvs_U6tATo;!C~~r;(qgIw!pvvcXW&%0jiP&v$9xTNOgdydp{F1{ESn z=EqMqo0+xhDn|qErXWk|Pb!2Fwl$;Cg#-G~4#U{EUvS%e$zm^Ul2=0z-a~`0D1?09 zYrlq+PqEfKW>#!#U|3 zg80g$6h}8gJyi(^s=MF<3S)MYlfEbhUd1EM!+Y-Cb&nKL2~w>nMZ+u?Hv)Hr__)gv z-&az-7ZH=qlXD0afLSK=t$PR{yxH}ld*JoeqNsCBVg2}_cr5l}B^j%?VgNe+V5C27 z)?*OCS8x^6>+{Z~jcea|0>gF1;fz$7#7uL@S@h#tzZ^OFmdHv;OMZ{-n0Lzw!>hjM zvW*|s_kPQ3EI58Ja`Md#T#;lm>G<2;4}8zI)%jN(7Fm%z!jb?0DC7egZ4Ju43&QDY zN(1kSCyH)RmL4r8NuV(W1Wfi5^?>^#YXq#u+86gFK3 zc-IC^_mRU~PO^s=9ks_LfB(z#Vzw4uye({#1~7fzxoBGQ53F%$Fb#A#{|Z-G){=PR4X+Aq8diiNWkdLs4`4&t_@!{w$jP?`N0a}9LskYG z#{WfI!R$#Q$l+l&tqhDd>GP8{^ia;zW0F2US^H0JhJ~*o`aHf0)ZcciD>M>qnKkQY zvWC);x>2;jDjCXhv6%*|7Mp1(*Lyg#rVcUEkD(du;mqF^&LpD0^7irw$+nqf&Vz~S zXy&XUSx#y&eDmgL^Teqs>`|^Jae9Nhtjk)MKn~G}Kijr`+vr zo@QI-<%W)#Om(FxIVR`51A^MTT%V%Rw6f`~DA*$vYNFtGL@HbeeRaDENqVy+fGZ9J zOK8Q_dMIFP*&A!hMfB%s9_P)s55pBX3pyR+NvV(H+M&_uM=VLIr#?|tI{W5Gzg|;y zE!=etY{XjTcXrQ)NVDg%+Q@%EYzGW2+S4D#_#2yE>Hln2gdcok)-j5pX>uF@(WSQl z*e#Q8cZ-&C|C_9zYM%XR7)9TSxidnk*U{Lp8^vhze$rxQ0gE3Ca?7c}{xoc~1+duO zaABZe2^y48CV4woAz6Y}SwJ6Z|HZz1BcKZw6uEvv#;D|0Hd9oaN>gidi_cPiJ}dSo zRzZer5iaTi>gM%tNi0D#d|#}8X(ZOaqkM1Sgs<#bu11=GZNX}Z;7O2<(vPrCYq-N~ zq{XS@cq~XCqh+=x~eNT|Gr7$`>U6y0GoIX zUuzmOtr@=3VFpV|eAnZUfBJy%QP_ge;fI z+nuQ3!gmE4S$xoPCWQM8hnCHYXhrQDQP$I1eYw}Ld|NNz?c7S1(NcZE=VFjXdEsTo zQ{)t_BM@8#QUrluqb>1E$VH5jv#}mLBo>>gn2i!y@HB2>{dIJwdeXYb=nvyr^oQYX z-yd~wa()~70pE4t8Bt<1nO<{s<5X)t zBV@owX z;aQP>z`cg~N8FYTjexU_bX)hybo6Jgd;CI#I8-Xo-apa@>4+#>lpO5+vcmFky}-g0 z9A2J!u&w5`-tXBoC978mJInVACU8g%+s%I($Gm;6yh2FlWazNNBbb|}ap!5@Pei#2b^eNi5Dp*nm1CFhV8fe_frrIVD;<1926$m0Dv;x^_LY{sB9AMI!@WCNh`#1 zE)gHH_pd;>w@fGVp+)@R!vJ*CjxYesg|tV2^yy|j(35JiK0O(q~NX$FfE$Q51b{iGEl+BU1&1x z)wHJdD;zyXIS$mEJDA%w*1Pa=`QULEL6Z+&^9=^&1qY&|e(%99eekxn_~6Y#KIy5~ z)(3Cg#Wc+QTka~jc>E-F;P$mPxu2vH5jiwAUyRWs+&)8B8eQz6dVW zO8iN$!%Z@|J>mdal+qWH{D{qO1O8vMWfBFf79V*(zN~EcT^RA|Tn;cljsgD-)8$oR zsrj`I_#-jE^#Pyx`!V3-CoO&~-kBM^Qs$E6$^i=+a_rImixzZy9-=d!WO*U#Ir zdXgddF=o{WHqkObLih{HRsm0fixiS?OdkSx~f`F#@r0q)Qk|A=dG5V z9!g>KFbG83hGJe#?F$5T6zTy6h_La;CeNTRlhtxF@>+>F1>Vz7C~~!XV*1V{oF)&hsCMm8LaF4 z57KCL&fR92MyF`j%!TXtC6(pNJF9!Ua+xiO6u!+6TjAS`6>eZ&^>5Nq2`@v<$KH4n zANQWl2!G~VtmK}qi$jsbSn|Mk&yjS=%EzwPCA&ycxh3<5hE6A+YgE-*<2Kc9zvVRx ze?J(z-nC+pMDN_^W$y(#|EDdyvCCvH{|f-1kPm2;@sk!P93K-(wBaTwjTrgxXEsFB zj|<9Iwm^xIrwK}(?+bt@Ls=buGcyf9it}E~SJ8{^r{i`;8A^iDNRm&j396I@ZdgGF zY91Q;P%0@7)zbV)XiRtpje&cz7>7ubPrq$|abj4Cwqk_Rh{o6oT4kIuxyX2w*;8Do zZjyY%T9j{f<;zAOZUQdQXR^3Rel8@j!-VIEcU^K-Hb#f=Oqm((wwwL(L<5Z!aXu^_ zf#(eoP(%UIZR49ca){^w)2<{YO823I(H9;PsC*p{!W|*zA5l#2xCm5t$Fn4O8z;gkh%p_i&gOV^d5R z4~U;}Ka1$X?9B4PFYM+YhSY7WhT$%%2@<^LbRy(ar-XQ`V|5=55vT;4;1+F9Q4Mz& z=?G=zS!F(1HW((~06=Ks-lCf#!ZTN})vCdSovOJWLsx#q@DHhm^BHYMi+AN{rDP)5 za)F5y)*?l zv=78~UwLu}beGB4af$qZI5NnN31H6s>&ND*A3Ku=8XDCLqMFm9EXrFDF6#KVWeRUV z95Kri3hO9Mb%WBMN-p1Ej|)L36{1E^`KTP&qz};!N)dRueMQa(7Sn-+zLtsyO=t{^ zYJ5<+HKxbh7{#&$vd~`g{4;q@jpf^Dmv(CHHaQ_QYH6wZ=9U?uvN@`MmbTC2b&~dm zfi<9RZ4cDd+N&oUm8fW}$=N1)3Tf8b8_hF(ii)b!Ym`ksE4$|8i|`|;z`3#?z|0$W zP|8sRieeP8)5Kfyf+gzY-!zJac$P0H5I?~xQ7YAIWUL6peoSa7M=>@@3>>1+x=R~+ zev???!7z@Sj0aIbU}$7T3S_k1U5{Eab~D(!A$qnA|7|v{3&A3h$KI|Fl5XL0LjWnh zV>7_Y*dREQdLWGSliCQYbrOIyUU}$+Zg_~+^mTm zhO}?f-_OPr$<&#mTXc|#ewemmM?1y#H!ckEy8v*zBGXuO*$>J~L=^3iahAZqLF0%Q;g+?UZ%<4OB#mvKLVJB;lWf)2Tr%?pZj4ngAWCLJG(|VFkPu@}~NaFR!^9flXHkMla0^&vrNi4{Qtqi*b=@2X< ze&{H0rBRFVW8<>&)~~>Ul1=sudn?&wJt22q1PN1BT+J!jBx`SMTkRd75Llg)MbTRz zBIsj68$G36&_=UIxR!|SMo-hjo+c>hX;6t{T_o-88un!*30|L{bud)*C|3=hAOqvm z73q9vSD}&ZJ-xVTK{S>0Vvd_V*dkd(e(uO|+BIU3ru4+G(LNfOAGy zslxt3HcQ!;W3M>TPHeeKJEwEnAkVf@q(d3C3xSxj6mYcBD)-+2Ai!42M?X+N)TCk4 z(w2_5PB*1QHb*unrx>uvBvc6YiF=c6{083?XP)S#Q9fG&Q7K8Og&9+dE+)<I@?(OChd4uGy>4wxIeQ!dR#MzafN;E@iL zSgSOxiN#^lj8aEuwAeUr3X)E$Ug?dmsli1Lio)}xtym)L4?!qapoPy3d+ivV@NtQ@ zj*M+Hy&Y#jZXMsK=>`e1x#4qE8_GO;t2nqQj`y-(J+-FMR+(XM7YVEu6K>94z@Rus zc;bjm5(rE}rsfbti^s?kcAiy~;(_>KulXFF7K>{wqQFF4bKyM>sg`m39&-lG$&a52 z(OKW%QO~0Dfa$33=Nwchpa>Iz2Uq*Y2G2#xhqs1$UQ(um*8?5a1}||@4hmf(A9&;6 zl6TzI>>?jQZ3k?82|DpR{*@ZbB<>M!Igc;>Ki~R^KQ6LNrjq&HH*uaIzw5~JC1QS{ zE5GG6Iy_aUQne((zSb`1OS4j2wPY$OC0hJJGiv9lGbwV2L8i2f@7_D@8qETaP zwK`Nx`3zc$h0YggxIPx+g&22K9kYn^$RO)!&>xmtD$WrWlJ49{*^XV#U|-s zqSQ$7Z*|{4s;QC4B&pcK(ajq8MJjC0L}&~3%Zh{;TcRX zVwh{$8Jsw4FB>_0+$a#km|KiH|1ajfZ2bgzw6E=$VUF+Klau*wbeChzQ;!BXn#Jiat znxZgHqUy9x7gA%Y;(G>NKh(GBtQD2B=)zViu}2@W;+sL^GAD%H7;XKdF6mwdfj=x! zCeuy#@bFm|SI(kO5l?nDXv1qvTX5LB3Ry&yRYDHZXL>?h_UJKq76)JmTwi>C<>K?v z1f8DZbWUJxn@(rh%d-xR#N|9l%gwWX41}nTZqs8%X=mT273Wu0Ai2kwmc;h}x;S2R zry{9t%Dq0iX*yD8f~eATL>G$b2rQ=QNFA*>?ox=bLK_48+C(%IumY;UivT;sgMqmC ztjfh_*%t#i*eACF%G;$0=jAsnQinA&!&Po)@ zKuD&;OnzKo8-xgdVk~1U7zUIKtcam$7|emLrYH`tw#C<877eF2uc39rMARZgT*fC4 ze)-i=M0|buHG!}6<}zRN*ze1)_8jfnu~#CVW`MPHcC0Y$tH5jn&p+(z{3pqNHiMOx zkKu!kqowlQpY*z6!(oBs`VjOLru?Kwhbdo?Ya(T6JW3S{T>JWui69f-9qHH5=Jteu z^7TG3Zb)_i2ab2E=Nuy1hk zI$*2%*7=q=rfdVGd(HSN-FXuw^G^dhS^gAP;n+@mYTIO~WQB_v5>oyN&pd9Tv#OLq$G*Kp#z7wjHI*@?*Q>a0q5c#R9 zPEpDv`6xrFfs_?(C51>(LX}cWBiiBTs{bBDvb3O-eR_@sAY%!Iaj>40c$t*3@R>tO z8tUtV)sBfJHN#B7FdNp-D*#GCE|k*K)qm3rZ6dv+kY`6{hfef{h{#bA4Ln>BrIsS! zPHuyBEMz`U{R2IlnvZUfsj;W44JhQ*TUMjP0=34^I0ZE!fnIEmV&T?h&> zximoK%VrYrZK4KqARBcodU9-FUs)ED{`2w<<@e+TRt#x5u~c@wR$f4OB`uGIcDsVQ znTKeiSeSHxxH1W$unupS8e$4U;skTwl*`Im|9Voqi%3{}R7in`m* zS22^adQR8BatADpo}3m?*F?PMpE?22`2ubys{oGKp*C;Pq-a_Y8fE#`9w>!t*a^ey zf?Z9-jYj0%)cuQtkb?R4mW5G+N#O}#=eytd#&_l33t~CxV!a(Unun^Uw9Lsf9H8@# zFT`}&h#ASa0W)P1Otu2Fk#!+wKhnSP_xCbNe1LS-S$}M#9TW*0EzpkqN z^{yj74i4_V34KE4pf>G8C8Y*HUsMii$^vXu>J6$Ds=iu04!rU28I>ompBe8&}q za!}I%Xi()K8mQ?FuS$lK043R31g$dXvh{4K?7mCV?z=sK3z*#X31}&q1yEgOv}5u{ z$r#L|a0eOB>VFHRp3}Ec1G6$bRuPGl3!|1l>9zI@lCZR@+coN-PEnc@8$@iIre<-Q zp{-I10S&+<3ome|uaSlAfm2M`rOJY027`@RVXH1yuj95Aw7fuRW}FuGU?8PXFsYgRrr#*zakj z8KAvj$tS9u?|~{u7!OZ0xZb6*P~-1RYqP=Uwl#YAn8vEK2U{KlrhZ;js+MVc zf@5J<8ULEK_U}a&~obkUM4JZEkI;>&k2Dm8P z^#xthXQ7zs{Y$7a>|_`*`HXul8K&=qKR^U}XXgeQ$9n$Zm1O+F!MM}M-I6{VW|X;( z{t2v`Ni=dOW8N?*-Yd|4TvC4)xLms1F?0uy7TEj2qpkAc3s|VpcCod?rIWz>^0Ypf z&X?&vgt!!ee;E^h> ztm906`lgm8`4|--{QzBR8q#jxw31vmi}X`#8nGSD2n)LA45Zx(#C;M#=`?ULD`YWQ zXVG!f&`n_KXz_6bl$af2>KFyX+5vbsZh?x+;Kza$U`iBKG5i$8(|)E7V~I{h{`eZg zy-&i$j^SEP+Oz@?zZUdkiw|ZyCMKmLfHJ?$CWA3HBR`oHCj&`usGEtbae)fXw#qzg z1S*$-b~`m`N8suVP7*_{zR>uQK0<&h+8? zPN3JkVF#It_A3WQQ-8%jQ;_! zOh&EL?f*W=DnhIoxB0BlY2+^1io*4KWrBw&=xZM&Wh&NAYFklBODsB@@-ySKMN_2J z-twJS4N}MYH0#7pku02Q4i=73QDhX&2?bq}=VRfmzzN8bu;4vz!CN6+`MJj5rNrUl zm8}csjW2g(RqI3PO%MJpE*WZTovAJ-3KHRo7yarb&}Hk#P~+Pxv<2L)%aK8DL_N{? zOodHiQQ5)P57$HOZC5Ts+?Eygl{PgLc#2g)1XF3dJgC*cS;`Z+!ges4a?mHQBibp- zJ63m%aXtB>?u8WIOlJn#rsv#0^qWKv)C-~TMC@_B8=S4bn_fVW|DiJ4ixun(;lAt{ zilkSDK+OIigz_`O^i6^6Fzuith=V-)_?j9b$;J z#%_my-b0KEQto4aeB4|8)DBG)J?}gLirf?b7$lTaYcZ)kiSdv3kmm0rY3>OC9PV7D z&f2J07NWe21+0eICIGO9MBqDBkkF2jdJf5%T9s=XeYHBOB7 zV4#|otU^u0gt{H7Ur!i-B{ef908;IM0~fPms4J8W zIIjnWOr0+vOhc5~=4Mly^vUML#Fv%A~d{3)}%BW5tu%JrBhbe zE6<+ZPm8x${xHe$dQPta>H}E3`MQtS5FtRM8+4Ci{g-zf$3k`D`tX}M&^4=Z-?BqY zd7Q+Q1B=yya)+9?_wev%sY2U2`+doD5q7P(;>4J5*-ZPqEo>F6-LXROf40A>u^xG( zumeWSH{+R|O_f3V!^3AY=oq5Xk7*Vr;S56R<8+wb(_QSd7yTB|W(8t?5Lz;0tV z1?+>pZ?o$+2j6ZHs2Mb!-$P7kUdyc6G%MO}apS$kVVf-Cy`^(Eo>xerrgu`DLks*H z@!qJU=OgNpm{x-IZKU!xP;Ap4IC%&7vG-g^iqWNNMQ(4zftBb)9`WAjpb_tQ1`0rg z@!rypZ{RTS~4&M#B8R_eucApfzwc zL_JlRne8uyaS8>)cZ@Q1RIMY`NNaqkNEbf34uI^C=Go^d(Q!(+Cp^t+{=}2MaqYA= zb&Auv7WozNvXXvMO_8UuZUsbjJM@xYZ7**hp_&)#QJf5UQBAbm**A=3jT)(@C~V&z zvD)ACiF~Vz=HR{vusjn)YGlao)QpzLF-aW_&ivQ{1 ziY=RYJ25+SQ8CSD2V+{IDu2@JWa8T!G!Ze96BsFe-8dN3U)-K{2ys9agAj5~P_QBD zQC%hhh^V63tR7~Q2_9thR?hGOt2nteLtyNDINOHD7a|GELym-|Z2wzmibE^m{464Q zXWu-+9LUaQ(#s3EBng?ema(d8(Z`O@P6IF8aNs8mVUkNb`f?mTC}%ff#Nij+ zaPfD0p$)85RyngITNZV9u!}rBJf@hSWsyjji<@&kf{3u9&1h z>g1zvXTTF}k3}Wy7lE}Ep0n*59{vnJAy~l_Hq>Ze$?a}D8M^wHN!qu;M?n!tQ(s7e z-r@gwKj7y(%n#W*9ex#sOJPBFz>u$ly{GZTR=*e>=5f6ZF#AdTG@YiTN34r0(a#ih z_V`9{lV2MwQe`OeNBp$I6y|oyI~dh4a+Jx)w}*;&URFNOZX^w6`-@iYPoP26jpR+? zj$|k+m(m-ilJo=lTX~1GOk7ttb5Q%SQ8{BYqHQ}sRpSB(w&8}6{trTMc5pn{u}ngS zo!Zf-``{&2&C^ued-^nxzn47nJLp~z zmsJF1Z1>&Fu%ol`Ab98f15ae*ANkI?Bd9N$qwW8n+}Hc0U$&FX(E_YS^tL*7s(ExHXvA zHNFECHuush4<=gn;xs*L(N7r-+Xm}Z56FR@d8A^o8@H@$^Jz^%xRrhqKx_;=k@}{9 zxjKDP5bk!}0lD=(kkq^yPs=VyV|ReH*w{qUYr`MZ|+vTDRZorsn($YdA&jXz-aM(J)TD zDMqOYt9ka95eLOJi@q_4*IhKFl_c`rKLmqa>J+@JT!~I$A|7NkZ!f-9jl*i=6FUGj zWi#NEojax5hVT<|@3QXeK#~`g`sQh?XVYk16h&5gAemID{Hgmw6?HA_9z}E@q{*6% z$m3Ku55zTKyX}(SG=J@SW=uq0_Uu@|J|779_;`mSrm187_!)>pH|c5TU)gfugLd+8 z{PM1K<0~aMMN%+HdbANb@)kfgi9O#HgV}~hsmc|$a}9fuaA~s?`z0%!0%*9jo>MTs zAhh0$Fh%Q$EEAa`#2V?fsXOB*ajoB^{!EfPyMdsDsoEd_dJIC@AI7w0v-nAEy*@mp zln3;7-FdtSs&^*_A-cmLj7{IOK^H=)E|EeYfv!@1!LoI!xqJ7s?h#UZgj9pf-6Nzx z#;&CJ6hf-He9y-1uj}I4JtAw5$l4>a%JtA=E1tjGB5Tiv{x7+q|7#VO^%ZKfMGP9; zvziPGU&CUl-QxK1E|&HvYEMv6J887lZ$s-6yPB09%W>QwAZC>-xMKb9`Z+B{lL#%` zCp4|N)J@r8p7J6rzQIq!9Y@NNihly@0qGgLuuHtL(n7fo_xb^5BI%n2%%B# z?ll-n!#*Bc7%gV;McwPt9o|kit%lrW=}oU2DtnikR~|r)Q@F|bD^B5RmaZRIcF!EP z;fQNxOWItV+(PxxWwC|n60A*Im;pJZ18Y;vn#GRa}>R6IIAs*Fp;nAL+bJ<+s z2d^7;%AE-G55e(a$9(te*~7s3z}9=`e~JyG>xSe0Sa+@B+l1EG1c^P8Ky)I-1YNpX z5zmouw$a+_?$q$cRZPbI6vMx>f2kggOPKe=M~CevFUEXAd+X!+6#Zn)+V=zR_5J-_ z(oNI$TU}*k+no;46(TJGx{+3myGBYdQ!1QZyrVS#clV^`iMKEDZ}z8a`0)GTo)MnE zx`XA9uECK&M;`n`;!NXb@`?IxSR5A)I!R}_8+YITt8UCMsLp-j4gkTAiQW?Z6=u&i zo8rG``{jA|MG5s|UU8HzKsIh{))gLMojOD9#OW~4-kVLsuSB;wylbFM!pRbPpz2m} zzKvaCe!i=)9WnDFN%%s2bFYb`y2;};=o*hnn3E*``&W73UzE&qtFUc{E<3`WshXhkuN(!&U+ z=}%o*DyGpPzTchMKI-2MEn|1<|22As;0_ah+T5h$o-xC@qrd-R&_Os4rOk7lmglQr zK2vdlEMb=JKNZ>c27~!$H-*l!$8?t*C-#``O5NZ$v*y1Fw@Qfi(A6xJ;u5r{yp=ik z4(?ZrtK(`(RN-4bye~q+EG|`BgCbrenf5K2-+r-PSzDGFPL?0YKCWrwHI^_xc<}4G z!xu?J-eDhlfTsOFwN)nJ8cSw?5}*xN#+T>$amZvUzDKs(M~yZv$T|Q;fJ(3LIgZt! z|Cot`JztRIg)G_AkHq__O7gQPjgR-ID3hIZw9MhO(%B4@K3^%Nd%dBM$GZSMMHsY9jo8?Cn_b}wlTE)kEykW&nC;)*MwYHR~ zAqc#>B5m=}Xi#Xz<}RFfnghYB8Yf*VBrzM+8yYrXpx(4Ega6hb-qi}OM zfIG_hG9Dh-tgP#zH9{{%i@RNNeC{HkJq_lzoz$+C(`#z4tiuWvDgL)f`;!`~42;fu zBEoa+DY^OizFr=MMQ-+2v1q z(_nad*l{g_tbtQ&i;63wi_(&IH|h!+X;;@2u#TVbxv9Dl9Qy-@!@4_f zL#U@)%N~{#Cp3h_VHD1EfuS;K4m9h5*oci3r$DP5(tf?S?&P0xYT#-{l5x;%habmT znr%TloNRGAivuoQYRkN48HtXU@8C2@o-Lu`{1;KdkjZrs8fqMKm0`v`wLu{w(ZPT& zT*s)do@5KDkM`o+)eKS|Mj`uDdYSdoMmiFA$q3P=vc2r3Fmg8lsW=m~w2&T& zF7L95W#WZ1^g1c7@1nw`8p!-85=BEI71|OtO&>iafBsj^Aj1xEWh5Td>ETw9+CFXJX zW`;xisPJ`(y)ub`V4Lktvra4sj)?ZbE7mX!BjKlhqlMlUgTrVgAkzqnCr4Fg@`t)? zPpnc6V*;khinB1#2;}kl!QSfh&N4`8SG-<6H;y|F=B2p6~#bTWHc2cl9QtB zAP4qAAju989HEPI)xu0Ie@?7iS&J?o?ugxD{yc}}7d)pl`Lm zH-}kwKRNP)angMDD_%;Adw=^%#^)78ipd}P_Ej=IKfZZsy!PKKW0?6#uPJgB1!uqF z1QktwQYF6j-Ur=%9H@@HYA-t2uvqToU}!qfw@(})4PnD4fB;jQ5AHZhiQ+8Mjqb0MfM9+!f6WGk zW4z%jYF9P$-U}-%Nowrqfq9E*DH`!H369hV1ravU2(hoDk;tfQpp^s$uz-^S4hA>D zVgF1P-3w#50ycmsF6PPK8SSnTOdf#1i(Q0w&9grk_8zT}EuD~$F#v+b0AL11)76-B zeW{>M$K3aYl(HFM7Hae8?91bDUmW)%_A4)yY5Rz;gA>q;S!klp>}m_O{{S9jl?9Mhrj>95^g6FxWkQO4pkk+J=onLRg|``zOI? zr-+NYy&BtSscd<0XtC-F>mex0U}O*nG0TwdeZ%JzQk!eVr0esBhxyKb&PgIaKFoIn zj#Je5{4VSKVBEZ2=hX%_0A9H-cr{pTGOx-xAhCh7`7kkj$*ZsbT*<37HfzIuZ7WJF zoLCyn);YTY$T^o)PpV#rSoa1KD8)PGA_{sL8J&VmWULb8A`=Gg^iwQVk2=+)$^ zX;53YV>1I;mm*2*ja*rtXRinQ|H3z@WB)&U?*eC6Q6>EDPSV|nkc3wPGzJK7F(61s zRDw?sb&_#(;)UmwW z0*9q#;2rG*Y~=tnHGi19`>1P2ffr-+XhFiF(JZKcQ7Qn;wfaF{)K9S1S#hgdohQpH z2(E@;yq{?6R}j28N}*kN!DHNPH0r{m@{)u%L1!bbNdoB6mn2*hr5eRUTzGWc=0(#~ z4c9xJ7(JI!C1?Sig%q;!$YTc*X!nH;vcC?c^f+pGg@U>6%aD7Y|we9fX~y#*|q-xI(b zV~gL7`7M^W0fCpaB{1U8OBR+f{s|=N1hq}L6)=W5Ib+xpGWba=fk(rHT~sL@Q2&$U4#v{&3< zaeP5pwaY9dAK>3Q`MmjQk-F~Q>TR_D9{n&g9m>mbhumzu`wmI2zVCILzu_v zIS*m3-261aJAexFe1o(Q!n~FwM^0Cm*M%uoSjjYmc?v?UFgG#+1UCa!hsGvxwb5m; z#6FY^VV zFaS|Z$y>ew0k-TPs|D051fYuZUOx8ib+LO8t?+}1scv#gCMaOLB{b8xn)@zcAMTy? zUS1Q3m5?{Vs)V2hQmQkWQ;1h+B2hASBE}{xZVk4ouphy7Ok9ztr75r&8vc&*;W$wq zH?2LuoiQ&wh?-Oo<4)Jp4ref;_*Un^pnH`$ADEW77`E3ZTZsoaj3Z@BDjG{62G-*%Ar@y*-g zwSQg_Y|Kx3%_raH!(A?Wn^eh9s>Ii+QriDlY3DsLB<%|N-5rQ&owFFlJ?KBfc#QXH zW&1}I_USS`<^meH3+oL(4~oCXMBM;lAYZH4-xx>?!^yf(`tv?7Nr>rZljZ5IkT&(5 zLvIdUD{djkCP+^0VkcHtTn{T53S&puI3_R#iN?qEpL8z0B#@MyB@`5Tfb;3lN4tyByKl12nmGhNX~+GQ=7|bO4bm;<9(5=dqmQst!t)Npff=n+yuQ ziUjG!?GTW$$(BxSi+TSC`8x#0(bn`voMU1r{<1a{mzP5^a+v=z#`p3x%$L`O`7fIf z^W~{hLV67@3QguDPLTfzrKs8|qTl|sSyI6g8%Fi_%^V4xxvc%EMdaKoz! zMn7*hI1FDvZ9Mq~QMlQW)qBuZwjgo-vi}~2mnqq3a z3>t=4!qreJ44fpDF(As2I|Q;pmkClvdUlf@531t6DAk(J;5kYStQ%mPmq38(RAv z2x3?Z1d-YTK@fTlg0hoD{&SC@29O!R29URkpc_>S{yNMByUWUdctIp@fSDcg3E4OX z^nUqrdmza_lt7XX4^1U-E4MZv$GQg>wR#c5Eh{l?HCGE3jinak*~jBTN@l->NVBW( z|7F-~D-U%~MLk@fe&9|;XsF>~To1hhRUD~_UKJ0LyiktvQ zM&UrLRafS;t#njJwo~%!Gj3-z%g$-5o@jLJI^fUO&{n4|kN%XF4||+it;hauPFZSi zVn!*H2&&RZS?XR%1hE#H)22J-R9b2(KWH$bM7E=G=p9%OfR>5gA0Bvjg*>f_QbCFl(YquU9!AP zEF-2Z6%yY|iP20fZJx#5dNB0kXkf!@!{0znI$uwf`I?XtZb)Ris6gfiORE=SWvgn% zQ~@5fKFnKC+2FW-VcLgU6Ali+eioY}0>WKRtQTbNR)r*ixg$jbzZEMOHM|>?WeVEp zyDiIhyMnFpniVL=KytOfagp4&F{WG!QWr7hND^-DK10&o^a&ZLAxK?By^A1q5$Y}l zsVkHBG)BEkZAH{Ok~31&yT%}Od9Qt%X^^^FcqJM$NL@X|+i}OK?K6z&Q4av;CzTHn zKsGY`i!K{HF6xThdNOjzr&N@O3YhSm;I{Rel`HZVL#DS}nXbihNGJBv=;Xc|(mKJg zo1dqCf6(H3&^I4-f+LnGs=ox7e*%lg@XtHB+A29IJY_B8)J&f;L};>3WbXr31IJNn zVhp(?GC4|_(;gSv2SJxfNFd)_G#8t3JSwv^@?IUbi$?*WFeq4IVNeK*!)>Ckx!vR2 zp{ZQy7qf&pC&)3F1m>JYGdv_*A-q8Z9WRsQ$O_78)N=?Ba}C+=<7NIEY)$8YFFzgM zA<4?`Vl6h zWrKnhw_x*PbV{F<%pkGZIf3IA;l$ENTRvj9hp4Kqfc^0`bcsdLWjt%Q$I!K?fco>3 zJ9K)voC;d@y9AE^*N?Nx`K1(q?lpSBW4(`urZ?)#|^?2 zIH;&DbNv&8%<{HkiC=dd(dJF{I-1N=`j9B?;I_!0$=c$$YaVZKG+9CUaAZka2(gb~ z#ib=7u0L(spFEq>p-BLfYB!f|-fx>h{j9SX)rb0M`F^B$^*%cIkP>=Nq*p3q6nAd( zWIsH84|c6=j2>5~;z2kxmA0A#lAo3~=c3ze-(zYbMloI0+>ydLUoC|YG#42f79ZWM z$`o*8Go+*o#K2FE)rvZLXMHNZA?x9!wM(8Yc5J;g8k*`OkU|j=@$6tG~B~ruNKf1eINj`9l<_f zitlk)&a*f5>!UOzPA6(-C49EKYm6r7Jtf)HniA9;HU(JS47k$Cv+vr0y~1SmyO+(v zI+1RryJdfm_b+T;Izx7UsCO`MN;Yo0X=daboV68)0;LTR%e_4Ndy;sVlz#?W<=3hu zrKf>))ZN?L&A9b2~Af z4ugo9))h0+P|&7eoFmSpmix}WPEBgMI@PP;qX@x!v-!m6J&oz;gf(WphW7hUxG$1w zQb?E3snDhrz5b+dze%U^D;25u3QdY64IX{pr$cKivXUlsm;glN;TN6!>I7P)n^3VZ zcFl3D!M#L=Pd+fHeY%k`smC$;cs`Q5H7-Ge8hxwGu%3%BAM7R9kM>ViB;zu2f} zkw*)$noh6H0}vN~tDRp+q>9o-u_(~toCNlJEtN>LV zs$*23xVnNL$r+;}Lk}+ME^Qt$iVetz;KvwW>TWi=p$0h4qXzheJdWF$9S_twXvJN6 z0pm5J^4M(n$6#)j260JM#?3CQf6@bi+!}!fvKPA&$=cCAU5z0US=%X5P{;X+Mbc9W z@v6#NRkg#t+J$Iap(x_l!y7dvq&VgAfMT)5CeTsa3}!2_DX|6ei9VKQJ3I5fA1>~E~) z;_in5S0ed?=y&}57cFPI0sW$)4#Zi*Cch0_q4aLsjF&=`=l}dCa%}(M zyyYN7ogmKA*OtmC-b-c?wh ze>0Yb*Yu+Qzd0;L@5Jy5_3hQgLU@v|_qUbZOzlh4Utt3G*I<;*Xg z`K2#>sdCc$TD!a7e(M?MRzBSI=}&ZjtkV6SD?V~{cjceDFFbeIS1Q&1Psh%;^_Et| z+KM(MQbyAyb77f{g)(+ZR@y6J9v9FxldW?T%Nuc1ta9kw6np5Zaf=(I8ycC3WUxdk z+>lYT#-`Q!21nEuE1Ov+1thi&+G(U{yJipFvWo-lgI88jsx<7|l16D;fjH>Xi!v{?ZktDg1A*!M}woN>lj5 zpM@^#-{Ng#ExUDMSLx}|vX7t0Qo1ggY6W$h|1>b-4?G5i=Z~7l%~4zxmBk%ZKMyCM zwLQy_it87{QE@3vtm-$ia9}~rXla+@TUX&4_qg{cQd$bWA&(!enK0d~)UK3$q+{8c z%2&Q*{Z6Se31`Im9+1(j$bQT=vcs`- zXTsL@jMk~{uRBN{9dp)>Q3+ItDT_@O-BV&qC0QDi;emdFPJ3wIv)#J7J_pEJ@$!V7 zr@SI9?J{dF!E`BF?JZEXM2f_>=3)M zcURZ9++jpIPU)JD@QbWgh0PBA+S4Wpz>^l`W-tKSfT@&Hyu24ng+3wa;vs7y``4D- z3t^l8V)5`d(0My9`Vz0diy0JXw55JX$#gJkOK?y@bxw}}J{yKka81xED5;N)53W)DUiXhH zgI9)ldiqwqm2Lh9>~NA%KsHDs!A6jUwDGlnh65|6()RaobPK`<5h^6d<}GgoEi;L^ zInX|*ZSee+IPtf+$JtcI)oZ5Gx`fN~zYUG)+fKzZ=o*rc5#b;K+b)Xu?5!)1OPKFfX-mimA6 z22uOX=R(#XW*zk6O}tK>L&Ps}D zi*VJV%(>|s0{xSj;1mZ5ZJiG-5!vGQL+2*-@pGTQxUQ9pKFI@~u{}~-HoCEi@jK*mQVaU76<}h=Vv!fQ0O?;t&R!B3<@HesQ?vVV-3SLqZ`n zl@qWWn_3`nEg}Y~>cJz8FuIjv&JC3`j57FJleLwv?x(Zvx#@8Eg2dnt^Wob9gV=ua z?7;>M`Xa|Rik%rEK)et1^CE7@dmt%v{OvY|$SaZ@qO4_Ew8$#I66Ix4{gm0gY#n@k zo_!Dqx-aGg<6N-$QU)SS)7c&&CW91MULksVw2t|=_Fw7_1NH-ssbA*5KnkOf@t5G> zh{t}SqZ)2^=_kD|?$q56*r(*#&nVOK_ed2ErpX-;1njbl&A?3YlD5hu%qllo2G4EX z0HT|Ed!#cgaowz!P$E2mAZ?HhX~k9!K)O9=yXz93`*m|28+VETc2w;En=%xF%HyL` zX+Gxjb&AKRdcfvc%ChGGl1>*C%5stq@N9~XoDS>l+<(&PsVnBuHQw`jf_R?Nb0H(( zXOsJx&BF0?5@%FsSfe7_cT~pF#k1vMQCQ$^dia1d4FYJ|<3p{Ek0Q8WY@S7Bla+?i zz!c!3QD=IjH;>Mvkk@(kYIjNn9g*Z-gU9|-U$*QL9~E7)F3txtHM{E#H7tMuO${Xt zzORo-$`h9;E?wPcicOu-YrCWNB@B7=ulwu*Ao~hb?tck75}%n{(DO&lam}A^0MWo1 zhz{a-oVP+1izB`)lO+Lx#2VEwf{s@5BXR_@sY)S7HQP1x2#8}T;2;A*E583?SIe@5 zQ9uuIs4hbsbPD1)^HgLqP(f*yiMu2vjBs|9FzAEXAf364L7kQl4+GWk&205T7XqBf zrJ!c|o2c1N2JA_x3W|3HXz!_yS|L1P-@%vBX0)!l5HK+u9TzYV3hID3jRUX+TSlJ> zw&cVvU1hWL?4?2N)W55&8@QP&UkARXA*#h;LNw6tB6-!RIuUn}P{F*?70@tl#R$e& z#;C?c-i)-k>FjKvf*|Q|mMCx!qfD3A0aL$8`PCai)yiWMYxAg~`v_}PzWGBgu3X(%^=lSWz~X;pGbHgmkt&VqL&`>zG)^{3!wXz zmV9$krFZH9gs^I`rlndR>1t7Zw^%=^d$&<2e?qWXJU~DJ2t{f2*uos>s8kiDWWp{< zS-oy?-UbpvAM}=D6lSXTM4+_^bIb~TuD?R&al&XxIKDVouopkK*zBJ6^G;zq&@Y(l z{J3w`=Mmj@WRF;7huo%l0Nm9Bwnn~}k zoiIZG&pZI`nv`l7CPWCVncts|Ks>_hv?L7;I*z2%H=23G0ocjaTS8c%&a0=vj*}Wi zDRo`izUDgK0%00NS);La1xmw+3VH#Mj)^5uir(QsxM~e7@{*BJ_;Ti|kFE8+x)$)k zbO@NAnRfJk;LZ+mJXVW$?hE&0&;bs_Otou1>Z~VB(@$zH^2FumfyF5b{B&XVWIE_$ zd>_$j7$;{SasD3Px9Sq9o#bRjEYCiJaLWEV)~|a=5OH_>bW?QcQaku{I>@FVSwTn= zp87u6LHDowq@~qKAlNklu&)kv_U4XdUU2-NpRJ?a{9e(^a*bX1#O>eVT>L3*BYhpU z&wvWC7T z8S#YkmQOgOM{xY6n_4pfrZ0K)K3AKFG^MYxQ-2qJNZ}Ubyu*2iSto-flN_i4BcGKJ zzaf+6q5J7v4nBAUP2Lkn?DD^1)lqdN;3867!wU2};i2x*wFsT_SeoyeoWvqLxAl}}G`_-Nm@l5A6I}~uTfIUy2CxC|OBp<4 z&LH?0@&=!wQ(Q3*OSPQSDpM924x#=^3Fv+W&LNDsTM-ohAvopdTS}Iz#cVwIQ_l1! zy~*BWGJ6;DouAh`I)_MauaxXxNo(1jA%}9sM2nTFKx9NrqBysSr{m?guq2?9lTrlE zS=vU4W{lfiy#);HKlf1b_x%oS?K=0xlsuT1d-vY%7#2zL54^y^x?`ZoswFX4ycO8| zGQX?$SN6#tUxSNvy|??-S|rTe7n5ZLCdprY(zjCsDT}tIMwXy}JHHIjh{!RZ?W%*` zCRKP(J1?7NyE{WCJ%OV%>@fS-OHSLr1FW%=K${*`Yl<-D8w@8@T_BCmCR!_hc2^pa z21;d2aTt`{O!_90Q8ZY`7$gHm67g8%X_L%?8p*I<_2;1O274H~tASqzLG(`rK}C|;i7}yi|8}H`tTcWxl8h<&cR?HJrxjQ*?VZCU+jVGfct>Osh6Z^-~@mD5RldTh0_<1Tg|4>k~+Wn3;ps z4hJ2c`P>_MqHXYe2zw&v-5|?{TC4>=g)!TN7L{lJ7k}p$d73~Kmf7jiP!u=$y6)ac z6=Ak6C0LSZ`b17;!YM4Bl6pb)kS$4GD1PJ!>nf<3itI$RM)Rm#Sj?Cdc90M!CO}#2 zPEN8kfSlI1s$H$qs1{l8Q9NhUuiCDUSV&x@?vgMhZTWf&3}JW#E8jQcV&kZxBm>+K zl%Hc-bA2e+Pl$u5v7ByIAhSPy!Fg&QtH9Wc6ao7$X#wPkeRL}PQ+07v1uXCs%6mdt z=#6V_Wmp3)i%2$3@?ZFZB>)gSOnoTRE$p5=;53onulUdZgB~u#61tq;zB-8(WYy=` zPPY|5%65(54Zmfm1NB2AJ zZf_?xw$SBRL-4UkVZ+K*2JJ})%P9W{!u{S0Bnl>DdzZP2cKqX>Ug6Vk9Yt6CB$@ke(V8aY%SuD8{m zWtT%(P&Jt25A50baH5C4#f4J(D4H$vz3ZSdnHtM>WU$SlwqU*;X*_#Z=+$dGkA(EX zLw%DZF=(Za$g(ZaR5(#!e|2@F!JgzrojPYN!l^x*Tu##nH`Qd4^ai` zJ*Z(Kx0JBMi;eZg=T_J|{t z-b?jL$T*pn=kJV}8k_xTFt^jpX2l-vCGsT)oaoyQ&N<^1gcLiE*W;{X9Bu<>VZNTu zCn{+Fo5BaO^5W-xo$&u#6&~^6PwSf=^~G^ALnLS3dWq3c4Ib-N%rd09Qi@uzc#_rH zccb$7jg-h&Ig`w${d|RRYU_r-{i0pH^cUuA5S+Vt=cBTC@#3!nsDEcO%(jdXozi#d zBp9_Z|G701E#%jSnd18$?lL@?Tt;v6a42UckT-(9kM-Zn7B`SZc%qgizBvq2I8v~7 z1R!~MdjueP?3Zr&rN=&Q*=vd{lJ}XsE*$~jOM=Gg`wtBMq$2=vrXapbX9`H(6CT~< zd!~Q}_KCkiTzT9e9RY|l1pyjaNA!0MejG?+3l5hA#JO0DTh#CbJ)JfUhnxQ_RDuJ- zQr5~KYS#tNn-%Y&-c>`XDU&L2XfjW7hBmMI{brPdnV!t7id&BX=1=%7Ta~<1z2HC} zjg?6%klG0C2Zi zvxAj^o0%FXNI>KLqRnsFXX2e|9~M90pf)dR+s-G?8=s8ZnSsl%O!MPdDd(6$*?#SmL67G=^pgP-}*jRv$I}td?-Cw z3p<(7(zVJh#-O9pmHme?2{%&o$>{%V_{{vYq70FVago;N7*l~5piYXpjMAwOL zS^I0LwVI-?t$koMs;&_7ma%vQrlGDPxs?wDIn-dQqQbHZV4|U8^=ebu_-fP#<+n|` zpVo$X%Ma%E4K70Mc^y{=U~NENW*-wXm$*GJPYd-~0yG8VL)^^4*4NuuaHb# zV;7;5eRoAzIH(DeqPKWZ;iC#*-lt+wHvLI_6f5jvcQhKbh16iQn*Icm%I0&mQk$C~ znb(IIJ)rxTDIf+juj}DdYgC-}{(ig|<9k5&sO}U%9MG?7>s}ds^$L>7?v~jX3Steh zbo!INSY@x_%bSyg{&)(k;;+Lqp=J_dM)<54t=fTIk;ProjDgOAm2s|WgO0vU|VN}gN z8X=RhP=WC!;_>v@V$yOEceiA%Ho$H!l;FduaLb4KWIWsyd`#PW=JEih^i0Qc9ef6_VMomtu4pIk%0Ys zXT6h#XU5L{w#;QSMcj}W&@U4a15FS+x40Xbjp0~VXrfG}Ob(KPa9hrxM&wiNZe;$n zEDx$^wltBr+$|rV$*jEte1{LEr_qhWdo~p4Ptq>)(RU+5F*s_!@b5q9Idjm zXG)BUM>gK03}tO(Bc6S@m2Zswr7{V_2Pas?j#?1OpxvDupLWJGopk5mF&kG>!DF#; zwUcH`d~G2fHWW=R;1OVzNpq+!{8Y)IN(E(pIn!kJ_HbU_U3=mbdx0MnTSEPA98O2} z@w1s zJKneegNqPtC6~o-Ga9A;Vd?5a-WHBQ_!eF6Cpi6tt55m?v~)DN=66Kst+BG6lV*q6 z?cRv=?FVI9hd8~(-mZ6p7Hcs(mc!T0^=g;$N&+Y@u%50uXgSs?Jj1*#791+%QLQ%Y z!NV7druksZb>ZeNE^xqAblNxPA-XV6pRS2{0ob+q!sYtqNMdpkJJd-mi?BUMY5rs= zzq!J1d!djCK|V>C(~N&tK1!~k7}(Y%WCh>arzayKMxKsDauAm8JPD<0T#v&1>8Ajd zKP699W5@r=u%Y)N<*FU*!qTm{x$jMSsPF8dmEQx_@|NEI1&l%LiqJm+6jjl4?y3w- zR91Ymm0eiynffbE#hvE|vzOxr<{xCzUp^3}^cP0tgUY;b(rNY}5=uF_c@fC*ZyT!2 z`)KRw1%@KNlTkndouK1}GzG`GVs-}-N|SdP_^+(^Xf0Oe@PE;G%RAUJlS&e=#G&Ef zmC;AlNApU$dcltiKj6}T7ltW$W$?tRN~P>n<0BZ*7YYnMt??+;HRI7O&MB8X`j@Fv z-0|`jG&Bu@75baKTN<1LNoOZGCi6mJDGo}-VbL5~($fV^Aw0=uLh9M~*%12txu>3e zT6g8N?o+>b8t1=<(E{>(2DANXr+wkn?$1@aKgnj=hW@U9iogl^_rsG<*WB=&PR}ng z1m|{KAZtEI4%~N=d<$IRXrcQ&J~wo{1r7+A?B9`sDvZ`BL2r?=y^HzMDy%Z5x+1r}8ymoHiDUS2jb|wgp)p52wGfbj7@>r6?6L{7I^2$$5Qx+GtsPGlmd2b>n8G% zei@s{5yb5|-bDUm*jm_5kWQ|DBm{|Lh!VXtZlN$dQ{$Ek{wMliJgh;?Q(Ckd3{t6D z1zSkBu#?3Hlu93%WfI)}|7+YDA%*J#nb|Izu%&$u|GO)3+=(Rt;bE7x>ajeAlm5rb zc{`@;^k5qAu6&Gd))0(0tP3C|Yz(oYDpTMSbt28)q`+Zl2E21U^KkAlPpeWJV6np_ z4i=ri(_Ol(X1dQR6av!d0&fgM*4>y(KH2>b#(CeB!rkYev-CWCm0GeclmLu0t>>M$ z^sMu`E9Z4DJ@d<_^Vykb2l?r`x8#rY&N_?d+`cpP7f0bH7zeQYx(BUr4NE>Qf0mfg zKJ$mvK7VbyM7BW7{BJeKaOb?kMMVBRx@%x?#7FIIbYsjg@3Y zVt2Z*3{@wzk)xnY#>BdTa88vA>GDN5KK6gGOYW#jqH-tS-v)*JYfvz#|wX zchDu;W*?}?tomG`5#5P#E0Y)xB+f7?PVR!MmUF>_9ET|~X%3}Dv(R0e+Cjr=HI``S zJ^hJZY%ponl`yRpZGZwYqRa4QvUTINg^v8iw!uwUpb35uo>DDG(e;*V5F7dcLMSdS z!ht*e?XinW^?0J=`ZvAira+cx^aS$;)NRlVocCp$B1s*SI$7RgJHxQa8Go*Ne*-OY zuJ4OHdzQtMCB`r{e#j-yZg6(xpDo8LIJWDb05ArOWeBpU_5>+F1fz+#w-^^F+qr8# z#5OsE*d#x%c&D3QciCj;mReGj(D5lWs4FD}@kgs3KRA`RcwqKUsvN!v(*ryY;3?Yp zeWo@EF{!Wy*=L1-k^}@JN9icSted3`=P!64>$!&=ha4dCjg-hAx9g0CE|8*PUA*)W zscJ}%^?j=lk8z@EeP7>~pcowY(oaHeIvfR>iMHjzOr(`~nChN^&|srZ>&eQcz~2RA zEE(mHS(iy+h!#*h-A_Uu?$+0LKCfN)XJIZ0Tgtc~87m(bX1y?qhbbKI#JY8XR16T6 zaMs95^T(wP8>w7IJ2tI*>!m>mv;&_(aXplY2qK#-Z6`3kNSd5HKO>%e)jR7;CXb@` z{)iM{^lZ8pav5uko7WjxU#wHA@6dtNc1Hxh_M%#mp6y`e56dtaIUt@gwC_-T9Q@F2 z4?0k})%=!$)(LIWP>B2(Soi_hPqvzm2ZKJN3jUBOj=MC1vEvf7 z?Ns->sZKIaR3}qB4XRTu9K1f6M2>~{2u7WPn-?CMGax>qn`;u;r*0Bq)rn(rkwtQi zUnb9HD7f$J>$M5=8J#>`U(%|WIP0);r{UG} z4>6Ty$Hn=lHl;lpIE)fjB$Hu^xb7WDWpnw9FUvh{K>du>q}o1oO6DzsL(%|JX#90cw+W9y5c8moqPmg542{`MQ|n;>H>s!#Q(_WB9O>>NvCU+y|mt1ScCHzbMb=OTnv()kGYs51?5}}F&1+%1UhOiW|GyZdK>5C?&^dW zGy!eHw7Q*$loS$Cnp!2I-YurqXGTOUXs0ezuy0g!#vWC$LaQR`aa2WkH)aded`iXU zN!?u1u-!3g$=XI~)~D!-P8szD;GFh90>jQ#tfTVx#sqwen0kaIgIDh1b+Dv`KL6Zl zI=^2xsyMzDaz4dr8|V^|53pdWa-++EgItv2!Z;Ackb*6eV2+EjRwgG*R^EZYVp(qC z%4`RX9eyT3H}`ok`?BHhD#xGnntf<%yP2)Ec`R;s>20zg2a0GA*cqha5p5J9m`c!G z$x|pZFV>WrrD0YyiIeByIT*SsTLx{ zF*$xckhaaHwp-k+eq)^AiWBuMDlNLmB5u)922pMpk)H*k32njPI5*$zSGUKs{j0v4 zN}pH0EDjEU_;A|MOlcxzsA!Qy=>o0Ev$j;f{4la+RThfZfFOczM~$I?t*HC&hFX>y z(^!NNhMC(MlD<|yxv*Tn^w=;gRZ<4sOIuV^9rGetS7xtQxqUYGxuA#__qm)-M+!}x z4Z|`yqo$|}FMFAQakEk+E>SF1STIdgaw^c)9r`cf`^RxJ% zG=o!({-UXrx^(XcLer*^(3tC|wl6(0Kr(cJfrvTq;95j3eRO=@ooDr}R2_ ze4N^;?=QyE3}Qta+@^b~*xfDesoG+HG57!qT@%C)S;kThK475#wC<_eE^tH6{z(!r zYv7&>sveMQ;(9(x|?5{HU)&mf{yA35<E4ReC!KNjG?6D}ht6NefzM_g5hU z)H#|(yGIXQDKziI((~-q8Z@W6#xtf`vtoIf|Hrc%2rKUCLVx^ja zkz4Q=hgbI0B=Zu-<_I5qGalJ1(aX_b2&_%JJBx^?L=!H*r~8^B`IBn2?EGqN=iwTO z3@uzmk}?tS+A`h|78#)TAyiPEyB9GmR`dXBIj_$N?fmO~t2$ZBtB+jbK)zg+`3mcP zS8wOXsXVp~He1^i>wT)9LQ@z$vbf9vXv-%h*aqh{k&}3>U$7&a{*L{b?K>dP*A7Vj zU=L#xSNX630$(wdY0E64xk6Rl4mtaXjPph^lYe0>wskUPJ+wH-VC1*`X77i}`dx^^ z6B``hs6CFPDv2U?6=C`>@R4mj2_s(!(^+sg$+OLEp?0gN0;2?99E@Xyd^KUrMk zOVexJKifg=_#p)b&H#(LD<1&R)vleqq7Zh2FfjmJU4DI#>vcjEr z*z)-Lo_Q{e!*-g?0-KEUk#&I}a1V~h#S$>@xnS_X*6y#^6<8l*%c-DIj>^BjBx5n* zH5LdZ@qzQc18I^NY7u`VO*av!g~;^mrG8JBUa4h+RJ<%6{7KX@8)|CT8N9N6U=GYs zS7{n7U5!2=NEyvgp`Ku8ik>>hl{g@&1!^E^S78_fY;&V-jB4D7^hb!j!3ip8w;JHw z%bXYhybt0*lIx^ZAMWn!`xoNL%?RxxBNS|$w)2;lc&rfcuVdN2@H3+`ZXq16#J8Gk zoN~-+5p}5zf^JbG4jITdGdGIiNAWaDNgOS(fSR3Egu~*M6?y(ewc04`alu-N1x1On z_UEn#>S?J5%v59dfXt!?HN(%mJiiK6ciTZ4(@>(ms^X=+Z-gd`r_WXfq22}Nva!3+ zbPz_PnDe4jV4#-o#Yn^vwN|zc3^7({pWMXaIQ!#kcu3_^7difnn4~xNGo(FXOx@hG z>{MoLOJP^#gXFjP-8^rv9+>Bs>2sFtYPe4(UPoN-ofM4Q_{g-=G)(e4h}awZ0L_9W zTa$P+hO$+qsw+z|mqYz`bJ&UvR-x?+3;gCK*%vsIN%&U4V*yl&aPiG7uLGQ?rLyL` z;&r=g!&+|+2YurMK8r2%x=HpztD0aa;BFC%tay=f&;IQ*_V*pA?S|sUqQ1JG8asB#pqWZ zusQ)WbaITgm$qHpKC>DZv7R8tUL`fXN+3^=nqopAgRgywJA$~lhK+;dyx4S7Iv*IC zPf91Y51!x6RfN>xyjh!0)AY*tFxAjhOualvZaMb=VlnqPp4LKLPGAkw+mtH?PfOk{ zL|kW}dVhtJn8Eh%nR_TT7Qx_C5|#TTPGptA;MY=T5f^@K`qBXZe9Ze^UpK6>vY$d^?1| zk0kQfnautI*Rb>xZ;A(RDV-f3MP4O)SV=lnOT}cp$;%fLEAG$-?kAOo{cMVRHII== zcv@=q5~9X~r_Cer9>5@P=o2-MN%mn+3K2l%fPHo6Q55)C&$0o!O@vqLoxu$D*s5b# zKYiqC=BJ~60#s#^dW;L3Z~XF`i>z7_VD4t#^h-m#iWGvq^1(xQ!$y;i`iWn?iAC6| zW5}`slQbC7U2Py`(bhGQ6cni3FAWF_m7gDnbKp(BDrAN;K8#d3H3ZU#EIW;CPyA*kq+Tfdhm%$qz1St4@$@ep?Jq zj8ObZA+)xJL&FXr)fsEJdK$X|tGRkbgV45&?kR1wC5y^dKIo>ZBzLl;M9Pp;X-Xzt z9b!&Kp&axBhp!mO`e>#>tl?BGa_Uie@+0Vc0F#Jmx)AYqQ3^Nbw3I{UG0`Nu4@my?#l<8E6?(9k( zYa=+;LF-}Jk{bee`O`iFxd)v;M7~CpleLjK+YO(@T|miT78#cm{hNl!_j8(lMW=pn z7)?}=zr&jrtal7Cr{F2v-$jvez34t9!#q-X3~>-@OZmnEvp$_{CwKyJydmADMv}L2 z+?iz5SUEs5Kv-?8-%RQ+n*t>M`4~4XA4dpp%?B3%%S311C^f|c5>KH#6HFE~P!k^& zNpSI$#~CBI>?W}^gf?@4p-pWu0c9ref_^fs+3SkSBsq#vBhN;Eskx`}m>{E{;7UJu z3BmrI-^WmPAUcKSmUivgUQKW!pFu4QC0rIfNNIRdxl^zvUly#1tk^tjEZ!WO?9%xg znQ%+Cg#qUydscc%p|_1#Dv?@IOBL_(d^SoKBLE#Fr@xef&Ugocxt6A~Vj=Xlqog)O zXF2U{cbualoNNk#QzjAgOU7Z=1tiZ>&@J!~DRO8eF5`#~HOVm)ei+eiver&m+dvA* zH?kdyU$LB)g$Uyl#ZpRQDBnR0*N1)^^o%r^>g@~->Py3YfYn7X;?11|&C%NE(Aftb zBzfVu+-g(%W8EV}2C0{DDI6_4#y11)EY=4XwGZCH*sg@985GR`)?cjn(~|y{wxOw5 z3E0|b6zUBVaxw=v5!9yYB;1o6sf+oBiC}O(v2B8E08)}siwd&nWNDAH!%U{RnFA`S zdGE=I7?ln6*Bl@r^B3A-}`!b2Dz=%lAN`;6r@)RN?U(YoP zbE4g+I-tlX$6a-)T(*m_kwLp17{W}7SAN**v&PcpBgVG-WHG=i<_5NwKC)fF;|qMMHj5vOL%`J_WJtz zrg5v^WZ`z3fbRU4s0`ud+!dAF;*V z`|QdjS#2i};1K4{7u+M0w|p$-i|0WrJQvVZ7E7)0&4uc>b*(Tdh)F>$wZe}wE6>;A zvtfJ~3Ka|VAl#7V)P2QbOl7E4SYI)`znW7*-TP}xO4#DY!vS@*Mfo8Bz zoiPfUdW06!ZRUeP#1o@R`6|K>wIKcQL|1DJ$?zA*kZk^v?0d)*YyZ`f>_jRZQu+ka z#?4*zX$k`;FBLUq5=Mm!!b=`xPzxDkcgmM1s8nTtw!|1-_xS0p(OY+ylJ*lCNa!MH zAk-YNsOBVHL^UVrBJFkzO^ce3)CBcLFOxE&8^d_8c)M0RlVSLgX;klwc?JT|Bro}_ zOzMk1F7ibm^{2@E{UYSB4r}Ga9JP{$3=eQ%;=zf+e+$`&Qi@nCo)=Vl+q(z%u8toZ zU)@_JpAlV>Rgaa$I+o#Amj5DZ!-KJxKQ3j+^E={zlY2^Wco{#>i0y-&hlxOd**#`w zKVp)li<{$v6F9@+2N3!iE)Pdnbz7}J>{V;%B5%-n=`Jjrt}~rz6OaVG|8r=9s}8&} z^G**vw|up8mUpUEZhJAkX+QLKFm5s*JT&fy<0sw}5B`(|{7G;8gb(>#obZz>iJGF) z%9Np5c%@SOmmNUi7>6F=?q7u30;taj-)HsU%O)+xb)Cy~C_f;fgu3KqJGCV0(?Bn0 zODq-T$@(lroqZ-Q0#!4KFhS`w$%alUp=F^yJ7O$-gLb*;*1Ut|qlYnFM=|W>R9+U5)?%o1Jx1vbu z2J_c(H-{Pmb^UP-R^O>9k{#l|hSZSzfzN&}*SagKptxsTS9t&|BkD?<3PM$>*^tota zY?CkUOJ~L6;PIF-W8G$?Ddw%d&(V2*&Zb?(d7Fay3E1JgtUe!9>hgd(?}J&mCXab; zwHTiQz2&z5RP)8n_UCpCJRca~B08vR`*S-6UT_%b zZvTD#zqq^gV{K!|=rL4_kG1j9?+d=}_E=;+^JnjA;PP44LCEJ3HDC*V>2p&n^6C8} z>vl=GEpHHUo42L>w${Iu^5;F1b~}&Uwy);i@O+i;n*Vfber6>*W9y*#vmE#4xrQxr z?n)!OwKA#XvwxzYo+UQ6*4DF!hZ=@{S1VhG;o0QU`Xj&Z*1O*IOj_)E?^eG$vfYre zEbdt1xv*cT+1w{{AA$Jz1- zV=K={wtjcMM$lrJ4t>n+JM(<(*%XQ&*6&*a}}N>OLHH(r{Lc5d7FN2>XSN z+!Fp^H8(m|a#DOkYjS>1&$8EJ!ss>F*&g8o@PwwgF#JD(eLR1Pn$wmpMtnYSXExdM zBbfA%8&u;hn8I%zf^{!rBeIx)$>K z7+ZLG_(_<|ZwQP{xIDe4iJL;Iw4pWmRT@-t022oMqK6WYaPo5FTt9+R(ruM)Sk{i{Bm(F4H0&X zpGSZx8z(Npq#Yo#c}K--#0{G+tqYf8ei8%I*`DE`3udV`60%;DDS}Xh5t@)Tt53p` z`Y$VJ^)q3H{f&0uWQxWfO_CRtqxn4lspvXrNj48mbCu}G2mO|Mcwvp`>rmyT}?y_Vj_ILdlvdm_MThmObW_bX0 zW3q((R_y2+V2}qYw62HU0?m7oTHnC}PF`FSob4e9yZ7_hCx-b=x+&};3DSiHObkBg z(h6XBMTw?;C=UY2>2UmigN>ZJKNDLucR84jXnvDbAId1_E9n9S2*Lo8ZW1%MS>P@@ zn*Yolrj|o`mbs&Fl2-#U3JYj$yuK3LMZhQc)r#Q%Ck?Z_At?8U~DzHwl zRh^C*cpf|P*@Y6>fx;JOoemdEYe7dg(?dAGKi}Qg*Aj;Mz-f4N5Hs^cNr27gnm+)b z#}0G)Nv|9Bu-7)IkosI4h=SvlOgvQZ^~W`S8o=Kl?yJ)WHzPml zVefJd8$%duf;b7e7ZMI+zGjoN4?2`7O{5InJ4xzXl73j{^g-{uPSxUB$_C5wp_w%E zxMI1!4M!5;y8{&L+eqqG;sJCweaRux30rlQm2(u1ioU9|&Nx%(E&!GX>;Ty5 zhcQDmq=zdLdUH8*2`xX+zu+6Ko8M5qZ`z_ot%GmiRm0L{^hQB#{~+dExfI8Y?$&QC z;=3K3Z|d8hB-#0PXVtfnJlg&tB_ne5N|*XP>OKCfDi+lHC@7#r(MCgtT(bv^iXk=j183O}D|tv0gJ9=UR?;z>&uq3R&d#pRvEEuF-g@2Uch}dE?d$nqqY`lT%U|u zwxrd}`a1?UX(*wtZ{Rtur~;!UcSg+W;X`R>_>Qi)Vhm5yMp2_ZmZz~ zpxdf18_zF<<1!iG-ke6a)miN;aKiJ-UO>0KT8ZN9!-_$&iSO0eXhQ`<6C!A1z48Wt zymEIQpVO>RMJH1A!cSzid2KUitI^=7$QXNYC62Iga0R2oN3NDZi)0*Lp&tL%*RaS6 zx5IRB1+PT%Ildwx^24gcvf-@AX_ewD52{2m4zJL(f2X!;$@Hb5B+x2T29MV*K|es= z7?@6PK$^h?(AL6H!O1p>(>P*brWzvg>ik4~oG5mRFsPtDWBaEn<)l-ltzca1{vb^! zC0dA`?eNo>$1&Q#v7WB88KXV79Phrmu+k#e5ALfk)<<3Lk)Hv+5FbK2Rj!B z?dW6k?umOkQsL-X9P}&jSk4tP7t92bV~mhK&Bln=G37C2$|D9a#)wMlV>CGR!Wisl zT~U3oM*z8+6=EWufU$T83J?icf>CuZYFH)J(64~8c-T#PYo`kpk8bfHpR+y@{-#;a zNwYJ(>Pyrm>K4z5+5>!G2F0Oxjs~CC;5l(pfM05_j!Fr>XT|8%cH#GjA^f=Q38zpF zPU`NN?cnfLoP)Da)KR!Rpz#&P{?`7kYZe@}VGr@);kWHEkr9SBdv_mL;4|E>wDF^&dvxDWTqtIHGA}LirhC5KZ zi9CWh;?IbWT}y(44FF~t4v-^=D;&80ahxfClFxE{kx^G{ELJBPJ0p?`^PBj)oeJ|+ zfR?XH7&^G~C{ZxQa`<&K?TE5R1qpMZO#doS!cRJkE!M)qin67o@Fyt~4Q4Ig{6wQ_ zRO9Ys6=d1J89kC8sC6gW%NZ%dx$$&gyBuums4<-i-OdoVO?!@HI9r_z<7-Gl- zvNm*fo=yGsYmk|+o^;@m=y6T~`zRTzVPKn=5oP@WQFU7^2ppLFaoY;5?J?=Z0nCRG zWezJ70)Fa@bIw=v3@bLv%{>2G-iPlo=Wy-SauM`Cfw`4O_A0~7&xS%0d1aprq@&F- z6!`h&`M>s9+h%5|yz0vUnHxf>3Ye_%XO1OQ{T|B2Nu*8m1-gg+j35Lr0GkrGo=n3@ z8JxKZPzIv1aYT{GULndOR91{8A>H{N9gxwLiurj(mnHTT5&Pm$PV+zJ8-CQJll2L+ zTd0>xe$~plnn{uuR`Xgb-c!KW$U6B?=_NlBXf)aoH9@7F02eFOMZZ zLm;2iROY!omgQkFztlbh2+IiQoh^;c7TiH}j0Z8A!}$c`FBbXehBH?Xv2@L4G>}-w z1?Lb~fP2Rvz}ew)y+>y!3xICvW0^O=)}YR>nv2B~A5qo&iJXAD-VCBYSw~%Rflr-P5@snLe9n zMIt5TuoGEQk5iQTHdk&RLByu2m?NoeS4bR6-wRc`C-%P1_WZ z<L(t1wl;pzID3^O1-hNd}S*=mVb-!bp-vB8w&lm|^8mfbjR3W3M=l{OT2nt0Hh)JXQAIQ2+YzQJyVIC~g;k>!uCft77SQcoRl&A6r*l1d)fR7PG=2Xrg*Dqa$OL#o|j$x*OTeETBYkl`NF1LTs zzHlLp-IfGH;4im@r-E)3Zcjmo3+KQJj3hmJJleOH{t26hyh18W%Uj+F=Cd0k@MBYj z^Ar}A5mtm;D8|z9urMSv#y1zXjgbZ1$XfsjXngReM_uM|p8fan%_$^Ub!8|5j+`Ld*?eS1{!G>{@d6;M6c;Xj9;| z_6(Co$vhXLZJESjPK|1akxn=%JK9NOU)U*|&C~)V`r~Wl!kW2|>fQ*Xyie(xeaPd| z$AzLaiLCq~K0^UuQjCq!`@7~-!SHM1SU&;sA&}4x*kVPbIE_q`S*ZqvUhb$^{QZ?v}yT8`e+@$0kz*37qm4dFS&{`y?A0 z;E`p@n0`g$MjsF5!^jwpI`EL`VxvE2o-0yey!xcJEgeU-Qwpc$?HA7ey`Zhi zcG|_Ue4H)aU8FLcUq?jXd2=xANqUC{d%Z=Lfuf-Ky?uLF=TYdHPspYdnbPCV3Ci^}{E2xIu0p;v`d(qIPxWIZFs2b5s$@nOXK0s}&&3*MV;C8f z^|!Ff$vTvKw~~z?kp3+ZD3(3~-Fk_wDom`B_>&a-U>AIfag7@fF9{HqN{#mheFxd@ z+TkJ-Egj;r(~nV0(k>s3c_;VB*Yqrwm=-nQ6OEIF3tfJSw0oC}`t0Decqrs1GfN>i zM;{F=Wc1w%+m^?EQjRtlid2@Q>_k zx&XJlT@=4l)OB>uI-Ksuwxi>a?P8^|8sA0wA%kJqiFsNIq~uHN&5nNGAy6~<#u1GS z;JwYW*2{!+_}ac3^WlF81$M{I&gFdjcL612zYr)tMU<^658uq*Je{A~@*kl5vJbOi z_30}C;vGx*GvmI#GP#Di{#|#9CbIHP`HUiqtF}URwrg_iY%K=UkXYBt%6bN{c~4M`z&4UTlIN{DRw&zOGE|yd^uiy z#ca2oPVDJnS#qvOdKsU&EeIX_R$g9b>W<*KTOmb0zpHl^q1D$c$-ahwnd=&hjU<%} zDJZRrHb@%bATtMIs8d`60WtV-$m-6{PHvzLZ@z;=(!3!QI2RlUM<5e=cEVg zK^A~160|`~U=Te}QC3bu;zp1FECeJ8%+>xi7*c4neWU9AftCthB1Nu~7!Gz%?;a)0 zUZI!mibex-!cDt(LN&xRx;SZ8ua0f(qg+d`>X%FZgS9k2H{0dkTHhU#;hmkcJ_$a4 z3(eN?QE*9Okc1CeQXDnlW9i_p8|VrPL_t^Q)#)md4Rm#WovtDoboHSiAnG4UR}?7d ziUK8FMY4geXtkuPNCsWeYMrhICndtd#5Q+T`&VGBxzJKJC9GFFVtksM#iB<|5{_f^HQe~@Ns*uXbG(A9nr zT3d8EfE|gAgo5bTO*L$MCVd%bXut*p5CbaWYS=(u8i5T6A`n$WOhGyg*kJJgG2-m) z<+OZbOxO`9WMd#gnW{lP+mWfru$K(uj_Tednco%MQ<%z=qxp{VC`bJ%d0gyYgoU}q z&vj%^#>^IH6nu%OXfLMw>}8>!{j5HLDUxg0M|oW_7V@zW@DabRNO9Nfn6SfAQJs3W=#K>Ej|O|rnA2Sr$R-&nag z&;KrX*=TVo7FLGfy^@Wy;CL&K z(x8lNbSxf1oKn$(jLAE)_0LtIWmVIfI!y6NFmg88igsq{oV8PODK%5-EWV2x`USM> zr1UqPewb$RABPZ&HTFXmhzXg&#l%2XER*8Trh zi;;ja@B_lU(~96c83VJIpkiC3CyTzy|7aUJaCr!hsapt+iFLs-DUx-; zv9T^V9*^Wm!Ljyv5F7-w>ZSiXvvxZ<&;Q*{Utc`zng~j}n0V_#xW(U$NRec{KQ&~g ze`mz)BJ0%*s}Bi)y3#|xiFBnEZ1iPHh0s1wDTi0!5z!~ z7skFDS)Bkzp<_Zzg^sC-uDOCqFhoMrHVW>i^HtvRve<;CSr*`eksZ);!9V_602#f- zh?X>o5UHEkKS?4X7zHsAy#*ObVmJz$#HI*RfQUARiry*_a7X9{{ogWkby$|2iAGM6 z4YZz;by`mZZk5b@PNmO3tygaW;6M1O{M0e0@|ltm8^s|1bzz1qX=;mU-Q^1?bxx}5 zGoxwh>3o%E_hN8h{vGh3slqb7y#R-9>Yuzr zALW}eS>(io>22C-0x%>8C-dy9z%m?xbr0AJ zKcmW)8HkVU>!IA>;YJ}#qKDFb|7NNbt{hasG8|&99tkDZ7tLvTFi^&)OGnY$khN`{_XKjdh7`Y^ET)(G9^YJr(7nI1p5l})? zvG#mAP%f@P389uyCV)X(?A$alBM2oHu2@$Gl!JXADD5QaZCD|wk3!l>tlA{{W8JI) zd}?QA1*i#MqwFLJyMbrA6-60=BFTu<4KE_J$gEr-kiO{xIP?fUmQ1TAEl5Y!Zw2`cf%fvk~5O-c6Jik}RT9(mBnlWPr9# zQn7f?4pQ%rg(Qxblyn8i1(u{-0qQs7R0Ykqp|J{4wiB9tEF{^E!KpmPHsB>0xB&bT&EGVlv2<;MW!!KR*5-05PXkqq77(S;YGPro zj}}vDIa-Xd%l;9g1*T)!A2}vN1F4Nxz#}~$$cM?)f`Nmirj~qI)I^wqVfvj+EkF*F zk21Av1LR?9334qUABHHx)RGTN0r=r+W#L_zS^$mYwXxc1#5siuoWnCE&Qp?{#W^GE z@JFbHw0K9)Gd#fHiN)%BD~iRAV93RElhyY#Q!F+ClbTGq`ra}A2JvIAzE5Q{cL0@b zCOds|x~CaDzs|U#o61oC#AcR@ z^Z`n={@;qEu-VW%|45nk%MT!idu@R#I$uH+R+^$tPcRk7^ld!(=x!X(7Vu1c3b82Vj&*-i}+9L;Mn-o$$r*$JewU+fK3VReJhhmaIDgp@*aXM3yRf zBmt%7J|t*h?G{Ag@Mk+`A&7n~^L|^qX8v#&zzrxRexu1?Gr}qUgN!tU_n{Tx39YTIn5;t$UMvNI?FOy1k#C!N4d$k3&UM&Uh z?O}ueFVwH{Va0bCVNiRJpEl@EdhNz4gUXRRoNQD(Sp2vb2f{f! z05X%g#-H@sJd(Vqq?`Hn0s9eruZ%qx{swpqQ<)OT2T5LVQR%idu(bV!v#@G9dp)gj zzOwzgwv}9fI))qv*@*LkV3hGr0-o4f>HUj@k0adOa_;&HrLkF*5H4m#iwZ>v@6K@X zq)uK`#y($C<(r24S{p)=%w`SWOO&CvY{@?Y_Pp><=&0MyKU^d>p)UInRT#s=1Edr} zBjizCR3+KhAjL`{2kow}%f96I5gs5-B__Tfd{7G45pwqNI|c$2M;4cn-s<(!>br#q zh7o3Dhr6i}IdmRm860*~m{V#tn+RALI!{u8Lg%R^VRj~u(0P&ylx#M~%w2`;hz&)g zq&iQwZznprl$Y(7v<%LEK}h?BtlGB|ii8gR002Vg$qtj-uL@5jd>P26uC&G-=(yn2 zMc_b#W75Ni`7F(1!;18Au}foT(09f$i#3e*)5Hk6<0)-}(+#bWb)Nhh4q^B>;!I#X z>})jZj%)U1#LFapX!m`ZlK!3dpYWT=x;~PAc`Qi%gf^+ZvqLB6hZ*E-3h){?1Pk<^ zpeBDat&l{M|JjR9;{*SuE%pB*iT;0|002q?jkfe#&1Q!8E9ZI98^q@({ei;y;TQW+!1 zzWsK7sxc9R6RQi?5P#JCbQ#A&gP$OaBhG{z?>n2QoE_Ipm%LvMC3jo{eM9L`2@DWi z5r2cB{uAy?OaRkBP%?r1gaQ&jiI;*+<)z@o`0AcW(jY7vyi{aGTW~A@1Y5u8WFmof z(sZS}dKyp?)>Gn5p(jU&4LvsH5ij(5LGTAQNg+k~&=)GUMd(Uc-Mpx@(mi-0izu8< z$S+S$ljKEZwmMC?qj)Sk9pds3C8+TSOjNBU@j@C{!fMhUKkS$$Gpd2fH9_p zdBjc_BWn!CSdmt6+c1@^AV$L&yfg-|^N8uNoz1qh*`KN&Hz&`|@tCV;;I~n4V{OzG z`*^Lc#B{Oc!4j)=?v3htSa#vXc2>$RqSEpSMjv7;jvxHxvBAe$!6jW)GceJDDht^f z()yb}*^BP@lnSI_{&HONyFbqqz-h^C1`~U6JD!+YdM3p_p?(`W+GBKeea%sh?u!6Q zCk!?_eKl2h`gu>2xMH_00_1h(^5|YC^8CLf9}KL|W*_d@^)=a#-mjm@;+0|G zwY&M*5owYGSFjTRvC#<+#S-O2>GeQB(vX*ZA zY2wB|ZU-ROn*k!EztQ^H09thnh07%Qdw%dNyvDx#<|3<>BvQ@YIwv0W6P&E}KUco? zC%sNr2l$mY3y@VN$-jTuqSQdjinh{y1aV2hV_qB}XnYmt*^luUS_)C7x{CIzNZV*~ zC^qHez$eFVuqFEl-z-vA@Y~pLzLyQigv1BM3Sp=#-Q7sfjSjd}K;#h!CPGX{y^n{# z#s)qMPr7tYG=$_jo8{;nb+7MRb%t_{FBuyG*Lrj?{S<>2GLI28%&oDk(qZAL?xn@B z6Lmk@uUv`-*#%@X0{-4vmr1GnxWBkgL^~y$NoUX;F#7!g z`9^pk{OTC0kAok{pI`!1ZoeE@KDssSB3{F;)TwYN&scoGxzcH zIO{8ZerB8Zbu4=~I}#@EF*G9=BEeVq`N62KlrXM{{hi(Z$?QKf@AxJ%k_Q zyO~1v6L(#UINs!^_1%D}4L&wGO0^^tvUQ9z0xvn#8x(zqgW3s*GbK~DLfbg8?I`@u zsNC?$vO!QH940Iq7@wR_!B$=*b+7ECxx3*d0K<|4AJ0DI`A4%ferdi@i!or!4@E! zS}X=O>7^JWHKfQ0zXRb7L02Z>_Z?c zQQOuWc>y5kk3q-vb^-dCL$Dzt<6XBH(!1ZfZg)ujGYI^LA#fO#2DR2NM4MhhK-kn- zh@LPkt=(T?hHywgx5S40!DX3`zRt^>fc_9{K~qR{L!Np%o9Bu<8P7fC@|fVi=YLfj1M)Xh2ZZ*T|Msw><^>5R*y5@A)AP7)V| zR~!%)o+;$(4l*<$&tIm>3|}D_i)&qDsCqzWJFcsxKO*d1`Xi>KK3%LxM*Aaz&G1Kj ztHU=*k)W&MS!wP_ey1%;3!(@CTbX18mrUTLf+3VWX= z|6%W3;Pk4hLw_bDGhcWN4>LmKAqomKAYNv)R#2-@wI*$?rg|&1qT-O=T&+TUftnz1 z1`tRD2JmqOtuspUl1I?frOC2H{ly{?*G5`Ip1UEn~(rf^(Oo{ z-=4G2-h1t}*IIk+wbx$zQF%#Tw-Dv`n+NmkOCSTT2Cep`DY1T_|49?6h|G|*Ph6zV z8mKJg+#N1x5LkpjE>{k2Dx-9t)6r`L}az;E{Xbu*akI%bkpfk@eQ0)9`Sec$?&ChZa_EFE9B~uV{h+#*D*Oqk+G;9-_ zIkk>@{GBCXl9UXF=MWW-O9=Ham|=eP{Skhv7EARwtH(Kl19p@L zxu^qqp1y(qyGE2Y>Tq17&gU-*rg|Zecoh!(73PI?0=bhfH_(Kkc-8CnWr_%cL}``a z1+V*A&GWwSvZjsMf>qq`?GHOQC`(OV*0m$5UuD$VIwjrI3G23VOQH$h(;<7=a7uc8 zXVjxGDsofBAkYJF`OV?N2&PyL#|bk|Rk2ofxt@NKUJ!X;XTj-}>yHNw;OTT^ig;jL z;*OMKTvHk+iXAXePLS-t?nf&}UOvN12o&TU{~o5qbz)%SNdHNUA|`?lyO4=XpNN~v za?&2`FpLeTI%lfh#_czvm8Iy(G&_%;KkR^TWfNUnpu(`u0gDmJKY{W{8@E{VKH}l$ z^gS-hV}QI!uDFzqJd(Un@r>wRgnJ=1Z}=>pD&qOu2pN7BN`|$V>CPWjKf+43c34~l ziRFS9s}=66RTpwz*h7Ax6-|h|#&AMjrFtOa^I7&X^X$_C_vuhm9u$GZIQkIkp%__9 zAQPItkSOJ~!#eYhzqhBOVQiUCr8y7Acf^)%a7vxP6Tw$%5RVbTz&ydA0}nJy)Hf>LdGM0gG2>ju{=?a6 zEaNO=(^yhUYDqWyNiF{AAox?z&m?L|2O^=CvlF#c-=A+>s;&_h{OnDh*3Hk4!|7a) zB(;j!jkc3g;Dn2o9eoTI^m-?e&f4kYu$j2oC9NP2iTK<*N@po>M_dzRKreJ-qRVIh zC=RcPeTOj-Hyzyum3IMnw3_MtWV6-Hmqil!I#Xm%iv|(5ai9crjmTa;frb+C-p`;W z7RKpM^c;|yf5%2X8E}VMo2v_B!lQTL6QdC5e( zLk;=nGk|WMA1)3V9Apdy1KQn7viCEnz@L8NO(EoY3kjpS10bjc^`E!JVZ_{oOY58H zFwfo@lh613G`L2Rp>i)~2(HQR&rl5I8YapjX%5PyoI_5p%?dA^<3!;l*)heii4XNf zBo!G$eULA&VX!ui2ak`-geQkgAt~mXK*1w4yRi zK;J}gQe-{s#M>o&vD%rbFoIHnKZS^|$XKqDAW{spP%0G3G{gtpVFJl!TDU5nq8iM# zA^Ab$d6dnWNx{{7eakYfr^Xc&$-rf3t301Qcq&ln|1QtT7e=N?3xR#oE`dMkwFX#{ z`#_BRxQR*ymk{zDeF1(Pa_rt}OMi{z+_bd9NKX|JNH=!RsLq0=JA(gcd^0tbYcq~2|yfaOkJz;0*2VRY-~a`#*?zzUWA zCTg(1K_R*KMr3DFVSZr<_WuI{v$LX^-}n82g%}81uLrk-^taMH4iNuZ$C#;snNc@W zQ*CCA2I>%I1?ngw>fQ%ZHexD(ab_?K+4R9n&V?Aj`vsMO3(kn75+Br&RhfGeLm9?l z=!LLWaj?7!>$4;Pix5Qp! zT3%5sC}l+%=jM+|{upkonr$ri9m-?^BC9Ok2tMIcxn}SFe!vu9=jhlW<3W-a=JQi@ z$vnF`Q0W~QSgvUpRSY!Iuhb7?xy4OsqAexS_l%Pi2p71@+}DG{P^D^1vNNJHsdIn{ z#Tjj)2`4pwMq^eow*?u!y!3 zJ6D@$2SDC>>^*EM$v~3TkAQ*h;s{t`r*!HIWR*w29MWg@GMBEFJR>gclWwSq4(;7I z_8!cg9<-`1mf0L(i{76rmtExQRMq&q>B| z6+q2Jk`6`=u`xW2x75BPTtsV#2EQ6R2KBapKrqM^0vUkX!R5?( z-%AZas+e?JL5T1$2@0Vi6DVDw6cCh2go?1Fri- zC?Dui4wb+$e@5xdiK+mORKm|yvj>e|Qvisr^nf#7i46C>0BfK}97GtLDd8yE5A{4c z5%XmoOPe`Ph>~KQ*k~zGv%rgrI@tZFE@K>K2M#=guk`p7^KUQXyzv&ui#0@^eX&`A z^Rl8=1&$Sv%3JzD&PWO^ikq@^No=pg_U{2A^+07A4I8q=SeV%%vBD}AJ{JxTSK}(Q zJ(j^jJHG6FMa-JrZa5R6^cp2?k)(pXKD?SC^l!uGMAjK5F+N0e9OC4Fe(t$)^56%2 z7)!_SIVrvlpE4+S(~1?a@HuH0zVvTn<`h{rYe@hgd=3o)TBUmR?clFrGblAY@s|&< z7-0w{(U9C>D0v9yRS*~-T)+pHGE=aYzDboOG(OD>*cd>W)mA+D+)}@!R8^I3*ai-wS<^IW6F^-%3Z(Y|(>{W_4WoDBUvw3ATZ@I_&ImOi|i zEm=dD*BRYof*RX47dT7+1Qc}~UfS86cd(|*JFX`#O#(j!C0ge~4<`V)_YnkBZBcEO zyem#aP2PYu<2D>0t=9~3tGGh8CV)$wYU;hmyyGC2(bf5R_Ku-Ge)0>noNW9dW{E~c zEy~#xnpxGBGcS)*X9q3PRX8uGeb*LMXU!PUX7GTZ_o2xI@ls@pi%Xc@h!9GC@Q5*3 z{hQ(3jEhT_@KqUIv>p=`JT=0lMoiL_l|qIZS2RG0lJeC+<>cBIdR7ryuvb(RTAgGi zc2$xX?6$fR5~;Xs0!q9&R>Jj&j1hV}?b~kWjDYG%HDwaTG=?s?MJ$p*mwyJ%+FUjP zii*o7g3j;LGQcen>3RpIfJxUoNJU@Kq!L0fUvFE}Rd|gMNn5C_n27?Olw^#|2}6N* zl<?nzNO|28psft?neSL`4S&?MoZh9=2g{4tNcLGh%AtbT}I*1ZtwUI}!j z^i_b;>i!F;bxMxqV&o!LS!bh?%IjW!POAB808o0vk`7P`d*W3E54T(m0M)G-anbxr z>-tj5)d1wJ_WL-6|B$zy?Cd~80(@U4L7hVoYFbU5Z_29y(jpu{;5?m)=)ldz)c}m# zV_yvbIH=w;f_H6pIhUKKW_@0DA^> zu3L`?zgJbJZ^%x@Ft0MedO#g99NU|Z6_JmCKwri3?ub~j_!#VYN1nf* zHRy3=M1Pz7X6$TD85a}Lrrca73Cs$fNwND^tuI7y9pLZNN)K}XMQCzgoaLo#3JhdP5S(&5^c|yroyad~XAVf(D2ZCL^ z12Oxbu934>`TAAr=Ddj8viZUH!--29zF1e%D@|rKKcz(%>DC*X*U!#9*ou$;CAqB0eMH^UyYud-kDK3l&FH4d_3hS zg+A*|2>c_W!d5Zn8p^GTtzvAe7?XwSV&o|oW8cv~=~|V_lF1RLqaja9b}snee8kJB zGpVA2zYz@xkOhddAurH+hZ3b#E;%W!M%Ygwm-6g_xB+%V*~Vj2Lc~^)AVM`!)J~3n z3}?lMO4~#6m<#!Y!Embt+Nwx7cdCgTFdL%#3A)f$MQW=AdWt1bp1(z%Zjjx(v>%Wb z$4%QI5!a5TL@WR|W9fXoMg$hs#ixyphRbUQ>de$GYl^1BYP1DSU)pti?Xao(vE`Lb z0qo_qxLI}!`myCG?7njBi2sHacDh87IdZdaPIQHh;@`T$W?|L3!bZMrYK4uVqg-KA zSL?(89Th8VruSln&Dx|~VW*@kY}ekZ0GnQ6vk;5DwQ#3yU11}MAA5xjIOPf(P@&CO zVbcM<)mm2As58kEtxL@&R^c0T4&U+>Hh>i??9IG9 zS$lrqH;)OE-ppd z7t1%G(Vg$`w88s^2CD3Qz{?G_G1472^nW};Nv4;T!EHO}13?^!VL&M?6v7k&{||Ka#k^`qz-lFMtjQTwsft5#x@Z$Q!GCtWR`I?e+`z35Sw=z&+i1lInY)kLS~uV zJB8eAj*yuTiOV^O7Xc)>OmaFkhq7>v! zKueN$2Bnj4Kuak#{w~@ICP+*ZQC(B~$x(o8gr78WB*mKoO4j2{##^n9jqsC3tOR!tsznUTR4L&n_oe=pJyK1cY!EiWPsYAvdbn{&3vKrgZ&y2}CS;i8ACK6D-70IJ zbXofcLcA$+(LaO*S zb|?zhx8DY0zajtb61p9#3mzDM=2z-2A3X!r28E!|U5@YiK&idBEnAEh{?^I|e1>Uu zBN|s|FX2O+w3qyRb%kUVv9wyn_Oed<4MJqv%{;d_3s8`{u$fj?>ENV?N-VQt!=rwz>$^?mzPZDrx%J=N)3*hY;4 zThz$XImyby@D*+Y^fNqYCv{K!0KZ0ge(K$$kL|n1&|6cbdP;SkjkKFbCs0efaRPnC zI0f^xJrZ(hS9R8sYVHQ4Qm}h?IkBFNZcsy`8+nfR zeV!i^hDy$?!=s9&?fQ{GaptI-QkA@eV=Pd3cVJ>W?Lq0Z8f%YyZLI3z49ZH^;6EK9 z`!=gJOY&E9?G zsK>zFb}C9X38^{A!qHOZ)T>P&SE}=Mvr=Rg(yJldCYCD%R(#BwP{EyX92sFO5K-aEmu)8^ z2)%S+EUlq~N5#>$!yd-l4)5?7JXP-3+R6(UhwN~N^e94*ANOc#>$3=m* z8psFA#GG;(2)hG>5rfM;`h6la4QCkX`E-5D#zj>e)T(*Zp`Ju1u);?$HKrH~SY;}6 zT&UONpKON6m>8(jGtNh*LZh+%=L4s+3Y)V&uD(%B2cO7(MnXf=UCPc_&p z!cL?kI@3L#P)`Dpt<3QB!gSeno7tCCMsJ}jSeE!pz73bUoDPEodOfb8%ay!G;J5P5 ze*^DaT|35kbyLQvxLxI$u{MVUo>b%12WeVk)gQcZs&&01MF&}LrRuFSLfFgxXl+G)(`oJMlZ-QD{}kW3$2C2zb7AHz5>t zqw(h*Z-Lo6&NLQ5evd2NgFRpBQGZy{&aWe1d%a6%CN*Pj_$5pB|BBEpD>P{mc!NR_ zKbq*Wr^gXL7QP%p$~jfZ+WI+!A^J$3uW*%b09rq0-5h5+6*rv5Pd-k3`#cROySZL| zo*&*lsFQ*50McnRoi~s*EbBd{&jII~D<2^!_3VrPojsTMw$lnbwJ=0Cwp+}?B$~rv z1mIWLGJ!sTB+d`#`6ryNS@}<8RpRFD$=lQ#YItb>~NC9dUtUOUq?0 zwNBmgHG_kl)vKfNzaw&2t#7K~N~0D--#q?f;FJTJmNe?&zy*Am6ODk>@S&msIzzXAKdK6?=JBpEFw@1;WaAD7f zq}wDxtL_6iLMhj69(Tz6Nw3W#$%{%5D*GkHrxDM@iYu4bV*A7`qG%)47g-aU7Ixl0 zj;4>V-Z!xykH50ip-I4w&wsrGGq>w){69pz&o>Ml?a{J8ltPKYKnAZBTe4ndA>`lS z!KMKLRW}@cEyvMDyKdoP@3&3$U1QXem~jxXqE`=_+jA%|+)i1hj_gOhy|HEZ8rZT9 zOpI~>6NJR5IpUK@b_GWC*AUqZ^K%%7x1VcuL+u7gSDDk#GkdT%H=!iYM0*Qb{vsXCecIiFPwyedgoXRT&-T0 zUlUQ{?@Brdx`p}~i}=vb^0Qc$Z5#^kKs_GvN7THin%1KL2%=5?sl_Zn4^j6CO$HrC zW&XSDv4`%Ce%MeCle$QDLu*u-1T{%BTHcu!ySrK_JYr_4S#jCHg%l!MmP-yo%ppey zQG5*hcEvwtPn=A3l%B9+mtF&ZeO8>YgmNU722d&14Gi*$ZjZYExPpz z=8r8e1-PoHp>py7aC<%x0i}vsJ>o=Y+Ar!Wv@+|+uDK1r!->`5cZerp6XZ=uZFmw! zsV3dM+J}DLXw2*YH{#!=R9&}5rxGuC5|W|gmY!tE=8C;BbGmi02Mo36M1?MYH_if__Q*Ye*= z5{8S(SLvz(TSz;^#p;|}|IbaIY;^WzT?_M#gsJWsyR`+QO{0(~ys;K2&^b;45L+5l znuegjKW|zVY29lsADh*)yz}zT%?9%cE#~-)M;lZ4G<2rtaozf+q#xA zObu&;U!;#+)U_|OaB`W_qOm1q9s3hQhq{C(%%OBOrkQQ6ENq-*aONufU|)V2#;wj; zn1Wl6D;vCa2Xn5eUPhp%@YRKDfPbvs{u2_-Z@0x${pR0e3HK)R-*D~GH(-glwUhWd z*ACp^V)!!_K2^f~YiITUl$iImaoy9yBa4*gW&FH!zv3N1_-KkLjs-T(K-V3Gh1n35 z#+sxj5#}+4os7kLnJjV)hFJ!H?5`Yk-BA=Tqf`P4rKzI`5NU9$G8y(}q?{1g8n)}< z%5~QrbzM>1mrAyl{Klt6PUdLTV^>m|;zf1VNone+=Qol}#v-bL{cWA%#8 z0Vb$XMK{5()8V%`0q392ckD4R^x8-|3&*+V3;_x!J~{AU&vspseHKWdJb-cth^}=y z9Rg{*{=T@&ZyN+T`vn@Gk8?kb@e^-~2Y=d_Kk1D{C`fU6`+mudfLtdqYIa~Pk?KG9 zsc=k0-4KfB+M76hK?QvZehp7te7K-AJe`mXPjov_Q=me80p^(zfaU#KwBF=qMIPMr z7x??ilr28NiVp)+hE7p<6u=gRM}c0g3`1M5;}v4r*^_}ps24#nu?!AyQRk#~q*khb z{O!o`sU614E>xoXQ3c3itD#sNq_-OWcY|)L`Y}%jHBnNkQG-> z>yju@ZC+Nxpyd%Q07TMq$;K(E}MW}%oJ3TdAN{Z@@*tH$w1r*ZsY zNRP0K#yS5sHbW-&iEcugIYaXxM?LZXyK~oJS53xMF08y-9|_Nvs<1W`*rd?5OooNJ zDw(VHU!~fg{r%u2b6MiNedrMWaM@Fj0U~aH&RVm!NbQ(Z+RTg{&A5|>`>Q*v)0L?0 z*A?xTW7E4RW__;Di#vSOs%aY?-P($^7^?*KP5sm3g+Ry>FME@QA}(hHy0(%1s) zmP+KhroqbfN!2D2HESlX8~(k#y|K28^P!AJ7%Es9v6z^&zG7{`>MyyYxr%9BDK#xf z>gy|ONsFJTnibjD^b?a2c#Bpz%@xLc2^>K=X^F>>1z1f$k57+l{8~CB5mBl^hg;gA zAlm2<^k>y}XqI*ofvLmLuAR+W$rb&ayp~(osZ#K~-oC;NkqnV~`FU$MbgE&J?%!Og zvKDu}hhm;WBHU+@ z^*-W2?PwSk?g?Yk4K|nOkUXn)IFypBFl6{3Df4=2b)c(u1)nw8Q9}r@047$el7GWG zk{-ej$Gtb)!%la*BH0C=rQ{{*QVTUbVe(AFDkfW4K)b+PfODJ+dft3CXh$^@+9@ff zdde-R*w1{|31DQ;9iBmW@^2BxW5+M+i1Om+gN8Cm{<8)?g>mOq9F&W!dJ==ngCnVA z{_BsP=8*c6Ugv9G%=Wvb^1HtEUt~S-D&p>tB>$I-j?u&tOc&{T3H-EYIHP10hyqpp?0cg4es>2BNltMli;ag9wTHVKqkYyuDs;tw;n zMD6)lTj_%nTe=HP)=9Gsn@kP=)rq?qn4+s!i$CY7;I|YtOa4oqhmEzHyWYGHgn*=U1J@O()#uvIFMJj<1FDb4_=s4y9jhNp?0*L>ut? z=I%u*{LaqdZyVl5+VHHpr^QnX zDt$9xbz7!|gvb;%@GL^`6qf_w&*@f1p1mjT*v*)0dyrZu7z84#{P+NhcOCE(AGeXL z{CI!u+Wa8Ke|$i^Mg$%36Nk#5^qRc=NmdWIO}jQfsS;nON-Lw6ikzYzUQ&I0nChXa z__l35d|1@8za6EHfHB%AVf>>dt)-CW#=C|NVQ_*vyc(CViqL-U0ScoRfD<+8BRct@o4G026AO@tH8E~PFu9S?k4QugYTrqJ5DmB)HPSxzDBT(goc(}P9 zp>kH@+3na3k%LplsU&01Bi{&MPRj5nuq!}i>T+WnaS;!T;?gPbS3B$&WnYI*W9hu1WfP(g93F&$gxxz2QB+YqOz6U&q~8>)Ad7{}KxHlT!Z4 zOlW?3SSIVVzMXo#{hJ09c2=I>75i$xoIU73tfAp_qZ-?FXxO&pHDF8w5D^dEcbkn3 zrB9V}T6_iIuM5v!iWMMkuysJ7MY_OO`SAB8L-r>Q9w4qX?^PQ@;Rv-Mn9|MT$RHce zY*ygYrDQ58b=5v}7E}$&OtFl<6`r0eZ4+4r`sch85csI|@{%AVbRT89l1_EBQ?Mbx zVd=lzL?a0YfX)8iUMhcu+;hHHH)Y${j%Z>e*jIm)W;kU9`kA!(zDy>;MBovys5l|( zQ$%n}^$HEhjLo-Ln@lR$>rYN*h5sX$$$r=qHt z@eCDxE`!*%uT^H;Daxdxsd>jS^{aI>slQk*P0;F_K&tk2bsxMU{Oa1OtPR==dQG}+ zm}4f;t3(T|PA6$WDUDB3jc@H7#X!J!+GXNq*{AGI+J_>rI(Xt5Gsb7>c5r5q+_qsN zN}yG4-$x+#l7?qX&GFz*g^54uO+I2Wza#X{L9Wx}kJ|^~u{)JC@!(I&o!6dfB)q>$_6#3qyviGV4DhX+-n53M{`5zT$c?cJv>BbGbdtla6K597z6Y&YoSa z((zRT#OJ5Af!bX*6FSQ(wigmH0zh{R$$Vv!lq_O&A|r-FHO{`-0W_EY7=;thMUK2Z zwPccKpOm|9j2Or*Vfhrn4+1|wcOnwAeH1y~XqJV`<*a}``LXzN%N$R**+#M*bxaI# zmj7Jz?gdmJseQh~%ve{C< z^h}QeI_P=SRh%Tl@OmtHPo^P$cJOuKY6vSu#6)z;tV7-L*D}IC7+N#i`3rx`*WItb zG9Dy(VR?QS4IYt~uYUcN!S(#4*XEJry(7g>@nIC9FgKhpRN@!_Il?7J6y%7LEb_j7 zlVkXDY($fSEAt_XGG1HwB|)afU#>?{PL)==YwZ92l0ofQhQBA;2;-+C8q7_g(azRu z;ojDHJ16?KY!Z80=RbE+Frv(eU=Exzmb+l~9|2L8W5rYYb=vYA=v2(wJgH#E!KBe}Jkn3l4zVJ{~d z2=2rrAew}6I|iC!tg3u^Nr3Amc*3K9*-n14Lp=IvX(y0DmOtq=Q1U^X5?q5A0HVC# zc`sLyGfl4}G%l{hwD=Y($**Ur1hh+^IJJ-w7C;~=6!%>OeDx%=xwAmT8sH`sfm8y&h|&QhkqW5tJA;ET zqc>295?Z%%(S#BnJ8J%<*V#4#8kbdI8loyI07j~QkuCk!PJ1n?Ao8Wn2Z-r*TR@~t zTxbywflnaD*XgnsyO}_=@3vr-!+;a*1_a!83`DFEq;q}>czIp)flr|~hm{H_+9M!< zXh-c?fEaiQ41uLRP2q_tnsD@*40J{wbB z`bn>|?GO33rCx=0xAMhgS%FC!-0kgWrv_3MZG9!O5-3qJK&vdD8@l!vUR$4>u8P^m zrktbdlhbQSu73Sh5-dOU$?3H*vndUnqta_;xT{F+{Ds$6s>3Hn{f+>KFp5WPzw$u{ z8((8yaka_T0nL6-W1@jhVREUS+i6iW;?iXtb_HNfZ{MY?(|MouI2!i^ImQ^o8gph; z9C5K@70y?Lx!l!K+3vC)7TdTcBz%3XKig4Ovdtu;y*9*UCpIs1AYVs};jmSO1q&#A z?Smxm0sP*1D*^mrzm7XQA5!O@26<>U5TO>Yms2Qtj)cK%?l65VI;>!C*J0}0j1EJT zTh!s6)L~3n^^t-V+l`cfV*;z3+*LcQYy7az_=|Uiova+l_$o%6F=TwL46c}PkVLAG zR?>LzQ+yj#0X+;<_UWu1*0r>2w2Mo(Zmr%bIkj6jj?V{>_6yWyuDUdiOj@9>9~kh~ z^k~1{ZD5XWyM@!&Rhw12wI{Y5*nMETaYk)DK=4VO+|k@7Fh}TlzLr-e=y^@dVY_@& zDvK~x=y|$pRIf$@YxIWuz@2KJH5{UwT z4~7C7XA&#eGPy+c)p3dH!)j3#?pC87M9PlFD6Wjq_RJCWfHG+gl#?lg$w|$iGLIZf z0FBs=%T7Rp0>7-S>kBXmE~zIi3xA1{<$NjKfLU=8PE z8l6AE9^D(;S79rzIDQyR99}t~8U+5iY#Kxc&Id0(P0fK3B#uu2HA~1q@ zB1Zx9Q15xfLxEY)2m1T-joH8kWrQ<8H6qFpm82t=#$f2XO zt|9}%jU`rm<1ofI&13m2dw<7B|M-6JcIIE%Ua_ofj7uhYN(fO`B9VHss4?&NO~9rU#)iQ~F`@m7;ikUVK)$okCQOQeBlg8UFTogF{QL_l=U>2BNdI9Oa+R*pv2wg*n#VSzWSZ1($&^LS&F58WiA?F6y}X$C z%z5^H2D;ya9Gvg~9e4=Bqw{M}ZIhp4h&rQuVaUJx6$tt_v z&rjBP`bSGEKVaO`tTsuOK#RfEX28|A&A#MV=a{Ccrfv|8kx+YY7%0h`Fx61QAX80U zQY4ubJRXFYrkXMt6CN{-(lkUsfRUtA2tf&U2lhD0H` zSycaktQf+XDbI|0r=!o3kJGFDDrk;rH1%0^zOY=EeU90cl@3@-eU8}yc~Hy_P+u`6 zFuj(^=rc_`MxWJOC-xcG6*oyqC}xS&Gpf&{YtG)eE-d+XV>9!QQvBg)dNy6`_)6S! z`etVL{M6Xjj%bwOAA#gO9c(dQvD=D{7s4JrUMV69!-g8mTuu_RkPcTS*2rw*guC7#67iYxgsYt)CY&N%#q`9k~=w1^iE>{6lO%%2HNXby!Y+11!>O{}I=2W%=&sR(mGtFvdyXe4!nj`a>bFS^ zPRu$<4R*>%>|9yeRbAA1a#JCxKn`y=mz0IZrgPH%_)eUGs~;o|&0LFXsmAh-#B#Sd zcA{nMUdPARO?LVf$`ZG|dB^+}k8p#Dx8(W8LQ!omx9}|YXabA(Fb}v0rx{j;V{P>%+!7dbxMkg5;h@34Ev~oFI5hr;xGzpg2e}!*6#0T z4jP;7NXr4Zyf%fllmHO4$|fyKt-SH&{B!@^ zjjrCWWi}d39gE167(eXl>xN#)t}2Q@Ux=o_RqP#X?+O|XDYm+>G);0NNyeMZ4)Z zD5mB;1>-rceDZ&Y2D+DI%c=a-{$pv4KNg~YesLJYerc!|ls(7x|1EZmu6A`FFK|Wc z<+GFFkL=|5Y=%jkI-v}4+%1i)lRRf2eH%$+GLXkNShZk@b*LB2uP$^km=rl|c7fvG zB<|veTEs7}Q5dMyPwBL6=IhMvb~boK#u5ZG$S#xo%`&w3hBnQhO~=O_Nfe!3Vug$y zLvrKiqwz}I(g8k!ZJr}pJ}uA|*0|{ETG2E^nIROK>FjEe6d+!vZ`@uY1E|s<+D3jq z=6tqls6YLj9wij6-&n^~8dKdi`^%&*MFngx(J$+D&n6sOf zhU^*iKK)Qj@!AnUAK+8`Kr{7*j<4d!wk+%ea16U9%aZM*+IJ|#f-MU$ZL(AL4$q-6 zmjtM0LhVcTXnEKbEGMK~cpj=6fYCpuW*-b2)3Z|uA#ggE*XC3C5hGka7V{6?nnxv# z*(yq>H$@di=})O}x;M-h#S@W5wH;hdp{TKD9U}16U{_95w}7P^rWYKfVFm zLcpJv0Wx`H?<}hVXFxom2KPAmmZRTx*s9lCHKrDk0->@$PTL`v#Ubc}W+5w4N@GZ{ zO11BHD(9ce>sRx3_>P)&YFmGi98>xxbRk1z!Qb}+{u)E2u z&m=}f2xAXLOFkgyhYZWV5T?2H!z*76>JJj8=W12Ue<8SggxpEqDcL*VqI^}@O#CjU zJx5vB)n7Q`t74D(EPM`5LAo^bI%i%`!K^Exs|8L}Y9i1YY7FQ@rHKbg^_uhp%7n~3 z%VW##upQTKHpha+U{;ME^-qD=ddiImYCqGw+`PHs`Xbg8PkQamPUv#Nf;i;7b@ zoS;!t)wf8R{*6QO+xmk)1I9UE*>uWlItn+|*R^6HntTHv^ep-)7N!mbvs}RWWD-Ia z(vp}-8BG|!^w1cKHaSJ>&}ZN^H1PJVYal|mtZbt76Y;iuD!NlWBAsKMx<840o+vh- zs{Yij=DM1JQ_ni>tkWu={LtX>1MjX3{Nu3;-mF`W9@8KA;D=5asC;1HLxTe!M>KSu z*!hnG3%)q;<;uWkF24MegO#(+8#r&^{K~)$*IfI52P%bk6~6p$l<(OQX7E3bjNzf@ z`pZpzeGs#AQkFu5du}JKj~|62IE3NsSSF#W5m=iY3vzsn!w$7&OajS?eO#JfH+adp zeB&%ziD|1$^0{zqFK=$+JhUC!!0pv#w7-qQH5}o>Wfa>Ke~usxr!fgNlEscmRD*P6 zSOqV8xx()2;WGMG>*WDNOcV%g{|;mCu8J@;ns`v?&Hl}U*4YRx9&?y8J`0ypZTdm- z+T&aKn70?d3uJtXDt*|5{f);xz6|fS@CjwIP*@==dzeDC6?Y`pq?wj@go!#i#XQ7{ z3EUy*5AV9Gvp~xZaCE{Fv5x?kiMq0i%(fDY;_bu@r@8XAp*XFx%T|<++hZ!P9MXBX zvW8}XMce@flYRmXRiU2b8?*7X;vH%#_ked)YV-5#=-7^fIC$m0;sytd%}V;LK9zo$ z(CE0Y9YNaWSPw@1Q813p@jP<9NLyEdj#o4@rsV!%RGkJMXb4c}GWv#6^o4Rk6B;4xIq0+>d|C!Y+dJw+={&y$0@Dy0oyf z^|YUE5N|m8Xf9(Yd>wT|Y&%!t4nw@DRht)s5Pc6}(^g3cY zL@Ay5$+DD5^6wwI%?9x9kF03RWCA5h256PB>DLE^+rp+|MNkuUN@spaOSD%eQ%OM; z)ATL<6mUlm=y6fX&L>dqHGTJQK?NgavcMHsBY$W5R=d3VBj$}Tdd^f3lnq;1R(iT5+Vck`qD8B%ipWSqdUo_g&nQQpPu&!2p*A#CKk>hpJfSfjD>C42W4BU zZnOewdx0UgDrQ2eHeb0N%{k!kL1*HV)D7kxdI}7Tg32{mcfewKzQkA( zIRJh0Hy>30_gK;B1N$MDEICI#`^4%O z!|;hAy6~(GZ@@>w%M#;lS{6P6wvsbG=G2d#0Y0Z^*%^$Nq96~1Q&raCRG*#L-+vXu z$8Jj3-@lE-cyI3+)NNZ(0wB;9qKrhU*5$a(>+Tj+^Wz#CB_Z7WrfVPR7}VSJNI%<4 zi|(7GDf}X-eaDZMHCvI=cM3w2s4AzXGhZ=>V2l|-mPB4nPRZ1$#Yfhh@KR}Z&dhp@RQY=K4KSg zL+r{i$J%4esxAR2saZ*5>_Ek1dnQtbxg>dkXBq~8!kujkiV@QzuFzz=5xO4?h!()w z@G2W?C_O0#APeJ$fi{s{imlXGjy4&PSw(=w{NLxTL?&z_>|wy-xb*ofKI}%GUDUsT zuHf4;u=LdQ5f(J}q+;XbW;V|d;pwmJf4GdzzT;4QBDT^vnkfdp{!LJ0(!-f*gZ#vs z;=!M?zdz}XMa0+34|CG;fM@+|X8g&#@j)+rXq%P3c!JGpgE%Z&5G`TcbA;3SZcJ-e zV{{$%Lb@ zr=DLS00^DMI~E~if8I^uCe^G-FUmVs4qmd5nL)jOf zyQ;YDWvYTSXWH}AX?B%G-q}1Aj1B=FBGl<#e-h6Tp8=5#o)NqQJ^ztsA7Q7DZyqPP zaiTlU?rnbvf}$O{@j*dud=NzXILYnY{{9u#`oY?3ut4v2gPKy2 zJc9=J`tSobz+AZ74Nj9~GJ!(L_b1R;E9coQ@Nk5}5vl3$WE`Vx%U2wrr2UcwX|+r? zn3en{9!`fRMQVG_=Kv)DmqG$q%Va~Gr17S$c?Ay#ox*8w;aaII7Cfv_<7mdV64rjq ztJxk>yd01{M-2>zR_b_o-pcbCTC=us@?FPN7iR0mAuta_(FqsqB@bt><1R8Aa=z_` z_k57#1$@(XnKK8rSn*9y~~RF(fKIMdWm!hnEd@c?3@!N&Op zI}u*D;j$7A#&C54yc*l594SCkQ96w!#rY2E&#S(;ySH}{j7}@zpaeWivXdaGNvuB+ zrFb2o(UDr;P6YewUC99!`uS57+sM-oav!fGih0oU($4P1c$9{cU(SghDWG zo%5Mv8bTo$H)Yk6&EiDRID7ER{{Gq`D5hcAkuM1_P z2Cry|X$ti;dXbV=OA(==TB@Nfw65{fAd6(pS8s0V$AU^xIl{N4a$qkjPszYuy>gh< z$`7}jZlj8!)~>(qSxn*hnVCO+YTgSX*8_hvplH?vw5pe1rg}`7#Mmo{>-UZe-PyX|7EZ#H2&_ zN%wQMc8Xz4$|Qh#ia#1iS@mQgN!F=+3L2%B&AvqUjz=Z_=*!geP!%8OYn2S8}G`3kY$_Zhl-&Gk6t=*gvvL&I1_QnP0Iab`w*XekSG7okd;B|($)*r&;G z+A^U}QU59g$6{8MM}VrW6XEZ{MEF^hdBnw(%=ZT6;2#F9Eq=s^jD(1X@(q2TN796L(>(EPu-5ZeYl2Vyj)cqCL*0w+Z; z$SA~Xm>JvF-`myO**!2u2%v<@=uE0KI>0Ce9Xq<(VjR2cE+n zGbGu_yX%8|>EHRtEAJ?>Oj`1#dH!GTbzFwexzYaegRbv`*Tf+6gOOMM8n}z(ooux3 zehOduHx29+SpYEmD|=fK01iG=hyjhZZgKeLL?np~q9l%!a~~TO__q%#xXDE)4;J{V zEPx9V$xRd}%TkS(yQONaYlt1$`d`pTZ-fKvMH!;yWabbA{{lv@BB?iH_gN$NbT1E| zmMt+%)vo5&i||ANc1=6kY?kcF_j_lQ!t7~wGi)+hx6QM40=2m&+g7Y>>v=iv?(hNH zbGL*uDXTcwbVtzE=0)0~DItZiB^iIyyus*-hagWA&S{op zuY~M(>!aJmCryM&Wrdic1JG-t-^$!h|69Sy{ZYtvj%2%iZ5~?79$MhCZq?&o*xgaJ zKOWE4sDL*=b#Sn0Dxo-q&;>BgN3jt1(XF{{maAhx1P6uv z4D4Wi<4ep^@zSg!38F=DVVxtD3}|*SYD#rR?X-h1nbKs_UdquU`n7ft(uqoD!8%r4 zK-X%&t2ZxN1H^>Y!Z--(wciEqi&1bq9GO5voP9vYO;Q_Si{w(uQldrJYEQ-aS4^)8 zN$8-i)o8__0$s?x1cEfp!1873ZS0^AB}gRvlpk1)4Ep7uhV>~O(fEZ-D;fd}-DJum z{Q*YvfCF#CKDKU^3iB6UC+WTZivVbn7<^w8ZhqoT@!(IZ_b0uv2n8w5*Z!nRqNb>{ zG8Be`b{q5z@e|+t?_M)@&e$A!@Yp6`sQ&?lqBhhzZt2axh}_R@+EUv@6MsEbj?rbWCv<*k>g*GK%^pE(io0IfGkYR@VDW|+5im>=@f zJABE5IUu&RcZauf4%)`infHFm8X-lF9i{s<5K(b`16$lwTTBhnt_1s}wTqnO)QlCr zfVTVJ9y3(5Etv&PCnqpGwGxDa$yY_6JEG5sftEfa1L-qgU9XYw`WPMEcyb;T7=<-r z9`SFGc8e3kVPy^H#u=>Ww8w}i6&r!R)7@Aad-hHw4|n}-U= z>9jv^Hqh&QbhZrjEMF7a=kriqG0x!d0m~7m>;xuEX?eA0dCEU~LK9%RP}T}%s@I@wcIkE>ZX{!yg-W*cYA?0NIP$3q9Q zC%|^!6eb>EaI~N1BEA5J_&|&pbc8)gMsuCOob~WB1=fDL&^!XZJz`4sixGU;iyexX zh&C}D-)UB0> zP#=Np`TpMcK6YGqP?Q%$X|jReNQe{8LC0J2eBE>ensB^;OH&9Oy>5~V-DHQmx*uMe znrW*sLpM3xaVlermjaJxk9vR*wTy@Yy}s5Y$=WkilX>uCxiSSGtW>u=P%=B=$GQDE zwSs-){2B8Jj2c;jXXI9M%K z2(7y8eA>>xEBXCtUC{|?$;YDCD@Y2Z7trzTxKPu?S^~E-3JfZv#wQ_iJb*(_~dHome=ybgI+C zf1?I?2{ZH7$C@LQv=MBf1szk_O9xpWF`7NwkKd}KtRY?fLZ zcaLI-*p}se`U!o}JkEzad0kDKyns5C#MqF!E7r>85_$L)e!|fuN;n(KBi@gKUreWY1B3&VOz3 zPlxU{Zc8pth@-B;uT)}NtS=(j>CN-cBa@{>*_yy8i?~UC&b+@ zWaW1`B)id2+Ns=Q-pUW*6!vJ`GZ04Ci8J05ls($D9LrLgECG++grPRgl9o~H(j?Wy z4S5HqE^Jw`7U{M#oS&q}h-bG<{{?V&L7mHXA6cMucF6I1Mn~qMC66Y|<{}9QvP_B= zI`235YodLxm6r1Rhrj1?i4_FPCvX?R<{)D5?U%RB9+axr^-I<3hWgGy{dPpQaYdC* zo@7>*gVVwwC}NW>kJm`9D+}v(J0Ee*R}ip!3ha3DKL4qh$gg!sJlv6gSJ1v1+S!$G z6-B@4NQtU^#V{+A1Mv?Bv~i)=b%>Pk`RBO7nIckU!!K1+E+YVNm9be}W3%RZPct`T zW5Av@V@~aqCL=cHWGu%G%QeHWNh3Di&V>;h^{<@~n-2;jHXm$;5j*VJduzl-jrl`2 zV*de?#r2xG*gk-64n7m&icN*tU)jSwhnGMVvD3iz1H{QzL=0_gM?jV%ue>YmOfd3r zV{Bxwr6G^`lL#Vhdq5CDU{UJMNYWsiB{r~Ekri#FZIz(S*lH0pz&LasG`~zKs_7&i zRM5JmP&bRg`YmQL+52`m`}Pp7JUL(c}jI@6I3^*_sjzS)j5A49R>zV)<`1*bEs z*)2e(Gi4HNWz(4D#E$VB%>XaU#omV{)$BDa6NZjtK8y8Jo*zwPbn;qa^%a9bC;pO| zo}X=LJI(+^AajliF^jqI!A_}aaPg4yA|!wasPn|I>e*j^xKGw*f%FqC-bP~S-?0#Krkvy9fy)5Llgb82WF z`43Q&v;8IV+r#Y0><{Te2bevXcQ=YfQVJb^((A0lG4WH)mSkB$wy%GOI`h2H+IPnS zN`ZYK+W;Zkjnz`R`E=&Ug385G+$-kM-1m4iUk(s=1f&HR=Di*HFweZoUv_`NjT^Y9FWlt2fY4)*w|l%@b<8UhEaBNaiGRj-~er|zyU9x z#|=5^aVZpR!U3;$Gj%6zNtOu*TQH^zyJ|;GrBh9zaaND&T8_%o)xBl|Bo%W;UgN}U zQqJ0$*A8XpbX*0kVHIl9VCj?_uA%QH?KWA3W_4cQRa-w*sXSX0=Y{Wp_l)5M@7;&& zOIGi-!z(YNS~sfsiR(GMASJ7~y4I0_fVY=HWl~aha73Zp+x$5aT(7h`+tdrSPJNad zo*|Co_QKsDUw3biNUt<`lqM#QE<=ZFgk+Wi@0US2w?krtB@;J2mv+@|&9grXNqp@{ zzv0ol56-hU46nrb&g})wIqO}NJz@o8^u%~)P~p1SgD29rxPd4lqQz0jjSzTRzg191 z>q%ysjs5-fsbU{vRhw-;)O#r)?j(iGvRAXsb5|vMMi6?(E^!q28KK+CZSJ;MfbNY3 z3vWL>3=C9SG@5U8vJXI9Ll)s-ie!-!lF}UD2oTrJ8wQT(TJj)?BVaS^F8Gw;&Lzm# zk%Yc1LIkGU*f|p)6B_!D6Y0H3`Fu4pC+=z`z+Mbq?2n9%^q;o)BrCctrpw}3mgCAn zvs2F}(m!u^eMVRjgTN8KgF*AU1Z{7>48M^!fCq0yv_R}>=8o*g5CH9aF*s-Gi$~aCb{IP3w2Laz4?0M`9$LfJ0`b}{ee=1MEI-H- z`uRH?_zT=!?I4CTz`w)6nxd+_keEw59b zvu%eNt+LL`s58RYYNASZ_1NJx`n-ofI5t2iBTWyCt8D7AWfTMywF*jv*Tex?9LDS( z>my<>-TWa{X^kKd(jbo!x%L+-*V%{t`=r?EV+$EPhk6nll5R1lE$7&6P7j<|sE@B0 zA-A0KlN_QC$Hx9IqHgi?BW1E?5nEO)oMXg_{L_N_523ys#~{#ie&S8>I1Yscpm<9j zA4U-hobh30&Tgbwv1a3@Yu0cda3vUPx^L2cvC=Ed0hGiXP+t^! zHFmL9 z45gW)R@{q)wt>K;XUKQ;CB;1OqKF}HMFyWKGOvc{v(Kpq zP(_w=(g6wKl;ErrE#%(}9=eVJi7jgh{>MwWAaCbhq{(gar0{x!faB|BLtYkl*hv4_ zD_~k8sS~%1?d1y~qO{)fhgT#0r;t5($r9@Qh8%yQsC$=n{g#!)Vo6*jx@u>}6>sa8 z=!#J!vvaA?)+=^|$p{=S5Tr14-fcDSWEjp|jHVhBbzJi{7Q9j>*#ZP}aVjm?oVc!f z04#C(J3{O(QNmmwb7zQwM;sK(H0~#KXP7b$v&r+E2roYKA^^SHYR;!qz{TTOwVI?_ z5lDc+cfp9(L66K%!YuwPN8$%2UiyvF+4lyx?h9) zKY2MddWbQ6qqF@kSp;BiZI~qg`73r~J|-A&LL7hm;a}`6`~L7Rp2-;f z=<8t7LyUY%l>6J>el}TUlKk)Y-D3lsf{CnpQc%Ea9Dh9Mx;vu6c`J{E96lb><|@bO zDy6G0ARDflptn0X$PbDzuG64U$0>uZn|G1`$po7_1n+PWg{1aYNt9ZOFfLXNcvlb* zy?bElz zM9PprNa_%F`15-2)SryIG)rFno(>VmtT;mpXNG&9Fh+^$Kqz+F`yAG9E&CPF7hobrDP&EG} zbSM!0`!zzfWeZaji!>RzV+EE6;EFyS8XWPdzg}oZFN3I?~$#3`)&hD*pl^3>*hQBbD~~lD~Nb z)R`a+nszSWY|!NfOZt2dtmcNJAw2E*o1lm(lL+T+y|v^QHlgl%zkZrwr6z zkF&AT$oOC*V@2b8w5eJ30#nrUl*&zDn~RJ13f3WGvj%$DLz>$~fbA7s(VH#K!cNd} zEvi7r8$u5&d?q!$g{Qd+Z0NScmtKYrRQ_$Y_N%1eG8xroc@=K`Z)gCgOD1ZkY#z+0 zWioQeFPkFkpHw?8WKPfSC6(Qq)a;_D-^&V(@6kOM=!fw9fI9CufS^H*`lD%7%{pj} zq)p*E$LDAj#BA^w_B;;i!hny3+(*&cH-T%=3R%6#h$ynjH|By8Xr)19rrwket8C zfFjmQRDKhV3$gi-Kd6W!gX|T(y>G^f3mxTO8`KDO4T>#4C(6VNZDpCMQ$jJ1pY+;1 zVB@`N{A+sY(=T~sudwPBy@&jV#Hh^BTb-tnfg4$K(JJ6^eP2M9k^rvvisp-STVFty zJ$Hnfe#Pg>?Eih}k`+CD;^z~?!29p^G#;PeTpAVnlip;1t;(If1(lnh7Yc_yE7|jH zh_6GH=05NOya6;o&t#FPSUyJUh@fn|Aj!%8$(n!)E<41!IsHG@N4V5tq1O?Pb zE>+F6DL^)={Y)2*aEN9tk-V0R;BV7Sx|IeQ{&7%YXQt~2`2kioa1-ut493GgHb}VapacJ{_T@Gw(-cj*YELq` zt1!V6eT)u9V3r)R%d6VwB2*gTa6l$UD$5EwCGgmopJuJ$d=&0ERcSK?r7(^Q_7_>& zMlcgDG0RG@;xnah=`a?Q-NDPg5-0Q1!&mW@?rTX+}fQaC(IKB+E;oCGv9JhzVzP5;97iJ=XE%sTygBPL^ZTXVL zC=l4d!_k0}>ygUqNjBqc>=ez@l*dp23S&>~?rld`Jb*%1vguY5UGYE-F+K@~XJ-Y&N35jP8q$xEYTj;mc6;9H?`sdiKQ^l(h&1!gmpoX?>Q2k*6~m|B}0MWV=poU zFep)i%>*`Q+mJ8qLL%)j&v?$s&db!6nK8nbY@zX`Xpvn+mBkGU59(ktkO>sk5pF;c zy-P$ljjs0#hky`E`nlu`1P{;DdFyAMRxXA`Flm>YC9ee{`9J}U;=WHd+}TlGcBbyLP26H zQf3Watc)NiW#{pfvUsYg>EOuaU3G~b!apV`2o%$pYHediG1ub2H@EBfsi3o+Uvsq$ zjZ1+(1G+{^IAwnvS@5mv@%YiL+CcrlF+2h~4{Nai>Mx92cIn zm4RjFF1>(FgAR|2XJBB)1s5zm`+|YW1p`aZJpVL`#r*M1GM1k5(UZ^mNTnM;W!01; z--AP3HveSCB8;~OUB}t}t5)nr5p2jNccovS_@^Ie0^YmGBDq&2@d^4MU9x0;#aTMU z#gDI76y{|N$Jk^0?jfs8(%@SzWEUSxuvzy;Tamm+P+pKq256PzYuoqK_UIZo8FRC7 z$+)iy*<1^Oz`bR2XCPypu-vpbH!+@flu@NTXz~pCT4sY%d;3y|V1P9Qvx$X^?1os65d#f9RB8HB)I$Ib)OW z)ob={xN}=GNorMdWpWXO1*Q&Rf@JH6KAwfV>X329<58O2_=v7)JZiRLq=u=c)q>;3*0tyb zz^E4rl<1{3w@ziyT&RS?7`X-Z%;nO3J9B29UxEmQT^(Q^Gmbls0KWi#+!X-DebsSz z7JBisvFWcziZl++O+IE9hw{>6(xJSjV|D=J5x$9W?YpZx1hb_WiX;8~gpOvmjWfrU z*X8*hp=)haC_LipkaHZD1sz=P0KAu{xn;|6lV?ZGzC`?R!$PW)6oau*4C8%A%h@WG zhkzyGH6zyhKZ!#b>)uN3)13p@qCeevy|*4S<>`g0vrdvi`37rcrxURf^M0_>s-VO4 z)_mm^s|EHt3pbzBmvl5$gWA=}P*9C3s5%WC33YM=hbFBi+t&OLd!2B7SzH!S`^;W(Q5Xy zyRlm$VY7!~spxScj@4s$+1|Z$-Qhrn8bOpQ>W&KSS967uh!sAbaU_TwOkB9^qGwDF z7XHM?HFZU*+{C!vO<(i7WeMpZc3ARx#5x$D zSX>k-2|TAAWkNk$x=Q-pJgrA~81|H#dId^Gp=D{jh|wcYs{SZxFCKc0Pz9I zdN<1(g9+s8a};mc8(pitQGJHF9@dG6eCM83V*YHeK|Oq2q9nJ(Sh4RS1#$;{WB^~8 z1g&<;Z@@P#lR@^FoxvE28g%#>aLoNZFS)*X<(wE6+sPuvS}-3uSXE9qw&A43ZIUNsSCz*I@; z?T3%y^6ISQtoYh`MOU@2=(SP9RjHz@>J_~^FI@1Q66_Uv2zX3vq{mvp^zEWU}59}is?!;w){9DhbS=xZM&c~O~rWKR_N z8)fmI*B|_;UhPkMoqxk*eick}B&UTZaDF_vMn!DRJU!1H9>gN2yi`wzj{`+2 zadDuie$*)bOavBmU_A|2*-N{w=RYbshmK{(IZMTZKW)UH^d^sFP9_v~B+*M~2MTRa zY@LvSgS?56MMAp-=Ek|H)CC$r-KCLjdqL-;B5o$2=x7O3tm9?YFH%AU`5SXYSH&a$ zx26Nz+nXJxgfQ18FZ*8S_9DrY!d_FVJYospiD=~?P&YXvrn#?%NuaG4NUVd~twQqb z@NjfLO}dL}6691487;|JspATmmK}{`Y2T1!GC=oWtsV~t&Bo*5RY2{krIH6ii3srcq^zoq!>_=x_Euk45Jb`|I2ZJj|o4{!^mP z?~Fk3dvkxOMhmPX6;^Khdrk>?*lA;L@*NR;C}=cR2|J7c`fozIVL(xmYe&f+6i{9# zs!69E-^YD%wicdUcOfS;z!DeId{yNo7W_(5ju<%B#4_qABU`j?Tm$Vm_@0|HqC` zVo*h1xp-S)30!{j{L0{q&oEN-n;*SzeXVf$hHHTp(Xl7i?-%y=ac9-Dmt-zB=E4Q( z2cL0XA*pCKJ22L+wxe5W(zocdUaI1ATNU>*$={Ru9P_;W^?)Z`zb1M+NA2|I*1x+W z_q|Z0Z%U)g4;CpZ_jTOm8=n@KfBd7ox2wcw(1&c6z1csGq`*n`T8841R^Z`vSeHZK z6m33s&mVKNi{q`S_y6C}c%Gjp`CkwRH2w%ARrkRIOv2DhySZt!Gfh&H=Gi$@O|x@4 z(|kK=0vzpHabn#~Zf**l@KiQgJ*9eU&y(7pDR6c>PL7zI^#`oY;uEEw;X@! zT$BVKSb-LF9TSCu9N$+~X}2m&H_K|Kn&1sLW|4ATh{|8= z?BLpVV&|&pNf;OPPH)2ec%Kc4dXS3(U64-b%55F@$1G5eEzJbMg`@i|Hu6;TW;8V< z)lJx}YC#**X|b4QyECD!&04wzZP03gHfZ(4&gx;u_dLl}#-L6HeD2bomXQGV1``Lm zrP?$~C5yF;#O82W654tGzq|$c1a!P127HYw!1h_Tn7g!UN6fh5IA;DO9RpXe`H`LK zoW*0t6<5&w{!22pL~2J=kC?IeA`7x4>LgI9T>ZZ|brK?63m8h7>zJkmR(1Nq7UK$b zkqK6sFb?^;jd2B2N@=A5jGg(Wv@l`a(U`;b?)`ZO2cKwUF;X-CaGWFS>tFB&Q2luq zn=1w{<|3r&o*vfaN1&fk}gM1sP5q9pHWe>cZZwDv(VA9 zXNQr7;CndosZl>W;;edjDmy9skZmrh{bHx6PX#QRBPS)zLFp>)zymJAyeY~sD)!Xw z&-0guYxl>}CD!ex!8{|g(=+WmX&~4zZ}_6c!22)Mng7qMgV6;aKUtg~cN}AOf@8#^ zdBdxASoJW_vu@;AMtDP54iMXyRjT`PZKwVY3I8!#4ojQSnmo3pDeN&UaNP&Dy|b!% zb?0(sJ&YwZu9eh>aD?ToG@^iC^I~h3#n=?zr$Y^tEQ}|OA`3ffS9cnd5*Pfr9C3x3 z8-|$4E%FB07tn-ddwCo?EvIZ4BX~8AKvgxhG8Y1J$V&K{K|tchw9{K~da!#;b=aF? z(sL)MJR3z>YN%X#WY6TNul|k%cu&o((D#7FuFZBrkK5FJ0jFs2^EIL-l!1 z#nzN+j+k=s0-uimVDFdrl6T!4lC;ALL)5G%HVK&S>Z~{n-qsc|2eoPxlDN}-zjT!= zNyz+b^iYlJ6*hTU@Qa)?yx8N5e3HVzC1J-OrLfzRxB=B=we_453sPCp_5av=7dXAD z>frlK$jm&D5N;VDfXF2(5ERP{R*Bb2?PoLI+Pu~_)?$rAFxpay))H$X0%AulN)(f5 z)k16gM(bses!?kzUT4Cq5)yoilzTvc61OGi zgihuapa_EmjbWUKN3^SjVrBIZ1mv%9^BVC3mAh7HkHH@dJJFWdsQc<5Ar+gp4nzU$ z3jBmlQ~&7HlgYW$^&~mmVs)wG5>}%MC!()LOb6XLv#0k<Z*^6-cFz6Zw59~`4f3lJkjuerC$uB*ok1${(>%z65kDSgk#wa~%1J+}-VO|E^O zxxcSRQOotGC=su_D}K|$y~Ncq2}Hb_s=Z4wzj5T}Hh$yCwG`O$YQzZPc+5HhyN&$3 zDCH}JoBs)grZIUUiC18GngBfg%Z#*r2aguCqnQDZt9~@^8V<~dS0-D(9G%rP5jq*w zs+z4rR)B_Z+;kt9&wo9(Dc`Xs=rkVoyoTJ9FGg{5ACLS}t2e z;Ff%*q|Gmk9lGBJ4cQuH+HcDR%Q0|pt_`ElPq8+F0SZSw#e<(XSPb;6>{${> zl2;#4$b!>WRJX$ibgYpEdRIpoYD1 z(4(aSiNc5JDqxzO8DT@`CMz_oG6k_Q#gRZw!8p)8abL-VbuLL<15qZop2jCZR%ufV|1Ksr7}=AHYS?p+a*n{ zv`kISxNmADolVSMuL3*0|H&eP#U`rl8-VMj z`V8bZ!h)#jy+E5 zsd4%8Q^X!A$kP6G_K-ewJI9Emd`{)!a}sq5?{>tqae@*9*qrPDCR-h+f`BrnL9!V{kh!j+&g{H-cgT^X*e`8BjsyR7iufHgzX$+1aPu#?xq%gwMw zClfF88M;UBr-q0F5lT&tk4@j3sihug{+u2zbgiw8=_}|G{Yl`(@~uIf_Wp!mZW+sM z^Ig+mp5O@AGy}S=JMFE6E*7o8cg}|GXqE~E*0wic&ChjBt7EA)thM)kdbp}v8$8fn z>M6w8oZ}`J|G#MDY+u~KS`bI}=uFb7JP>H+@!x3V6vXK`Nlni?)69ZxLsZnH;ZlgT zUv(=5aTLW#tOc$gZ@lgDh%Ut1uevvLMk2^$M+<)Fd!4o;HX^(|_O9U^QPc2I3%#=N zQUg1U-H0VU)niwp1@4lFysTx8XeP8{VXD5PG0)XaYb%eVR8^mvR_ar0%vW{BVCD-! z{A60e=zc7&a14VdLjxpw5$r%4hm*k)^oJHYt4i>pa!CynQEL)IiVp+$FFncTQE=K~ zEiAzYZYTHCwhilcxki2HFU4aTji8hR)y0h|v|W6*&AaW*6!TyV>5enI^%Ya-h6Twm z+gZg*e9#Vb+nhI*r`zG>K+Hg?4z#B|g(yP{RaxaQ7o1=`*{mlFwV@YROzPpFHm?3S zaAmJ?dm{oeW)+m;Y%kRZ4z2~Oc#!(dg9hxAf$&IjBJxyQ{;)cBC7k}&shYoL&|q%= zKT9m$MZNyl#uBUK16kr3cw2_;ToOjy-XN<{92pA)se?2AUN_MAkr=|n#3U3j!&_Yv zeaVeDm;j!5B>C#48et%Qlgq`ByNO=+fhC0+F)U5-uWK`ScQh@B$vr*7Og#O49hjE$ z8OOOtcStxD#$oqIhlXH!p~LwNX5PwbKH05#D-6cE#bNTs%uH_hzI?^fpxq$C97IS% zPDoc4XbO11i<`J38zgX2z)0EYg$cuvM9M_iBUm6tJgSMBf7@~O z|8>Uu%e!6P7unrJqcSY3)HQ5kpVDFjyRV#NklFN^TBSC#P!V(l7KI0wrCY+akp zzPI@1=Bzx|8C^19KgBvvWrWNpw}$%4MSDQP>+Ytq%0;u{mGrRjSKt0{k(5{8zAavn ziqE3!mUOqk4zm*glleK{=Kg`D@&}$dmn2L1O1gUq(N;=|wtP*XL`ioG^!a`6uS5_`BIRbb1n`C$}ycnAM*8n@q1^#%F9FY*j`}ST=es${R0%s);mh$2+r`s zH{bm5aOL6Qn@<=%ykh9O#eKsMpK$Zx?q>D!`R3t=iCsMGnB~N^2b>Q`r>;H!>`CZb zz)tbHC5}gvlT*_knm#O&;`PwtP&KEn0gDsYn#1^09Knvm*c{%r)S&=dydD+@xXH<3 zZrj=tN%4A%B>{i~;0ZLbQ(e5M@9^OhgxQZURd%M24SLJZA3Ejhm5;yzaszw=he-O} z!3A)3g)6f^QleKOeFUD2Q#kX7hW<2Fl`r;Qg8Wi9T@OFu1kkjgdXmT~T%?e#4L2I7 z46_FN2Y2&YY!?5gL*sbI(k20UQHmYxrmf|Hm`YNmcBFINSDBlm669{UvvW4`T1IEr z|J3ogye@qi+3Z;S=7CuazvHFvKwiDs@$vUUb7=A4W8e5PAC9aeWiU#_Z;IWcschf1 z4D=nF9j^1UF4`etNbzvPDk%nW7LC9#6uQMqJ{M;O9@FJK0#(c~ClT*M388c#8hs>Q zAqx^Ee~c!Ia3kSU{RZc*r`dWM@?qBvdd`&5IUUXe>mPm+!uh$OWk=tGy981oSD>`dsfm#tS zjf9jj-6hpn8#SqgWt<_{56#m@X$VNx;3vLUu%HKuV{8qpJd|iY5Gw2W+eak~^5riQ z;zswnJ+{@9E*F-zQ*aqh?l`u9zw_es`FU;v^BJv?G>8!=KTAU^E;I6k0rK>SY6A!o zbp(i8W;hUEM-=hIN1fw$0#h?-LN=GANpJ}q6}%G%2A*;7T5!|nRwOmU)BrEAPzR{o z1TiwYPz910adG)JzblKF4e%pM-A#SOM&C@KHcT+K>X-;DmaxGTumd0ixV!?$<>{h? z=C;k5onR>p^WiiV(YI@-~Ahq79}B0UvYv%-Wl z8#N{Q3h@?F*c5nl>lhM(U^d97^3}x+q9X3P;GEM3*ifwLcn~K{_|RQ_Mw4uSRUNls zm!OiyKvzJ3M`c3_#H?_LfgYkG*$xyj9fmnA0H6J&J(j!!o_4=y?Yi?B+grzI?NKN9 zFdcLWE4ePB@x_$L&L3A5`%pODDlP!G} z1+W(4vcNKLQ-oS`Uz?C4to?L%LjwL3xL*o~&K4AAQU-BBtpL6LnkZ}(J^9ro##2{S zBH484gBqg&)H?aRU;YwtK99ytHGflzz zyp^nJcacq{TkDIZEVw-VB0%t(jWDw{xczk6FFHb!LF^Nl$2tWMl-G$XF1_9 zV5K2Kx-m*ed(q%FPd*z?k1%I!H?48ywQ#D}My>WaZ9jAzB=Iqo%n!X~HS3Urc^y;*Uznooa*DD=2@t@+B zbeH)xuG8n_Sukx#`OpbN$`>q&QpZR~+J=;`zarhnA!CDo8d5%~&@mm%>W_j*`K){m zD|cdbmH!Wrqy(Bes6wiyL6yazpE{^Q*^fa*^$61#RQ1h zs9blPN#7Ln4ys}bQc*6ELW+Y*@z+2~dKD?lnvuewg6W5bm_GsmA^0`(C&4dR%pau) zZks;>kEt1vXW0UePq><5>WfK)fy3v4#lOOX( zT2I89UvZW>e+0uM(Ce?FiLpFo{&@uO~YdGpn%5+7pe5PLTOKmucVpT zh6-Sb0`Z%G@|RaX-`5rmot``nHi(8E*os~=;2OQ2h7P|W$T+cjyX$cUNj&!Imztid z(q2*d#v8x!4Oes1r8lyojH{;KKbA^T9Ikug8^7_!qPW$8yw>z+B(fGMF?`FDSrtlC z+>*tE($rB;Z@fC@=>pJ~z%qEg0rW+2dqBTh(|YCa93beld0nm5=(J{CE446%I4VO^VHXR%@0c_~QGNl}yM4 z+Cz#^895NW)X*mH5z*-+p`pREnLYxrWE$rX05LCwabC>#5%DMDcXQIq^IzTA$oLbn zFpTi%@hH~3SXXH9dm{rq?Ab94&ozP!ah_v~ZoAvRai2J`<+wFGZ3?PtL~QeP_U0kY zumt}hXZgyihGjHyu91d@1;RGr`J7micf}R~xnOzX|itTi9p1gbDm~sL_!L z=uW3eKe=^jq$}q3a?0xOy{M;cZZD^Okkn}cU$~1+k5rdJLl>bW73`HMlUaD2m^V=ojafVBjMnLMY0dA*MU)Fx zDh%k+%WTcsb))SAq%C&h6O>`1rVPoLgk(BQpLhLbKNvVt_l6x?!A;k!aQV3t!JKcw ze&n>0=huajMKmThwQoKFe}IVCM5WIha>U4sPiG>^_K#unZ0gB)06HIvq{u{e3LR}d z$%i09Jb`r@D4N^*6grO zPfJr!HdnTdTxkLPaq)2~t#loa?n*TbRa&e;y);H*e1gw&2rWTDWh(u`UP zFoY%AVqi42RlYW_!%gR`A~ztkt?`Si>0EagLZV)4Y%ZoASv331l-ugq&5lj1EYL0*;&1BkqU*Hn@QP6gXB`i=DD-g|oLYFblRl!YBopE{oF0Od6=vP4*>K7#Wax7)?_-(A#Mm}pYzcH>BpzO_v)P6Z70HtA%`CUQhz zmg*V(XH?Mlaw?h9$LMN9@)sNt{DP3RlGO8ht5dqy=xH!TXY$#37fGq}P(8HQa(*bV z*4PFIYe9GsS4U)2-^$-nFz_)Eim|_g=Y$&1yN|D)F&us2`i+vu!Jzyftbjak;fQjQ z83t)Mpjr!>C;VTKz9X4-!WG5f)wY#LU;RN5oG@83BVv0v#IZLFPZ5$3TS*+U*22jV|VEf%Y}Y2@EL7i#X+B(S`>{{ zd0hL`6rxcO4BKe&*!MqfL&Q_+Na6*%I3z~Hgw>dyWg^;Xm7guVwV-DM_Z6f(%9WPH zX&7%>sCLz%$9oN{YuJ3Vp;8G_xWPhvkdydR4244Gp#*tV;WSCnS9(Lg<56^eg8b#s zf>@?TiUJ9OX@UWrOOq83?PK+@gYu#=`gEJ5Dv6D&~hs8iBO<~z|kkoIzrGg zPfSai2?MvS2?{*%+FB{l(vWyAwZkU%Yo1hDKr|r1OlXOH)ydHf?4;hS2lva(P38h7id}&z_RV^Vh zNe@PcL89_azbUJK4^$wLLv*#-=u<4|PV044?vNX82KQ=Q&_rFD1YkVtOB)16P8Ws0 zGE+;QeH&TdwK6>Nk6>EMuZVCZT!Yf{@*Z>xC%L#ptLZ{1All!#hAXCu2S1%1XApn{ zg2u$Sr>oTP4U4&vW8&xt?{{Lko6sQ21g5xbH$Ob+yvCG7JgxkiV!7F2oG(fOH5e&| zGpKSLb2gb)8_XoL1>g0j(7~h>!>=Jzi-v6V;siyur)#ckjx#e`MF8i8igEcJp3SVI z01o$8aI%HY7CyB1frk9PF^2bTYFha()y}R7x36E04_$_`bDyfh!Pa(*X12XS85OKe zwHdm{s%?s?nf7?)L#29~4&D(ky!{$8slr7O^!Nzc%@#(;m0lr@4b(mT7`8^q5HvBb z1WL5BzEpK(`^?%K`)VhN)=oY#Y_4#rD`%+)$h0DDwXo+(HSI1kCz**0W9L*anRI^q9?bqqtM1Gw*nmQ)YMu;@RIO5iMh2Bwk$6h@mdLT z-zTT-kP`RX+V-HtxWD7>2?&8=XUbh7`2iv%*DLs*Y1gqT%dc~W`W7h5^UF}XR*Dpd zaE!>bLl#?hX<#-6Du6R^E9;%SpKJcPj^G7Sf#VlwdSQ~-&yjT*Fk~mmj6WPC`E|1H zhn0h8nbG_0A}SId@_5l@pP?K4Ic6>WP_Z>eU@=tYM-Ly1Q4^i>ky|l+N5jMS6{B@O zzi-gy@(Uk#h>xq4)<+I=9*h`4NaU_>#Kv%hchCnWn~i>*wqX3G(+hM9?iHWeJR7>r zw*#&HW;8@@b7aUAYm2??1D0=ol2q`aN^Lf~Ur{e#79RXZtX=muT*qlN-#oMowAs&V z;*ybAdtXZfQI%eG(1Kf=RJAzWlINea@ijg^0O!!xGlLI41$ndOk}z9^J%c}CE!oEr z_#9e}j%3dX^Y+1VVH`$!mf-!CeR>WcWDEN3^6&6*$6I4bJuid_c9oC9`196zN6azi zewyWQcEW%re#h!+U78?B*Blo|<3;Fdz@RM->+8o{Zsda9)944YaV&M4uhJaxE z2(QR?8e*s>Ck}tpq&ISs3$#3aaMK@w5`R+*NyiB=b>ru3dE3igo0e6t4GHj4aX23o zMOFrp?upLR0>2y?%;|9^Q3kbLmBm>mej7vm0iBMIw5cst%wMiV zu+ar^yz3{L5ge%bzgvgx=3j(OE4BG)*6n}ii{Kd_$9?F3v0-!GcNqa+KLnVLJ&%Jo z^S*u<8zQ&8mzMpVkH2C|k;Ln5mgF$aPu~4oAZhHp@5;PmX1mSDPu>l#uQEfFDqn9Q z9{Ry&k<%m^@Z`-^0$^K|1c2i{q^#_J=ZlFOIqDoiSiTfXibHs!9tDF^xZ;r(dq-VR z2tHwKuyiCT)~1}U@goG;Zi>fJJ@Z8eGHc-!YZ055p7*rf^}!FCM}k5C$SF-BG# z`io^R05gHGDMfOe7NnO#oXzmI>~w>6*Eg_@gwY>{pvvT%8nc53Zaq9mW5p}*NB8t! zb@Rc5D=U+?ZTEK7IG@OGwpwMRE_p7_wUI!p<*QQ<$>yPQ;3I(+K0fxtJo|$oD|#0F z$&M0P2YD{>+46D-$-|g996V2yg9r4FDL8nj@Fv_#D#=E) zeW1FKA(;J9oDTh(bpFeb|MP_C0KquI^f^-E8BP^A&%}i9Jugicx={Ns$q0?8{SG>o zZkU@0)LsV~y*QGNW1z^h{j7I}RjM;WX6V`%<1_A)J9-tPvS5g1b%yYGbHNZ0yI=^a zC>TQgYX^ZLq)cK6CK%b?f%dSJeoVl&NmM*Ip===FAHhZT3h(P==$#7h%)@Y)eQcau zdxZ`0K1+qfj+|wgf^UvDa??;&0 zJ0-_0;39(ihBBBOYwwIfxig8Xdecdweq%amdR$1;n59A|obPzsPZ^wF4l1%2V1Z?m zH9Y|D|UR+j`oyYYtv}5 zX^%TLds2}pJK|PC%dF9`ASz^K5W3lc0`o&Ck|vdS>TzG8LumyB4b2*rlcZQ$1R z-fQvNI8VSK-S^Y&A#&o79GZq1f55OOBfbehGzn(n0-J1xn$8}~=->&BL* zK#>dAwARHCwz!W4Z|zcfeb-!CtC#kUE+n!p1v&N#@k$TYCO`-POAwm7srX6VWJqm*CXE&6 zG~EOzMK>WYku@)^&HuG_r~^)$JM>?sTXp8jmj3*gF*FSoIG%M8nD6=&ldF*g(G(fl z>HxbtYi}E0X6V6>oQ)d9ERa`2ZFQ=akr){{eopcE?6WIzV%8vm#3)2S7$ndOHjB7_ zlX5hBB`HQBXCGzGhoinYt5F*qJKLI!>~OV}{$N%9mOMKlqO1_lm~C>UW413-AvT{#s^8+sAe1lx>-JopyC=r2Go3mnk7At&cC9LgXOtx>bNXgwkKNp<5; zMMk>nSbTtT$N`R!qib1>cTAFS4_3{{hc47ZUYd_1-~`v{-?!_-OjaW``|Gq8>dmrPf~;4dcM%+1yMD{q#_ zTz+YKz^LSPU|qC_mJDXPMrg++PpHq?40S?d$TKp#Z6RzmanxdWRxuyLUTH0H2((X1 zMbQJJtGCdn>W!(Q3S5oQ?wI1MdLX^s&-5E7s^w}(QnKl$X>4_AmCkm3aJad<-?GxcaW zQ>%s+^Rk1jja{4O>P5ZP&BN_QfudnDP6O8I| z$Z?1nYwg{f~@Ujck}blrm_Rnq|8vSqQdb5Si)t!xGe%hzV${a64G+=!3Vja~8s zfPl6dRaF-^A9IQ28@8-VH=ss0RIsVSG)UG&%Ww?nD1AMl+e3pgs8MAZ6HSqa0VQUz zZ7S6ck?_}U?s!RsfVP;i;OLOY5rHFO+je^?3}cQzF=?TnMDS%I+h*L+jE{H`1;Yzb zhI%&a^7eCerdn_VB~9#V?aLARzvjTs%ZG`P+&lc8lP_n)YKiy+cIYn-PreXA>tYN6 z?@)CKmiE5FQquETQuhn$_mdDukl8=Nr3hCf=yXs_oBQ<-yc1FC^fZo0`|~_MC5A_6 z5L*~HY4W`bVU*v5z^&vvZdyH);@yl|)4&ybR|srd-yPyc_^nt)&=htW@&Y5U#1gTw zz~juB4a5*Hsz&)vqDskH$R^%#l3{o&&1 zXR9;l)L81;ctREm_Vz&$M}#B*E<+O;8;NQ9qCMcP@|Edz@F;TnJ159dk>qPCX*eW+ zBtnu(7k}VZ9}y+Vs7I+(Uc7Ear}6%T_)S~00W@MG8JsD_Mj~KS0*!sFjgHKE7^jpT zIVl4ul#k-n7dJRm-%kCJ6ac118}XWePQ>^p0hppYl@XsvDqjJ>xKJV^`W|hiS1b;)Yo+Y z`fdU@6%zcSP@cUDHkxv8k;_h(k8u91JVy%ctvdb4;FX^Q!P&dQ5xap7fbbUj8&D^R zzot(6FO-nm1?92JuEg~FJ#PsSyr-=-`j}?MF8BQ0KR*k!`0kVW5!PeD3K|a&XhfAcKFH z>oR9#ij~l?!#dMhFzgaM-WrD`v&$KtT|7YUv@(6cf~I+wCfhwF%c;s~*KO!>!lY-+ zus(>}v*Q=U@pHcm+xyYGG#S!|vW3b7pp?I+hQFDZC{oP=AZh{iZh@LrIfmJJ)CrgtjRNjjm2P9VrqRYU6;C8Kz(;;XLs| z*E6Cw0A4`f+FmmUG@71DjsoHO$%gk{*pF0bks1xA0$$SP-+A(n`QWMGHXDH;3W2U1`5&iOy^44U1Im+p1GlLkEmmuRx03 zf%T(1A)#AzM2WdioV4{%9b&-dCzfd<)vjr8q+BFKTMR%j-;A?ez6}j+{RXAfD_Ezp zt*gA7r(pXMlS=mjWXgH{&Pu8}>`B2vw<~n;RgHvug+WduGk^t^LhC0 z;OQuqb`RxucLnzhCm*h)W|bJD?MR0d+lqZDz)7cBY9}-4=5mrslbVxMxncR5c^(GG9r`xPgX)8Dl9;v&h{R1#1QN}%BtciRN?esjNC$*Y+f zeelW$8uO3d&G3O~csR$_fq&9x(|F~Bpxxt0n;}cw!viH4efK_A?s@(uxt|Bv0_(*C zTSUyzlth26EL320a*eqj*3n0%p~>^=Hp@uw$j*~6qmb2kJAAL@wSwWY!(~7o$b#^l zdKBhQ2BXK!zAtK8WhAyy=KQ{u>^ZQS#nj1!km!8|IC<#P--J%R&%oBmD}dksopVY5 zg)3K+cy(6%^f&3!Xi+kM;HzQh@lX1k^_sz~+3b^XWr_-XrNO;kf?j6>DJh!uHGvW( zGeN7cL)|q%$GVq7G!^;*MupEHwld0RD0w0L^O78 zE_BgAzL`-tkcI-O)Dxp;)+BQc!8vYqrmHNqJLQj{6OzP=rYHSKiRnUUZxbfKuhB#Oa!nlx$I6b^qO$MsxF7f>iQm0=*3hcZlLi}K)uZtc{Nqx(XLGt=e4ZswcmeP1{RQy3XWIoh(^ z$Ie{?P{E9cFz%BKO9rFhquEi|YW8%v!9sFl@R1=B;ALrHn~}ZCWLS}$Y;cg%X2Nt1 zmFOr8EE)MzGWM9`+)nh^vyfnM5~s;?LOY)gk$j7g5COOQ9QinK#0OWQ3_W^?F^8rD zGI+WB{53ID8L*Rr$N;7eC0O#2uFd*ntUZD#+n?!-LDik%br89HC$^dIVs1 zCJVWu2u(23u7gMb3t=lh5qc(%jh#y+NU?M2s^BDHO{eqiLpFs3BV8GVpnm`PXBG%^ zHlv^tEF3pNg5?hZOp^2u#yoP%_@AFmE*U0fGvcW0M@7_>qzlj7ktdA&MACFnkE8k0xT9K@skskIE7VOtYk^KME=U!ELc+b|vBq$(3hU zw1sW|Z))$muewxinr$iu_@6?n4%+lTjDxArrr8B_H2A032yD@&uM?$nWOAHivGY=n zu`lr^b|di2J)}k)N7SLKN5~VTN_OH%vkaqNjI5}83Kpe0C%}ligrK9SG%|!@YMzb z=_Kt7H#&+x3ks}^-Ag&TCoYuiQT)alV}??-M>)WTsI!G2h3egl2LMHvkx^N|KXD6- zGh@s6#VcVkXJU^a+rvjanvef1BO@TO@YYm+8*XX z6CimtAG4Ff-QnLN#`SsQL)&$v3!%&~_%KKbt&C0z5vO*rImY7UU$8kc2~THmtj(qR z|ANKwmG81RIAzJm_u+O_GmMyk5eN(~3&n*75b!C+j^8H5Eic~uq-cNY=)xy7>F zj2ulCxm~KI83oo~He03E)O4UBnoek-6%O`mvYaNvE*sg9$L^%L^}y}Zp|sDp&v(m6 zhvYjj#Mt{i9g4=>_%K>Y8lv0!9lQ)E)r_+h;RBWQhuM2@>>R#BGPZ1be3QO99HYz9 zQ0!?ihT*{SmAXuhJx+8vIGqvv;qz%N!Y}C@pFJ((akz=|1!}*LkF!{EWots^imo*A zRTEMG+Z*{Y-j~1e6X`)QEs0UeDP$<%57=`H{#1`lC zoPeIUE8N;<6aSHY{gLaj{0oH6j^_9RuM}KaH0Pi6nY7Xr%O%_emn6`ll-=NXdr$4G z<#eiHk>+j=$JbS8hbd?(Z^+UyMj<52go2{p~;>#KJ@YbK=Bhz$5KzWAbcf{RjpXyM{$t7bjT&Z8_|~d6RUIteUi{F z-0a=QV=o*>JM<3;p(@3j>%n*fBOk^Q1+^}yrK*{zhnLA`#)oP%VdTXT{wus!{!w7( zs;GUJ`6k;RQ1WY;2I5T3$5FsY$_RMR+T6aOhF6xeJuz?HU(t{8Q_kUVq7) zJJZX$Yb&CQ?i-Th25M+`$6$g8wml@J#f$-IVhgR2QtY=;0j?88O|kuz9(?y^FVNrE zo@#PF+2Hg>WvTHdw~on~N5WTozU+mh@A2&`;=wB)f|FnNLdHIiNXsAm_7!rnf6`~u zc;y4R{ZnL&B36oDCRdMbRRy#4srz-9^C^ir>F>~o!swCT$P!x`THv zV~)-C^W(od+uqC=$iJ_0^8Mia;uRv{8ciNjQf|t@HM)>QZTVzyceHaS<1Si0o24;u z2o0b3)0tMRi0BOWSut2$>?SK+w!iBw9d(#$9hv)U=;hov%`URByZ!h&-qL+sFOU>3 zh~-%M8t&odK@Y+;&2lc`$u+% z4k&0uD>?1w2OhlgfofyJBK+FY-e^Ba^Jg|AlFL`ul>KYsYpjszj_A)`=Hq!xT$y@X z_A-i_K(fo|WD1YX;+h$;NtJ82^3`Q4WURV;3@`vlj!TXm*`=VX>vK-t=?K!kYst*e zc2Nh-!z-GZtI~hE+J%WV@fL1G_oD>;jRXwK!VgXS?IEyV1y7Yv(Z*gPayHG!syW+- z9$#`RBRwuJ*^jL3qafYps;}Q;Nu2U43r1+^K8I*nx$Y5g{mIiR_rZ{**V@*Vu`Tp< zal?M&A6!wa3SOZhGW)GT82G3`?fssDjez#qOyBkes5}g)dsEk2CTPun$u;%3(Kuz@ z7}`zJAgQ38n6;`3%CMu$hqTF-X?M_Ff@FUJK>4THLU3gPY%}N=XTSIuk8~>Ud+PjmrMuWfRAKDiZmQ$5ge+Kp*TPthH*GQ4G&s1=o z724T)yM!Y+Xu%dy8D4`oS&Uh?X@dX%?P5Y3ROeT(P;Q~JgGHeZJwB9sCQ@cG_VDdT zun3O6?7)xP4{Wd8QWH`t=oxASA4w`-VIuOA2N1(dzgHOD?(pMth5JfJ(cus+|2{Jh zpf?VUHXWc{(z~*!S>VGJs-Lh4vG~hLqJ6sA30GJKhJm`bg5FdxCYIV+95S2p#hq~B z^4`hK>0k>CS1%9zQJzC;lWRC|wtaKY^nh_1Z8~;c+`D}#=QT&k;2H)T%Qmco=q)66 zuoufCU@w+O7z|teIGR>R)X;fpv{H(9Ok!#^A=;LwIV{q( zn?0B@3+2r>Q;?x-0iF?daM1iUBJ7TOU=yZllITsdz@l=v$IwlWH1sIaStlHwPA4Qz z&s;=*BffLiIB+aJ%IMpE2K8kKZq6jOb$$Z`MmilzZ_&g6<@ z_6w;zsQ{lfGEkgcW2$CyP+Qo>!YPgpvSf9*u!QYivLw&HJw9|=o_`bmf-9;x?Jw_H z(1OG!ye}y6-Y#b2@I_+GV4fJYvXn9XfZJtCovRX;ftOx{3@J=-FOLY3=vk+2TmlCy za1;ZjQ=iNOcc?X}I3)4kV(lpdts z$V6-#vQrQTv{Uk~ud(nun>lch8?l^lDQ)*2b4Q_krDhBUJZOj%9^@hUekK2ZV6}f0 zNYr@GcT_EEIg@-=qm|SLnTywDrmT&&P)68kppTS&6@{sZI^Ena8jiw(^oT3**mRBp z<*RL3raEuEJ=kuQ|4mfz)3Htd!5!!90L2D{HUO``QVRJSol9FYbdEY+9Dqn# zE_187g39~KJeO}(^IbEA!OLWq0q_5evfv?#!olFu0QZiI~mk`kd`~=)r!bVL6sKn_m^d9sW zIt>SqKyql^^zuj1L#i{s6k9_!i^gh-2YJiaNNMy^l7bz2DYmS!s3v;pW)($!%z$Z% zW&@zql)E(n`6CQ`+n_pnR`k*i3T?D4iHlwaZtMXV6HP)bZ=~zy1GDs!WLYWo65jd| z)kUx3@`dPSa3j^jedX)A;7Ka98gWs1Re>FOGP)@zj0er4UGZez)UJ3hUmYQoab*Yl z;Zad}Q*`f5LBwukqCwR(poHKddz1Z^Rvl-2DITF-)Rqnpk5-osch?4(e0Hs@x>sj) zBkF0$K7WQ#9E~_cl&QnPFbO!8L|rO-0;tpsfmzJXNtr1sw0jl9CHUgE=#^R+6;~G2 zusF3Qk;_J=QzCY~8~wTj;On5>8$UoqwGTEAS*3)^vNg4abIAocdzP}I!!=?C6A;Hy z&mcv>QT2%U- z`tGE@_Rxayl{6#YG=+QouKI>^QC%Sh@M7}dPx{=XSpT8*{~Q6D9i|k$2_$9MlTp4l zM0mgkcTMHw_9<5;9_@vnyNLh|D>t#`;}=fzb;(Zh6di%T#4Ug7TuIM@T^IE%sv@z|Ik}evE4$Cn=!J7ZE?nHq%E8Gpk({ij zd2sSO;NpC%Ob;t}pPsllzsV1BasIyzaH9~6Vo70J1l2B{n(ArWihpR5_ z^|r@Qp$mDKBwb=LiXVuA(2^P2DKQRxoZ0|28$f>)lvz zt4$z=3;qpJ9Q+%icnbf1F!3*<*dqY)FD_qiWt9j2BF&b?%CD7wLlhT-L;h6?i*Cel zL!k(EZsOnA61x`KD~jLo4sfRw_R)R%>4|?8!@!ORuAJt0z%p4^BjEcE{M)22&^~+( z0TlGch?bp)E3N$7WLG1<+C}hh63!j?S1Al2NZ^X$Sr&s@F9juB^1vP5#>rs+(&&N2 z4EV+^bf|dU!jY#_e@d}0jUK>p`rkb)lH&DHOWuj!+Z)e7i1#N^p&EWt6VNsp%(eF=+CA%Ux3s6mec z!D1PF0JUD&b`RbtUjya!-ZD+t+n@amT!7Xf7OxEl3yA`>5J=5ZF9Nt4*=62t4be+29tCZTQZeQ52Hp7r-mOAs6UX`>J0k#)nwhRR#@g zLJ#Fr=P}?=NTZ^OLP3gN2UiD9fM_Krq}NtXDEOX%F}A5Xvmk{ff@09V9MBM)fl-?o zVK!a1cR%5>AFXFr zrW-Pwg2bJtlN_}n!y#j1YUe9G?wrndPN*+>I72|p*Q5e^xFf-bCpJ}vj(V2vRz;(! z4H^=@&sOl*uY+6IpXS=9?6kG(HbCFu!7F<>W%3fFSe*Y!HsNt0ID)Qm;6aK`@W@_* zSR{xr9etX)zY0|1{u9Gg^EM_I{=6nS8+Nz(;vp!9o{27OjR&DUAOw9qK${}^I{JXP znx*pW%}Kn9#XU^k-H)zOi8&i0vo|KZID)DbhOD6A;UPW5X9?!R{H9XCycYO&0VH(V@2YEGproaLRRJEfJoL6vOA{L)-##cMF&4nqMbAj6!tRf2C^~WC1 z$-iTl{AV?*$2Ki5Ng1|2?@AQ^EKnrnyDG!)k^|r_SqzF{A|eYl1PO8BqwnGJ;UUl< zVOp!oVglybuEb&-N{p=Z{TqzYoXBuMfXeod@H{9`y)Uejkb+!a3MLLa`t>xPWL>Oe ziInnJfMBQ;&;`Jnrvil${s^?%{jD-&Tz0!n&I>cxB=VFb|JW+n50k=Hp@f8%vPXM} zw5h>g@^#`E5yc|@N)TBV3dtXY7oP^$t0{(*O!$=(1JWNm-NYR$r8=z z;0nEuyN2d{a=7mu=_b+07c859* zm7?z+Ap-TCL!r9pG@Kw^gfgAdKeirs>0E^Mj) z0fE~s$D8i~2TRDM`x6yi(>B()yz)U=e0LNXqQ-k96QlbKD*Amt@Ib_Rs)Gj&xj&xY zOW?oo+WCb59BRG@4F3%MAN1PUM@|TH%WOL_TaT|S&ie4^{v=#$clNd5b~EfZkHvQF zQal>*W_uA8?D6gI!bR;4YkYZ8d}bE;_IE|5fBZtcd_3YP*yIcsm4q`~R9cynXPue><|X24`P z8PV^VAQk`4@we)Qw9PbxB%mcl+8q1_A0hnG%%^T2zhh=C)>2FgdG^~v_#)3AvBjY- z;>W!b*r2fN1=TUm1Z_yIoCrFgvP|sI#kJFPkt&LQDYDAfvWrl=Qy13)VsjV4k%HJR z7O!?5JP6`WyUF>|vYT(McN5*X>`Bp0#;*PQJ#jHaTr7}>!J=Wn@F#Yn>WZF7AbE}h z5L*Pe(MFK|)XfkKSH8l56lgLkSSnRb1aeIvR$FgHS=YIh(yFn`tt={A>Cl(UcC{}Q z=-iju%oL!_4tdnPJg_+nd=vrNi0Z~G@D~BvwiQW)T6;KN88FXcBt+1-d#{^tC_dEz z+7M#Ux9dYrAMT?$HN?Irj?zREI|+RF<`#@>MUKCPJ^b?_tQ@lHw*KS2Uh0(&jKgC4 z8D|ui4&izj)^G14z&_5%X8VV{Sl7bCtB5<>fq%3biLBdkPW*rhB*JM0j35Z zZ|UVI%Sg-wLlibOJYm1=C!!t4NcPd;tbKZV&9?IbjXjA;XX!cE~WS<7^d` zA1Zj`f;Ueb-5gTb$ZEc&6q9-7LuGPq?Y({6`m|gWGG1T=23>l@@R@f1Z9`lZHP>=C zI8MvWsluw%<|!kb32vP%IK{U=-G96gZPSF^yl_?JLIT~*61^$h<_6A6MNBXF=P-yy zOS478qyh@fvhI1J$Y%l#?~U+0i6%-na=J2MSM*5Nm)y9+$SEo*-911}v%81f!1Sv- z!_&u?_D%rCaxnc+U^>*X$^LPvT>UgU z8jS;w4krYZWa7u+)t7ok^0h_vu?36GZucuH8yRp(Szog;2R#@3;wAnl>73!nG}Aphif z>vtA*6(EEJ%mX}?rm(AFuX2)`b6?eCGw{+H=gxypg$%~|G42j~055qDc%Jk7Xyv@p z?9K1r8&3Ty$c!Yz{lqKh9xg6Q=C>{n=NSL+nPV9d=O;>K2Y%;VaAzrBY4FhZ-JjY@ zNzs<3|N33h}F|C@#Gxm1!V`3GFcbOxK?_axH2@%(71#`FXr@=<^e=1wem0iPCKOYRRJ*vahzM zH;(+=M)^}r%Y0^aWQHhJzTU;%I;VVzoF>uM{=jsL0MM)@0RT{F5YQ?M@*TM5s|mc) z_4q<3sD;w#mDn$UzC6XU4ThX$B2Q%+7c3LFx zZMyu#%_d%IGW-UxFBDtiocjglxM1yipeVj(ZvYLxTLd`SGB2F}Pr~tbApf2>|UcUMEFQMMWS5NUnjCSyZAO1=7CU-~X*Y zrYUip5DSVybcu74PP1tq-T7nT(0;(sDXh>X0*@{_bg3OGE1U#$W*J=a;>} z$I+qpG~qPv1dML&HhATyr4eHg^wXR2m8GT{(r$L!-8>Uose6+hyAv$tn?+^lMYl1l zh9hLq0aU||-%dnIXwObFNAxINN&_*tf-IvibyCs|wt}S)zJO8fX6M#uibjQ^fzx0w z8BI{(F%|-Wekv(nAz1(z?9~t5NFW*Z;t>=++UfHJIO!#%5m)4ozeO`8vj>_%c7b=f z=K@i}qkGPM!_}FLN{rA0x8LZe)2&v_oDHkKcnli%lBme zSz|&q6Ad@*Ps|wniLw+a#`ZZS%Glg@$GE;{?YgtwBhjK@!Th$tv5m-Bn+a>v6+1U1 z80|bQM4R8N=!Y5HbvMcRwX)HV!#w&QK)~UR%b3WCys0|WaYq+CpkIn%O2)v5g%{x| z`uo)>w?ch{0S_%u&PQIraFOl2cHUZs$RmTBRy)WcKdKrf(0i0$*E!yv4#hm~NNf$n z7IVxAM$_bnST)fHuY3@}v&UeC7~HehQjpL?~7cSVe`DQC=ev7pJh)W!zRdp z39uJA9Hf(v9EGqcNLC-Zh?&m+Z$g{#Db*6Z+quC4} zd&X1@c)|;-<@qKv96t{*{u3;L3IC+ert!)L8{kioF^arMf3+YfHSMYNngIb*Q!@iu z01%*nQUUV8>qGWIsPYFyR|=&mVus@h1^H#j=c)p2lvuhR%7s%P{9`m zK_4Fo?_P@$^Nswooc|}qf-&G&X-V`$kP7+F!XMDzd{oSxjzHjH1ukjwQ#I&j@Am75 zWF3k@_o(36sx|T32Z$1X^Wd?JPh8NPagPB9iriPFF6P6*W;`VTef92Rd!c z_h_WW5OKGBpgkhlC+PcJvpyUhEK1o|8k`y_epW(5!d1lA-{+Zq_f#AY?z*%6vE~hrd&T( zs>fE3RS7aF9{U8t`>MMlMax%ey;p4OMjJ`BV{6C4>O>U+nnk0m3qjQ>YuAmAR;q(l z0Lq@DXytJY3D0pP{uCKkf2A(w`j^JnlvF#Z=GO47Lo^kBz zJ`%QyYwdP&`0vRaK~0DFhqc>_S`NLgx)`i{MN#)E^vG2h8Pajvk%2QDf^7Qs5E>ic zoYZ3C4#CP#?XvK_2_%QKeZW`eJFeA-1uT;(@ z;E=#5wHr{zJC>rgP6P;pTSo!}`k90vN%08uN3enx^w;$kaGxkqCo`Y~nWwfQsMMl5 zK()Kj%4Bk(-NjM?*HreK3RlT~b4EEv?2KfWV#Nc8zU}=Sd6&K zT1K41E+I#aM-EAO`5csiw=U~(%cHH8Hv0XFTKN_9r8alrZvb64#tn4IPP6U0i9#tV zG%4=)DV3^OVa9>tCdqe2Qc{Y_i+tpW#{E7?6>`(2v#y7U3RX}Jlm+3w^0lEr$|^AC zoJ%GTaeN@-nWa_Q8$NFvU^AWHQi9HjK*1h)r8c{F{d8%Og^90-TZHh}&91VrC%vQu zTZ%@N9v$0Wk6HTz?PcI4Gt-|J1L=-i-1<$+@vs8M$}ysAM!;G2c0i@R4MftJ!Q?#Y zUMnbtT&?>;MP&_3(ZK(Q{QB;-uL@ zDmpg@V12d`I#mF2edvgqCOA(-J2%o~{)<>N9L{wO)8IFuIiX6l7dRxpwnU;CxHnW=D%*nIQ4sMF+ zW;Y44kNmO67({Y63cnSJq)9RBo9C&^L;bLuK+Y$Vk!EBvxoC$}o9|nAlgv`#eJoVy zaK)*)h~SVTwVQD!EP#)WM$Y0Dtxm&YwvP@B;Z;Qf%)z8Wi=^Z~AHk5-yLR3C&g1h{ zgPT5DoWTxHwHv5V?+KEEb?+YnlH26pTV$9?miGD8$<{B7`4GDuX%{E>GS_*FjW(G4 z4&mDkb|M@Z@L=|L6KCe?%w-kb&nL~$v%1S*x|euATN7OYw~Y~kuBjJE9ck4%r8vTY z$#1$jFwdV87nA@k1Bi`=G!TSd^-yzR#)X{Sm?0z_{e^nCv12dXb5%}`@crnaFS*i% zEe7zh-Aov2N#1S+deX5&OdS!72OlpuAO@$+Fupd5z!!(h3L9#L@alph;RBY$(gR4O zbHN6MbEFl6U(|=;nA3(db=SBju5UJ7qf~x9nFz5oR{0y`5nv?y*B+8`oOF*AQ0h^E zA#f3Iq-0%Isy__ULdkR=L`x#&9`m?S7ez>bIqR=b(mt9e?Ez6ObPW@%zL?oekdxx( zV))_By@iNmoZ>1+c%x?uHK#B+YeU#y(m{QHoV`DGr2njRqT;h>?jND|5~=K|X|ukVvJ9Cc~yd7F=9e*;8YK{PmZg{GH)S5mS-f@0C4K zh@w;w)wl9&YOrXPH9>0CSWxP(M*ls7ZhYSCS6+FjE$ldx-#gCKM#)#$ zE|P8zWBWZ0SBk>HH##t=kh|Vq#cS7oTx7jMyn)omYC8utmz1RpoJDKS-IjKt5uv!lJ- zVl4uDn=BHW9>*fFi4h$q$zt=lG`FSBxRRcn>2fDKUJp}k8d^8-MiYx#+9rBySJc+T zSbst`)uVu24O*2P;+3x|({p2iDAPBz^Rz6S=Jrh%h%5~qY&IR*dHof!TPzer7TWn_ z;oLQs-+TR>x!vUeg`hgc0cEGRZ%(F0u&FXj7tURUP4#X7h~-0JQ{^U6dPTM65|{?3 z+Ek~zeUtE1bEh4NYqSDk;?SgljJy?Uq~m8hi8^JO&LeSg&uDFP-y$wLOSLp=O|4WG zAx%cHp%#sXF(n!eL805zVhQgsj}e|8{3K`Kg1-T(?z6?vn*tO#64s8+VsL^$HDb}DAw8+kThp67rw^Kn$#Zm3pBo|r zJ>1^QoIg2t*r~t%sm|iwM9MHIS-P3U#Sp^;dIcBC;$R!aUltcbiE+hC&ZOM8#B=T+gD+2ao!Dp}!B_%_e- zal$<}u`Xk3Ah|lSl%fu4nG|xMFNa1sRPh(Am8ez+y15fUwbt>ELR5@@n7Y&W@9#MNH(hvfiI|HhcTIx0dS zmenw%nuligfV^Y$Qi%cGu7imA8{-tmELkYlHhU!x0e5lJivKztH!ca2k7X14#0K!3 zepUOuSibY8lP0qrb$GaM2?3}%xDO~DXVJOvj?-6TI^&fOp7_u6X9`%rnBOTcX%fh{ zjFFw+jB%_AaLhB^eHTX5+`k>^uU@9|6lbiaU-Te3ibW5iL5S&5oTb#@gvvS}TOiNw11&M#t#)<5Rp zeB0QMy92Va_Mi#FzNArQ<$7mQgh(Gd#;|8SWvb|y8Rtk_@YCF@jY}H%!OGi$pB>&R zKC?)-s|9b=TZ3LZWJxSLC4&8g`Zy%U@HLik z{?J-YCFLvB!9g!4iXII)LOJ)mntN385iNRy4l5#M^BUU1%%M}DS;#levpEV7SG4jx z2q`CJV?Si#a`^7fG=z82itdjEz{&iR*2lFNNfCdzd#K@1Pz@Is-;2JP?& zjzHXCv~*GrxsFZ}qhxjM*;tE$Po#C)Uv`n&?uzk&p11T?kEPh5p$^7zh{nl><-Hf} za*c~XT4RD}^6iMABFgS|*N%%)Oy00Jg@aj`$e=WG@>t;e^}sOFOJklw(U~fEg$J*| zf#d$K2ZjyGekhU7rNrSBe&R3&iAG|RXBy0w7h5&i6Pj#bPC8?(0zaVqjFrKupW-;R*G9XxO~7FB5#?&tXf`SxT{%x@bRz%}ES%*Qb2b;`N?CQIBV zb4@bf^XtR%8Ye#qtaTP&wV!fU$rAp-mMfR{+QtsXE8$->&R95`Zw*$M9c+a*@ObSY zL|WR>f17qBV{SIK73#-^k4cUIhv+(X3kE^JJ2h8S)VX3d~n+PjkWOl%0-!c{HYd>Uf7 zNd$7AeR1*%I_XD8bN&5iuA$rC1OPqiy3bq#K7>7a)M4!;b%ZboOL}TEZ-r%kg$IRe z&#hiMN)wP1ONrp^=i^|Jy59(ogAo+N5oym)p_<3-FuYyJZ%wi3u1$mIr==TyxX|Sp z_yqFj%~Jczmc=Xlaqn*bh1?uR*ltwoj+67Z!fAx;jxE1#IXCXkE8aMCBXE)Nwi0|r z7F}{+ZGpF0CquV=E%bkV&)o{Vh84AFdU1R`l7~qm|z5(KWW_( zTP?9*!O)L!nKaA$D4?*tXbKt&v%f(|dR@gKv=$}0t1`hYnVhx*TW&$+JH9e9Kuq|u zfFXg~^wX52)=inXC&M1v0G+)?JHye4^#Bf@K~j?~#4`_G`5@W&V@Kg2)9fH*3p6CO zh?|{j!DXvc)b9lg+EGC(byQ54nvb9A{zG;Cas*N1dWGzzF{n5dA&6{}5@Zsl$WCyW zJU^qC?4#3576F=YV3+cZ?3lVN^#*8K4_=u9L4f{+(f58keUsH5JThQp@4cL+BjDh^ zLmUHE7{qSf$-}Rg><>9Uh}9g1Gv*C$g2^9_$ZjXHy7#=;zr|q+vKC(PUl*C*w?sVn z$==b-o?lcK_FcaaeC{sCZBq>G)h}U;Z{o+w_171v{vv_oB4eDqPi4%_X%33ODuyO{z;!DAg_Gj zv(){HWKJ!0;GhOGCH81zp@lHm^kiNrbHY@R^0?{>cR-AcN9)JDjAQfR6B~C_JZs$w zfGJ-|H(p-cHjzbA{S_yXZ*y;wB@Nmq4-zDcUSIufMnHJ&-gyKun1BA(50g~Bk{+=D z_rLM4iX_@fuYv;3EeU8h4_6j&2njcem+4+|<^RA}9crBDsJ*XP>;{vg$_{xlrR%H+ z60>&ghC$IGO9HP0Pny`Ug18Pb*Al zF}D>De!BY}-${*myk(wbx{R5#c<^($a*3*4M@bw4ISJ^d1<&cKp^neA$mle)GWd&G zyADr>c{`#7JWYt~T3V3VT~#0G`vRElZ1_P0JR$~ z9s07pv8~i!A<@3`)~<=KMJop7vK3Dl9YiS`gpDYw+aYMYEno=%*!G6X`H$#9x&iv% z1QvrfEqFva|DhN?&g9Lu{Q@c=spDa)IFktcw<_CaTlj7pJ|q;)!;&^wtW)@0tW%tQ zT)jX`zclH00D6;S$VzfoeNl1#P|Wa5oDQ={&n*0KA(Rrqt{DU=KSH8P_H`N)4~~X zzJJLvaql0(WmZBFH+ZR(V@fCCmx~pvDR4;GZ<41~O+wX-?cv)L%g&Ow^u-vTVsydP zjYZ`&cCokgF7K^v?PUwBx)rC4MZt(U=s>4GC4A6ufUvdq%dS6@&MO}!bN1XVDXC5=opU>=(ZCYUTR$V=*l0I&T-agRFwa-xMS7XgSTVEr&6w{_;5@b$wpbPX8*sD8XSeDAXI1Q-1&&vk*@;xO04 zjO!MsHgq44%!JPy-dJiA{z;#chg=yFjimBi7wJ9M$IKlGD8O`uh3Te0;m>0P4H%K@JRfPhd~lsL!H=tNpkNjl3E1X@)*7y zfPMyEv{ZF-zm(ICn0HhNREOoCGwpfg2??`tN6iKagX_AB+dzZA3tSPBZ{}O0KXaK#@TTky}PvwPn3nPd|5qi`(oQ9N- z1d{#OWZ0s6*tZ(?uK4Nl{2`BMXDi%_;c%Cz?Ntrjp^g?p_%RjCY{*p=!9=>(pjfYU zm#7+ACgH1`l=yRYeBulS$qd}Jt^+Ml;Dl;;(?)3396O73%P=O0BY|N3F2Wt!z=Oe2 zo~diH3r&NWi+WHKq(-d!cyymF!04pH%Nu4^phjILPe-i?=i1H*w8)e&MO}(b@J+=*{#f@buoH2Sbm-)61WY5JMB@wg|)ZGOx`r!Oa2Wu2AFc6p_q3ijOsd0gUgv8`y;r_F=19Z!e#W0u^hX)V2d#H-7L=Uwd=8) zObi}O%c!1t{smL6AHuF@BsDdRPV1rC3dq!uC~Cy?B!t4&-s)B^+m0QQ=es1qdIE>X z=Hv`Q4f7g^XGv3eKmkHf6U+#Xrf$*CE|Z*qs})TwW1q9OBF{dDKjk#2f_FO27PS?^ z-f6m+h7v3RMs=C&NRMAtBQ2U%fD6#23`_76kE5w4A_#sFNU#)tlAKUjB;Z}lJmv8j zaDJusPO_ep4b@9)v-^(hJyJM3Nx3BK8hZfLqzUsBzqV3qQu1ls3^qvEM#^dk*`q*+%|S1e$5y>q&CJ0fkm)<=T3L- z@F+(Pb`2Mc4DclK8a-BY(Wui%C*rYOZ=&lHp%DI>N4q_CjZV<5o1I|3zU^jE4Fp5^ zyLDGFj2E-@@FE>p*`lPXRcu!B8}aaN#8f&>;OSuBimK}Q@gphs1Ib^$PT3l)LG?c~ zwrJYG!tJ?_fbAaX61rW2UYy#S1Y-TQ4t+hT1$ylMy97O>ybcu-!0TKvQxmQ_&8=ueKHYOZwP~ zI#{lxUIl7(fL&Bfw&cZ4|0^4?l4YvJ{}q!X@g!X zq%b!ga!%X8Z{dsvnge17cp#5==60~-#sIrbApeu>m40u72I=2_l#|}qx z{u1yO3Kpyfg9&&f72>4M?J&AHZ^3CZ&IJl?Z=p$mj<_h>e?p*h$SGl;W8{sZ%^cGS zbVha^^!ga(u#M+8exqf(J?PqBIf<~ub|WtBYRHNX1S_#Hg*FL6mhZq(b$*A{ky!jJ zgk(Mc6DA|Fx|pCGtJ30>kT^eLNTma7b$;l;zm#s7WT$kqL1%kd&Mj6MlPuc|9~ydV zUEq6M^~qqQ5bGYfqXTK|+JWrUx+7K8+bZ{z9CW}jQ14?g22$<>r2s{GQwK~hI)IR8 zhD6fS6^C!VXfsKR{AJ&!%p8TT(l|4CUv=&z+q_L{pjNBOs7J)uoMB5TUV*!l@*Jnl zF6!^szluwU^nzwx9}iynpjO7pjWd+Uephlh9VG98@ravz%+NBbK7x=JJoM6G-RGh* z0xeWo?i*;qnwGL$cf_B>Rj1YS;MVm)FH-e}2)mpMTP4iu@}D*?zQB{7IGgIaTVlqt`#F{;-0T zBvh;2>WTGdMNf6+uev`(ReP_?(BE-Ie1i6+6NK+~SlLwcjqi9gNmQyKmm8D*B)=An zo5MdFR}n_uxD1A4L*)ap4A9V5C$!4|R`lrrL$T&Zak$lLUFoxL*UU6c2;0EM>{Y$h zHL-dwzy{(1Y?fFFY?r4`Y-=om?H=+pV3$XSmCNT-*>4#~-E=|Wx=xYWW zCXGQ9mg59=6KA!8J*Rp_+A{iYz8^8XeZXP(>xXh{>=n^;v1>A$X|UWtN&7@@@FdW1 zSzxUyl)~!1PPqj&D+*Dx+>Ii=*n(<`tB`i1TDPFuuG%TLpi&S)r1MU zpV$`Etk~9axTSrkrzY;)QS`mU;Lebl?>K*asP;;NF33_5)n~ao=AIk^8j2Bl-WZckXd^Rn_4?lVoOYLNdHE!0?73 z52J#)gH=&!E!v-Evro%SCA&yIX3ksEHx%?rq4((oRZm*)uD0-4(N^uk50MQ71pq9jN_p3Vr@6im$ue4irgDKG#zHl$YCqo&KHb{$j`u zq~!eo*YRnLvp@b2uEp(t12C^V^Y`5W?J!W_zH)Cqe0v0?k`AJ-P6yB#SO8O8nNm;w zW1>S4Lpnr8p!2-+P0%5cGAg6m=nMUye9&%nI)KigpEMQ|P?aS8mk#L>{|2ps5ejl! zP;#fr*5CCQ{!$BP(i85JhX1@vI$h<=UD9#WGqB;%8cFisdFheimg@fx(J&W8M^h)Nfb=^6zIt#}Xm1`PNv8LGQijMbnRCJ|1MsZh47Ycbm z*YGloO6ov{-{60Xaln;b5^!#x0X(-#XB_3&Ppy@qd-%+Go}-yG2a2Y1iqe)nF9?D^ zH@dH^TJ~|M4=%ltR$OlbrpmFE3v+RT8RNsxf)B&fW@rAi=8Ytx=HY%FZ%XryzGBa=I^NxbiKl^>U-T6GgAqiyO*_LnmHS4;_ zBKeC*qWdiWXeya=?V3`}A79U>g3qGzSt^@+@^NHs`n4vzlaHgpxOuJfc}yOW6>XUm zl%iySR+?6eFvi{&7xkt5_!9VlvPe=BbyBnc+ftF0+7C#83CMs?Tzg4e^f*AZTLCIi z7D=Wjdd-*q4}6aLA}N&$P-zg**k4{R`|QAm>YQ+)-jPkjIq5RM9oXhw8r~v1vKhLR z;09fP(5P=qW`=WA)3L(=0M;E6b2OwCgM#Ti_VgVJxD0P+Of1!mHrA+^ORSy93KtP3 znsBCy0kbqlq-*WM;Y4!?%{-&#mmKp4>CVdPaj83_$N#AA{4UrWt2@67dbUG?26aYv zCvFEGQ((4o2ZE{K4h&A=STm-`T#64d(c2S>#*o3=tuis3h9{e85yGtuhaRC!5Aj}V zbSW4vvQ%&2?d?16J(K2*Yx6%Hs*8l@B83`b==aNTT}Lj}hCjA#dv511$SJ!E6w0*m z1>z7k=Zx5&nuZciTxt*Cy@=IP3NZVU*p~BVo@`HqF;+44;|UcGx@1!=HAyXfFT(B* zoX#Qh2m5YNT*vgpS)6&yma68c;!K=(_EkR0w$U#cJaIH8uUJ=v>$_>_)X($9uX_eR zQ=C={4gD4uaOvK`e_vLf+dkdOF(Wf`vrY(8at5K9OEYn|){biT=+;s!`cG6GhS$|5||cLMls07yaS<0y*@Fjnx?zlrm1dG?xR zF9zy8Tnllv5m0lg>0F)?Xe+mPV}i^6*f2cC0HhPv#?8#M24TH076=41zqMSxto{1V zj}}4b*O$u|L3yChIwHR+A|?oI)gD9`bHd$-AsOS}0G6^@e?MyEu&uA>Gb8(vIV59s?%sbjUDKLM>hp>iJ2{;*wOPyw4T z5)DvSM9j)y58ly*Y&^R~AR9UX(2RP#GtZz-1d(mAf!o+pg+r#=4R_Vk9&#f|PV6?B z6}zofPEFB2eZ2f1nAv|ODUIB{twnxRogSk?N;*cZ-&TdMrPZ%Uude`L{#F@|6iq!& z_lB=x+rPS9|5>0Y#wnn<<>SB1$`lxTsM7XWBNVG}+j-Ep?FU#e&wz>awTYS^clA$* z`!k44xCI168f1!pJ|LVjnWgko)NP&@5?)Sl&rRaYG(O+Z)5fEEHhnz<^(MNQpUVz= z;II|3ft^5Xo}H-u>PuYb=aT|FNQTI3Qlhm$ooIn*x;)t+AiNwiG95#pPMK0|0+*&f zg|`_7-%A-qcv9sN?W2G_wcjLqz+QWbIC=yUnh;Etn5>oQ0^TN*6im|uRWxvK>xmwQ z%udwP>crrhI#)#j@Yd)Xd>TQP7z)31C@j)4nGWYV+Yp5T9z;0koquX%Fg#CFLi~dj z=WC!JV|WQXcTX86H2{!fn&Gy<;T>Pa#FZ-D*^jJ`Xq|`$+ThE25?E-H01McvY|eZH^J&EG#OHRMan1PgI^I=w~TyanYO}SUN)+3nm-zt3`M*(1wq_jjp^AL(!0MO(5 zpP5MrH}=qN*JFN*#t?q#hDWBIaTPR+jA}D(Y=~(Lhzy#K8JB7C&Ayxwr*c1l4ncu; zLcDr7COF_T>e46Sy=zDzUGCf7g}{!HO;?0x+xW17^gx=FOR?o5RP%;Hpm2aHkiHhS z!)#st_?gaFr?OuYKZtWd*eDb2j3nlkzmD2*P8Hl90N9pVH`Kq_9hI&;KhM640L;|9 z09o8axS<^()3s?<-gZ3JLyk*`fR`vovJnwCW0VvU!U2<#3iuhcSC=!O|N4G<8V`*L z?Vj%YCN@$A#|shYKDa+%Ml-ToLs3f7m>Qb;lj54%k)iI7cWfsseW9{Ne--edbF!>y zp8aMk8$^Y(K~SjB~0hU#5K9!Ugs2}g;J z?PR4d3Xm*jy>V1IZ`+MiJR<(cmDM$1y&0A4*^Vr+@}F6l*FzwHpH&s$SS2>r13htS zBM1qn!5s|@-)~{{+ z5HK>f6O8QD$kbz<`<@O{=*c2+?VX_N>qO5i1wg*0nh5H|2NgdcB81Yw=wyv}VE#*o z(@%PBED3r;TnUFeol2G1$yBi&uVW(nNr4ZTslW#~9Sa}4Hb_MZua|y&1Dtn=y$DNI zJu^ISrVt?{g$bGTgXfl6(^v2N5|0U=<`)c2YLozr~P5lkj}=e`Fov zkq=KvHJU*;tvr1*Y_{J}If9!ES~zXqmOOjkkQM!gFM4SO1x469Go*|$b9&)l@OP^R0(x)xUX5dFKNfiwF12FX@43iIg77d8F)7}gj zrcG?Ha07w^dr0nKf2Z>x^u}!V8|BOUSuqes(3bB3z_-PZkG9fuwzbq$?&(?l1*+Jq z4`wPX#ZN_yEybC zC;a?2p$uVttF!C=dc)o)sIh9zZ@1|`vJ>lV|HbjOSaksukM0^y4;*r%?w`6?8mV!Yytqc#AL2h_%aG zKuvx&^5Ij+ciDeprE-o1IW#(|!`o1L+YX273wL~p9onggp%Vue$M4Prci2%ijnN&a zY#(Q-;DX|8zzdQ2Rq1`U3&5{Bvd=*LoE`mwDSSacElVinIqA&r|KR_>4F8*Er3<*8 za%?kw7Gs+s{QDl;G3a7!GhDYfwx5F48QYlb+74#Kw_yZeknh2S<)i(y(-@bx&d%r9>9i4#Pdc_f>QN;JSh-9rBVFkss-xax89iiL>DR_@4-M^15H9zK?WO&pmy`(mI;$ao zVrAr9!g{C(VdM%pLUPj|dO0x93doGuemjLKzPV^7dT7%O40=h(b!ICJF4Tw4UYCt@ zo2t&m8qWK%LM7SFUZ~tGDV6{e3ng@@ipP}KD|6~MA8Is6nL@VbSf zDpea&B~0n=a8R*&r&LWG9Pv3EJWw^k25l@Cd|RiD3;dU+5OFbfM05(#C6kf);mQg) z+wL{HOEOWF13bhvo7GJ~Ce3vZYrR zc|pr^ii`Rs)a>ll_Tnlv2DJ&qx54-BBD{ogU`CWx$AS+U>`;#;P$2rIj+^L=qsaDQ z7ajprAe;BX22~;ejN=A7AD45-#+KSkhB_xWJYE+T!V_7%MOdGrUr`odqip&~Rc_m_ z@7flYgyO-!;=!NvCVN=``C9t2jKDR|tu^*L`v^C9rg27$3j9Sm+x1;Qd?1VTXM~Z5 z!EZ9>hH+hUsaF<*mEVf*Dc%#95m}_pQ3cOogA9AjYvGLSQ!a1YTuBbUHLRRdWg1Pb z+3v2&y8O2fRhb<~7g79rpB!)6l@)rzfV)nZ? z8ke(s*EZ2lR(z5otbm_e9E_`Y?9&s>1(xTDm;D4MIG|>ne;?H3-@sJyc?5>!)ECxR z$WG{_9;i4sMmH~a5Z(%s^ViSg)d_r^UlG&tqkWZ+vn~p4nmD#zD|!Mvo*l1LV#hYm zzRkKgYRTS(rxQu)PLWe>kATTDJ`Zsm;MwYx}plo|Nv>K`ATNM6n2q)s$+y1JOlR%i@P%UQVZu2Zi2UIrNwrlHponQun~kJi3e&EyFdW8NGSN^9Qjnjk*dS?Rc-5b)up5?66j9~hDpA}qGG5+6>a?!c)A zGEvPqlAGPVolf5Hskoxgk>sc=g|-q(nc7QfI`~CrBUfv7%ao6!*y&tgQoR`kx^&AC zbfWW!TYY$4Yw4E5)d~24vbP)&ubKRhxK-x!C%q=`aFW#{ZgU#sCspF>RB5Rz&n}k} zj@1O{Ayb5R2tk&;e4>Xzn6|U=fpR1|n88>6LhxfbFJgBVrP)hSDc~fx2R|O;QZ9rz zV1?Xz5=)A-4GnOD!#b(Ay*Br@FwjnsjV_n`(Nh!?`(a3O9QNUG42qJjM10E%lEX6N}irp|R- zU8y-X-vSW&qmU{~=)uM3`ZVi`Wiuq}keM`t(-N@e4_m?dm{h)O+2!^sDPlxarhTE} zIu+j8zX_qj)RO=aNO4Fzmje2n@=F;&Yk^vy0;*ot3j~iZE3?OkjR^s~#b(z_3EVuO z0IR5sXuy;O*Dk~nr7-A)bz9ARXxht zI@cpPKGjP(N-<@S1V?&rM*}Gi4P+}t>&e9N_z@_og8|Uw zdVBBdEu@Gq#n6cS=BGtY%)F1w?KD}MmS?Bx1((D)Z)h3-3Q%adv_8*fGlg@nuGoB* zJD+gyERv^sb`5M6=n+R?1E?GQZ6+N$pnPO!@4K(}z9&OQ6E8){B!e8}(rSI4FA6U$ zsW~P&rser%h{gXVXY8s|s%ICxIc9I7j|ruT?M5;etO;*EU1GeuK@23XoENOQzCK09 z02wtlV9jY_Q#JJ2J_z}&AJwc)sew-Lfmj_e)EXU*PCDKbUykH1coXJ)#*!j-P`tIO z6ni<`QG8N8$UAGdp2eLPf1*Wv2Ht@pNB}Vk>6>sK)KFdoM<*8bc8VVbP7_TPI2s$6 zRpT?j{OM5t2`yMBQwbeaDwk{`q=h0`pROdACJMd~TzoNw@q|)AaAP!Xj>^A75igQ5 zKKaUylr#%G{#h)>$fM?#4$dyX-no2KbPy-%dIwX|>4jBn(LvXKXs4cd~g*u@|e&-Q_5cYiNERJ_wzIi|0aU)yXwO{Up)kO6e!wB$ZGQal!E+YTxQ{$SE ztF7x0s^K*qJmypa1l_sp7BE*NS$MqesDGwI$N1&JeTPR@B%hy30*Z3vxJAd8w#*7g z7!|pT4P4 zT0yHuX#(|C6Qa9LWSq(zf?qsI9kpI#8BRdlgU|`APJh`xoF8)bm}sdedpIpk&W?QI>By z?Uht|{XSzGoCv(5#^_S%(Ks64 za-yC1Q_N>yXHYn;fyp$>!Fi2YZj7K)T}NJ$IDcvzJT>WSe~XvrncreyoNx*MX7>VH zJ;@aJwqFCg|K}U?1Bxt@sbv1h$NOk~^>8dI_~A3ZG!o(Uuex8U!><3&Hu%5Xy@KV4-|pfHMk8J}98%gWmzl_x=c;0!W}1jj0!tGWtW$ zKp4G@Bmg}8<>%QHSorPN-bdGIHIfA+8&3cxa41jtlU}DD_$g$#I7+cLBT0kZZ(Erf zkV4TGYqKC;P&8;OXr-<#M=j^}SE`k_ANlwjgtI_V1DFE>n1J&TtG%~?vyrri%Th^t z{39m=e+vE))A4D7lVkvs;JhRCHf7Oq_=hgE_E69mp}pHenK(~Sz-E**SzxmPT68`E z{5wFovH>O7tV3C_bBvTgVChG3xLC$)@NB?m_g#}0EDhb8+)izXM41UFie1DL!hm#~ zn#R>8^RHo$+F@W`rt;{ii0nmbDnCI*g`gvV0j`WTxt)8m&=7v4A;Ov78zTI7jvH7Q zGQuqM9M%flJ%JwY4G&n)sO2>*>l{Hu7po4P0tJZXSV|uIu>(J0Ss807T4a0)ch{4s z?&!ivxyOVW7FE3WSF!BrgsfQ@Pm={Tv_?+)LEt#>=gn<=CEsltf_Qv|p`Maqg*0$~ zTIpn&YHJ-z*(2wi(~};)pQ+Bx!Dgzrb596~rw#M1^()2-Z?`z?!PNuAFon4&P)qqO^`poj6v|Po$#0t%w z4wP<9xmA^4j{DKnBW04|*c*4U?aG+=TxsBQ@UE!kUqmQ{{RA3dJdJ|(a<-M78kMg| ztx+6oNKSrVg-_G7V})(oNNM}*JjLZW5UDlM4U&q~XJSR2IlRw))tO@G0b#Rm(f zKde+;Rr{;%xJR(LHQ%yfPVdJOr*U232(ct?-Iho{qa-xm3QIAJkG)SvBO)mf+p>UK zJ$VE()XTrRf9hqGFQ^@G6u{a22?u)p}DcXtvclF08-6 zg>*`E^V$jf6u5|i)5HZqb=Ah28*(kjOKLYO`xx!XY|tW=wjWgY$?x0vV(m5E1(7k! zBtuV&ttJdTGnd06PilHhR>))lDmK>~;u?Cs_gC%J@p<-sHUg3*D9l0Hiok&uo%pJm z9CNFpc!?E=vrB(S{N!!Ah~*273nZJa@7^weEwe)8Ei35MfgUtsZF=b7Fuu5#@C*4F*k*n4+Go0yLLMBTC38h8cSoyVbP~J4$U*PTU9~VNbd+6?}rJ)xlh5qavGl(HdIhI+J*`# zN17U|`&2DHn5?KpOMDfMShd_--;fFC ziVmL9*{$vv#L?lc%gX1I8SYD+R(S==0U`J`dA&U;Zev*p)$B=k%HR$MR9Q?8fBOsq>~LN;aE+0i@mun{T93>4#p?VPDfT zLXi7usdhk~pQWQ!zS*I%*)Kv<+={h1Cy;@i-O_ghon^A5A}5s=#bJ48?F&_>l`++q z#HHvsqtzsAx9V|pYa2D`YSB+yqv%Cl?R?MXS<0 z+%z}09QjM)`UQgJ;k=eJdshRM>Xog)2F+GdW4;+Nv7XfN-(;^aZtx@?XDnFWzYIqD z&+u1Rqx8}%L9;gOryz=D$nLOfI;@pl6k$8YE8_4Jf&1k5c*MWsAwtea(CLrDvxB1b zO5~|E>gcJ7aHsfBP8jF!$MHKca~vAyNGDwR1O?N-Tiki!7E$Va4K6EkL_O?$7!608 z0WlG7V>C=3M$pT~`m}ChB(EN+UOCG-nietJvfLP`-mD%m)>5E$vu-eBst^M;OKnGo z7-;9B5JwErQ26Kx7%Kb~^O|TQ^h9Y|bpkbWnCN0=60tr>DH5JJI242HdJ=j#8r!V` z4<=5DpQ7K4$&{7mrST4qDvNhZrlb{v@QrZ44Sj;(ceYuR5yJcA3Hf5H0_O<@@E=wq z;Qj^>#CZY-V%#zx=j$g3Qw?vO^$|9LHQXndV8MVw4rUTsZnaln70LKtNl7*iZ^(pG zkO`&&A!&~0Yw#CNIE`eg7sP{J+QB%P&CW9E63c4H1g$zGSi~0SKpGj+Bf`_WH~TDe z%#&~@ie)biN+t;gGM*44poxbDfeF*vKM_PO$UW?baD&VsuIfqA=!WP{q8vc?jT15Z$v)V}I5%T;=x`4W zl>WJ+by{(5q4&%)%$YF>S$#3(Fxhy&GWIcMMJ^Q<5eLEo`H*-!QpGU)NSvLaJjI=- zp1MNfXT;fqP>IF~VoC)W#sSC@=fiO}7CYQ6sc7-S@YWyEqRS#CvF&w%3177>>#5L`DVw#{W>^iyHsHJ%|MjxCg}~wk$G%0 z+aa&rHY$V(UJZ;z3iLP;exh^S#-wlTNiNccyxZnka zuf-{KZpSvvs)3g!xH=gO)#yIWsCTeGjm|=hC$oVvSrmUeL^$DmZB%+6jpsMG+({Z6 z=hirkmdjTNFb-GREX-w9K<7_&{FQSRMkL9RE*O3F5CW32Yumx9uo4|<5vHSDI@%MiZ z1b;*Ml;=M#qmA8R=U3=uzCsE-B;m4hKWs5C)Zz}Y@aW)Tl=nv@-=nB6su~KDB5BU4 zm-v=G;g(L;Pr+(75SsC?VB*@;Ac?TAW!!{VNWDm%Nv~V_%-X~|N^7&a)hS;9qrkX8 z>VqUN9G~i-;S3QvXlSb(K;cwP-j>-Jz*a`~EIu15fOUl+DZ2sB)6XK=e?Bi9B~C~A zR<);OUyL))2Hy$gpXG%!dfDtY`+|3W`;uaO?6Iu(GSdeBrluJq7Q|NIXmV7V`5cFd=g z!XiB|phCrZ5!kFF&Q*_Bd5i^wY9?41C)g^ASqMuG?=E2Gr64OZpxG+O92%ho6dX8L zJ@QeZceYMF(j6mB-a+a{5)t8t5pE)aeKTdzSK8s=MDetr#E6I{jZnycJ&P~;Bp8v) zXq^IKo<@VE5fL9Cjb|Fmb2%#us{}Lkr3?4~df?5ws6@CwoX#)cA3E^ zoof3O`zSzw6w10Rx5m`?A&RW+LnvV~u+@~LvMm|7TqBAm(lat3RrU^(ABg49a_91p zDr6=p^Ayv?N+LBMvu&CB%6L0d%*u2{>{zo;61mFgB!ZDpXclaTD1=Ao7*C-l9mA=a z9W8?kedsbGB)cF2yG?|H#q7DPOcPdN;fbNPu9ch3Cof{C~dEuPH zPo6W4785E{DeIy`INVb^y_+f7iOY$ZsfmxLiQ%TB9^hk9i8pE9((=OcFwb7a)Yh2Hf@WyL}UC)}j8U@X*=&`Wwg!k~%M&+1$y0`vS$s9I}0yS_u?fD`INrvmg# z9ffZR#V@u$V;2xE&FOHQHMW+Rf0IVh7h6A zx1_a7nxwTV;|i@+NmWmRVC7t(S`|339LKgnQpMX9uVp3h&eT!Flfa4>4n+2TI!2S$ zKg1Md=hes3C;8e+rS#5(6ulFFO3g*b-k8B!-EjbA??K&vHy|bR8$(+Slkf{eBYW@Q z;^o~JqdhXsg|5*4k&e|fS@EYrepwW*p@Bq*S#$YT1c<7ZVLsH z>%?o{QhU1t3XHOrF9gb_5vRrU!$pakrp{#Jrh{+{RrlzC;h9~=NE>6Dm#W|LfMF}E zw`!16I~mMXyd+UR?Sy+{wL=Jc5kjG9JZ;%8rk5&9u^*itX@eOlLWYycCZ%h0Q<@>J zy22Y~uCT<$F8098|6dw0_AQ}*QE~bWOz{NcJk-a+&RFg}YoK(NXp{NFJmx2%CzV$- zLN@fM@felZZB7O=V~sP3>fZGvIpRE5jzly_mWpH_h%|w5wg(oiZV zk{0;p4o4x)_Bk}<{CL34{5SDh=xvL0$o-*hlm$(Y$_~wnyGHrdl?qO@Om{Iwt`-oI z@t*V63d(}t+oU+H(3H&FWbhV#$J_~J>O(@N34Z@*;`ii+Ad|yWmqUpniAoh=qg`bM zzmtK}d+@tru08Q?L4EO5P%ETyE5EnUFO8AJ@A(}Oq!fM!lJKQq0SxPV5;*-NeouMu z`=Sy<6~e2W=fUs8j`k)5K^-s+`)fH0p4C&JaO;4pKb8D6ma4UB+`=Fz0zFd!CL zY+gP*g4J8Vr*Z0Ta%JLD=oKs*9wdd=#6&3`4`b42Q^Zabw2u_#X zQ+GG^t%U-<^8%L;~uHw&noKC?EJV+q0Kd4m_T;9$92K9fn|@_G`s4mJ#Pwyc@B zJk18dL9N80TXO#eys4(^i4M6k0kG(z_PcDh5o**7OL0J*Q(JHmr7}jnzBY#`Lk; z*v62(Z9leI8*ST>LhSGwGDb&Gr*^z2aV^Y_psRLzgFTkok0kaAMCw5X+v!uSXD%WVojmn{&1&`?e#|CF)3qd+Ii(KZ7>5VK^GuhC^e} z1e-P(u61aR=Fs%59Gd1Pd$J9~J#|-LxF>6+82bq}8M$O|xgRd;-Si>^M|D{*o-P~J zU1KXX+(U?ZTRO@3mygEW;%$ccBkCjUiuh8!tVlAO?bdVoS!uzq71S(@vJ|eU!8SJR z>Pp*FBjM@}P3FxZ_tbcv+p${xI%0d)dYI5AIyY2#c*YWC`p&+}mN#}U} zRKmDb%Z(KW%9o>C709+q=_dwZ((?uQqoq}-Y|)We(?sV~>K39yv|$OZ{t5cf&Rh3l z?!IU<^YC78MO}Eo{{FiOU3A=o5X&5sDJNg^8*avpe>c6q#8g});V^Tuj+^ddDt|xa zsFv(a?}t>;f;rJby}?anuR}r2FDz?$3pE8Diype0pcUbQzXwBd*v5X>Q}yI$S%|4D zOoG{bN!C#k>`yG5O+ky#c2i9WKb1WQV8=;KOf38yCE0J2?1wou{+tbEfbb{1HjgAP z+&#L@N_#AFmwL3jj2bK`!So99x#OZ`q-1!DpM4Q(Vk|#4Viuj=xnu;Nj^=FN#KNGM zfKKW-X~a++&Dp^*=@CQK7Rp8=bGPr4EhEwa2@YH+Y2If1F=iV{bxd+H_cO`i;eO`T zOTb^)R0)F&eg#fsEKGhg6u})~PSz<2oAm&FpIIb=F+x~CX7qnUn>)-h2ZC(xjtY>cR{HAfj(ZVWg^ z%dBEN)*F-v**IO+Z&h+!FElKNz#TVcxB~&94=U@SaTw#G;P_zn^k4wEQt_u410y*8 zL0LRpu^uY-egqa`!N72kh(FN8*%+DNbEyA32dR9b{j!n72B9(Pb z-R?jbtCs<7+h^&*7vW3CT5RfsFE86UAE3^yav(#Fmc#S#0XrQ*d57q;>oE2XT{mYJ z#@dSzS@g?@g)Kq97|D~XbsfEqffu2m^Cwu&KrwT+SSog7F{ARiTT1*8rYvAB z9!4#r-=CSEb#Q}d!0ns~Xl2BGLiFtv!8t>6JiyvH+qF`e`xUx3w` zcgow(a(Lt7r7#bR^&ZX|yTul5Cw*}yX2^p!uvP}Z@PXJ*T7>`JK_OhnSsec2OSX8w zNaK(}VkNrCW-ui}A&7D^=H2BTj>Zr}kPT{B1+ zKA*@qCj1G3RT{=oyM4@Fw@ki9<=g)3;dkgS)jp;TCl1mBWG({4y%^7>Pg-LN260X z+{FsloU3ER2PFyu;_fzZ#zEpY%E|=ZI1n zmvfL6_im7+!QJ`}Pi>{FXv<^*B}!(3R{GN|I5s1Wy>4M+&mdKA0z09JXXEADNvI7@ z^1M6^X`A20kmeQ?u@u|5s4!@qV$^l68ck^WfKyMVoNRZL^0WqU%HkFo`Qk*CJO?sb zr1^)~qjODv=bP0-r%$*-PTX0{$;c6j_NoCQ^I{Z)JfKVNbh~ zZe^jZMpD*X9iY%4{8U<2yUxvFzILQNO{C=6-Aw%KghSf=7qK`0yRp{G`&O(`1nNBz z4!Q@Zlq&h)GR#eAsk#C4j~e3v5;g?Gt^%+sxbx+GmFH8%@5WNAz=00UjFkh^S+X6# z3Y+|MJCL96fdk_nP?q{$lbAg3YT;bC*(G-JL|PbH(Hxl?hb};G@2lV?I6?m1!r`iH zoOj6hbNM-}LOpK9c`foRL2Xd=Kjhvo3U2l)srN-yek>m_KOZ33w6$$E2RNuJe#k?r zr%<3QnYcVR2NZy4JcR5Yi}e(Xkf;n0bWMz(fCNNkY`>(2Xp(AZJ^%rGUvzK*9!Vid zKzllXj?6yCPA#G?e-b2gdmNny1=J%XSs-Si>Ii;YRI{0barM>)0T*a4iXQdj_?Ynq zk7b&-^WqT1y-FeVm-hZYTpEy~8nI`-fRKtZAeySgH&vMz6a$zQ=kt2b5KO7>Sb6|g z`}snOMAD_w0u-}D@e_b(NU(YihfY6QL8P4ru<^SLk#wJ}?t17RV7o{nlDPLtP*hnA(p5b@;t{>@d-t*^ zSbTb(J^xQ|3cl^#U}}#$|BjONB({Ry2dUKm^M?+Wg!N=9ng8ks?sPuQw&%%%I6R0(Z<@u6OB|ZV; z(HuX--{MuC%_SQYz9Q@(FOmFvm3e)*#5)Xog{*@kzLS+5KUgiEY`;zGn~vML2JFgmXoagsg_k*43J0PB0g3#9=X1*Lup~;kg>Rst%|Due)n02#Nq2 zNMPFU@*`$SfI}Q|+h$6{yW+w72yNjPW6YvUDU!r_tj(>Ee_5apa7>GqnwN2o@~*nM zL({k#;VS#SZ$QNM6k*kHX*$L&`V?h^o+j6%6huRxlBR=`-bsK!mI&p?ZzY_i#&U47 zs#J9;MISA0mkn_x0ysvFGAm}{!M~F5!`|sao@KopJewTw?i^wtalu6dS6y|jUmgLO zz22r+Stk9xxj+g%dHGCaM27InbBV8%BL&zQi#=0NtxF1b`f?Xj6Xc3rVI;EN9}bQF zL#BLIlVO9JSg52p6(++$kwQ9UiYT4lmoKOB`l_N;c`j7=x?W9B!+cxU_DeHy3XChD z1r;b2`XVbFq0_r?&}OL2gM)!7CnCrBaFWc+P3{~OmRW#0U~O_9O~GiR_-)9HV5y?u4uRMgbcGyYJB)p^s>f{I(FRDwN2?5Z=7)1x zJjT4t2~$v40bWm;rYT_pn5s@#-+nPK#cUkDZF5|eT*zn1r-yM#3|2Sb?vWT4t*h|k`9#eeG z;1Mq~N*xfv*U2XkyA7Zw5K1%JN=ZREI+YC2I6JxlDA6r|0zD(?Dylr8t56b+Boq=V zMNd(AL1RIUp*^&pNHQeVZDO&GzN6Sh`=J1uP@xn3*5wc?Q&Gw!4Wdcez|_#LqAg${ ztI)n;v5rQDfw?*>;N)A1#R|_ATUZTT&{0;afl2Z$H`e+;R(Pyk>>RKQr$6a+GA7gw z3}n^K4&>XjhFK!oO2&!=N-|&w8l}DiC+4#&E1Yi}euNW)E#%oLaBrLvG=nbhf4cU*B89D5m=lQQID?W&hNJG5-VVFf$ zD57V?ccc)h^Ru5LOE&aTRV=roH&aFRU4Y29P`m>v)!%wij>?5fCFFxUm36RCI zpm!=ty8CA{MbfuykI=_H;GKWbm((}8B5_Fzv+%WQL}12azQu_oG0-SAg2e)LQ3}+E z&vZEW0CeuIN2`0xLGB%{sLBqjR4zpjfk96cGO+%}Ws}i4|&rf)hb}<)1JaNS+vI) zN+YjC6v5X7X8(lo$EF;Q7ckbdmq)L|tv2l~?j$$q#jZC4>~m8E(!G4>-E8SgL`&x1 zbJRza!L|L!9s{(XXoEOg<)Sa}{qceJO?-L2I|uch!8LyOSdWp<*Q8<3AzS){lwAPa zh&KrE7kxLVBD34M0Cff&_+FW~I|M`SQl27||3e26c@e6(F~#zJ@%yeY*|b$wEQzh$ zVjks~Z*eZ(RLWtxgcPhZ=7PwTNn!u%}64JDKiAzRafe<9Pt4l`VXDpFd;7vUus52>E~%at2Y zQIi2Lotw3Ge3V4JKRkPQ12NEKC%_t12>kAltp}#IYoK)P^y%#%Ia_0U`AoEXzV|pc z71WbpM0ewG59Ia(*hH|`$&H4g4FkP<#{;EJhrrKMqFw!p+|m%Ako^)s-nktAX-*;I z0(u1-rypDIrQvmEwqtFpoxZxASgCG@;FG_CTc(P|SlZ@_IBw_D+5BME->zwn36G$( zK$Y85iV;yyEiRoj2c&Q4W2Dd+(pK1GD7fO*JA^*hldy)v)#CuJdg$6Lx@M|i8oXT6 z*Tu;`3!0?fC?jke{HBvB`=q6`EJUge^_0s2`Nwy(d|fz7UfZ*%gOjN5@<#KSrcP?4 z;$19S5WQck2QkXI_CqE*A%gR7?>3N@IO(Hr1Hm#?+y?Sy)Zs^~H-~YJbN^y}*p6%x z>aCD5AC9&1;!v(w>Od7An4~;D#Uv(!IhU!I;ty)v7(bzZFLS)Cj`em#To___aL}K} zz4O~BmE00mC7b;!Tixgv_o=XKu$l)o!WvozN3Tuhua}BdLEGRc!(<0((}C?CvQdZ} z!Mr0r007jJkj;u&iNR{7hL_DKEyvL{y&o2Oh6;44pp%VX#%JT~dLzmIG|xFPvg7o? zgvc)W^7)E}xc}@>MZ*&)-Pt}Nl7$Xa48%K>lxig#rJ@P@04dho18z6ZI~nF?)$kv|zRD-C^^W0j&;^VD*M@KuFHo zVjOBM2CP6=_Ek0xlr|3Bi=G`~M++k}553JkWtk61n#!OCACAm`2v#7U^clLWKQdD9 zg83Ix>;qD(Y_Z3{c>X?$bv=r$K|?+jCksWgMI;LaS59^;>8K*NTkI5?FcN@BbcH6M zCc8#5lJalJ`nkA-)(|k{YXBOp07^B7Y7&)9a`}jBRMec3eu&9>CkxgbE7m--3T#>70{MvO|H-m) z_h({p*#7R0&p66fv{%1HSUA>umkumEYhZrq+|GXk|HVH8^UpeKzUmze>M*2vdRt|3 z$$w)<*BO28A^Cx8id=*`-DLuigmk3rcQ!=nbh2A-<9tJmvyf_=`zjY8gG9EH#t;Pi!v=<_e>TG^6$gbPC#aZcw-ap?{|0?y^Q+Afp^XU`sLkzXnN;zjsWD@592iIepJHj zp`vvSQ>z|lul58k8=i(qW5r#Wr}dIgFbqMGd6%&&_a9d9XQ+jf1ol(t4Jb3_(PXzZBE`C>7h)VieRmF9G`>U(N}bd9=8n@*q8UE5F`ULUKm zZDN48sEUY+XpI3xeFcN(2EBIin4=K7Xs z4ldZyRteFFj&poek}54No&F4sHDVaY^kH z63`tFCKgAe5B}xyDk?OQ-nHy`#GWGiaK&^Jr=g8D5Uf3B*Z~a!OX6JEvsU4DfL=Mb zr8~;3)E!1b>W3Sq_8XoCObIi7 z0w*7ccD?(Gmor_y?>q2!JsBPW)Z_p8hEM&kBFkhdnLm6tMx@CVQgo?er?&qDyvnh@2cp* zLWuV>X%Nbx^f)Iv{Y_LcY9z^VV%%N1O%Bp8)-Z>uavd99a`^sZ2?K;|`uyylxwXa7 znSSEc2f>s}uG>Q4I*hOLYh$XXI$Bpt&>)kVEVLH^F)48Db%hca+Ka7!2C>+b) z=8oZDHDQvv@(c@m-uA9AgLJ9B{>tW52=bd<<=rhl(S1NeJ;}BJSc;qC1Krv4MMEPA zS^zeiuJQapYV@=iI4F{w?RlQV_5mJDi!8T)m0a-SAVdB!QF3VnoZn4E-EF_cdj4zC z!b0~WH06(K2$h2bbw%JIss~y1WGJ($>_&MTAS+|xgVKJF zkR2T^y1o4nb%A|&I|#|9(M)zkUr_T1*AMYJzr(y1&l^hR&%>IrT#5)Nc+v1(tbW|IEdL*J z{WjlfwU);=K(*FKwKk^Co)}$AD}eX7%`JS+HIslnU`?q|KG~(Io22r#lc_HI(ELo> zqxcJAfL0J5NBsip^(1voqiYalcAy(tiS>^_4511VKGK7-7g$H}PDx1VZQQ}=HqvN4 zzo_qq2Wk0ksW?fuxX@Imi{LIF!Fi3OtOddLw$}%eo%ptqq>KoxW^i6)noSP*{g2BGg1}|HwtY&W=+S#C7H#OgRJO#e#gF9(i<~Bm7H@pS=_V zJygd>mkS2v7Pafzmdx2d@)m{s_`Vfy$g?Y!mEQn_S1jwjo|=#Et6bl{G#|bY7O#RN zf&jKx-$XDYR&%TnYj5iGSmnWh+vX?uOUc^zg@CzIyu<*x(s9S`74^Q+QL)DFticOp+FM)3F=`Djt@K7)zj% zpE7P>{|7aF9KoHaf zoH4JIF82%pdE$t`V8Iat+zH#B?k0&N_za&E2*=t`q0~lYQEwSzah{z*oZ3dqoH@xq z*P|cU#T#v%`y-xYq$V8VZ&UQAao37`wHwX!O*{^j_jCpUxh8j6+g9|QZpZOpzg4%DUNBL%9 zOPi{{ou9p5K%9&tV(l7jni$5;TnGS8brFPT}D}eemaqQ*G+k&KOZ61Zd9r(9)DDGe53?5%)!u7ME{*#Lt;%7s%okBkwpTXL( z_Hb$kc&cX2a2BkzH{hXK|1ic8I3ccjYzbR zAoN}*m_RLn9LN$`L~Hmha+lR@Ka?MPp>a35KCQt*11F>YO72V~9i7-qbmCfq{;+Ew zN=gi+bO30EU%e@;p+JQvS)QE>dfHyEV9Xy7@85QewV^n7q!$@^v{iG^O_ovCi*z?T z@^x?px~P{$o$Z0!jit_?*c8#^4~LYMQW~GT-oPZkA<-`Zz?WdZ;@)JS}3r z9sUJ}hmr9Rx{$QX*j?jPD_FCde91g6Y+~b_Bu*?!2Q<5fV%~lL@z1_|Y`zyL$#Or&=*IN0A6!FqKkH^#IUgLjHzDCDmF$6VxH-B~@vS#)n@2vUmWB8W(v`V$9T z5xfo7%)fy1mJOwgjB!xYFowPD>!Q&g8Y`|ik@ih<93SEz7iO@=H750IHmxL8TB?3) zj?+P7bFd|=+3fL+X0J@mQaY;kF{)YME1Eq%eC5J$)x?3wu}p{!63n=S@Qz=)bi0J# zs&u%^Au%+>zw6=7d}Z*(9kNqU5k(8p`nWiHRL-hXk^QL_DmKQ4w8FBav7yZuZZ=55c zZr6EW!e2~7qF?R{C;}c(h9Pz;9Q8nde_VC)?cn+;A#x{+CC5=Vp)roKy;THad=CFQ zzTKOz()f(e7@vS=QN|}Sa>^iw=HeX6_XZbpLkKW||l7O*|H?2=g~Nax{+xd!+{qKawL`n(aC=&u)YyZQl#ceh#R~|0)F876o?P zyov_~5;rc3^Ij~vr(!}xAb4w|Ba=yi2DFQSc8^;ZXHRQ#UrkzijhD0Tz}Zn;>BLqF z#i`nTSBoFiZ*l05dwH)?UlK)eoMI>*Lt-<6YT%=VZiS;)L=XIz`5FFV;$5CelR@H3 z-5$qg=B1r^X1PV6S4GXoK!g%#w_Tge+NmzUU=Le~D{x*ajC?#O9Qy=4b;bUWTcAzMJl`miCL|}-C4!*WaY26MsEzZ6)KjHW++VNIIHQgdC6Jmy!K-oVh z^jKTi6i}=jAL0l(0i6EOvTuTTF#~IE@DiO(6S`vz>&z46`r&H}zzwQ+7-GwuzO>I7S zw;s)xi+dJHxgCHe&)$tY19Ya6e1oiL#YqvKD$8w(so@1?KZc7vxITd`M^9*EIGv9Fqe>v6<(NJXD^>bTm42bL|v@n>%OhFV|%~(t;_8 z9^H$KPCvyY6*CoY2TF6(cpb}bug>gt{Wj5*ukKta7nXvXBzn_szw{W`OiwAsl2o1) z(CQz>8-9>8QkmB%SF$aR0mPSDy)wXV?&G*+L&NBx>H1`q(b&dG5B{xhyU6J7U9{{| zaLBa@9xPFS$Dmpo(-44@A8&c2Pg}2CE^4#)Os1Z#Ued9$%6-Uk#?+12#T?)OJI&d!t&1LozIC--p|X2@i>Al_IB)l$q5{DNE|jr z`nqCq>B9qVZJb4M2;(RO#(j;K5qYt=aru@i@cb?n?Cpx>#Iiw}mB0vc*lvo;qS!c^ ztkFVwYfefg(&y_?Ai_tALvS~gPU5cX^FGG%^9kF-%8Z5=;2-x^z<2-Tb(zEx8^z zx~7Pt=oTG}e(*B-Iz@Fd!ZeQ;w;b}L^1>uUkdWsve<(WZBKc9xsz$cJwT~$LljTg=(`U)232u(fagz(oLC*ULXTwwd!EbcD=N>(GT z6T-uS$EcO9D-vc2RwGUptPWhBJe@U4^#!hj+hd{)V-l=+0ea>#1>M`Swg~zY0y8wy ztdA9{LR3aYDGkY1Px8MgL}r~mP%3x43s*gL4$O+cqz`d6K1Niztoe%rdzfALLvA79 zvhk+07+t-Ru^5*_SmvU`-yiM}f?MDiaq`TB7=fsYvt3TES;a&9j+Roax^ zcnQJ_3K=+8CUvQ&?E<8(5+$oFz?L?($4`14_xOcrOBr3N}G@6>tWxgfk2-GE2b zZeu@^wHi}Dieg7A_q1?Btm(%Z=!GSQ8eo&CCUqye*8zlV7bt3lmCnPuYy}Q<|A0+5 zZAe*%XfPL(2x@4!{D`Yzc2>BmPIQ=m72x8~_ap&+! zY`@u7g5D?+VW4VsCiRl8K3C>~S+#G)T#q~ogO~Q!Wa%oc?r1f6X&VK-SjGsdoDxX5 z5Oc~hZO*0XFo}tjVO1b$SEhCSe%DBii^Dv4lNj0$1$sH}wXeiK3kr@1E|1e4q!h6o zvE)_c6(aMA(4rR&hQ)_};(hVpUj-)|1sE}?khNU?i0sw1^S}Ids6%Ss?tbrkzxrPE zjo5=^YYRYe{cyPjlcv4Uda}r2pqqoZrwqOOxi~x06-Zjx@fJ)M8;eW@ZY*ByxnY|~ zk952%He;zAtk#pkjH4isLkGI66RzmK`lKfV^(1l&z=d8HmgqyY3Zx!qNgcHhbr1y# z5Wo7>_kOh~?l;XY6#1!SK$=PdRFsS+9f2{9^0xQG0|p027~bPTS-GFz?qi*pyCtUT z_(`vmpU6ETbPo{v;~&#S?hQi3iLc$t%)+61?k-njyCe1tud(kZwQgs-!X8`c$8QGw zobcwztG{#@xHatRKz-!8h}1G?-ssZ!o`(p-PrNm{55g4e}{+lm^Kn_hzI{6^*fi;z-pmM5UGi~+Gio!<@FA~ysf z`6;+$H(E-Z17jp++G8Hk6z<`XI1N?=MX;fm;-}!t;LzYzxHjOJ-GGBe+jhbc<;}9# zV?r!2+!j~>ldsVNi7O~Syq1WvtY>)xm{N4$0J{T=vz6;(3YpmvNpbZjy^cYW9iS=% zFx2^?KBUo$VC881A!o4zD4xEfBFa%(Zsw<`HR|6(J(P+{Kjz|RvdccT*BIq#^H`P5iBG^e{3hr<4%mw2xdYk5ZZn3Mrd9n zA4g~y`h(D+G)v?UTjvKc;v*u8uJwZm4jHi=O@h`Z3-h=J*0{~3A)x8Xxb2+|r1S4N;Po4WynRsQ{=|WDRp|d;vBwA9dvrP;ckjSP`~TJ#FbVW=Eu znQ?&0C7ce0tA^fwhd^Wi@;{J$_NPi`qk#u$GMybbG70V(4SC)xif1~)zHDtT z2;165K{n1|{sioxcQlomy;CTq^(4(iwr+=U{?rrLn`TuP0)=}K26_^DBz7ptu_KB` zFKNMQch@bOCU(E)3NVbn6Fke;`H+u|%29*n1K_9F2;!fyj*aKp`yzBocMSY!v4@Hp zwNd(B0mP*Zhz_;`2QTZrI($&CjWeG|4;%!GAAR&e^3|`L|Y_ z2l#7xkomE38?ItxP_mwcE~0ad-Dcr!h64Tw=ZRImL~fkH{F-J9*cRf-hJ-zzo6JLvdspLFJA$)|6EVl2^vq!1$ z^=o?Ei|7*YrV>wA(B(&|@N+wA69~G|QJql4O_4Sl#)@Ix8PT@aNJMJm&Xh}7-mK)=amZyM?v%j=-y6r9L5ZBGF$Sx z;f0*18k((8;qG$Z-R01o{7J6^koZ*KD13b?+$3KMH&>?)ghn#P(Mn+X-FD|O=y=vgv(!jMRU-j03(pv{k zK6&7+rGayqipsq{o%k>@LYvPI_5YPDBt^LP3bi-pc4&|8f!|xb>|3uo865$4k0I8Q zVJk<+fu6hii2Jgf1Fb4-c45C{Z#$X+Ni{UvfmFvx*h0nO1DlO7jwzPFqoIuf-1m@0 zf*ZC{z(*UwY6=KMijm{^p-BZs7*oS6ND4>+$HD@bKML0+1HZZnoxonzb>#Ah(9(2! zX+4>Kvr^bN6Zpcwr|KD6NDZRNRDhY3w(*jQ2T5KO$I?j^pPOwJF!;?K)tm7Oi7JcQ zsFINo*{Eot!>?5-X;%&LgqThV5a_Gc^2_i+dy6WlNlXjj$k0^=01Mz09u~MJTrGVm zIudJvH><^r8A2Axib8D%99XD9K^WB>`@EE-aCxwsyuwjTB=Kon3c%*}ceF%oeoJSHw! z=q!h0+6D&o)E4sWg|Pr}jgwd?9Qb0_o>QfYFMooAu~SMMKP^U2bp1wl@ZU~lu~uMn zlrQ>cpGOH7gWRb>Di-1XT);mrL+~$*g%ohU&=QaGKU)pp@J6eNRZW~~jZMmAyPgo> zWd2ZrL~+YBEr2FSo!T{0@}mnPY0-d&dKR5$J@8a?nNR5KjdD{I%;yd+ zR-LX#oyE2j{Nn$GooTC>-~l^4#4!X$vEx(F^4~aC(Ay(&%0EHJoCj!Jkk&i=6bCZR z%T7b80RL$*vfp0h0V*#f@Lr-w%w~IvBCbFFJYSEqBoF%7vUN}7=myMfrDNPL_RNrM zLuWoFY+rGeoBt%p%1%>kHvt+iQcSKY7>ivqH{V#>CMCq5^txqNWOA=wRC(^JV~iO#=BOpiBt<&1&S8FVPdz&LySn4>SboreAIHF08L zcv<-(`9wU`>?2HPt3|Q^FYTCCKs^JMY{AzEK+93rJ~lu=q@@lY*5(HXzrKQ@o*i0# zv0O@lY#bFtxFx_Kr_EKoyZ+5&A?w+yw<6F!A-3lp5cF3M4z_;|;SPSXeS8r{T@7N{ zWMR(!s9b(~`-R|b*$nI+!+@k!M#l3ep0p&kol7z%Y#*Jo!pzC( zT-nNaS`V0gO<+32LrtQ`{1?6rZ9%gDqMo#V%EZu%LCDcGntR1E5OsUdJu14>l6dCK zNr=qvk0o^0bEzAO@9^W#nu?i00pEQejr`0p|2;*3`=7*h~|mG zS-U}n;_{sF=JPf+m`$qCKow(;h8t-73pzl!Ol4{zQ3%)y|H^7NxR;$jNQvz5IDzHq zI0Df-1DKl1@FV^h?8EUJz&gU0(;2DR34NqqUIB>rhsfkd-Qont>yHKK2=elq-Cl?a zjId{=WJk1l!iq77!w~hLptFGv?jr2bIvNR+w75mJwjj}t>B^NtvUiN{btWeOL_{fb z-AC(Fg3DeqbZRH(Z2S2jOy}2!#Pomd*i>^phffh9>qL53@R+vwml~6kP#iP$U`k^<1s}yr%9&&L7J&f zz^(V$1$=DeJXJgs`6~QCnAX5J*cuT{1wJa~J5<&uLE9fHp2>*rFc1V?rl`9=?Ra-o zckN=!1irL$$uR|So~8PloI3*M+I2}(Riqkt%$DTfUFp+JSt-A>0)f;^p z(|k6kb2>qUfzri#*niSO-j7SmTLW@oN?|y!<+Cs`q+?c1V}r#~HOkp!06iE^8Xxm9`3iS|!k)ERxF+(d+#v=-{z5AWq@!9!l`vQZ>4lce-sTo$&cg00lmRi!77%L8vkwLaETY5;$F#_Ml5zWiXz01R{RD4kwLO^HYOcUvym86y!XkvZN z%>Mre1=UqD1;h6}4b6&;FF~ z8aMO4Qu!fAXq-&4H$V|jW;JnUZPDxPMoecl-L;y))O^JnZ`6vi`YlG&n2Wy@YK~QJ zTJ*N8g||##INhYW&Z#CaJq@mN_lGToFKln9w05%v|1}FJTOG_GIOL5h0oc}`^twd? zRj=z@{=x2JEXB@+I|r1C11#a7Tm>}u6(|^6I%Jb1s}$#BisLSzp}FHgl<=sF8n@X6 zsh)BPXsbg99t!AS{gDSuTHvEKXX1w7jN$_&y?5y)wZ){tBUN0AS(PlhQcN~5Rfz3T zP1qA~;u256P;Y|%Tl459qz@IBYNt4J`_U*QN9a&nORYb3KOzihQ$Y}hZ94LMZKUf_ zOzz?e9lx8HBY5+(FjkNqu6Q|>MJJ4Ns)*(_wuD>zDlFozQQ^a_8M>@jq{dPwQ?3$T zl1G?NiX>#t)ncbAJ~+M+-!O!~&RN6k4Zge>nVB7_f_5&Wk}Q9G4JCfz>#zIxrb;0d zK&bV=Mx|)&u@IU}2ht&RnvWzMiiNn}gu87v8IDD7MUHppZ4(9jXD)QJmjbn6;Hyx)u!_T?rRDh)ksT7VVaG^>FpS)$gtz5Kohl;t@VW4|CViy7$mo37mMO?NqAD#BVl02_Vq!npJU zdT0Cu+!2>;>}YOW8b{Ohz;E=Wo=hcM;<7#aZg-p>q-~{i2-~px6(-Sz9ZP3+S7#yI znzFw|jK;Jx<`hklmIcB&p>Oi9+}+9*)IDpl3{hA zPcd4|+I4lqn#8*ExLEaHrl9HD!(9r9KC%zb3>ndf(TIlF+v%NsZrS;HW|&*PJSb3) z!60G-^{u1?`9X%K+@DJ{DR?FJ=SD#Pbaw8d@t}LO8gw7MD_Ey{oq<}wAB~2mQ&AJe zBYv1ELNVPj9CoHE*&G=(%u10>Z~jlKTh-6fk!;$ zaPfTB2+logEV28bW|-l5Tlm80H2G@ZPBq`q_Srr$mFyQ68R6cd=s~}n6}slDA$}M& zb~nab8ci`UVl+8eLKyH%&f%(N|&+f3cDEZ1}d48|BsVdA#E z{PyYT|&cwPX=ut4Df4R2fpUS}J;50w+V_%TjUGA;^rv1Ok zg>hI#;na)=>eT_V<2~XO&H(!dl<64{I>j=*c5OX9M$T? z6JI`X;x(lcuNk=GnrlkeTyw|3H76F;`4JQu`F*o!k;Bj_m>Lim3<2H&D849UH&ERF zdFYQ(HH#fii;eP_oVjR%Wa}mtXo&xdO|1HU z7H>7EChu-u(xuOF#*r&m1nl4<@Jj{no`+B1y9Y0C|K>OO z@YwPU#O0xQ2c{oF%F<(oFjDgNp2eq77OgQ}5Z&uu@`i8ca_6-5kxDt&o?0;zaZO;9;p)!U-xChaTB>fDJ(_!NrPL|tCUgwz|25FLE*jwQk z5R4qV*3ZUIslR`*%<*Bd^x@b9zR{8|jKO-Yy+4=0ljog>omUDE`-Ye>3f7fz1M4Bl zNR)OYfsWn#MHgHURq{{0N|G-OBlfupKOXJIxIBT{vNy$&VIPyRMYESE-kBa}JNGdc zafFY`J}*!l?vZeV%s#t|ku9v645WGnpCYo;4`_CJip{G>`uh!3ySqTY5uW)V!e5qK zEACb)V(>EfTdMz{Uo48v-ZhLMtxnxF-p>w3yYBz7cP?;tRn@^i6XtOVVIVL8LJ`74 z!3Ha4qF*f!TcO%Esh>)!)!-)<7-Az@ky! z`0kpUI-SISB;q??lE{0?neG55C7x#@cERtLH!#mVpi8x3-X{db&D zY%x!qCW$S#jH!y@X6{gogloSLGLe-WhYq|HgjD&%A+LY9`ifUkOKb|dKj!_ia4eY# z!-c$MP&T&g%yVcEHJUXQYbscfH5eph^TUp%2n7HR>wNCOSs$kY$rxP*WxEruwaF(a zbxFojlRA4aUwo3z+Cy zAELrGYGzP|V5o&N2=QEE%7gPBfWNd!Z)9 zeOusx-s)(A2O9V!cnB24Llii%l#Or11I5_*z;3Ig>}n~9!sMisK2lDjV>*nDXHPg3 zM3&-9N->o-1Jb4;D+b9b+N_uuDTRt8sbM9AkY{oEO7u2Icl&AaO%D1|3l-j_J2pQf%YrigmQngzM$hSZR$YZy;4|#*%-qf-6uW(z>(M#49Md z!Vd?oZI3g^Tf|M~kf(ZMp1)9GHpw=OGGr^OWF|n|?+LH3_r<&vM-8_1^P_!?@rr~U z!icwCffW0462mwdR3)1>W-*E)xppB5g^|!B+T>n0oog;*bNhjTYd^wlm!@W4E(}+F znj{Xniaf!=N)0^#;2lwEo_)fBHEFKmtik+=0EK93MdAzNhN3PCD(hPH_ zs3}oUCdoIWiAB2tSw2E8dm=#5jDQ=o`6$xTHSr>moExHT;U8g=$Wf$}a*6sH;wJ5; zv$tTimuG*2Q|iu@>2iuK=>OW?R;H^sc4WZUZ9v1W3^PVtkw{4psMP0!ARi`LT5dux za_@lhN`DAsV56W?y}M%#hpS`TsA-9)D#Fe|p~Wtwx{e+k)Ozw9l>9)T=4uv_?1MDQ z%bKOPnXgd8tnhov|AXfv=VkSt@D!V)zxEJ5Z*yo^e#P@HuNo9 zD$`sPSUi?y=>`z|oC^8$avXH1XThnbNDnjmxJ~h*g^B_v+xoXcFxjyvLb8(vkH?W- zrqzuz3WeHDLrw%}EhHE)%FO>ciS3v$VpY!&7CIUVFCnoNv(hImn0U|pgrR((8`$%F zQ4H&)65v9qGCS3jlRA4AETSzeQ~89LKrv!{*sT!^V#pZ9UL$4yR&U?en&xK5_pMO> zY1HT#jTKt3*vH|xndi0g{2g;S-1>!Z?fnqSmgg|p<}II*A}^LVj$VjDa^;Gb7L`F4 zxOT9=i^cC}M9F6`oD1_u?ic7eURo62u58ILYPDQzt+f{MCZa4#ERymYQixoUpZy;? zr(7Qs^%g1oEpt~Q47^h8|I}(nWf--#MkU=vUByaR3Lfg>=0Lpn1i`-29{p*w=Q0V_ zBVp-Fe2$-N=(}6sSusCCq~ro9N^o58@(bMA!uSbeVMn4mPgCs;FTaapaOB^+_vPta zTpax*p34zE{ke(D%h>dp`(jGk>syrVrS|Fg>kImCroB^Nw2KB1Hxs|#XWel4)&2gy zi5~Gxt5GO8UPtxj%x~|%Ns9O7XWHVW_UZWRZ{{8lxz=dUK!y3O$6sOv%!v^d)fyBO z27);QvZzGdIyM@>?1x0oU6V;i!=J6PZxNt#E4`p4{{*DHYm3xFGeG&P$U z1}iLcIcI5szm>>wO5gU8LJ;tta@Zssk|IS8gb9KBCvlFa<{(Z#7sQ!oe` zW6FUPL!zu&R@^Y`jH9B4QIrxoiX4SD4emIG^;2{yWE zV7sC9-NE&53tjBF05$wbT;npslCM_Jl=^Btzq3w(u8ExO+*+U$fwBxL2BTezG#gVI zrE?JmcD{sOrqUuF@On-(qksi7rgoroSLdu!jaDqEr1yL6VEUuWU}Cd#hv zp{_S7B3U+?rc0`qYWb9XEavpLfF1p6fyEm@^rDX5g`}*@Vhw7qIZ4a`hP5^;lwuD_ zalI&N$xnDBiuj0{a4UHsK$;@uXpkYfgKl2k!E~x6>Qm=2vHSYo6)=>#TBM=C-f+1& zD#~#@6~)L(S`-=y2u#zwoJ!N}N+_VGL3 zu`%oCRA{AospRBFEf`4$E60~l;-dU9{?4104`fiy&yK?bRf^A1@Wj9kou z8rFb%I<{aP_>?<^N4{Fk&)6e^d&HoQV;Ck1moaJY*7a5tXXRirmOH=(1W`p9Oq zWRnIyvIaRpQAl?V(oU%O3soXanWPenL?iSeECS3)tdS zut#`gSz7GnE60zOKp4i=OE)>k5KHQi@au9*_0owrT(pKZS$6}&teJta$4B5T9{dcO z;p#~Zr7@^v;xQ38Y?v}Eh=x*RLRN8&c{8-ZO<)^Chq&0*{!ImW5@<^?^Ce5N_J1XI zXGmM4I|h`U`GC?z3lbB^-8~S5rNfEzFFRcBQC0pf&yVldRdDPO-q}Z<@L+krx$j`m z;1U0hC0d)d#n6t``Xs1Ze^ZWHX8-B{5EYgP$Yi7iMqrsNUdGAcAswej@`N#r_yq^M zoufG}B`C`cyySR$@8AFfJw4S6fsu>Hl3a#Tj`k<*o#&Cm^Aly%Me{;`2_Thf*QU9# zVZo{;JPGOehwgB-RXJ}mR8$6I%?058&cmC~NHFExLLWryW6fnpTXp5UDF9joCo|dv zpzedHdbffVODg~nz!VRFkq0QrA3@5CHDL8!luvOK8t#rPIe9}0zeud9M2xTWIeAes zsjVt<8KlBkrCL{t5-$Z>0$1U=Nzf{_1=Xd~dY3Fcg}cJP+y3^BBFNqFULyWooFEs$ z-p`z}bcv3Uzsr5%5#*wAFOz?J$J@Pg++gP=7b@^IblZ4x2~rPoX(V;YKv`)U*-I$j z+}&~W2t;*rAq(W9NB4KSpoUoQk>l?Zowj5*=Qtx**?CB#QA3xJ>gALyR3= z2_F&*T@{SW(a|!4p@{Gu`yP8NW{ZEC$to_p_FB?Xxirr{>qd6gu6fjSN7VxZKf4G% z+kcQ9Hg8Z{o<2PKTyO!Ywso?M$ClY+%yChxDl0uOz7X^nvpqdea;sTctBP@_GvsqN z#7py!$^|C@R}Z`PdH$!}4&89%1Y@BkC?#Fe#QtwNE~^J_S~`lf($J#REtN<(iS2gTIyPyK3*EBhzoGb9!*?9i$O1 zF(kg4;^})5Q9lVRilii>3gRgh4XbN6ec}kC*~{tOpRPNc+nuqEp@{mFx3EL=f21v< zl*JU*C=$USC0z$p>I;vUYl>V20Z>)E%n7vxUNOTikK*R_q+(2*a1K1I)8? zZQbm+2w@k4v<|ySLu~3q2pna*52{LshcR;x4E!sd43V$_4vPa1>XB%Z)x~z>c4FkQ z(C&6pNWR|Y>0~o;gPqi`*OWf=wQ+hO4B$}YbkNGaM|v5ilQy7SPtHbKvq|7RxOJd) zO&_HKjdKKR4q&LfJE%E?6LR*?{(JlPP80}9{Bm7Fe|OJb%4d8f7h=NWW}if_C*7Vt zB2qAIhG~CLIHPtcJT(v3=FwNp$;##U1Kd+I?h0;;(K?*>OfD37`q6DN~VjRTcs z*a&Hmf*u-J743yYm{dT2QQpydrFvvbKzr_;xFJr3(AM{-DQNacp+5R+IEMF$VDM8tc{ zNkd04O>YdL)!jl22f&x-cA0_Q!rUl)suy!|gsgb7-EvoqC>LP)jp4@c`bhmVkrca( zcZuA!5}rqr<(BXU=0X=C>ClI3Y)G!J4-8^!bvj9aprR#fO#Tc!-1fx{q50L*b#%?e zPrav|Mmlz!EOA&c!?+@atbEuG4(sRqi`sP&&>`;_ z=f_)4Raxv^gpNRG$t3XZ2vzarPi*gM@!xH~+$fF_2duE4t~febfQbGuCJj535&fJ6 zu#u=QLU+t)`_=gG!*l^x^){JOcVsdDIn%Y;(bxuH4}vv-w6njZvYx)kSs1HH`K>AS zf*ShgIB=i;OgP*J=SuIFFf!5_c_v?@%PJ>}Jqy>^Hagc~11#aefE{rZ{mgBrWB?7zQGO=gZbo4L=P$LL z%4h*G$%#Tma#%|pB>FML2V&7}A-{fy;W2!#=EOlx`N~jT$1c%&X{>SQfl*lQJitFp z!_LEkB2B~2LvRU@PdkH7W50pP35Eq2x;6p~tZi)Z9uCjCS_HV7DIVf0aq1%)RDtAI z#z|LCpvPhg@v$9d@kFJahX%3i z&%m|yM_qP|oT9$MBdC z0o0JorMorbGD(b!C## z2;n9|uFPrYiVT5c6+Mlyq!|@c7o-+O1(;|l#QTc5BBnJ)GJ}?nM%|%o=B*A%u~m)f z&e4vI7}0LOdaEcj-%YbNItKJW;`~Q+1gnD!j9In>Y_WyOh{^AVeZ+{8Re%zHs+0zC zonq%Pq1@SzLy89>k)cy1`oM%d#G6B`FO2X0EGGKQm!FB#dL=v1I;JQwMFBdTp-|v= z8IgENwks_ku!ey)$wu0hDecj0^14h3RAhKGuDrjP zywwC|6+0y)-ZmQW;nV|R41x&9qyXwa45ECAe1r+6Y>>zYAlsADz4sx^@Qdc|V2POG z5Um}p!WgBBcn?5n*-a>NYu0N)AiM@z3R7>RV$((OTB;>xqg@m>Yg9(`%{GlgKY4yl z_v}-HlgnP@GJfhlmn5QhvYkRk)D~eEsY*5w9s3Hb?KD>kvq=aKY_<;2ondVpc7g$H z92GztNzN77H|6cNPICF&%;G$gu$#3ozSbf_zPndP1vH-e)t18K%2gKI;f z(TPo9f-7U5Bbu#$4Lz+CXz6|c{X?gud9ebGo#RbmrL*ml>;q6_PtRhgtveL*3l^hl z&NtaFY{DZZvIg`4p=_FNzl9s^H-}8Y*rI1*X+<2}#jyxd`0_KZKWIFZvLrhfCHBsO z@LP=5YP+?cK?3E^w4pr=>4<*vGCd>4aNdME$qTVV0afweCZ(aMvIbx2vo6t6(m2P6 zdw+1r_Vlc_zsA?Vo8JxfP{PSVt{!WjO_c|@ie6k0MID+5ln4vYe zOAm0DN8e!&=Y;VvU6aKBhpoiXjqrZF7qr}3y+|d{0ox!_P5awhuyNW*ukG`d7hy}# z@)JrggOJp>E+wVE^EJbV6;TFL&iqy{WzKKf^dZ^pO`8@mKumjUJM8>+icYykf*hlG z-CR-{mFgM^rksFse1LXZrMh39zcFOGH^k+ShfN0~uZ{K6n?O6M-XXPoi%0f4>Gun% zyL_e3#u4O$&|M65WER3@V+@%$3?Ba>`tEi#q{*0|$zBh+n$5c_>IMemr(|d5G*gd~ zfmgIoFsG83xwe?rk(#5*uvEzQLA}h$o|e67UCXBw`80A`3ZXlI+I(!wB%;Lt{3Ry# zF}pc98mA3maUD`M>vWVQ41Dhl(WF}4%-JwzMj)zgx$LhcyuD-Suat0BYF3zbtk^OR z5(aBF(o4NWEcmVCyGb|LJE^zIPU8zq(oUlf5oB~3ypNTww+1$$VfI1hv;ED35#9jT z#_426G-IBwv5kNc-(%y?N^4i^??0{`hGe}nju|wiP(c~zD#6##o ztVL8J@t}KYIfjY!Eo1Ukka!ULn&Cr(qKPVl#6##-a8(gC5=@}PFbU8ckQnGpUpDmD z0E>)(AQp})*tb9$cvM;0+gAP&jnMQyQ~`S?an9G5fed=nqh=_awilyaVrP=0f}cx> z@|C{m@?%L+W-_}C($G>Mi3y4s#iaj7{d~NWuE_S3JpT(@eNMy0z{|fpNDqDAZvT#b z^)+*?52eu{Ykf(*DK%1n`kA!qjioO?CG|y8X%PieE}~*F({!_Nc?{X6QFJABLl{o< zjs||R&JiZe8af1)i0y3L_a(x=j2ZeZa87a_V){MJ`SRe-CfG|EAjHmOM6M-63_uV_ zhyh6SdJrO>K{K;T>VoafO}Eek9fQZ$PJrdgDC>-K0=`;jxrswIaO09$11ij0h~?pl zmgM0h%oLDH7Td^j$8qC=S(iMVZHqV1h!*(fv%qVP{de@Q^py`-j9VHUV_UEk9e)TH zcY=9vb_o#S_)Cggk{uSao}Y6lFV^3N;ieF<|4|BrlLxP^S-eeGZ+ z+&4V`d>#YO2nG9^}Y{*`dOqI&5M^8_wFq^;o-JpYCZTj6x*jmg^KX+^D?LKJBa?Sm(XDWTqu}`(+txMVMamlRt zv+Vcgu_;&CvztpZQTbc?9dYxliJ^VE;GsoBPgcv8LjKg#Q~M*&cI`cXyEQfT9KG3G z2X!b4Z^?33QJY;#OMQBu12>N?vuDB9eEHp>>d~m6Xk7}Ae-7MYn(^Pa{tF9|>wniW zGnH~#7K#U+1l8DN?1Q0IMMIocyVCeH8chjD2QAo z-;3E2I}a+xcIjqG%_hsgqd#k^H0$-O&N#i3rL)=^$Ja1B<_m5Bm`+1swj?`^ zeE3;9&3!G@*CH&hf%mYgd6!m}JEI!H;A~dgx##k0YmI(SMeQCZbg=%SnBDC;cOkKa zQDbCiU!VgTU&%lwUoQcew9t$m!DdayBX0K=%BrvQX$C)9|gzIvN~ft z#jYzms&vEx7WKTq8Zf`W8^aV}z^P-*scb5aZxdA%J-T3|M?d@Y^eCNK^k{50+vn*^ zlOAmn1+4UWbbs;SXJe24R@AoI_AuC<%)AkX^4bm7eKN2QM73-W!cBazMR*D*^u_id zoBhrzv>m1c>ewEP4LSHxu}A10d~6Q}zS$mxFN%4#2<@dW#O(4dxDR`>ZXJGsW2^C^ zW84f(+qkhA*hp5=7A>StD!t!iuCSl>v2wNt-6%XBV2V9L9g@`Hia9XJJZur3ximHj zEAawZv2~gzKlg9Ah?z>KHSEB}86RSgbu1Fu>n)JDppHa7&JKGK+!Pm`JJRLM$a4pN7bG&z zH7BrP`M?v}f8G2?K2}6s69W9~+f*LNZI%>xer+ge9E$P3+d?H^m9b`}=2` z)<}Jo5X#B1(n?A zaRu;K_(E$J2=U1B~mC&3CrZLo&l@#%p{NmO_BQQUD(d4>OIp z1NU6tx_*APqoXewq~l*3j-2#Bn@y(U-LPjsg*1mDUFC}iSJKt)u?f*d36t)u}9x> z&|ScVkLuK}Xd%Q+X)IIXBhZ(@;9`mR3tTV)qFt$^WG9QY?K3`OZ)#1eo9Fn9-C6sG zJX^}N)N+OAMYDF~BpwhH9$~?;K_V$BjF_A3Hr)qAZRXw{`NQ) ztN7l^G41aw%yOWYu#IWDydGtHvyeOxS7}{wd_lAPuG}2Ub_^Tg3M}K7lZ++3^ERhj z;-E|M;HToBuk<6V%oD^? zXGdm0YlC$Wt=p`*+fR}t8#>;0lAW!gMT*$ikfL%jI&RWNVF#DrDNU zTu5?lv0QiyGE*3GY0?EZ3ga$p(qKt@bHM1TQn*j%VCt5_JdNn4R%%BwQX&sagZffo zsVj)tX$8SzNO@SIS>OO%R=M@yV2%8OH9`S0c7jNyGWLUsP2-fbuBK+6i3?HYPOLC} zNtsl|G)g;MkZ|8ItMP^HM>+tTslF!AZ(YR-{?{BKd)~%(l#TB{v#SdtRiPl+!{l6W zYdEO{*3@1%&rYEOKqG3Wt49s0M9CVCrZEsUqz0)#lTR0J_2H3%jmDzmY)F%7sUt~4 zzhY1sgcdYP4a3nCH4sHjRVaJ?!w0#c?xdq>Wf>&? z@Lm5DRr)7BRz!^iIZgZ9l__Wb=HT9cA*u`#f91VDvI1ds!YOURdhq%pS3JNYT1yxUgDdz?d^xul9tDad*bw0c%IfQ%3<#*zf z=(w{OHGjssV1Es!Ir0bX@4xJ%2;z5hnKMN%NTp+=qbZ zYY3Aoxv@5KlBC!Sarj^p9hB`TiwQBzrOjSh*}jsYXi+0kkXl5Q6^z-QnZt#+W1^hC zfk-q8SX9je*6Y3u#|2}!Z^6v=d^b1m=7n*)4ygBvEqHs^or&yL{uTVUD1!|CRNkjn20?o>&PD|86R z5ndorxx={R>59%N9L7CHt-q7QxDCMG=rC@( z_zkj3XI=@Eayo zA1cc+@TzkF*?`StS-w&j;L*Vq3W^F2u-pi;7Uf%>wMT=7T4zqHt#8LsT6KLpuEd_l zO&vGYi@?p&z|GRz7|4|j>flArH4gURD2aKbi;YHelm;%eXcR|LbO1Ug znaUh0aym*JrIZ?B9C0F6wiO&JJxY$!$m*;38h$!KzME|@vk$|VR(sX?ND_s4ba3=F7INZp4^>UG%kUU}u;XzafEa9$< z2;JT7U@ZhGS!Mdp?n~;L%m@U`|8NKLz3$Iv@7MbqHbLTAuzV2YgOf@4X~I)1e${H` z!J|f{_T@aoSqXAv4b~eQnQ6y;5*6h5UT}88t2mAIc5ENEo1ON~+Bef)RLcb{z1b;c zKI7gD>PWpFsMit6?IQe%?pY@! z6y^61ta=|k&g;SxaHMg&IGqHoM}r~c=&RHt7MnUO5g6*1&P!J0Eq}okoLDLi_Hc|} zYmltVZR%`SNQsw<%h2E@wPtAWj8sr_5DkbUMqs*!MF}D^oi>0qX7Ic zDqKHyp$K;H)H}HWjVnU)MoQAh*s0GfPga{uO%NJ};))K`C{HKZW)MLP&ywT>S8N$VqOhOV9nz=HEbvxw1y+7T zKKYDQwn;W_;nsJ$qoRO&i^}ezFT#p{XQ$L%5$Urj4!zx>7{-~e^f_N=f^e+*KuC-K zi>n=2kY%6b-rzj{ZXbj2Lr86e7|S0W@LGkx=;=Yro&Q+VjeKSQtbapp2cOx2iaYvR zL)r-ke%nd;ZIaWr;x>D`sV?MT4JzM?C5MMp{l>A92tTa8(q}zJvPp+Oy&_|NRGhf7 zxIC6a+fV}bm~`J0JxWIbM1BL@1fXWWQvhS9?ZX2=aa(scAb;1hJgE_%9Q3ZV?}~Za zhfH{i3#JqMO*Xy>aPYDT1V=Z44@DC~GoIE&wrorjKw{Yhf<+UY5>pdmrraR(MH9NC z31Nd(pn6~+P)&uR8ViRNVf|RA45fn6l!}M|Ck$a>D#Bc4iR;Q=ND4a+vJ-LB+<}Wv z0Ktb3km^b+ztYYo)ONxNNYmnkG${|68%g}hD_hZCUD~byR3A$XB1Ls zpK;yrhfpQ81Lgt!vro8~&K0ig@Ieho4+ruHUVO35fPN1M;T#u=;-%&heJ&x@p}mrDhr_JpO+pPUlq_r%JT1)}mX`*72q@1Cc(dshDG zw9j;K4J;J=fh4XZN7ZW5@yv5<+eVmYuy>i~#W2rc?{drpaPZC( z)F+8Y(jCb>56;@l zI)*Vk!eH~%b3HvL!(gL}fmM}`T^!gv`D7;cf({Z0F^44<`_k%uF#~c`nYg$Gqkg^3 ze44X*Nyc-4C-KFu6*FD2YZZIzO}DHNk&w*rvx#e5pjq1k>eR;O)xN%ydp}S)uA}$3 zm_iu|c=CrzWBu|n=hR=Ki5ywMM+XHRK>*Y@Mzn0`(L8%6q9+CugFRnBQ#O6sWgze6 z6nksOa{lqdRl~?#i*Y_Xw5Kug=tlYBBcguf&955mSkBoVPlK&%*qX{_aJj@TKyU&)^dk z<0GtZ^Xwmbj-SCum?`c45}M`qLsl5~*$W4U)mMfBzI}q@eO23BHUWwF$&u9BfF$}j zFcmu;LFzP2GI!^s8Iqn-4PsSl3~TkByULDS?|#}MEJo(ozGTHZ~vfWGMrZ>%bFk$-)N!0YG`a)l3r zI9Vlga0H>$?_KGYORovO_euu_4^YGhWw-Yt`|>)Vdlp^%B2*9QY%I4nQP+L0q;_#c z-DR)FJ#2Si`c2gJ6ha&Br&w6lqmt7`CHn_Zcjwt(IVw?hQE|mq#*KQd{d)8@2ww>& z`zg4LuWq66l}gy;q4+BYP~LLl8Q4HZPh0KiJmYRSYZRsJE{5*wgYd#MpIc)hf{bMl z+j?E-R4#JbI(w^kJ8{ZunrJArp!lOD-oCe=ox{1K1YLQ$8aa zrES-V=Q+)xIM0NZdyqv zXU}nSj>6k>Ke-f}O%<`QQZ*N=a)IEOqq zE6eI|VAZJ*0j|#_t&PFrLy7HXU*hX*01_2fADxqvF&wQk?JuBAD$j5oUo7ArXXw>~ zQEHJ!C-e$5KyaAE@iLem5D2C_fLyWwSR73{&1JmaMXqtxKAVO3Q)G?Ikr`k-RrZLK zKo3=dNqU-=-ia5tZY2&~{KPt4@(PK576S!yG`Pc#KK?kwjW=}9u6{)|gUiFDVV%kV zqOGVB+qjZtB*^%FsQVz&FtR}**b98*x6HjJ&leB;7l?I!K=udmHOI-OKne>B3IR(H zY&h=dV90SN0r=@)LV`HH7NebmC(woTj`r5d;0bC+$Xo`gl3ur9>fs|%xAZK?^Vhkh zsWL*@gWlN?EP-mg|H0bPoKpLSCE0mrq_bCV%(ENSlJGaoQRJ6ayM_*QG{a^S*A3QN za$T8k`Ygpzk~})k7lyVT(>_|F=5;$rE^D=6Q({-h1E%JS>nPd>XxW1_*m;;P*;Xt} z0eW1RFkBhA09xF87&~JRSZ)h(`8{z1QmO~!W}z)&J!U;})P|AjxCB1NVP$^FLoO4i zKLxw#!B2MHd)riQ!XIVDsLgxs%K9=4sM~jG45gqb0#P?RC*;?0RPnJ8b5(9n2Up zcYqDMV?y(ch$hjPX=Y_9enmm)VZj{SOe;w!z$$n^;t?~ZX7@@FHn*bYeE4=t{<_8= z7;!Ky93J!B<=x$o^YKy3<2i1-WAsw&1 zzMSX#gc$`F{4oN;Lgde3wqJtXHe~}eMNw@;f{UNMl_LSCDe;oN8R=d{`TGxOQaeK^ zXGp7jP?7E_Q6?KK!WpTy5a!R2g3>qy7+UJ>BQ-8)aCtuqceww3_$E7as=Zllk*{v>V*`9&PYVAWns8+YM2MlNe1+X-xb z&tT7mFb7%Kj9`L$!!f@s;miOiZ`J-Xr>P8X1E`%3=PhrczLq1qq0+qS^#J(Ifsvwm z`Y_xGR-ax((w=;s;OCFR3pC0OI<1p+6PlAAsT*3A_(cJV?62xA=N z5fS7oi3SfsZ^OWlqP}w{++C@`%U{Ar8A6KY?yfSFRnf+>@ zE^aetHgtu0rXT-qI`ZO~AS(O(;DWY%_{%l}v6*2Pl46XwG+bj1y|TS->pwP5Y?N%I z5k2mFO%RtbC_gAhxPzE2x6vCq4Lqr{x^xcXbpAj7gsk&EN~<}W zGZ7^I9iMq=I*vrteYE)cKi@M_<@t&afn{^%7k&INIwxL!UKwYTOuAQ|e{3` zf{`!0^XFCoaYocgP*7f;at3IXnbl1)uHrBdZHvP~aihX&aB*Oat}u74o80!k$Bu#& z?|}sJ`z1=)BrVc7DZez-?~Er%b&x)C*yd{ou3b!!L6%;TgHdNE^7VY6zh&URZfEpd zh17nybp0qJB%LVoSd){j&G(f)3u%H}D~i^RE4rtp-t%k8g9*~rG7{*&sMI(~CY@61 ze0b$h*R5_##+HZ}+ELJfj4DTVIK?!LN-3l$@LUiY^U9H1aWdO! zuPJ!~P_yxM)u@B(qlR8*b@s&4&O*qhj@**ep;kIb_U)_AnMZo$icz>5)%hTOr1QtF z=dT!56>@>YhdLOIwXWK{X%J4m!=kU!gZ?809oBsHO@~znYI8^RSi^`eP!>HBEXjVP zydy^8xS@-Sek3g`F#8ddYfN!O`~M%f{J*s}a2cWu56~AnNqiN!+Cm^IPS#1~;gcpb znYb>9h{{nM0EG9ZsU$Nw77|Irsw$4Go3z*$Cu%QA#3 zA}W_(UYtIp-~a*GX~9T*J%~KW6f>r69DTgJEtatpE)f3544K=1r*1y*zzMpUF;jMP z3Xbk(Rta`9y2PwyPAQHz@2U3;_EcNjIxk74PxVQvfzQ`YMZb5j^JOz0KQwz3pK9ds zT}ZFzqj!KdcIt+5A5cmT_fU^ylaLr=JL4G1cSTsDzbfv@E7L+Zq{WB9xOC@-)L+hT zqo%4kc-r`f<#>po&h8Tr!2$D$xN&&tnMnT!o_!TMi_2z!mLiDE_KLedNJ@X_gRl8b zME%6zo#w<>#LwRErHTV*hm(Qa~GrS25PYYjz#`~$_al{*PcZb z!M)500Duyp9m;KboRMOk?mB9W5`Zto;vD{6EZgDV#o12EZ?-C%xU6QP(ZWspQTqjK z;4%B!1*qL)9{xDbwC?g1pUz52RPfNCx7B@jKqQb~NIM~2Hwp?3a=N;J+0$YtzbS!@ zEb7UF9?bI6gq63PtaVq$p#vakWktcwm9x(|r*g#=P<1;r#X}2j7n1qL0r(TYEAy{t zI|J=$O{PV9LP?q5gYOv}{TXwnKS zyLD`S4t>t*p#Pb`L%7MwaM8NHivC!bx%bu3Ry{?VK}g98&kN92R9T zQbE!3x@^#ZPsjBnHeLa24b{(dXabK@{b=;rR2m!-0FTsm&jNNK_HvP9Z5|^)@xSFG zbFak=1M+2`Bp?28|E!J`;La6p%PH=*W{XAstXL}S5O}t}sVH7qNiXRAAU1a2O1E5M z0(F`M$^dyx#VdCjLNxDjHTY8s2UI2#%*fwEgIgAbZUn0>wrebV_@A+1&GRK95Dgwv z+#`V}drW0JEB4R!eh%#5GEKI!?EnW;7%L!jTj^I1KJ*!GO_*9Hut6M={KuL@u4xY@ zx6yjG=>Y;3IxczOrW`(jHxxmsY%!aOnR?V#R5GX&U2iW^37m|qRNm<(`c`gZz{J^?ZUpb zv6f=kjvuj>mC7blLoG2Rq*KwGUTHMQV~SdSPf$Lmx_&d(u@j?-b)C$i$=Y)irbew3 zkj-YN4Z~(LMs?Kxb!o%GMFW60edp*OA?7b_$HBvQdVf?& zTHt1@*g0v9b7=4a$K)(oGJ`J610`?Rs-*$(EG$3A4IOa1jk69K;|gKBH!Ry7|R)extPKQ92GN6kQrIuxJOJL9L*5>TX)JnIkk@BQQ`fSF9fPq-UN`FiIT zxU?upL8h4+*KemZASWW|k~f(Am?&u1@aE%(4CI27pD>>fuj5|qm_8M#*id2mj6D~% z;S^t2iYtWy{HR@(5veUnUes3?<}3!(Qfy@n`#EMLdx&!Nn8uo!4vLy(Rc0i+UnZy- zE^28}FidaJsAl|5g-rk~HU#sOs!@=t;M}!PlA`|%B`K{hf{4zjM@fmQG?b(f6+r~< zP>rG_rFAGsB|lz;YE<2+`p?j(f`lSCBh;g${HC?)x@jbcz{gYr#B>?uyP5(V5c4i z2p76j(5k`Xoea6T9qGoTYyu$7_4S=jqzCQti-YF8nLgR(w;Ou22Rff()d;9mSO(!G zR^=|N(x4G;=I>_mzl^IqS|~F)5RTpQNhaThlfjiwfCe@<(*`pF9AIpM22g~$0xqr(C@3uMunzzPMfeIRlF$Nr8A6_mO@gbwkIh5vtsyGSLO=~e zUBIzFL|bT&*)^xAj_?yNg4wsHs6abCMO)i|DeIh0rH@CYLSF`pj!ZkeVY$b-52Dv+ ztseuP-%_6TMIqdh$#%lF98bR9L*YjL7TmM{ZMktuDM0{l>t&$+AEDhg*e?!#XwUY# zZSMHul{{W3qK-ZS9Cf@9QI8<;;ksw&7V%?LK6DGHAV~MFEOO%6Ck+05rO)}02~3)= z`{I?6OUn4-mBfd0p^g{A9wREMHJCt&ocbQZrw7iy*8_6hl}+*=Q~Hzv&dPb)fy@!l zGkNu08{#)LR^Rnl1gUU;x?$TFbukl=k|F7QHg`??GU_MMwIAq zA%Z5ALF~pP7t?UgosrA3gy?F!ylECpxRPG9zY==g0t$3yq85%3G9djY~^hmYJRuAs)T zK0~IH8OjL2DTCPOIMa1W_Zw^i5tQ2R*zDWohHk#9s-_GAh&rFoFq9g%|uQZh=j@ zl0;!2MDOK;3s@7r+U|Qt|Ey^o<{9dr^=Wv4kaoJVX3xie(vo?c`_@jv@Mhv zSDe^G1>Pvcx&&h&BkNYG=4rk-GtZ}m=0vvKZ>a4@-)amKEF_r_#THvgB#$Tg%+4Z6 z|1ZIfaw|dJH@>4Z~EPe58jqSuW5UfN?se&gC)VM^pNa}F0)Hm^|1Qg zjy=~Ks=}iVA*;kAI)Yp-iy@JC5ho5che}+#8qAaBIE>HRuJioUJ;UTcGx87D)vgi^JJ0D&)&*a%J({O>wVMFN8{lu?X@V=gsz{2 zDI;324m@|x40i;q0{E(7XM+}zFU?j53)v_5dhEF@iQVtx1@yB9^TmjUytMX-+ITqG zQYGl~H3Ect%4|C*j~-|9|BXlX#vV#qhG083bqR80g3zP2^(LRk<_hDc(S;LNpy}>O zXm~*fdT(0$^*8u)ybGsGD0u4E-yk%+=-%YQi8VpEa9Vi7M99vwIe~*i!~y%URL7?7 zmgC?cad5LZ*k2r6w;3Gd`9IoC@7JM17RM3e3{IDrer!4=bcp>2;qCo1q0A6>V8IZs zDIjusLSn>x4m4kD%uVU86XP%I>%01mvQaoz`1!~Vkf2|tA(k^Yy(XpYG{DDJm`1jv zWg*B^)*M5MoovUDIfCu1v2;%PsDNHpd7sAu!=_$dh<=Ih?0cVVcUStv-T$KT*);~U zDh=rUJY3!;h2CqbkqqU^>I-#9-uZAZb1*4!Y)Z0OKZSH>z}j|R^(R33?(Z@5${^hP zxU)#wR;%|*hY zUmg&d@ppVHga8sKOauhv0u|uV)G|o+@Zf5z1(#~dVA0ZOGp!c_U+Ht;HsJ~c7P-VF z6^4&lEAV$wS3-OAMf8fGRciYsT`sM*UBC_&QadTJY;KB6?QCc+3G2;GtxowHP6ZiqCm~9$+CnejadAPQz-{Jiw&y;21p*ye9ZQ&=>Q-<;~^+gn{}zuzZ4PV0>%; z2UEh#syo0A+@po+rNgC!suzzb!55K#5tTi} zQUn=^Nj*ctY={2YAB8K%g?qWWl`_bv<6uTCF3;kLG<`OU_<@0Q3WdQoMn1u3_^4~^ zQ2Albj88Y7OZdEVJ`Cei=#C?+Xp4H^R)KjGn-&1bUIr_qSlz`d*2O+Xb{m-8Luc~R zo*s07d#a$!9}S7##W^7?xHqVFM59iWONA^TtNC#f3k*~fd`tEZ92}x8q5Gt5m`h-I zPp;hcP@G3`C7os6HrurcA*0hrisA}IK8jFR-rQoze;UM&Bwz+AI?1b2g8lGERax(q zc7wXRwI8vXnLb|03IznQKgMvKWqGR%#!eWo-&kHKLw^hQ^s4Yrl@G=n; zRz-F!>cSVJ5!5l&kERLjVN!Ots81t;++U;(!%zt9kcBsOmmZ!8vh)@jUNo~@mAjf3 z8ei$NOM5X>_tzYS_&F7tM7gJ>qzRzR^LN_OkHsM7EEzMX`jp(|Z#Jd)oh~dZf1z-U zQQ~itfT}VGq+N81Wu&M%D_J*}zbw@q#IhPP@~2|x%b+?t88wnkb!18)>A_D4)}iW` zG;~E;+J;nL6FQa9*CaJk(k6|lS{n2<_;*FQ2aSZ!*ZhV=9=_6N;sg*E)RJmZHd)jK zsq+yMUzQ#9mSuCkBl{eUX!#U+K3Ia}>B>JCp+~(XFLDq81(ZRVB?F;gmJnlNmS2-D z=Xqo>odtPFsB3l;*X07&12Ouzaoo}8**8utQ;Y}TP-26w8`{rn2e)XdX{gmsGHOpl zT`;e7I&zOVZK}ms#?_Vu7xn6Mk7zQ*hq|#0TJ-|ckdXb&NK-j=F?}i914v5GpgFPQTR~BLzca-KuurlbkA?eI7YqKQQpGZJ$H`LfeuTpBUL96k8+}=0)&x1vl8% z*@MHl5kd5FO3d$GGKm@L4Smb5ll;1p!Yt? zqASE#JR~p>xVGB~1@C_6o$8qqA)!9V=K=YSRRs+vNJfH*#M7125|YFQpi>N5Pu4YX z?FU&83=ANa&B<}I-8yyZ;z z<}qk6NBF9IHV{;D;S?p82FYELZOO1J{rwbI{xT9)7{4uzYNsq_ED!|WYhVimv6wKb zb|XFrIUCSbub7xRgEuE=l-C;^S8=tgO4dY=J)&&IjZjMQYSKt16uT_6MDW5+R1hh!3b+~VzeArn5%Kt(8v6L z1f^$hWdwl`Bn;XT(#5xe43RM^y>Fv9yYiQkD&vET)!+n+KMc)D8|x$GV=$Z3f{@9S1`|9D)>6F(!;R`72^Y{c%(`iVq}U}1_CM-aOgS^s4rkrc$vTJ*C+ogM z>LR$$0Zh-)h@tzPQxlsx>qu|FKal~#5yLo~mxd-gGivChU0 zrs#N}q5I>xpmLrZ^0fm4VDQy)KQ>5k0TN_&6{Tb^jD1ox{qP$eTF##nV$@mmkW+NM!R~-bGdss=q^J_M!c2_Lnw@vQT`$c$fk1J>=3CiPfBRvvCyRde*vovGybBOq)INjU~ zDv%WvF`fhhD&Qj~AwK|8LVlG*hT{~19Jql>b{6NYOEMCsQyYhLLqNAk>{+*Xyn{Bz z5-I9TySn0szmz$?H57Pz`atM zbT*@kus3w~CK$tm9%AL&}(fQ8>VJ2MRC|+{e zAId)VbL=C2IVWD>8X^A=2ikARmfv6jw>v-Qp9Bxto8t)3_l$o}>?*q(ogHW~e&_Gru{OJBL{IFKon4qLErC4#w(Wr_5iOd`}L)1HajKwxCHu8B2@Dk@K@@(Hyp* zQ9CSVeBLTw{sU*4n~ZLO??V0N$@rabqB6Wc@l%r)tqH-?BrA{0H^SJCJ!o2qLkC6= z2R-Q8CujyPcj(WW5QppA{7cAQ*=z81m}e+pYCo7(%d7w1wj|gqsezrDkSd3ib{<{Q zQmNjP52NcK{Z0bVX6q1cm?nJeXbWR;Rce<$#|evL{fY$Cn8P{ifCKX7%wS^5plK<^k~*t-FPVI>`NTpzGf5St|%5H1R22eDH`wwTT9d*fw+5 z6YY>Dkk*$2l))~XI{@rqN;Ei*xOSl~k1RRs3O2SwH*du1*PjFB6}4r&33Uj@L6#<$ZYw0 z&)ei*q|@?mIT3pK2$Jn4$>UxTI>jiKhG3B8JWb(uHeD*jE(<^%A#e$zk1_by7zv#l zbH=CPIMBD0c`C8K_|iZ7BfK!;qBU#8`@^9f{x}OP^rbGtuf#}?p%`7VC=Xz0NP<`TR~`=$I&*E{$0 z(9-KcbQap{d#z@M!c2Z2Ln7PLMrsV4{h`=+feCk2Y{`yvlpO8|Os+#(#ZD}V!nGIc z4vFZ&?w6-AgLNc`)VWvwh}{b3uN*$V4Hp+o8(~Pr^|DOhA&rEVBy=r%0&a1&3v5W$ zNWvxH+LtP&lbyX2gd$=EA@7lp6n1uir9x~}izQ8sLZZ8nFBm!IQUpb&q$GKlMr_o9 z3D1X_3CtN9rXB5FbPhzJ{~_Me=}u5YnzTBnbeeqfz_ryKzzVJPF%0ARHp5)~3Zz!I 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 E358lJiGkLD}?t6j%n}@>x6Ff?DII6pVz!@+vI0 z!ggyA@fsdzMjqs;2KlPs7ZzO+wQ>;*4a(Cby7|JTF;gMhGXjz-4~|OqX9N1B8H`bt zNFM$HxhILJxIedpR(gm-eYP}Q_RQ(tINM#@+J{!TjE`?N|MFXLxwy$G@qSpP*N4HL zD}d(XB*2JCmAcdo+5m@R%;=-eL!7(15Ol`?r1`d?bLC2=_08I+iGyUS7DjdT=vl<_ z8}y2Tc!<~mLE+JS{PE27)R5n&Sj)Kv1)0m+!xBlYMkt&^wGkZ}y*sDnB6=`1B`CnY z%vl$RVWE%i=C6E3GGQ9oJ{GLl+JV}{zy>JU&y`>yNd(qDMBLxCi(+p zK4URFbYgIU3*H)i6WYQMe?nE|)TGi1wUWjfcf|O(XKD8FGu_cf1ZWwa>achT1~fN^ zCPeYE>>$Bvt#w7YGK*`q)sVz@KDTP}q2->9vcs_FqaJMgN~g9CFSu^TJgdD;@a{G` z9{h9^`$}K3U88`#Vun7Rt&c)o{+wdly^MY=+ZHI`GHUN^UqRj{985Pb)#q3Fkw>5o zW&x@w^-iaKa2>ya^H2-9T-5OuMnEci;TXU+l@C(A(y{zX$c*+yKV_yx$W6aVadFnP zIbK0g<2-MLZ>$M%3aX}wtK%)CNAf>|sRmflWI3aT%-r7B$=1%cG%nL1G$i_)|=i=2~Gc?iX#tY*X6N@d}5%R0X<^FT-_zD zD0>eyj{PtQ+KI1Sbg9(Nwnnh_t~jEPGWgz(j^;1X(r(nMX-$Lb`7~mE$tXHc=7q9{ zP8tNs+?JtP*yXT^{KyFI6WYs$D8UgB@DoJ`jSXgob-DDGmNTJi)*(DuK7<%RkAGhz zy+%D1az6uYvNYzn3?;a(@gy|N2Ox<@jAKc00+MAkccvds?e(Zde;2`9gNT0b1&$AP zgPxmYpRsm($ZB?tHO0e_Teh>nHTN>zK?6iVEB@JuVBs`d;Kw3d&bQsMOmlH)@ZEb)Fy?$e#>~^$lYc()A3Ne4E;8P(% z6P^!_Mkeihvn8?N0Cr+PLBvc_JD8J^AQOg!SX~3I{4|yBCQXzcx(`95&pz9KV`UoM z3U+*q&R`30$i~xxWNi+e3KUr?Ujx|f%YQ%zGGS%A*iM3^LEVfpC_tcI z*^q?cD}BzN0BDlZ8@~_QO+f&+z`rklK<&Zvy)-OieB_?RcENAR6~f0+o6$FmT23RG z)2>VV(__6$HD}t?(?YW_OV^St#u#HZ0Lp`O>Kd?$F;4~An{3!C7^EC4a=~+HK<%ut z2C~bqqJBL{et@VARu?PMbW}m?PlkJy&TGlDvyD7jj^=y>Y~mqy&R7To>kul2*|O+H zG&M*84ae!A!?d*@_+xq7DqOL659xqP>E-1N0wE9-5=$rS<_j*7>}~=1Q@zXRur*@! z-F%Hka!(`erRgNSU1FUHCfHq9elN7;c7u{m>IPK^)ows*33ksUTfP_p>hwf)3VKj3 z{K<`LZ}*A*YI|ZWH>Cg@$0>nmPnH@lbkb9Ay)WxYPaOM>9q$wwbtgRs1C#_>X;q&8 zMcg5M&A>6O*I?}xLpAM+OtMeIKd3ukYJQ!hFlCe-HyDdq`gFa}!)``3%cd*v%RyWb2(p$*%&T+vh+ z5q+M}g=rwFJwMXNF=YIvY(ezN9dlZ$IRPv!s^i$8rK$X}3;|^~+%mKsh$to%qOLd9 zPK>yiVAF)cwzW)7Ml}o*QU_D|Oe~*Br3Ay{Xr|s{nT>nGi(W7p}m;48ne zp9b8~-Mue4+)PahzQ?+?{!rH!Rxq_7LKKB2N4{ZEx|P5+Z2&7xYhurjkq!||yN%hc zIC>873_M7(bpF%`GAN6q=j)58yz7mT<^>`sTfiY}VLnRjY=9uxr^Z_E3zsy&d?&q74tq zpyIyR2xJUV={+!hn679@1Lw_!xexERXGE;P?fW7 zcxKB|jgpp@%1e7rxzPUEB?Qr_?R4OQj_7^u%KhX92N;?*w$^|g7E+lXQOYYH21Eg>UW|A>;Bt*(%CgBVgvLdo2jKxfZ+b82%Ko{dGxl&! zXkT90Gj{)f39rfy2F)l;Tx{p_IF_zx>} z#GqqAi{ppOlwA-pqGGKbl4@v_p31W0$$Go={mWPsxd!pRLU@8OKFTD>{OeLkneQPY zaM8np;o;~QnwmczV*MNSmi`k~Pc(U+4EkcohVXpwl3&QyAdA1SSKi99H;B(IeIVx1 zu-8j}yMzFOtWVDvg?WvWG2|ImBGZC@e6x$_4~BTU1RjuovttfZ!nc7n`=I#Dza~s< zff6*E=j$yBC>}JlwV`K%pS*#ApzPjE4al})oj!Ecg@Dj|Az;ll$6?V;nS3DD$wNJf z39hyw*OMaup7p3d^dL0IVt(&+KnFNq-f zXxIuy#8RB;+F);t1SPZC##!oQd&v}ZI%#PHn?tlD#0kz2G1y`x4bY$IinZ%)**jzU zm;+IbpIgfR2n`Qdii8^%0~RdlsjmK|@+p7`9?giN3q$gWX$~bNYpTyP|A`c><+YpF zlpLgBFi+x|-8^xJ*@yGK6J1Ff5{B{pVP@uHtMp2rEhH6M;$EKPnEh&&?Zjz{y!E%= zC)?#^y%gfTN|vAMty;Dd_r9q2vQ+P71%P}`g_hcedg5Aw{0kysrH_B5L-H>fdMer+ zR#UI@_d}A){&Z)4mqX%}SR5o9qwEm*@4M~j*L3yYo&2oTD5#E-M|$Q4!oVwZMImj{ zb!_ObY{|35D!8)+MC2=i`cH#m7U8)>F5?i`cqWXm#4%Q)ERn(#qd`%Anv)OZ(EpT3 z45i}>RnJmUV}hU#I85W#yA9tE^34D+Opf98b>Kn50t2*~e8kHyjy`AX1m60M{A||6 zr6dWqaoAbuS{cwbi5lcoWmu6ngnRS2!y(z`;1k~s>*C1HT+lcFQ$Hoa;im!&Cty7) zdoQcH&WE6fI5kU<_zz#WAIh(H>}u4*;Nqrt>>Q~ev=ixdbLN+x^%tVam!DU~30RZv z!pdpY_K_=s+Y$fOkNn08?kS?if`an8lruoHK&UwTp;mcG;XWvx?d%};3{WN{9pSlIfv|fseozMK{Yr7>skeKlA51RMb?1DJGrMCx-bEiiMq!9GR9ubGz%L z`yEri7>GS5Uw)QBQ5{L4jdoctMH7rV-SVem^pEkgIcBD6FeW0Hkxhz2L#fV3{rxGL zY~MM;@PwZg)h(y_>Fu576Z?b zBz>jNb-ai#e6O*n#In*o%eCRR>2MxnP>#4L{z#_CJU^QgQB`$M5JSB)ec zL{RLzjVN;&1Oa<2W6om5=J}jQ}^W%V5~P^LDi*Iv!yCdzq49deE>>hJxt?~ znnl#den6SkAmY^WFXlG|!?>g<|$kG-cC@`-7~2R1&li(IW@lPFcs}X*yE_s)gONa8Edt+fe_IQ|29i*0U^js-s zMc80IF7<_AfdqR2K?myAA)!@+1xrAfkC@QEpdWZEg=BxD;a<~jAs1KOZ9`bIq^{lV zd3+_$9T`&>CiOx1utR;I_F%lTwwfQeJRHM?cF$m;KG?)DTv8u|BB7y@vF2$m)fK8p zgGuqAA^~hd5~X~@t@591LuDvJzyi8Eog8IbFB#>G+N6*dUxVBq7F94#Cci6Ui!#|X ziJ&;j9Aa)s)gTeF0Fa!O;*bZvDdH=GUp^bjH^}{nfO-Z+|9gE@>v4iMCAAYGxAtoO z>^*^!_i+_+bSOJakVki#zy-+((X*6#wC(|JS?V_Fxc;_zPzJ5_YP(yMC-G*&?L9Aa zg~S3j!52$%SRA`~iSU92vYVNjakHjYrrA!kg;7?UaI>F5l)?N_e;b{z6Uing%FL;- zI%gq#akC}FP^bvPn&*S=G8i6{bT~w%%#Am$0cg5z|Gu@b6u_FS3{eSGe z3!Gn7b@zQHmzjUKgj)g(pn@QR!AE4qTk+BQcsFUamiB#~LR%CZLSw5{ycAF(aqfWRq1532(J`x z_Pj>fjsnj^#k@7Xso9j8q)fRE)3m55cTG#lv;glYey?0Nm`)iA|NhhUW*Wn^||Gvecq)#t<*!eklM$EZX- z<&2j0E9#Ofk?n@nEAT>)1S`mqIm0p)Y^QUA4ZKa+_ABi8kctR)ZW@I#V;5br6cJ%i zp^mX4EikdMxPYyDR_Nc>JMzhFtctkbnqli2ow7S;{(D{levhuVG7Kh;Re_rL08HEU z^kPp{Zp18#a>ZLTna5bna;7i+{5=W6hVW)Y?CG;?2_Ap`uWp^nW!<;@hDe#l`O9SdV^i{BbX> zucvYTSc+_C4f`L6{d;c2|emS(!MH?AYC7#CQP{Cl?7*RfN6dd6Z+|&*9DVUQ`S5cN zAU?3`=0wLKp9pn;OR|LN`eLR5yyvWzSqm7fgf;YSRNeLavEorw0_Z zA0XnVe6DZyvsvq}2F&~E*0PXzsFycdBquXNF@)dGoPzP4>3!oe)PbF0=O#9M&oq=K z{b;q-ONKIyz9by^eA5Ji9d3XFUVtjWTMkZ&^to8;-mV$ z%=}F#$edrIKUcKvUl%E9aCDcTibPhF=&|@7r##>tff#lDhhW2b$HR+Hr&tjMy)-s= z+3;D+YJD`mWpysH_XokCMHRUK;U&@*mN2KIKKtQrwyx13@z2-pG5Pz2k&46BJ=fUIMp0BKWj1YlFJ9-=?$0gUOd#xgAo9Iyh5BcYIR814mdzC3F5}Oi^10HCUEivp+Fgr4^;(#O=WV|2V{ic0ooVe$gm0z*N8~_Nf6C{XyWS>G-YJ!$0&Ez)(FNX& zBhTKEMIYd|Pd`H{J+ULt_RW%z{Yf21o_Xd?`}DJzZGd#zk!OIi`}hC2l$<>B?AF$j zdg7zZWu(5*LH1gS^jbLIKjj2smY5^_qkONgK&@l~CfnG!keAP9V2&>uUj&10wtOBB3@e%3#8hz9$3V^m3nT0tRX*-R^QuRglW#lvpxWC z9rD31qUZg?Q^2MDif8Kf6UlkV2k{h+yn?g%CSNW2Lgt5e%6P*c{Gwp;g9*^Qd}f(w zhLKm?3S!Ock2t7r?=#4GgRS=d!;_PgSBB5+0strlG%okNg~@x?jcH5O6ldF-rs{-T zp$27&4_^78U~c3Mm^Si;mDq}DqG(#ue_H-x1pv0&JBYJ|PDL4odpWd@+U-^0<+KD% z0@Xs;QRvLCl>x6on)qNP1aRmmm7T_Q%ywYEc`9KQ3z*6ke+W=-!J@!xvfX?xpo6yCJIHnRbDjn)^KWZ1N$AqNr>pwljAn6Y2!g7dNhvtsKW>Vduye=2Lo+_`}yn-JlSQFX3==UXA2g)iIu?}!-MBROZ+80 zoaL5a8@G$HvyO$|BVl=6$@Lx9!?OEp%gDfa9}L5RJ}P(sp@j3Yz{z>QoS(v%P2AmTDr?2PAqN** zrn_)Mh`=2tg#Sr)!7KmUEaYLNm~-B;;qx07z$bDXLn)V^dG|X^aN;R7Q)PAiWh{=b zI0G)4|9SM(p&L;J*3E;UySJd%1M48-g5A5BAs1W*i}-iF`L0KkMAvM-Ys`{YA%&J5 zQfjO3d+muN*UvtRDfYD|g2kh+Xp(n2jA1|#@NHfn#k8>bD9GyHHq<29yd_`NfMjig ziDo%^!l(|{DQ%>v0IDF98bW!c+PDN&K}u4zk*J;?q}!++GqjO%q^{bCN+h_nUm<|% zO;IJ=nh#nfuSFXviqVudawHQg$|B{iKo)JSJjD0kU^xV=1uI}dhpaI|Dk<_V~GkSRPEOFtYL_#5JxWxCy9kNO*B@Lo3bobI}7 zNEUustnv?-V<%c;OB3oM2b7>LKA;%nhEjOuN2^zV>t#4}%=RRtg_*&E641Jr_Q-4v zYLEs|Gai&FGF}&?*wwaQllyoOlnhZHMsF606qnzlOoJ!a^_R44m9@LdTGb^`2*GpD zY@Lg+HJra@+c$4X9bm7LRMB{b1&{pF&6%#6LnDsRW|&s*Bht2dA6}K0eTx>}3+3wf zB5?kD$ujGbrJnN!yY)w!yy|xvQ55B*tSOu&-CRcpZ%>z>ueIWZ_RQ_l++eHvCb{;2 zM9!lPzzB9kSh>8hzM<#-XF`PjPwsAb6_rl67nQI{?M3K1uCeN0AJWVD=E1iV7f9Kw zxF~C*-I$!Jy<`D3vufW!w|^$MYOi6cAUaiErQ6+_PZYmFCpo$<@qW9Hz*NpF3f91u z8V=)&jz0zkjzDM2#vcd=^ErQVyu!2U>K4?UE(~eNsJT{C3C7%bU*>9HVMNp5*Q%~# z;MMKnP>dK1)J$b)b=15m(E5|P9g%6{o8(log8q=Cz)}kTUT2ti&GCL11FnEjTd3SreE&Y-}wCsY$nAI9RY6vV=VHcVB{IG}SMDtX`sJ0gggXGHb6{<cn67?6XPHZlcZ`gGB-Gg=@&uN)b;%Af^E@~f()i+~V1LaMB~|0~aPe-I~z8)hO( z8XYY|O;J+z+sR9f#K>F3IVQ6=f8v--GEQj;fw^q;MNDMBJJi1x2Ef!0ZBfLc7D3PZ zVBRBpmX;;xE;5{nJXn{Qxpg=fIKX=MWw@YyMg>hlyd{eqUnEUd2|uM2cVKlkW~H=C zQUg72si-Mv?X4K(aK$!0)A)8hXo`X2Px;)jRrSATrCDdCu{YOUdod;D>cV1S3*Lfr=uPlh)98NJcC%;>{CpV)rQ+ z#H(Q-Y2`-dc-a;@sm&u}wTA;?CzE%wax_0)qyBaKhRT0l#AoX?Cmkl2Y=@eMIAmD_ zQVIMDNXUGWk=!2#P=)To*d~Oe$k_z*=8?IHb6sd};gEPaZ1-?78*hU{l_t*v2OR`DKcu&9qd^Crh-I>R4=jz zep9`ef@M=Jc(T)N~~1bh|Av$Dj2J-hwRel z!L}v(m`#M}2=r2QIDJQ7@i$yNd?K2R+;U$t!K|Fi1k-Odzs^c&dqyt)oZ|OJdO*4P?kuQpW8_cVm=ldZZm!tL{@$#A zm%hCuYFmbPCW{R3IQxj!E1btsQUEb`h&5&<3uGe^i_z9N)5SUz?EgOuz>h!ztrtaf z<`i(WX=r`1ACHMoI(vr}Iu=Il%|(6vgO{{Ei5B*NT{_QRjA6TsLPi3`FXJLG7@SUm zq1O%d>9?4sw7CjV+pFtO88>!f#uycGxXLMT_zHP@mDWW9T4>;2N6wQO;ruORd?eSa z9e}USlEhqoB?H--0%WCuRa!SzV0P}fl(_Fip`_{VOApua!*(xp33h*#$Bf-yZVCh9 zjn{nHS%V+65{i@ue_ESA<;%3aVvSbvi*})PnALtkPCom&zR;DKJ4F8md4-f})T|z+ zW{>b-5{{U~S?39iyXDv;PMn5>xnpdjw6|C>$y?Ezx!cLFr}eJ+441}XE&3olC+qas zlf*KXKXt{>r>_tSWO-CJ;*BCGuY@N%R&7Yl#=LNH-*dYWp(C#LC#EKluI@QF?zUAp5DNoJ7OaKLSh+1tiJ=qgOH_rmkTxcb_*`qn`!9?W@sc zYPnDbTH#GOljAG=fu~o_ z;=$`za0`syh{q6B_)?l!JVFhTw>xV}SQr*kZWjGj&tElHaTSBH;WNDELmD2gbw3Lv{5)uT+m8&K0_du>b=M&)4J6ZvT29MxS>=X( zaK+r)I)_=pHL^*G08Jdi1(uZXrsQN zfR3%fPAp_?-Mi)xjue5;Z5F>{6}H7v?YY^&@g`iv3A;f~oji z9BM_L4jVY%C1DaAq@B`Jh{!pHzQWIaeu)Q;xJ?8wfLaNqGAZL-<_#3G?1gzCEL~u- z1w;MRpWfjOHp>y*s6(Q07##^dyZP6SBC;D@L0u9E*kZ zS-*}@j17k>xBT(*A3))J=GrlpthtjKRsOEW z{!OcyNxagdmaI3fT}d~gd?}x!gN>Pi&vpHf8ORC-%_|x$+7~(fo3@;SnparHl7dn| zbKAxoH;8tr@Zxw#FHy|K@(RdZT?G zRcJ(R#8!z9k%^1a)9l=8ZhUABo(09!aP$U8q4tg3r^5yHcXHZ<@!AfnQ*9SuJm!ws zU9)5RRaoKBQ4dkr8SR+_&Sb34bfefa!~2OZV*7tshvtMXk`oz0KosTz#{n{){^F5M z2F2W8SO28XB7c;`4vRlDoXyHNe$9NsM?ou`c{yJJ2rdp>xCw!OkJVt`I|TI{KOE*5kvp})&V6@N`&PO8=o9LxJ{dMl_hpl%id+0g-`IeJGoCNMhjB|D3h>+M&KrF*n;y;UVXKq7|cy$wSC> zlEOpP@0-hk^Pw_ufXCjILR?A(59LQN6;Ym&S_V!Ea=NI#1gi@V(FAbGLo|6DJOmIx zE3el8b-VyN7`xtUuV>%{sWvL{oPN$p*nu;e5u>i()Ixu+9qLaliIw+i!#3Vf~|9*pB*tB*z(~h_hV+>Y-0;V4Yte0R(Z#$nX|Zk z6ZZa6+pTErUCk^4*K)sJ-SylAoGfiYKm9P!6s^1L4i6IZ^xi$RWw{P4@y;V@?k-j0`cpoe#w#D}C4QQWDMDedEUBwU7}NS|GB3aeTkOrm z_pnB!6@o(;FMkr@qYB6}!|Bsl%{t3*R-`n0j~@wE`@N50L|L5)Z(V5eO(YA(e9Eo& z?O~M)c$NV>Zp->mU7!HrLu%s_A8anZW5`zc58)c8LE}wSP+_d6NTWoeJX@nqjvPBs zYGJ|)#Lw(q^B(AF?L@rrc^UgNL2obQb%Mq#a^NyBH>sAz(J``I2tdO6U00Bl`7Q+6 zA)GxBf~X2~y{%mCwuAax7B#5e;sx>MW%;+>65CnpH+^#zUP}?Mmp;Guu0pdW1Tb;Q z#j;{XqMRYfINDo1aR!0C{ILmq3nWN5~9s8Mo zOdt;kK@X%-=*kbmLFV8yWbi>&+fyo00aLZ5Vv+Dq*SsWW&)_2__oyuBPavhh1pWVQ zc=%HK)cWclq`Xt^EjG;&vazx7BSdr%p9_>`1z=az1-DcyLG>g{1Lx%StEB#Oz_U}O zPC>WuUN4nHN2TjB`pSbaMVB9`P60lbp3qg9HYpHvgK2PP>zZ_stD9H4gm^jPxcM3o zDakm4ee3>_<>Q_gQ)_E0Hv??2jeAB05d9P1*|vJX;73bn6z+w-76V<^L9kXTfU~5* zLp-WQ?6rzzMto+QKk}8deFSlRYW^NL3<~6HBvW7PV;R{G*kd^>?7nc}y=J60CFxwb zkk26N8(E=4k8p5&xIHHqmOt0IWdR+92>N?rPR+ZgL9;#`u@B2fd(du;D)|ZE+8Ys* zA31%f?-Ovj(t|8b{bi}JD~%S;q#l~Hm#i|crpR^lq$xH||AJxH)4&LQ`aO!%^vTe5f{e^U*ZmRd!RT7z=1E3agfhH;unjDlsB)XG=M#tLhf z^y02We}L((!VxjWXSll$l9IJ)vtXMcxT{RdI_bP%YLLEPuoE42UWhJ+-jnxS#zNNz zvR3jg3$Lo6nMHD2jy30RDsR6=VF3FaN5}(pw60$)-+mu(s;5THN0v2{Mn(q)h0_&< zWTzx2c;-sQy!E{?O<^Ec`@Jnc#l=YHyL+JEQ8!V38BGqUu%8 zu=yB)p25AvP_A$ z)Q!HiXj`r0)$1Qyq}^yX@iw{%AV?s2D+veU0FJCHI0te6l#y8h(wqlw+tG~ik%7N} zOY8#9aiEiqL7kn1brQXTE-HB^-W`NmQONGebsQDos6cuk@3xZ%?zXG(h9F_2916&=zXPjwFi zvN@h=gRdN4#qm_H2ZsXzpnW{mqT~`3h%$9N6-qFBNlzf-bM(Pgua5KSaH;y`OeP(^ zt9n!Knp5c6KJWUT;2;`kOne``KmwCi^gV;?0N~XQxWgHnJZSLMDxviBJ>1DhYVzxia(@vS@lx zHek)+x`*6MZxn!?n(#|7_JbswjVLR+FYT!(uBG~kbwkR7NI+~=(Wb#b znwJw}>&uGH;c92=t0``wS%L!K*9(R8tC}w7Jg*mJ>@^1Cb*!B_7D8+hib8n@ zPx=`U=-P6|)P*C<2@8z5Y!~w0((S33^A->ONg4lT|K}YxxiK>!Gf7}BR}PfG|Ld6` zYt}9>fZmO&OLc=lGv1Wycr)UuBKc-$n{!y|wM^F~ocgPXS(3Pa2|Bptu~>&ntZ6F6 ztdB^w2RN{nCCZKiz!1aC&M@Uir976V+vw)u;fDuX4-ej!;AE=PURzIG;#l~!R!h7J z@H75A-lRsZ>0`Sc08}q>98}Mfw;l@XNGX&-lJ*>ip`(>Av?}an)P1j^yJrHe+%Ar| zcX^3fN2b4*^%kSK5{Of`qtw8 z9_#&Dpp_i|3{2Ge<&2824}oPCTP(|wBC)7WIB8b4O614cSKr#$D@#+@9CEgdHW+|U%N8NC{I<|aBiDUROz`gptDPGLU-86G5W z6uxp7E4y=97ZNy3)fhfyO|U9&@|%ClJ}97zS8&YVwzz(xFBg1*sT-A{9WE=ukuUD^ z>(>Rr)oOZxB>SWEPql+R^*u_Klh0=7VVKv^tp*0gsHob>hDIH!<)St5E2qhbdDX8z zy_u@3=cnuQQ$Cj+PdW3Y%#}UBIpwrP8>7cisyK?G>ZX-yt`zo`fual(1!q#axXbh| z)6>u8&8~95BaKkYdHxu`!R3RCF9!iuXFV+Pv}{D|#1ULHZG3A~yR*IT3Z~T zmeXUgXJ7Rr>Th0e&Ec}-4 zlVd`YM4=q$8RZByiF6JZ6)=>zqJ-sePt~_%1=IE`jC7nxp}sY3O}<(Z08r!t+ILz@ zFIqf^^=}u);QsDyL!h=a6k7h)yxGpAo5LY||j36ihGZqRvFQXwAUt{V4 z>RU`n%&-c#yJYzvQok21k<@-Ag;Mp6z2+pp1XVFg8>PlX-fz{k1Ay;na$`q%R7(Ag z%otA0&dyZA2g!n3R8L$Ch7-ir%zqDKkr7>j4mOM^kcax-177|Z5(|8f4Gb{DpcU~s z)5Qza+p5#E)chYda1KEMSl~0gE_770{heXfci4w0;tx_teT1|#@&Q0@wz1zEVHH;$ z6sIN0-0 zarQd50C5W5ebIfk_xnPtA+OuR#h z9x2bXMWBsK45MwKToY7FA3X6eTguNC!pkMv|1x?vO<;CD58j{`svT6)T`|djyW0)- ziPSC-y1ol-6rlYnpF8emW|*-Gw}F)`B6zrGRDWuvM^ ze`#mFl1OBF8jIXH&<<64RRDX65qhSOs{VXzJrdAc?<6W{>)pzc8pNSU)a#HC z)!RVP`wBHcb$v7o4PrT9$X=$%ZzY`3zX|Zb7+k-@vHXsNDl}j1Z?ai{#@3I#3AFyy z-Xkl#-+ZM)V#GjjACJT3V{$>qKOOdhLUZ{bLqf?K2)t{O#>{18z~9@#|4P_ut|)== zza;nb;KmZ^=}-AWeEGC1N;uMxFU^CCOUTV&`4YhU*-9u#@z(yaX8-*4l0zw5Hp)qol|SCyRV}cC=fequ|b0*iiGimohM`GBNLpP_EnELx zd}G_LRS*a&_(Q?`M#RkQm|MoANz56c!F>G2ZcZRagmj*Q%Xz-2Pm@<$L>1%lI=^mp z0SAiL6;#a`z=|Y(8s*I6fKzE}1=gb^y?!!D?WV1~{oIG|!pn=cMqs8I_#2HbI2!`f zytFe_%(ZI^{6P)S8`SW~ibjX>Ig@O!x7CnsD{pTH*)HdjiLENy$`M=?*avkWM~f{# z1tAsRLfu0D5II1G$hwW6HJJNz(*0o>hlJcPlVv(>$dhbVET^*BB+K~b3UocwhP43! z%mo87HkP)j6}AbrX0nZBna*TnWN(Mmn#mg-)RLP@jwx=Mj(@vZwEr2MP1;tpZ8rJ$(zcTRZRWjV7FqSv$4ndkMp_s2X2`smJSL3Ia8)8t6U-g(uP{`oLuPj*H8WHj z{QnGN@85aeUJJ28m6?QXb)fU$W%bG`^~><^<$|(+s#xn~r74$mc?gZSn5-3C-ARD@>G|Irh?58GBpl zX|y|YzM3}Sa=^!BMN!boRp<8AU)J?0o8oem5xhZ%i(a8Z^ST|zd+%?Vx;|xa{KioF zmk#CTt)S#WnQcSaR!gHCJx>2PzU=89cy%mX^4cWsbH%ci2Oj75LgD~2PF3z_Xm_io zEeJZayDf`wRl{(T&~-7%axx3WAT_LN?>jG z*rJ}%D;u?~J=kUXGH#37`tu5WEKxV+y1f!_G0xhJBc89vZpIA;tl<1%HR~Q6_IASE z1IrVKLeerWk7r?bw`)8imQTj{H9S1{4m^9%=A9OM4d9UG6EYuoJPk&px-}H`P(3w+Of%7zXGC7?M0iGb857(t*ST&HR_2n;P)_GawUBS;5E3QBkZ%T7Mj+Qh{pOwLG#ePl9KK0N- zJ0gI9d)hQA$8D%ozz!A}ia+RRS2w%!PZ z{Ij^X@VWdEg)WpGFN73F9{JWBeMhD*XmG2)hO@1%{&6)}|D(5?CuD^O;IU~A+9rhM zkpMX#GI;|vrb-)dQ*ZHAm*fh=rI%RGGg7HKII`xc((l$jdj<^a?`VyzeoIdqUOrmu zbzo-S^e%X34Rr>xAHysB+zk)}deaD3L%*Bh90TQBEb*xkjEhG>iTYA0Q5_a@4q^#_ z!{7`(YPgT)%1<*GGa~sq*jkEQ!|+3@u>p3<=?abyOWi*TClJodglTL=xU*g_lkAfi zRKhuNAQ&Tf4XbLaGnv+WRwvk;|R)0&+__`{o$r5(b3j#wo+ z)h!dZ0^wnBI+o$9)u)Pzf_+67w=rXhWI6h)5-6L5|Cl0%COlIbU!CMx3ig< zy601O*~6}{kbenEDxe6O@L7}KvIExXS|pm3fbZB4S>#Y| zS0L<0g5%i~ST>>q&{KTHw+lV+34@2jp1`ICf9^dI?ENAV*doc~DXn^J=ZBuhP%U9k z!s{rVlRw5x?CK-YO&1WGDR?YX1|&}m5+6+_8VOV;a-svyLqyZJhn`!mW&9$I>w@+H z?q6iS2Zb9}UrYyo8A2ooHi3=M!uAfgyI^2?VfQ}WDQpZO@5C@I;xXle*(_^O=1}4^ z`IEtBz1a%(#q3$Mz3MO_tNtg%@q)yIg-w~}$c`rEKI-O8snHcK3|IBoYf-++8(d%Y zNPlQ_+^96_p<&NVhA}L=hqT{s*jPgMHqyd^(1u4kqxL2!yNI!v{-xu1<+Q~Q6WqYj z!Rc_2XkJl*HdbAy`<15W#cg`4uj${~yV+G(3k#KjyCtnNfa^g7(7%8|jkMtPrftV{ z5Qx#8K#*T^ z_E~4QPHZ5mZ{Mjz4@rx!N$k?4m%&ok;!!<+)OL{a2e=R&Aj9z+>-v;zi}1)*YN4uo z2L>qWYuWBJ`AS=X#Ri1}ShDrzjgrkHiM*kUTbNdi5IX#idi0;rM9sDVwdp4^QG4R% zqOwM3LZ4+Ohv-j8SLkO+dO3yucLen-p?v*$U+e!1x=U3FQ0!;u`J6B;JX>c zvMuqmkgrz;`n>)3`;qKhcHeSV79$p8sH(!&h zvQXJo(`-Vu%Hh{Ql+Q5mjL*xl7oQov{xz@3Hz%I&hHAGkRLkCLno4yfea`l2i9T*$ z@5t-z2-C8!%t8S&l~90Y245q}Gz`2b^gV>tP)p7EfXEVSX)1ic*PWaMHVD6Ses(TX z%MH;_Z?PF3K+gw0H_+c8<_93B6TMm;Ql_oNVo@!kjKTTV%o}^jrCLbUdoiT?9df}f zegPJBFC$iqS!WURc0CGzWD@}84l%ZzmVk~2uY72b${&HC4a*JL7Sim!7NabssTqMp zH%d&5L#0u;z^gC0WPX1^J3Jix6koH`Za?AQ#i4pY8AQ+XTs2iwnvL2SEwa2tasDmE z-Wjm?xeQ|A?GXS5slxGMW`^`e`_-ycyn*<<*63*}6t5V93Wo5(J!!g&(G)8jPKhTyFJCZ$SeD8Ax_fyn=UY0y?^_pMftSpE7527hv zQy8&XHPX*h`&T-xfi4|^j%8%)AJBewveOyKiZd4tR=qRO(J4&cWSmciqDYswgtuUX zQCdEMw$Qx@#Ae(M(7-$&TnS9=tJiRF1S%NqT`Up4prx>5t!_M#EnV}aOmYNeW#R2- zzizcw?+CaO#}a}?jLcM+k(Z+mf@GP9%Z6PhyJqs6oDAWu_7vHkGVgaZIT4R%B4!R) zo{6~3y23a)5uaZs;`UtKmm>!=5x1vv@Mp56RoaH&(`NogDVS6efMvU7Yn^@}STiLv z%qCXRYh@zt5b;`Qfu+N2l?8Ay+%^l~?#^k`r7^TZDz{h6B`=>-4W~MC^z~=fV&dqG z;pi}AWECJgWMis*OB^i&Z@$6?gAiT2Ba)-Yfh29$EMf~mLkPQ9e#k>Lkgl1g4caa6XSwlrRvp1)GHK6|MGJii zOdOon27S@AA*OZ#8Off4v$T;O;vo)D#vilNxxd^t{-fP}{f1><=aig*W6OO033$de z_>(24_Kwz&9Eq=~nP=i?Vm^@O{*=!;v3SLE#jeYf91?fK!(|cX0;=4^cV9;Pee&my z39LUv9mo55m@2=&vieJ4n1jqN=Aw9NvRZfcZr#GzIGlVK^Qb-^7dRc*;Bh5dGiatU z^Uqdh`Z=yFsP%i)uNY>2{IL3e*B`8`wh$Rv-zcp6skW9$0@FBeeTRLZkOm5ILj%AX zr_Sv{QWRyuVL8-CjoB2lAl%S>L$|#tC|jA9`NH?fUIvr&GA-zmVn$zN z`*RdcyE#F1``ATg$Q^W)j%LnImM*SIXJ_5l`!KisNfetCBZv{{0EU<8hbyZe#mbYX z*2&jeEBd}~)VfGT^0lgzcVoQ#cnY!x7av@FG_?oRsk@%z2{RuB;*sSSJIkn-K^b!G zyRriQyM~AJlvr0t!3|c~ypeJROkFL8doWl?Wiet6&$RYH=B+!yYxGLCgy#b38<5ptriEPb}_!~(AUhlk72zh>+|8s!^s*#xHEY*3|h2yif)SQXS* z%Gz!1=&>wh{L>V=*U5%Ish322-mb1!}2f^m}B@HyCPX zcCQ>f_0++Yt-+P6PQ75Tb!qpd-4_h5S~YkL`?mf4p2b2Utv%blia7>*DeBuXECij+oFLwKcjD^>~; zNN#qXInSdD75NQ4ntf`>#VATAv3oK(VCnW2wk9d3J4|cbF-0?_amJEUM0u3PH)ZnZ zXuLgnjDC_W54S*uDY@7oNsp9r$k?fgu91vRMpHAOk`_>zD9Z&_%F;;@TdHO({sis% z7v4pq$HL~LCxF*9GX1nZqCk49&`z??=p(o&y3x(W9!Z+n_)qJj8I8C5=vkwO zFeW$lIuYdnXuKx8W#C9@w)irH=>2BOd#n#71~1i0rcyZ({e;oge&uW2S%%(JbKt*~ zMe)dm)2{d#p0Rbq%g}+pHB3VnrU?fsnvLyYcPCjJV5vF#@M!~vVKpNovg5reI$={e zMl_{_ja(+@K*TuEa!gwG@e?&crRV}^L|)@p>uSfFdmPP?Qf+VZV`y6}w1p{w3EP{z zQM!2Bg0mgT z1UB|lfBO~e0KWvcxaGgb+)8pR^fe9R$pcRf_(bRceXTZB?FfPDaVBr9rzFQjYj{wN z)05;Z4CCO`7gt&;%z8rxG{YqF$Mk>Kni$P; zq{QaF!$6~`=nNSy8W4kTqkz@_92?(@viEh?)e`*8%C0QB3QxHRqxc0Fu9q%>b^kgR z>}?kgFTzT!z{(1a!b`m0q`qdDivL_cM7>$QeNa9F%rnbtRnD#TtFQET1#iWPwSbab z!6xq?3{P;ph9+3fGo{vP3sI*$K(m|qp^P2kf{;I>(;W}=Yd28%w?$)mVRbpOZM*P| ziG+EH{Q8FY$rnI*;S$W{1mXA!Zt_y;ov~Hfh_6o!%C?`LmJbVKN^~gvfJ*8+sMJ3t z^`E=Rx#Kj-jsq-}LK2{YGwWR6Rap-Q(?XiCGv|V6_aGTKcmwxx-a-80o@{};fvY;H z=|RRIKeyWS-Ig$*to%vP0f0E*95&P5VuIzcVl^rD>1MYmgyU7hwZG(Z{#29ZD`{^N z)V$WbVwzf^B0%&upftPx>5;9soRKy&gqp~#CWN{<6rdwadNR^<0~)%!eCU^Qioh>4 zfO@olLjiyGokMQ_ZzAJ=N+fnj5FkG-q-`KKuo{&(GgCse*;@to5QfLhRzGc8d%w~z zchezTTu)ehbvFoQ5t9+g*c6lYfKwn0HA9whiO@L`#r*1}i;wJHp}NK=b2Qm9a$(U~ zNB`QM(H+!tckgAWxLdkB_;avzLpQ2>S>wrb-wiMyjXgj*YcnEl

+?=x;qDa$Eiyfc~IL)7Gn}`2IGWl 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�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%0Aw3E*L}#Q*|~1w3Y8 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 literal 0 HcmV?d00001 diff --git a/key.ui.fx/src/main/resources/fonts/MaterialIcons-Regular.ttf b/key.ui.fx/src/main/resources/fonts/MaterialIcons-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..7015564ad166a3e9d88c82f17829f0cc01ebe29a GIT binary patch literal 128180 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 ohyj3!`#ee%L;DTjx@8!5k5~VH0QmdE^#A|> literal 0 HcmV?d00001 diff --git a/key.ui.fx/src/main/resources/fonts/entypo.ttf b/key.ui.fx/src/main/resources/fonts/entypo.ttf new file mode 100644 index 0000000000000000000000000000000000000000..331ea3a1bc84b9d20d2b9eaf3b8a08c32428f01e GIT binary patch literal 75800 zcmc${4S-bDc|Uy4z4QHf=X-W`zQ1>O_Jf(7nPqob23S`FToDnMl_2Vh1QC%CL`n=b z)DL2m5W6Lm8bXYrCb7g)YZFVUC4?AjjUh&9VyUriYW?%~Wz|?-_VWIob7x^OnkIew z{@-1mJ!kIB+&OJy!EHJauyK2;;Qyy_2~^?V2lUx zdc}>mUcYWkfo-m@MjC=PpW<9ZP{Y~qJp8H`tEz}7U8cuhY6@ud1k;^GzV4XiU(0O;^;!BAG ziN8!7O-7SnNlB@EswH)Q>aEnN)OcoZ=3wTj%+r}enP)RU&ho4w>&tdzXJoI>-juy1 z`%rdg_RHC?X1|#|mNVxZIZw`?8_ZpmTb>)u?aMuu8*ee5luzp4I55V>^kbGW+gNZc zK9(M98k;uOHP$`WH#U2$e{5jvim_Y9ZXeq;cK_IeV-Ju0>DbrC{$iYu>&A`amT_g= zGwvU67!Qxf#*^cj@%(u6cJ38`TO#@ zbC{zxw;ku__Q?6U{d?+veZ<`KQ|7iBbNeFZwpYx}h`G6QzWUt$IQLb|?R1M?%#$J2lwUe*?__ePy_KWy0JiqwzFI+$W ztDpbH&u>5RpC^8D;`I|RoH%;oA5Q$qi6>7OP845#{`sqqGWII=&#Qk6c;{76 z{;N#@{>pRseHl1ouYBQ^M;MDg8viO|4NgF$0s9GmWArY1A@MickZFkHRW|_i)Nq-& z+G~H?{vYtNHs8OyA~rn7F=%lg>>o69a^ zm$P|nK3l|wu#=avPq5|e2KIgSBlb_g;7{4h>}PBpyPd6PpJE%>r`bk!2iwH%WSiL^ zur2H}>@N0MwwHaC?PFhKPp|{*JM0kqJ8+x7Wq%J$Jj?!to#c%?#B4Ne^TYMY{|IeM z9f;!)=OfTDmYA*q1Fa->Xu!bn6JOK-7+{H=8UPn8@g)s_6_z-l0mE|=f2jeZ_m678 z{)Dk)R0H-8XhEd}z$Z(7MFU`#rNB=KSQ2es1J;8UVC8whb*Pn}Gch+8=AcRxy?Z zS0-SXYZlx)Ye3(M)~5lx6m5qF%!_u021KXX>op*bp1nx}W=6YO1Clh^TQs0Ol?C1j zmsM06T}}j2eKJSeHvh2vD{-CuoBwSfN^{VblL)Z5n$)B7OVpS_8@D)ya?D<#!iCP z3D`H#;K{>;4jd>A1!E@0K1lrp&@`i1o@x=_A=z41}u&i>qo$HXtB!(SRSnl zKsdqrk74b{fEVn3HU_+m1<;?47I+~5H(+CN4H#%-ED1>C`Yg0`UjX<88>9OIz&Y4h zlLp`+Y>b`(05@S{(=-5IVPh2l)(RYkjbWV#z-ibRI6eVejFz4a0RLfQ^lSh)5gY5% z06d9})c_a^xDy+jtpU3MEsY7lK(k{sCIAD?jx7LOf&Ln_S84#B#>VK`0B|=p26`X> zpJQV*j%(0Aj+Vx8Gy3b$t^(kGY%ki|HDI9Mv5gvl0Fvm_Gt+(azU^ z)BFZC;F#w)=z)M^4~#F?fUJpetUm$A{NKm42{>@@KITjS83w&c10H73ECn#;LhBH~ z*pp~y3t;ST(0)k(W6z;|Q~+bwq1`Wlv9F{3rT{4WXul;uSSaIvfj+G@eebUXkdn{G zj|yPy&l!LBascKA&78lxOaNooq8$O;gzI*+UlqXEO=$5v0An?@j{&}c{y(DqO970% zi1vE|7{gfJeMSI-M#JAdB!ICWqy0MpjA4xL{=ERku0(r80AmZ#eqR7%G_GR;7{l1# z1ug)L{Q&Kc1TY4R>fILvz{)~P&-gJu(~S0~0vKC}_GJN#U4iyB0gPRZ_Ll+}`w7}N z1TY4x>fL`6z*rkvSYZIhu0s2c0LBK<{$BzZ`!?G51(06H-~F`!#@vj*hvk0{G{9g< zzNZtw*iN*10QMVOiPk89u@z`70vNj5=?BtOtO>YU1y?1VC9vOV6PFm_ZBc z5x`g&Ce80C?0An{Y{{B`0Q1;P+-T;h! z5|hH%XzL;bM5lF3YbjazrbVhBul*6cE|hH2LOf4m&U1UDAIWiO4Q;Hy=HRTg zr6FiGFei7=eA=4x8CjM>oXb)|hw)1?EqoJ`afM@YGCEKv@Pf0n9&_VGzYjx9C6d{) ztFuz#l^lki%a&8FtkJ;(QC_W-T~ThtEK0?ybaQiePm^lP_x3dLd*Wtu(AeD2+cYiR zX|pbB9g4L8YdVA8D95{-C;FRudh^mVO+9M5&@nA$=}LOl9g5plOUD`=(SB~wcf>-9 z1v?7!>6NaMwnCHbttnoQOL5q&W}`ueH85gcX{KkA9uvS7nhTgE$E@^vyr086Xv9kK zqPK(xrN5^$CH18ptst%|G5YvA0f*PAQ@t$~1zQbzQ{a;B9+$xsj4tpKu34}AB|*nIR;pPt z=}5?7({aLef(QiOHo~tgN%idrG=qSFca4BOl~SUJz2=aNz;Uik%4PMHDH}AP_ZO=f zmlTu-vu=}O`p)0zlxW1I`YAVMEQ}a$3z2QLP<2EsDX`sn+i`nt4_%c?>K8 z3omB|2Gm{Zu7QCW!%J&j-7}UB)ue5;r)R5A@p-eKu61{Jc6N6QoY4KBfp0cVIEjXX zey77?5;#ePY_d+G=Lk~E(gO)4GPd|6PT0kd#*^!`<^&-S#d!l}F5glgOl@fCjP9=5 zfm)?Go2`|~t5$bZYJ&w{gAHTBGL^&Rz{}IeW!b6MGD6yCaVZ>Ro?Nec?m6B1C%0(pe{PTb zJ;;<+)>bow8!-ZmD4vM~Sp=pyJ4GoVIr+l)WL6T}7}BCz?zZ|6)d_*fC! z)$fDQpkJ(zKOp_@=g%&F{u3#%9INbg>m53yUf0@sa+$&A*1Ns3F&IcCKe2UgarNrr z++->_BM=I;`}F#1+@Nb~F86eAs0^8sRlVNV-Wc-trjn^TPJfNHD!|6vK=OU=rPDg_ zs^IfN9?m~tSvXW{xqSZYnGK5G2o|0oSueMU8PS|*Hd0O^IfG7G-&kw@(5L0<$9{Mz z*tuTJ9P_VFznTR$z!fKF?|?WG{2mza@u>(qk1yh9l?(WyZ{WjnAR6DC4;dtzo!cxr zU*mn@h~8x5cAI1fHFw?aGRQiv2A`UkxA?MHIBVc8r{1EIj9kCq$pw0gL+^BPLpB_{ zY8MGYfaHuX zQ3hIngUkrk=P@nl45;>$d?{b5oGO>fC7;}W_GDAjb;(p9Xaqg#96B!9Ey=bwmgyXV z>MVi4+~o(0qoX7EXITB5Vo9b_z5Y;RyVYUu0O=X*<*QpR&|X_(*w;IdoW-xCFN}`X z`P(nS-*WJoeY%$9ER{^eW6?;sG1L(Bxt$KX&1x~5gyxy!StoEZzlrN)o@nM$3)f3= z=y7(T)Ie3UamhM~2@BVmx!$0I4c=tp2BQ&5eHI-9NI3^p7LC9Ul(+~_`Do%AXbRm7 zUJ_^^RHVwJ+*NZf`FpOwi1-OAXSXOptsijl#b?e)&jeh(;zF+y3@Y`mE1=p&N7dsb zpIzs+VZFPU0}4%!Ev|VLCx(&@`kYZEVzV*_caXl;l}V=(aUJPzi5S=MxSg{YW(sUo zvD7-LikO#-<;^l35FzE5OpTa~CuxXqIa|$C0-jXPsQ<8nTF!y*8-0n1&IN0%32A@Y zv2tX~%TiqN`<00T{gw92eeCY-%cLceVX$*A&u1MUSX1hoqocz?Ma|R9_$x|K?cKMx zciZDHFCjv7o_j(5wR8%4U>dYsWw+JRnT*BE8ARQ*w$@A`Q)tS^BcXuXX-->Gupvy` zE&lnl3uQf=d@=sm!UuNld~xSa{<6dCb)l#Hm1BCE-J}7f?KbSMGOZvM3u~EjiN7$b=!*1ZUnJ(4 z*zbu&d<4=Ey}ocw8sa;m`6&L4XpMSYYcT|hU`YF=??P*UKH_mhPGO`D5Le90>-q?p zC5dD^l;VIt#yJd6SZvVJjog&kDhLVc-9R?kAvIIz0=xAAVgn0lA3HyLdKf5A8xnmb8n@-dC6HUb1T! z-lD&|@NClOUXa_tGn}ygF&?OG!VVYXA=Xc(5yk}$K;xm&i1CPVP1=)$GjQ)I8ouDt z7e*%ih&J}beqnGQU9$Va=%iCHvG)rTd#I^r;_f9F@Z<>V zKXz>8bI+}ug2QHd{oHdO!X3uOnGlwBjA?ijV^g@E$04(2y)5a=P;Lye-hi=TU)S9> zu#ibAG6Omo8>eQHrCyBIDMKtxMXQuOHgXLAE=H)@CoYnSB*r_5OB-}|=$RzZH5`)P zB?qZZCG8(zERnZ4RgF(M7 z9E!~B4~5qix+STnwRO6#r&V(N;TL=RHrIN6J<(*y4O9NchIGZDxLX?Y*>s_)qz?BM zTB}mea1}gJ$eMFI#o0Z*(45UA69KO?tb`qQqn;>xpo<&z4Zsup zE>Q5`GlK$Q)ai{S5Q+|R1_%Rv$)q<+Q2ua#@DM0}#B*s6Qllw<(0R$oBJ-w8O}^fM2>qXp$T6)k}YL&)*)tcRSbJa}VEgIFggHQ7N32 zvf=%qRr{C9YZX@}Z&N&;ls;s$>(|7?Z5C<&=l<^Sz4z7ghy9SB(`mzfMcr7tn~>GVnvztrBE&z%iEsdl+tzQW*&ed_zQ-u#kfA71+qBFHOiLB&}r;v3i@;YOdw8B#(t zI}5FTp(P%}Vq<173k*hDe2@Tk%XuH18B1<3N~BMkhzpbYfesWBT77ChZuL6_%^Uo3 z5hha()1RDo0B##>lhSXj_`Y;UZwoB+)T~IgTy}Q^oz~`t_DJuHiqh$~+0voj53jcR zuPur0U_(PBrgnxJ%E5GY?}|vYHIz*5TBYd`MfrN@jV@@6HkTp{AWVgLWrqV^FuS09 z*bXsbUX#3hiVI%wa(bOMOcjyk1lU%t3eGh_l5b*A+OFDOdPxdPVYsDV%lAI;(%ORu z*J439Q_ejv@03nMf9kC{f&tJL4Fkf>fKg=eT)hOZa#<*@U6T%G@+p(?37;~IPvLCw z8(KJA8X4R5^@%F5l}k1s2=9&p-ccgKKfXT^nv zR{~DHQ&ChJ)_K|YzgZGF9P?|+z$LZ5X&o(1!Y3<{^;BY@i7M%sRq`0Za4uuPX#)^b zsC&xjRz&TZpNwpXD(nGaEeQ7mkv(njJ`VoiCH8=&hY1Wd3~#}~-$`x?kOmwWZM=RF zFa3c}mD8C}vjblESgg5g!Qi^Chx5ZXH7QCuSzPpRNZQub^~hzH_Z1!CW=C=EBjIqv zjA+s2@j8;pV)u^9_Dm_1cQ}&ieAl`KgIy2jTFf~muf$>Da(p#yh?pd6d`aU;{zY`2KG^om=RS(R)^tu(x`8x|kwt>P5`nEwA4~{hR!BA%q z-wt`{+ugm2qo}$U%$Z%PTBh?;!Jt2cZ`arTDa39}$UsT6xi#qV$wV{~40zpET;q+j z@@?s`1RILHRC?lduq7qrergO(bg2)qE?C@!_zf+e(EllhLvau2+Yz2277y<4lOFQs zJ9?X{>TA=RdOM=tk(-X*q#i$3US8%u&-v8N9d5UxxZNH69)EUV&F0N(28M@+i@m)? zjGNX&i#es)B{cb;8MMNTFS z`hm4;^WHU^-`pf*FdKj^zY*461{q41)*O}2j`mDC8X>!n_Wrb1*mFQKb`Tfj7u_E@7*hx?&lAthQ+f*xJ17%6efY+{=^CRkq7!DcpVk%MYkTBB#|K@>j zPv6ky^-jBe`nS=&t>pD~te?*J&6-vFZjJtY!W6M4^*-I4;aRmdy`DqDNwUczZJd@J z9EgWv^X6x#P0P-`JQj-2o0nDZo;fr;6F@T5P2l(4)3p6!f`|TY~Bzz>Rnb_ zIM`8Ww0m8(h;+xYS;27Vk%&uuI-2AiUN}tVxUIfA> z`)xJ2Kj|O=O}7sC9CnKd+J{x>^w6}yf~6=FFfMfwwggF|GZj_mH(}1z3AL-UWSo3* z4`iGtHX8GcsW zm?hv7>#SL-l|pkWsl^VHaT*ss(?t@L3~qP?&=W$5n!5B95kiQZG`2a762E;Ckr2(m z=g%PA1v@R^hrG&V;S{1zl%$8M<<1#>tvl3{PTyd3u(Q*i37KP-4v#mN=js&>N3qjl zYbu5kiDV=ia(S?B^b%{ zo$kwK%}8J(8h8Zri^I27rPx%p(pJc%wfQBI9N{2*rV1WG^AV{5n4M4<>eH)Y%;z<0 z7sRYl7$T%%(>{R>lGP{i*M;~ut%oZa#ilA9#>YM+wdgS-ht-aJ_IriEJ? zc6QM3LxA0BMr;cw>`!1*K zHbKLacrXADuw8+MSR@>Rdq%Mn(~$UiJdpYw!%FKI(155d)Gs}+lGnkYZ$S};T=z79 zBfxoUKtot4e(blPzkT$V2nSpbb<&kW)cWI55r8{E_voR=q*s1BD)!2zch6xM>XW~T zi#3A3un=V8qtH38sJWqcg#BKGA8l}AKr1I(s~opN7zPlAkzkNA17;1W2qjd~SBU|V z0V+Ix=xNC@n7rn>7DBOwmrFS#q7QYs7pQLwKa399c;lr51DD=-X!U_7R(){Ut+&j# znWVwXZ(H{HK!cN?>YsCTPXD1d-#oNx?z~l!;$O31!7W$7;?c%!K|K9Q#Ewf8i`8P2 z8H^d^Y6Sm*ibNq>V#p#uNz4bYu{%j-XRtd{J~Q`evOC7*FD==8=a1D9L ztp>}k*47hzkk39`X&Jih)9qo6W}M6dd_4&i_~6?!xD+EKEOZ@@P{-lB@xsB)Vs1NJ z9KVo1OsZ!z0#AS*AAqJ!j@ilKeEm2L;mplfT%H{?>#n})s{83RUw33XjpUy0 z;Oayl=n|WDk z`N^G|mze$Wd{@`VO}kG_5qf1*OZyn^Sx0grWt zT~l);;(i1KvFqwq=&TgTC*6RsKVo8u3|_>OU_nGju<*(C3e6O;6w*wI6_aOAq>h07 zfHi1d4i7~bAa+u&M7c`-4sg{nrv>jMWfabivYwZnVLy-0N?7iewvQkb;r7I&Jzu|k z{?&1%mTca>fGss`THwumW!KVBd=3d=$Z8%lfb4ES;WF431UXdFy zCmSs5NKNPUP?nPmc|dA+63!9Bf-L_fWO)O!G>Yu$TC_1lLG~O1amje7 zwXx9P_qa9xhAi1huA3p(FAIxyIblQ zP?rrzX|vwtw>U>foff~#d{6jtXU>R5?dSdKGr%eFwIcQ=#WsU%ImIA+B0wUf zB2rTj6SP7zsMUhBEkVXHxZk7TX2=P^^6KG^G3ewHoV0ikiou6{{xD3SC$Y2{EU$_k z=}J`t*rI`I$%tgqL=Is>pX=48o|Oll7=au5%*Yc5R;s7AQb4wdmGn>3(g_EQ(&;dA0XgfRGkaF= z%-)%OGkUtGpHI*zv=@-0(bC+MPv_FPY=)G>a4d|p1qxkx15n5XI<#0r5_nir15=NO z5s!3Q5q8tUFO3chg8ad_D`tn zW+DBF;-gVBtOamn%n6DBI@HUN?4wyu3G*R8HAhMkCEM2HFIZ#>?@2?_GYHG?Yw8&t zMPPn;QxB~<*(h3$jR*QGGAC9*` z-z#sUxMXou-3MBImWWn;PFki7UD}Rm9H&0tAtoWi5AhATMfSlDQD(DPtY#=pYiuB% z?V8yIg_|dcsYBi7b-AJW_P~TmWC)i7j7H+z$nOES79PIyY?`j)TBWKf73fI!Myrj@ z7XOOt{_Jb_Yzuh$>|JK_s*qt?z@OYUyLQQ?k%-RN+;ZjOn{OVN>klBotaa&<;Uy0T zoRT9JpT44N@B#jK&7yCK&KS9&r`vAttE{j5{Vi)22f93vcT)fCiC>qqIji3aewxWn zDVgF&4A}lXWwbg5EQ*%UnK>y$`FqxjWSS;SN+gt$)KV&MHLTm98 zRNP8ZksWUL#C;=A@PfumXiYvVKP=BgrNkms_+43ZT(jir#fz?7c*S&tH)vcv?cjVS zWF{g=b=B`TM6wJren?ISN9KwFMxN-Sg+~b*Rt0un8%NtoNekqqZU@K5M@3d|47wJ% zqVZX1&14`MyaE0BNpABPgM)ztRHj&9NkfOZ&6ZS(sq$u@-&`?;{Hq&Go+V!I67i{& zWC#ZrM%t`xwzREiEi~MRoQ7U&V{m=Q=vm@+t1r|3eoILj;O1{!6!2RMHn(@kFL?($ z!UGOx!=nv3$)*b{3tSzp1>$3c(4t_#nzgvS3;mLRiO=a+Y`2FV52huX6mc$4ib`$O zUanKm!j78EE4sk1OYDwX9Ob>uWRFi~iIEaY!bjt_lvi{`oLfN?R(PPSx-!zIkWK_% zK>1;2W-=o94C;XB(QBedk5n;uFa^nAgOd=yDAk{Y(1-I3^a)oYqveKC_`bvgsm>C` z3z0StPJr|xGMFyp70L0lC*`=>cSij1Vci+S;>y9UrhxzApz+AW{?)_1_*D-N4EPJm z!j83TI~FS5m=S}X!p|B}1BCcTUI?E?c$CdXeLemGF(T;@6obJHCWYWWoZ<#@hvVz8 zeZW$&HHgV+&IoNwAV@>eUxa6Jpe{;30aqYrIHeg;`iUV0O#^Zl88ZLqZSx*bYM@XjLs9{Pll+GwU4{Zrm;pvEQB%(eAREMq9U>UCO z#YH%z>>X@OA9oq44hEpL0r}T%htc}VD{ke)D_}9oTW;l*6KE6|i+m^kJ&wQo9S*$U9EUhK4Z)n)w5D%J3zKdS^@56G)z3pl2i!CrHDm_rEq3bIwln#W45EE~Ll}>%OF3xsh+_IATfRX3`Fy3GS$w8b%LoH^ieWFh1lN zU(ie2@BiVkFYEx*4gTQh!dp3Cf7>H}w)R$u-+tRy{!G2o>}g08S{Gi~+beo4S6u&qXBe&ivNxQlSW)BrwW1x4@ zYeA2jJdws$f9E~vPJ#a;(qBs-0Q;Ch&6dr8RD52y*+h6r0SR!MVe(Btg`j3owV-b7 zN7|v9!h;+%nT#qCEkQ=72)h@tDJkNJqy#YaD9Uq|O>33s@ z%l^l6jleV#7nP2^OIrBFMPg@9O3zI61Cy?EYmj5S2vzKa|@YKL1FJeafYwPg8GSEFwB#MvGEz8$=> zO?`QRC3_^q7xR;r^u%$eQ@2)bRF-vE($a9+wpr(NzB@ee`f4eB_DTNsBK6Irt#jov z!VmWQuc7EvJwH%)mwDL9*vl4Rp zpheq6j;_EK?oPa2i^a-y`!CLJu7&hEN;jWOIka1J9=)4VSgjb@C1Gyn*xjILq}tb& z1~~eu2dXxMywQV7fvhCz>17nTfa3KO$c2}Q8Z9jw2G>Vv5Hd0{Tu5b0{E`RNUvXg; z)awJqu-D=uy%FRO4)eFCRt1hs^z#pf)J;R8mXi4KWNjnawqfK2)$;-`){e*~?+GHk zH9j>BP)nAf85p5PQ{@2Wf*7hc7i?qfUcCNjgVTfIQ$F4d`ZJYI^ZZfBC=HhLeHRm~ z`U(yH$nY>Ckz}J$9O;6dH))r5vOli1aYj`UFt|S~f2XEB*0nz$=678O`5a)p!_N!K zqcn08LrfPjD@@SB4R*1D4sLgHo8FE(`zANnd$^$7q8& zXn|Eg4t{(iS~*qjk$kBD(33+kMe3q-!J&!$(hyWZn9=w_1qH0@B3`TOs2XzQVq9UZ z@sT2o=ws^fV)0lJKa@vGc>5AeR;tfUg6x`6*IHskDvcSh{Z^V;lom6?f1OVs{BUaidPY>x2Wsmet(eP#qSFG{c&}@ z1T~{aJuJNef3uEIda8e6|5m`ngOaq@mXx+eZHi_0Zi`}zPHauu_Hw>b>exE*qWHtR z@kt)XA1HwoPu>Dv-VKZfYlemZ6l+iwe0y~xTNI*dS;tVAJ2865BE1+lBod@0ls}og z#{s?cH|_>!n@wF4%M@&Um)otkI`ww-PD{w(2tVA+pOqGwyb~`t{AtNzGrVH)dV_D^ z!U?}Yr!$Wm+=^stY-p_oepQz-TAhUhO6U3-;@hH%fG8id44ZTQ0Xt+f$QETGEzr?O z5HqO>a#@qa=umG`6r)}%kI$k;>%ab}h`AvV6cQwrNK#5eDFbI?9~Ws2eoEU9$UlsD zq-H4)IeVg{ZuXed8w2oNH~QA*w|OGTXgY}9 zhC#TI6aKHT2Q1*vEv%cpQVUtF)lQ8Ho13CxtHe>DdUT{YxX$XIu4qtIpYe%Ds zJFWJ)BXW`R!`xrcN$&i@h4A<-TsTsMMNXa)vc12AwJ>8Xf^1>UZ$*wiW#yy#)MIsn zQ<_nrhJ8`=0{8In0=Z;U64ylX9m&BakRM4+DpSH5p^60t0gx-SeDYW$YYBd8a_1$e zik@bhkUwp6pWWiN4M!l=n<8qQTrScnw{6oVym7KABDp6{MVfGw#|UVK#`(Hzlun@b zNP)(4buDZLn};3gL#DT4hNB2|RVLW{23aDBK8-R%ksO3=&}0EMP~IrIAO|@9Kn7GF zhWqA>aM4mnSePaxFDkigqU>kB6l}{fQlhnjqY}T2AKLiL_tf`J@Z!c(``5S6>0j1A zr(HUs&Q_0d^E2PubV_~UmB&tPRA1>|HhcE6e&Qm|b8pB&;I@UiYC4CFY)hbvi3B}f zf%UCrKOmC>*0wN)bnOz=y2HW|2~r>x*h2j0`wFHQLK3DXm)Fx~YijSPR8VX7T1POb zb664avMZPR8XH4qTVvr-o}^E^JgIaia1(3q*tzrJ(X}1HfZn6PFQ|Ac7P}SU4SZD7 zXSJODR@5dA!M5w_EVZ{K;@Eo_ZGXN20kr0ThYULxY!^vRz_S|B@fh5yB8c^08acwr zWiWzw2Jkm&%;sR}(O;xgR;sY{=x?zK|01S}zX%!&%brS>{U!Mtn}1+-#oN@<_N}&- zCU2#DmD96mk;gf&uj0)Y+8=K(IH8!HM#q`2ffOoMT+;z zYQ-BVcP^aUS&q2-s#p5#3m4jbgWY}Zrl#V&xocN1n1Ac7^B1hvY~9D@(=z2H&txlb zJ_vGMQgM{gdbR4F#C6E@;E5Y=7`TMOcZe-g(hE{Jz;{L3k%(nu*eSAD$#vH!GLA}Q z&1oT>YB4~C>zFE-0DTIJO*Q)llsv+o31keEftn-VEqQANO$Zv8BnMs%(5^STPv`oi-S-EPg@qK7<4nCKhpxF>+UfFi_!adAlWDHq zD21kdva7JDzaKSe2HRGfO=n4?yfW9Fm5}&nws7U_sJh^Nue&V};Ab2OE37ZOYk|&W zFhWPR>J5=VgWl*)4I|`V@ppB%bCbbTF`0B7!HLs;rP=P{ub6CoCQEuKWGf5~4&(=u z!LBZY+h7?$%~T}cr|SpHmYg?FeD8xLr@cy>8wC@zXEYtLz?RgMLQ5(UY4p0C$U)&E zkFqcAN2J$>+CjP53MM1N=Kyt_3eAWikr!52r*%mv*wqC|NDD-@gZ0G2a)vBR@TOuQ zLk>N3=sn1GNO>qH$guiy-w?v%l6tmf+Ki=3XH*e=mX^-I#TlJh^$f!HhIHlFH8cB$ zhWcc$yeE#T!RQ{zEA8L<-C2uUE#=N>?X4}@G=fU8#)g0& zu?m|dYK@w}l*qezC8Ce;UE<>wxH&CW%5Mf6qeumlksFSWp=;wt6jqYgP}nLsCxqU& zn$0Mw$}+1JLNCY6)QBJ(9LRM9kRD6U29cQpbq209qagtW7NQ?7p{LP%f_O{(#^u|V zUw8L)2lsBwzqIXZcZ&M6!A&dw#Ala2wRdCgAGbfc(d~8x8{CUF+_cMQtMKJ>KJke; zv#z^t)}}3cHi$a4MO&BkZrZeWqq~SKT6$6E>!jPlhYM?_jOf328cHXd@~OC%eS;Ga zMC7JNg#8(Kj==;^0(`n)^+NQ(-oxrk2;S7!937$kQ1}-AMDV6`VQsUSDSh~sFhwM) zK-rnWvl4JZj!=j!Qcl0#nU7WW_MeoFVuY0ch2JarPbl5%dGR3CI zIIXwbr?(pI3%uS1i}`}Z3*l&2Px*C^V9KBc<=l7W)et;h7KaCBan0}dUGQ`J;{Lc| z*Xl(p$hakqmmF$F1W;+h*a@-@u`3d0P~tIL2(ydaNBLw?Ocr z6cp~1TZW6*D6hW36rFe~Y+EkrLe;7($4};5*95}qVE0wtlxbP^;q0>Ze~=EZRl{qg z6%(6qh|8LAGSt$dD6_pGb>%?$6MfG-qrS!0ZP=jkPi-x=m}{EtuSJ3XDIR?6Dsl)o zpveY!Kt$k4lkJUjb4(}MWM($WtkufFeoV43wJ})~c)-IAKNH--dN?9Q2)&m*@H-&e8U#XiUOTPtL_e)?+g_gBTbSsFGvvL_MyqlQus7_~Te}Ej~$f9zcn=b<$=Rp>$-J34Hj?r0Y^f!rTa0 zdK8*#kR!1@0W}LXPtb6YCMUI$J&(i<=vyMRfgDQef!IM3T08@8R=6ES+MQ-8T=r4- zGeWWb{cl*C_wn~KNOJl28J(~Xr+7&LIbhVVuJ+}-Z{W|WeK&OH zN7O$4?6G4POlQQapBWuRb>|J6MmIfsg!b4)_-5w_-*nOdDzPc82uu|8cLCr1R0rI^ zbp|N`C$&-H$kqpHbOP7dVAy5?*Sb2c;r7vR4ZedaXE;PD#ZklcB_F;Q@ciGnS#98> z?}KCtf#ctGRFgQSq5ZNH~b;^Ae*2u^|9+~{+{kixm0XxZOSI%jX^g&e_AEioW4%U zWcI*H6+r^3NCvaVUez`^PLzUcLq}!*A76?s(jF8DPaUe(665_wz; zz+YUc7tA9!23#G^1rLl3h%zuh6nJdkcDL87?(%x?-nQKv_4aggOLq@C>g6e3NG11; zsmJj*nc~N{FWX_;=I!<3-!|KhW!t^p?X#!jXZCjQPeYMM9*Kkym_L3TO+zT6*(Z<5 zpF=Fhj%dD(I)`a$&8 zKsevt)Tf@l`^%%V;yz!km~%KS2ERL>&O_F^&OIpaM(w5#R!tZ3Xcn`kn$kVJwXk5` z^#1PtLRV`SSq?5L^X2d}Ve>#smb8ooQH!L_lcIPuH`IopORwtFtIuC1{E$j4wBw}E zjzx@HPXTk%+eH1UKfo8Seyy#&IWlwBwV$}@=ABkcE|<$jn)%l(gVQTpGdYXVV#%eu zXY|yl&)solc{&}7tx%qbM4gpTv&(9!IQUkJLs{WA1QI5LdxbA!kuGij`7O6JN3yt^ z#k%w6n?A9uHZ#(!<}zC=(+4fMX)aUinbDojQJ-&(d;Inl@pwADT-n&(xS|lWWPB?^ zJCH0{F=azuOT}Wvxt*lnvJv?q=^9wE{u=5g$R@NU4cdm}f2!8D?)g?_2k)KcAlBXno42^wKz$mBJ795(1n zw7u}GZmPJ4IfLCn&Qr4MC}2lU-+IK(K!OD8hGQHqirJAJXTm|Gai*B4oCo?MmGk&Z z9$&zib3s++4)wIUO+5{zYVk`ijehBt)yE%Bj}{*vkekoHv4uO%K7ah-(U(5?$sQ-*ZNP_SUWoQdJgQyQCza^={U^<9DP!HMzSd3Po4+R3Rp+JbBKWNhD zt%sMO`+|w|AtX+FkaFxq)?Efh8^s<;Q8s!+0R~DIs?es81{OsEM^sK_DWsXAD7B2^ zS;&YNSpu}91EPYE?pT(brM77|tXOjOiK~~axM5mbspE;`PpCGX>DteHZmA=>V)drQ zZ}`12mu;DJ)9IUZ!*WjcNJb~LAd?H3)J8YA#Ztq%zV80cM7(dNZkfK#;<8&MM^{T* zEV^uE+r}iQHkaJkwsKiC*4EP1S=bmK9E@))JZLm_7OftX?rqJ6Ls7-HELyBoit%Ax zTK32$kCodaNK~`Oxic0Yj?b2So{nTRnneAbSDqbRrf;>l8a$|*ARTtK^c>d90DER8 z{4s8$v9EVphcRl5x)q9t5ViF*C1XB2vNKTO1_Dc9Ehzbo)&gb>j?*MHR%q)aPc^k& z@O#a96iE0CIYX|BW21ymL>MGwILlZrWW5*-ek>Yc9{4S36*Kh(I>|!=a|Z?tm$bId zFc_~{vj5MQEVdfwTUrANYf!xhhxs8SV8lLXKvnEea8YoG2ZKDKGfT=FuPcUC%@S{V z^pbn0wcj&v$2CiqEZ)54CVO|h+)(M|??@RgB_*59V33l!ytHZA@{K&1l-iU`=m@kA z#5tgV{0K2HE248z5LyQ~C2H(&YU@&VOD)dX(xIytU%9YSrt<-s^5I6RyR{>pPT7>= zyc}k=;k;mS#o<64pqmO?MbH&x7RdP^1j4}fmVEa@&|HdgFV$CH6TGO({JZA&|%!#1NenoM``^<8PZ z!5&RM?V@*aqLR9NcKup52CX`#)8~{^snG z{+WH#yGq5@=IpdwMF`(33l_uxLj|@6T+DPG zX=sRVQqO`sH(Cda1*UfntB2-7g*|26Ktc$RNmO4gPJlG3hEn=HMBxWfW1s~1H-}hiCiZRbWU3ulA+t=^iyZ-iY z&&EeL_H;j{M(|b{3)gR0xNz@=^?TJ7Jy$K-xN*@{J>BnU?;%0PQ}o2>&qU^YSNdWy z6i%DMNj&VJ!$`|b#DV8accsHtqb+Q(r{VjtPt4YymJCCvUA(>tWbRC7mtb#w0wgxZ`R(4)b z&!WW}{^u8!V)j~|ksX;v0YoAa?9v*dMTvL_=UqUn26v=v%l23lCq_8n<>v{Zv{2a< zW!Th}7IY{&0pU(f-K($dI3`LE^*EqY(_8|=&kTjTB>goHKJfV;9#!A`!h_%E(!?8* zG_vhaZdtvWOS@iPG@)MqM{=L~t7G4P@C!V2^wkK>v1G|vKi)1lluF7%1QoyDk=Eu%__lUZVQrVFaUck^X zd-5aGnTr8L%v&J?ro-o&=1fcNr>c-bb1s7_{g7+&=t1&H`4!CPa#VnTi5p@58?3U` z(CXkexZ!MY!@;8KLOmH+J=hSYWo5F>N=gi6yOBFiqP9iyq5{7M$51IMm3>t)O)%A_&3ZK4vK4_yT$# z>9V84DyO)8M3R3%3-=H7N)Be%U)uGTIPK$#|s-0eojS2l>%I?J3u^v>{y!WQs`R1Ga z`8VZ7>cbP;_*!)*?nC=y(&qBO=DMyH)NC%C``|NqV0YOyy9<83+DznN!swdZDLAD< zD~^Hjg=3w-C#V>ZT)K5Wh7K4Qb_($^?GT%(##b&I7si*zM|Cpgi;b^E$5-E?o`Cgr zYTa|stvmP;*4HQTL*`fW3x^K9K(ugSF64)g!6)sAM(AWahaIWyoH%b^nCs<6uG5DQ z-j%fjD}gIGzeqL3j-wJhT663IOodAD@HQqujo9jG78g7?4i2L8$0-0Prh%Mw99Dy4 zNWY<(iTB>eH{W+J%tW5A&qT^Uf9TNjd&#ssbod_p9DevXj7Tw!;dReFzy9Dwb08Zy zOmjf&&gWLFW;(K5%pm~NG06401{{ziC&I!6#t4xYA()hiB+(qmol0}KC_S7kU^nIf z8`v}{L?}K&B{%^Tgp?_f9|o>A3%Rpy0*}5l@d^!J3QxWz3}mqa#~&VE|2)>{!)p-4 zV{qaVV_MEy(C=2jYpgg)8!IsbBy0$jH2+zPP*MEQ6-SgKJ!dAi;lQ&FTYZ{8>?HW;rtGWe%v{VZ`Z{t8q60XAiBc~ zOjkPzf&y7o#Hu})CST>;9%{hdzx!R>y+e6jQT~-n>v4Qf;mETE_}1Q5{6XbEz|?S~ z1ncED;F<6w zomLt`c4__FO3;tT;mOY)p(jt`@F>puH)%L@Is^_8`4d&6T4EB+2rUDh++;`~kmg)K z-&=UbTRb=we6bc>jhmM3o!O-L1 z;xC6n{?K8+|8U434DQ*3do;3b+$wcz_b|)2M+T_H4eI{70Jk&cDHhB*{91krTl3=({I6PYoKk^91;isF>fDS(XEpd}jP~1eU zOC#p;lgYWjqPMqH@l6auWK#e+mxqGE;E{&F5j3I4AAd6t2!;-Y@Glq$JVL=Z8pqx8 z$mBS1t||U+3Q^@u2f7eO0)u=#-#{Z!@mJkINRhhLJ$!`bqHgm0`F(^Kx>w`5XZTML z*AuuEc%cF$x((qNN5k`<5MIYB}x&~~J+sZXg-9p>J{kel1UAD659=su{I|98~O8|lmf(pYIvWl(p=cmlVGfnR~v z@v`n2qEcJDhfEz2#K(C76e{BPtM~BDe1*E#%jfZVht#(Y|CW2xe#L$1@Cxk|0J^t$ z1jH0QN!;5kiAV{*m2ZJJb01$W?bl}DKScMQI)55AL?F!{dz0=vMR}l7b5{y^NMAdw zzI~X6#~0G==(c#480RSeX9-ef3ZE2sth<5{^-u7BCd~r{nb3W;?`+~L)#A^4?QFClOuSar!wa(NaI%^5dU>o=;by|2W@-@`DmZbzS|w=T0LJ z>-SiH*$6CdJIptIX#LN#waoHq`yJ_Cy2Hi9_l= zhe2LjCdWDD{!{4dze@urv{`Jf)4&BB{7y6=8xO1ZiP8NA4V>G6GYjVa4*MxX718*W z1-s%T-CaQ-rs` zNNP6xZY3%8&5d{5vGJP^-8!)NhPw_Pylczwwfw=SpWedn-1L}qa?_pa6L)SJn%8@f zvxC=Mv*r2cAN%yDwY`4s2*&#XS?$KlN-{*C>2Y}9Xl`sp3|rPuxejd#`H zz^xDMf9IY3(gzzhJods1pSgO;!L4^4>|HSQ4qq(nLb8vJ$a64O1-3KJL&G^+Vt2;j zjPWQI8AcumAPKKStYMPtV~%a&kS|vT$43BD7y`vJ11PVCguyS;{P3GI%6x|U_Rt22 z-*Mv?bT?hPluJwa0`3)8u760nQCwLMR;vD7S+aTm{!KT!Rw{FcKJytIRbRj0S>?Kv zE^Odw4YxRdoz5t*v04rP8w17nQ6>zmwjnbH)+$us1ng7LD%m1HBAFSV?|9PWDS`U9&t=3-0v=g=YNJ$`z#)*N__e7-p5A} zjQ; zVnPx^Fd>CBq=9B=3KUYBp_Be0HYyV01pwf+O#l; zWaboT6)ZT0ClzTiz<3Ab2v3W_!_r{c5I~3#Xi<#>c*OZ-kSfa_MQ^D9k;#E67JrU$ z%pMA1$f27$yI@PT?`kq1F*mkde{6QP{l+HiLypAA+R4bJOY`Hiv-cekN-C{sHGj>e zOOelis?(%aPfSO6p*w0jV+*A=-q#ax{Dr$El|FRg!i?^|J*s}uXdSJ+ICJ5`p}8SD zuhmj=42??;SmqPZPlv1Kg-jnp8_8=dXN7f*^baIzTQD@lx=2_mbTAr=cy--Dg#m=B zL8Z+W5X{T5L9A^+6?^~^^qW^-y~OW1^#bpMU+PE9FBNL;BA(Stm+r&kVdzZ?3As@h zn167PW-4Y5VmUNZ^O!6^3s7cM5MSMdiOWPQ-e&eF{RI<eK z93HOqj6@RLOZiB*4qzvA}5qHxw8Ha;_gujdZZ zH`fO7&F|>-hX1=<|9bhmhn08nb7;3mfqZ1QO)nCE)Y5jTm%dObZ$Z=Ub2T=)#&Pn9 z)9-h3v(!GL=GR7xmwVf5K~)lZWwLp=w>>t~udnhw7AG)UR8w$C15|mGMp0CBz)um04Z#dS z-&cW3Pc#&e|GGBmDmA&>^qwwRwm?kpzKmbtTJ@gWa7`Q)!A+}fG< zos!r-{A}}9Y){Lc4-WU-E3Y6dy7c)dC-IsEd?R^4t~@3ALEGVdlXDX5Pl-fg#Iyn6 zUU0kp?$3$Gr0PW<@vM-?A9~zhzk=sQY+y-$5>_<;+%a~|tMg1yHc25soA> zL;YQL+MriPf-uv4-H%&<|z9>y?x^wW;Q=6ohUXnIVeR>e|5cF4W=DqiR zW!N-zc<}JlCc{fF8R;^7!uZlltUYGr_xwHT>yeHF4%I|F4BltEmG;JvlcTgnWLnU? zWzrufO!(s!Ju7LRjP@WNlTl=(qmT|StD+HCy%-Slbt=!7wVLL{wqCzh=WgKnP;kri zk-vCDufA^Er?}w1)Lv>zA=N*f4)8sPlW~(dyeiY!6bhR>(MT-%mk-`{a4tM?ww7t> zVV5ben`1sL*3JooWvUA`!4FikZNL>YusS+JgZCSEY38jy_fuxN}FGo-XO=wp)30HuxE8)K(*9@o^JmRZtFsAp4t;vUN4-#D%a&%eyA7i zsUPZp!`NSu9~65aRN>BQ!p-`vqu8EpNxM_-WFj67hXTmoZdlr96s;>tI=D%Z`~?ql zvq`HqAW4D(I@B%AM#jyG9>WQBBEoVo1?&BV7}R2$m$p9whLjJMM@0aC@!BA_ml|Jt zgNhV!=_}{TZ~32^IqUbIoq1~h;VW-*>4myQ3mr1^lpj~puPfa^{eWD&e?Y#N9-ih# zEW~{zN23vnQo4}K#i~Bk`PRl@>w(I~ZRdGaV}nV8Qfo6K`$ZsLQbNFy0*}m?MX*qP z5<&H+8ACY(Y$Yvd04z(3C5B+p0H+cMVwSSC;)J#q-< zKR+msJK|?w1eP=dFfgH`Hc)%ljaF9xuiFONwS?2I$Z#2wW2KS2&p`g_AojyL>pQQ3 zRO18@?L=eYI)a`rm$)IthFnRDdz`&)w=@#Gb`gLBp~gUdgg!-^yHG<6esoAWQC;LI z8wR)>fTJM%=rR?_yI4G|yxHiS$aGXjgTaAprnkCf`kqX7FxWI+>BvktjZbG^dO17R zsy-rS*ZuwMbuO#bb)9^e{~zks)^%C&2>-g>_?79lOe`M`=lawQ?m)!rEk*+F26bO9 z6zGU$+NQr^v~S-0X4j_2cXe=ikHaDvEe;QkM#^Xs)=m8AhZH_K}eJkdj|q`3`bU<54agRY+*W(DI9^}MnaS# z@l0l*`Q%_wV;3MEo&{M;I9x(3GmJ}J{=nq33?=&-!=Z_qbnL{5W8AwFp>Shga_GVP zVnHM)IUn|DH0IBxKue={pY&kyraXSXC+6|_JhArP%J(qhhl*fxZ)wILtz(PL^;O;ajz6sZp0e(oFJA3T;j#UGWuMI)69M~+^YoIL#c>*wa? zUU&_E`f0sQ=V13cAr*b75CURFB0@0e2&X{SP{srRAOeAI{0hJZ!!{u+eiXg@ZE6&S z!Z54|xYWxEaPU;K7<*Uz&B?ZvHR-y~MBa)c)-J21N+$yAZzcyd>6kBXOlt;{d@NC! zzk^TDzajf4=1})&2y+p10i^vAhJduGrPu+AT()~j`@=HD+>=)=mNK*`Oi3gS!{WvQ z#tB(=n2B^)T)|5*BDMuAqup7eC+Gf&ROZ?}l)3PNH3@F>27N!l!(bd zPronZhnVFyPjAej1O-T;c)gNelXjaW1(J!^uU%2h1J^RUvcI>>3KX&`h`vFh7R6>& zN|6fk5c^m=I7XBLLnvdqp~VC(gjbAN_n4P)yug9sl5I$nEwP+f%AF%Ii_N_kpB=x$dFcV99a^$`id;Xw|uX?xwLxI3Q_1O znhdT|x2-Q12LQ(3QJxq*v~PIIlknaq7$ZF%zvS7{Yy`1Fh>u#j+FEZHYaP=A(tt*4 zbUpO7@9=t|x7S_G&ggE~d#d)f!Nwam>@64f{u%f4j-fTFXp7mbHf1}6({ppXkL|s# zn)OH_ZX8wpdca*@J8LmG{mHCdv-`N-;eJC*i^hy#{Nje}Ol0_hdwApZ4cq?38@Gxb z!pVa+mo6#YTbtEs+`hH_N$K$ovK+XA$v~BKs^?}x1z}kuCJaD?U}G3Ynsh+z;4|oY zoREhOYn%=&YFvj~D3(mNWiX2ux;IT$DJOI}zGD9LR8MshX`r~1S#d=9E?>o}h${|O zky#!=T@Yf0wxcdBWYU`R60*ILP*;HMSMewY#vY7zGnhm8A%+>O13VTTsPJw|zL@H~ zeeBgNvke;`NpKe3Wl81rWI*sZw<86x&M4omeD?~6ye6S)GHlm7{k-6J`lkb8qW1c; zJLlinS?84pVdkJrB_Bwrn+#j^PN#pPUz^k&IG~xC0Ro76(enOJln?QLi?*i}tHECv z4K&t~V#TXhYLjB-~L16`_yxXk{@nOe&2RKqI(Z7vTYoVttCM@ts zJZp#W#@^fx5(}jNrU0c9G}?Bs*}%+X#m*4KAW@-SlEOgqLQV5R@2y_<$-V;q>h@3g z9c(s1cY>iw+L8iY9*eU?b_Co@%*T%e3DS{p?t;by#fgL!$sr_7AUKhMZ%# zyu~>T7yAfON#q}vW&sU@S#YQkq3pkJ5q$Zd?p}8%IHnB-OF(UKxg2nVgnE1?GpBRA zqLcGK(<#+PsWq!^2wFRn7DKyH7cz-CwXRX?GdWcU{xmMwokCdayA2%i4nwSA-Z;pc|5e}uQTsY!i*PRaB^0|9z zW0?WopKfk!wB++X3*|TnexuH!Zr56EqDyPknjW|TtJ z@K>WU_Xn<4)C&_}IeqYAIrEjJe=jI`IcGZ?GbF+K@0+l1+tlS6d5qzi?0r?;0 z_IRO<{+tk_uwTIEZnqk-BB+8Bc2c=ry&xXo!tnhRusDH3I;5U{C_xG&OgIoh*X@~!f(T}noIngia0tszcC;7!l zUeH)9J0kp9lz?D`A+8KW?SRE#mrUH8-6o)hcUl)SS|z7OX*wX;H9Gqas>1(KgqD6t6H`I&9F03;k8FUe7 z5G4_TuLH6UPe9$ldvCPch6<^p?<;B#A8l?r{KbcF{P~SveCX4j%+cDZ_Z3H*3xhWM zjSKg`7kYA@#e%KaUg0~C^L$BNBh4-E08ubBWndIhu0+kLJIJMittQ6&V6X)sMuA*} zDuC^VR+b!HQQ5%oRf@N}+Lhp0XZ-=`hT%1FL1mcp9=@=jxODbkI6U)X&MLX1T7838 zA4`N{wc3wo4uRt9si$TbZW#qx)kdQ|V8J2+l!EM)Un-yEe}HF>D1f9vLmCy0S4E=mj5yXIqYF0)PJ1Q~b(OyeB7-=D- zgOL_O2N|d}iwtG(a)>THHj&-#2HtTK5c z-ou~ff7P|3x2tDr=hn%mX8r!zjOrKFYd0S~y7}5_?~b0W+qZ4#>FQkpwsi1Lynm1{ zDo2r>7N$6jFJx9BenJ>N)+VZBMq&W25E4*5H0?0PaD|lkVq>Od>(*<=T)Um+;&!9Y zZ{4wO&FaRN8o`RB56@!QMesD5!qmtqy7JQAgvT}Vr7sKl&V2#fDXpaP>6$Eg8Q$v4(xlXZD*Vi(Y_yf?2^9i%r=#-R-WF; zMSMozBR=C`e0T#KgH7 zSwEHY9*EDY5vcy{6xoPILIJ8rNWL%RJD^X&>ZPT~Mo%M(f~dny0Q2)fuyt?&EC5(b z=(}b(PLSG0-VbzN{RMLpVK>B;3o1FG7g*s>6=TRJ{~^bae+BX(7RYQx;Q^M&kbCc$ zN+OqQ?&44HoBzSS?GJCS6go?#xA+sx!BFOZJ_Emk6#)Jd^H%=nvzg3npWEFh>q);* z{3B3ndWxw2jw?ELFFU``c6iFxZLapVWUAb2nQDJ3Q(gGZ z>Smm#`*2`$U-1j&lk9tmTDn9YD^G(1tk`nf$U&Qw|HcYU^X>uOPqZ1{pNYT?DaBq{fJp82y|BU2iUbw$vm9*7)% z>uujtQTy6dYh=O-8hx!Gc1=$3f8zPk+xsH}qA|3>BE1B; z6)yRil?M(C+|B*Lfdeb=P9*ML$vxG*zEC)@f6qiu_l7u5~=;+bKA66#>Za*KF@X-JBZw?f#2 zUPTf$mu-$=ol`t*aqPESFY|9XJ@X0^QsbQVJ`Jz?xyLzccXM~h>--Sb`2c)j5$HL? zRU`{!TT`faA4ZKhyxU;6!NbgIo+q_vC?)yR%ElUp1%qiwT|=kjoP;7u2ev>In0gDg zCegLg%en!tvOAfVJR~loQ@rJP|G>8ScedU;(7$c_@x&QW#B7`2v3B=b{P3p>bXZ&9 z2aa#w)<1CX)@=j*$F~oat9>}RoF5&{9A}x!lt+5isIe^$O;aqg z`rW)=7`V=FI zUC2%UYBe;_f8*?~neAJqH*K64T-iS|Fw)sk$hWt(q$6R9?wTr3|kWlSn&V z8^W+lyRkrmoLbT4;0@|koX&9F=BYlAG-`uz3*u_jGJ=yrDS?nu;w8ujP&_jhG%Py! ziMb+jGvRXp=8v>wgHC54h1h^l5)@(`9U>CO0CK_v>NC<^F%*=d1zk2ygMwY1x$@#- z7;*$JzzbYrD6taQn2PO15ZaQBLuOxC-GkAKQkh)`t2GiwJe8dk$zhvQw(B|NSHW3* zUYb0Na^lTfbbNppHdIrgYIsLiVMV2_m;bz)=DSL%Q9j0_M33%Ko#tohO2!u&=0FG5kK^S?5-ck2?uq?kLO<;6)s^L1XcU3cukp&|Z`td95ZT)krbmTkQ}l&8wYj`88< zR*kr-bE29v&j5vFH29UMP`5|a@S0hts`mew*5~>}QF@1e%{e_NTF=M6`rW{tpjbY4 zmKOvg@6ptrhnV~|(?gCczpc#SEDW?trK$@wA%)9?C5rJ&#zI;GmBypQcBNmKg$h1d%6O4rxBxZ{!MT< zjY#FB-9KzL^D$%EI4sp(=T6mXwOI&FGhIBMEK4K!3obT~*nP>0aRi^j#l{im{0Mgn zUWnbb8f(jLv}Ftkw*)x6R-z7gA)O5Poel~6Hp{pV8X%NgnyEG9ge0^uT!|#6A);tL zErX=fTDHo%25EtS5wbPvunX)>a{FF+FS_JhT>Q{4k6jnE7XWP$Rr+|r9^}=>u3PEP zp?BohHNJ7lYINQ-UznKOHQViFtD4MD&iz;v4$KGyF-un1%!XeDDtS zK>oKVda7V`0>Z?K%99w$G}S9;al&hlhT|0LN?9-1OCoGpf-XbiqopK|nSWeXfzf)@ z0LO45rwvPr;g8?}Bo{chH=91Ve*E(I`h)3gtAV!~N(clXBOswkm~7UNyPV{(A6`3K z`oo8ASwB9${+2I(sWf}TL5=2M?dK}3D3+zN3ouQ0XkhCjf`?$t*7tv=JcZnxkYZRd z2mS=(jkA+b)KbkJx7kQz*#@;3kuM~&Q>2AOB$07|h@=1s0l7@gVkdBd(Q6r_zTnfL zR89Jinnn_W0s#a90N$SD1?iXzZ$O!b6@XTf?y>NhQc;P|Q9peAr85l`C2wx?M+%jd ztEQ%V2Lh2oED(s@vHylo4Ysu>lP(|sejpa}`gWwQ`*i)CxVhO7h^1oLwhdi<0}F4) zVzGcxWm>$nkZ5suuYM`-bOxK8#-!D0wVTbkoKk6Xw)i87z`nt_!Duv?tzSw60uE<* z;bjmvP6Xl}my5Sp;`M8Sf!dD)F>}hWaHYX$vR(a3+AFlM0~Ts#$n^^4T7?BgAJ<$R zNv9JDpAXxXqOZ4#`h*o$pb)zT=+fo-go#uF1&e%f-;$C=Zm*jtPcR>^;4cH@MM=V8 zenXKnZ~+$>d-VoQi^t0A4Wd5fg`RaKEYE`tAKnwUP3sfX*a|^D#n=y>W@DpZa5P!` zCY7W#NJPvDY4BmZfzDgqb>(|R_*Uv)WB>}_-}js3Kn8O|FuyzFHUut!Xe2n`69V6 z_YLqWyqkMpL02sKxA;QViC;%u#GeCuPl%53s?~&u5BN>2MwiJ6>JC=ds{>oQia;GI zE`Zqqf_3yQxwh*0^+XhaA5x8ZvPI9zqtr~)PvJ8U5Jf_w z!pCe)wf%tcr9qx~qPgp_c}A_kZS~uK^BcP#;8R&P?nZrtacJ-!#bLr`$sQzFZj41! zVd2cs-(}AnVm2Hnrqn2n!r@UlV31`h0Max_apmLiDbbO_*hyTC>e;``#Wc7?mPmt& zN(7|w!={B#jmrJBt2a<7%L;d~LWM17YLBG%M9xI^Wln)W{?|TmS0;V)8lspdMpkN* z4YllVL@kX@U_Idb>|a}S)qS6kU9z8(2`pKyBzcDaj7suqEci8K(ax&jsv!#HYe)>% zMV<0SZPpizG)6x^?r=LGK5Ft8Ya&a~A7V1(_1&SY!-1ByKDf>CCYU?(5~ zs|EPnlK1Lwp>h+|vQ>)#b|tg|{;y_tec}Gu8?~y?e|+=BuyN-NUwmliE@SY!Km2Z3 zU3>Biv%7ZPSbOKjUAtz#@cPW^p6>4M;TavL?Wk5e>_43w$l%|=oZSwqr`^nT#N)-P zT*j$djKgN4vA4W`8JL7O(B=rTj&(tQgVPE$0`2wK;8{pWZX_gHeS;@+3qH8IKjH)E z1AL3xgflBX!sT_sBB5LCDFEGQ2@Hod@2a$#$`r1m0i)Yv5>-u?FPtI0TNnPshp&L4iD$QAoO0)mwKm9*$-mh_|96+Uq9~@sl zxGEgsglkV1e2M1TK(HLR0luvNbJb(1SSe!`y4SEPJ_zlrJnBydMKMEhjG9CS}c$m^!ELn&&-04sjyWotS%IMbJ#XHX{O2y@Tp{Py>rnHelk znz;kD2ThyGgG^3Y-k(!Clzs4^WfkzjwziOGLW{h$#o!xhlyMVJY^X4W;qAddq1+!- zESEE0#EV6V#1Iwyzh-y#uQ)h3YY2`2m1WjIy9Jmqv}$wvO;ek!_LF{lqOjj4XauiR z62Ze3uyOZakAkf)N$0#OqhK-Y@O!vp?JGNX4rR6W-roJUHMi|-jK&K(kHMyCR=G4j zhL5=yoWOaYehT0`T%-8IYK&8?86I5OTkYxY>;SGejW}I66rg%#sNDfOPm$tM7PRh0 zr-4`LW1N}{4ZWn2^iv%tdu4WR2iV2fvw)W;Ota3h?U!BMCl4 z1&VuMdm$m*cl3#z~1a{?6s8^1aqfw zi(VCMuJynCT`WZzy6NBg$4a5TTAR~bV z=tMvhsnhDdWCT0qj$)2vlIP%rBg#Ur6s^&oW@>$B$TivFe+VX$^KfnK7f%Lnca+S#cKl>A_B%zr4?;4(Kj z8dv$psLw`2+#L(O_nIGpb3--Jv^JNk_u2fadY|#x#m>WCK>nF~VfW@01L(VH#SMxB zieuHtfm`<8v}@Dyle}_Y z)0|_w<6>VI@<{85JFdJYm^C63SKdGj1|LERgF;@(s-m(9YpYLJbiM7$yRt)YVK^?( ze4W`)pSdG5sW2N3e`Y2uPv%s5*WaPZFHd(!0n_`>Dc;9?w?P1=D2E>Y zQy?W8y(mTuPX)!xicnk-*5@I?v1t?F1O30qIJ?}X%!C1a%rdNy#qrdZ#qOlJ4MsM$ z#HsHVPH6s4E=<^=e7!3M7JC2_bR<+ezR+SeGe0yF6L>npG05we+S3fI)R1C>;)bek z!^GI=@ak2NmKbxng@^@F3sEhh+FS0-wo)rKaLGV1LlhTobml4M1U=kLP`oKo2s4vr zAM%6{{G#=YDM?3$p}bmSm&^|SN@H0}@e_B;3-y_?qrQIs_!D;6{K178v9M$x{$0Bj zqish_XI2?vxR92ruDjCNk!_7dNXPRgoDN7%X-8 z+$s=?+tn6#-rX8qS-ow1!>>ukM#gV9+{r>kk_&pH}{9-Uhw=`;p|%Bpdg^^N|0 z_aqEWeocQ_zeePYD!AeDLw)Q0O_F`XH&%SMkCU94Y9cxE zX>TKr3G&0o?HKxQ5cYA{SMdvc1bBl~zh%B2rqn=tP@<#+0!xG;hg5;$CnL{^ADg*? zV-Pz&C3)*p@C38m81xT+=LMs*XK!Qf>1?bv$Uk)QKd%460^;jW{9uJ@a5YLj3zaNm9O zM=FQLtdZ0j!5}m_tiCVZuA9~{neeFc2g(5SIj}8? zE%sBLCQJpF7~$*3LxtL~EyGLgIPn*yuZ>*Kp3~x%0s$aW)(C;$JK~D>emRd>|AG?Mc^;n%f=E zJDtzl^X8FCYd91Nw^l}$Vv?ywb6&AlaZ}Z|c4YOcfxZgrG30Z^ppM+Rdd+5%BdZ2i zc9hz(k+2486f=Ef`a#sp)I;O z0Teq|fLS;Z4%vhD0Qrl7*)@{?2BKKSs@hP+!j%e63jy6iDTlBtP_;-vd8`!GYXZS2 zDY8%&FaasNhZ=PnQG}8Z!wC>i2+N3cpaO-fycb)YRIsvb`tO+#yu#WA^2=#`BLfxU7=hV3Vszwxw zku}4sSN2zXz)}M;+2%NyWc^;mxvh0BXUg2Yp|{LS`k}rqhoF?&qR6_7HDd1(f@Y~RVrBN;KaV0cV zxI?dWHzl$&DaGO!_)}E1tk)N}K(z}l<7HibiO=Ycpu6Y3+4iev^rmQkI%_ShJUW1C z*lBJEh4rZ8pKd=bI*0bJEVz@IYD6b?+^bpM@ijLp#&vX52C%3$?TK3w_G$m-WKTIZ zm(8m3L?%(KnFMzGQ`J~Bs!&j5pjc>YjV7XrrB%1=R(VBHpU3Ww#w_ENkuU&msm2n3U)Q9?SiyUQ z69_?-=<&uRdJtdE)Y(iGhJVdxj(K z;p15;GBepp6M6Ue;Er?l=YAZ5h+i^)eL$6UDE>>;-`Lnu%C)6a$Q5jkz$1z%3fQ`I ze0UmE1r=5Z$!o)9U_oI>zCdWp1Tj-loPd(`=98pOzt!554lpMF!o@wbI83R&-)_( zTQ&gxACB*zy4eufx;-Lo?*v$LMm!{yd&YJT52UZU8*o#%KD2XU>(YIw^GapcM9QDF0gasrp85+Gl(00jlF zk&p&KfRQAP4yYLbD5y#z;6x(yJ@hZMPV_HRS5WW@+)bduSSo}+u<7-3n`>v;{|B$$ z#v|iD8|O72*{Y>;%77(NK_}LyE+>iuU5qQ+f<*l)Tlz^W~TyM&3K1uPXvz0;q7FY9JW2+bUgz=?`XtEr}S*91Pegs}8-fxB^ES zEzkpK32J#QEkRhjsFMfZi-v1q+7VV3^&T(iq3APR|DzNwZM_!yAnu8!x_Hm4+)O2) z=I(F58`QSZWe#ByXZZ%^~MO@xWE5IY0Nr+<_;aI50Q&j9_)Yl=>3S^Jf2R>BQ6g zm5jU(sd-Drlvu1Iu*|B8|4utpLql~%xijA$%|+YNN!psRC<41KM>McOA$t-Mz`)h( zpWK_tzlYD1*mOp%JwNeib@89>{bvX-aJP6yMh%luSg(P9r5G<5~w=N9oe;a=N z)t-!_cy%nbx7?$bj$OLszv>`yuj|aSdW*(hJEOPE>da;xcUWtA znM;q>PQyGu@zFwxd{FY*vm(ChS5OW{k77^N8V>h#)BYFEhLKa_X&@_l97si~Q{>bz zaSX_UfRtG1bHJ->K?%tRgK#MXAdm?A1m;S>g|O_f_@F4r&M2HPUlh4tpdm169E%sC zYOp0ZTrj7RZvyK%?uZv$j&>k@o4{&U`fZz&vOO|bd-E7ScldDIMX#6`)1BnsdW$y> zjtsdcC%0a`zs8OJgLib?%V+N><{cw?-Wn3DJimEzn)mnv{@sy@2rqqPr_p}(L*#9} z1Jp*FVzpvIv8x&=<&4Qu#n>D9Sv$HU5jgE3x$?*#07w}l|Ku97y2jdy3I+UrI zBkN!VECQBYB!eiGFuB7qHLMC3CL$`AVO}!0&f=Zg`8$1)h|eF1++kj&-ZvOsJ{Bvn+1qORnq;qXRq?k3~aXA4$v57XeA5h)YwJR&I z5?A;2bfD+?Ak1s6lJ*6#moDEBNO@&ID2%{IZ3ra7uX;bIWxwAInScl(dO}1Faz+@l zGt_cKCFhcl(9f1j2pbS%R5U$~Y&v`t!9Ms?T(|?!&VAady{e75JgJtmEh&%dPpm#` z^$EMr8Zg-=mn?wv^D#lGG^Ph_EeUr%Yc!(LW9<1UZYOe~J)vHk-EQj*d90HbBs6C- z?6;5iEnNrmn=h79LNpu^GolzW^O0~=NR=*L6?ke*yuInlvpcm`-*lp*^T~xU0 zzv0(y^kTW5<-%UmUq#|#B;3SmS3u)pb%Q7ukUZcRb(!{oVlH@YJQAHPHCrg_V7c-n zyTfVUT^2T3rX+oRtWp`PaHx^WXps^h(1e~x@#yYuxiWNxj_3x=^Vd-a@-j3Ye^rbC zrA@h?fM}YOj8_>BYPh8-#fJtP!8cm(JSz@Q&S?5iMw`%Y7_Y?`K~dm}Q+}c8zWZl` zo%yKV_!+IYWRHk`UDL`{g>q8^Jg+{TUaiqJZ%7V}xNe&r9dq)T=f3vjpp6Tq-9GMX z27APq6T|v&u|qEv6Vpe`yL~>r{@lp+%w65ZOzT~EDu$U>aEBlob`A1&p*+*t`W>~^J-V+f1^QwdPzNp_(@yT2 zRgXeZ1TH;``t)*byam4v7X1Fyw(ZCoC-%Jud0Rc{R;7Gt^0ymj}#CuVx z0x(Ri1YaO|%gST40d*hrIwZ5R+;P6sv*>^O)a+Yzr|Qw`A@ zOf(<_m#wLn8v&a1ON~vwWW2G%i8YMmqE+oD$kvum`2@#BQ0M=OmIP<}(nfP$)Df&Q}l75+iPn`=;P_6?W zJq1UMFHjvxcmP0%6DX9}IUqZ5l;bgQiZ-HRI4hI|XA&i!I(X#+X8W_xKJ~~Ww{3cW zzb9zh`EYHJd+y=M&G#THY3V%IBcbYA%EUnk*|UIaG#TJ;gpElyc^BL)9Js2$*@W?d zr-4H;HKCOE)tpi1Ei>{-JO=5L4V3veO1*K7Mee;?=O z%0ubO$jIdMv~_5qcTtJ%RnA@e)@XlgdiX~_8qWLZ`Kcezf%lV#r)0DW5-%TncALcv z3UGij;q{1uE4i310GliP=9W@l^9rFyGr{vIX2A~kE?k%Eb`5`bi#7_Er9 zd$rxcg#)~CMeA^DXwAswQ7^)d1F_ufT>p9jdQaz2Ye%(x)1bBM20_?!cdS_u#(E+; zUBYVe1%ff1CYiMQxhGDY+KL~})6#78r{@aSy4nS&QM9jIv9lv)wzN)9-B%3pyvICo zXx!|}@Z%lD4k;9EFZOUh#sEu_2k*hGgMP3bO!0Aw@Ho z;t{XL=7bY$;@Gv}Ey1pB!0tr;-`hUfcbg36!oSa#lY`{utA4ox4PVya4}Ga#O9Tg4 zm7lE5654F||F6x}gI8^?meXkqR~_gNwfpxQX>6@*fdcM93zX0h%lBC&sooT!qc}EM zY3U&tNErA5KcpYS;&+`f_Q2!3j>_>(-}(iAN4nUIFN>@SLy#)beF zSZ2BPnf(LltP3^dLz%K&5G29mXlzaAUEZzDPOE`4)2&9zL9Z{w3yofX9Giu_*X%}F z@G;;x-BlqRBm}CoA}t}lwg^Ivq9JN@4Ev8Veq=NNStTT(;Bbi7aj5W3)UU(efEQpX zp4>WjsHqqAlhszgMeSCpCdxz2UKN~5g`r(`2hx?hr?)?t${1A^2X8wms%BezTk<4r`nz45;v1jzi*6b#Grras(0c6iKcVru&w-&Me^FcCa zV4BsfM;7uTZ5}2hzr>`GcYT|awV%W@%yj^x(G_yy6dkE^$cmT$2rYl9LhzIE&60ja z-&k)x&k4g?osKhHySXpx^9?p@)E09ziMm`WS#=-Zp#i{ zZ*f^HX8rbTwl6z#y+M;{fpunW&qv)|E8Nm#^beXy{((~mIdDlu0Q|8=;6zx`5Y^Ws zJEs81tg?wB?MV1jl7de;aDdLq!7Qc$kp{nvsgQu9D%!Hx1Sp`o3C$2pe=GvHqnOF+ z;8$FCb60zNQ&?}DxvAAH9{b@BHF`&5d|%WF+JCj1C<~wHE+j~n3lCeN-b39Ssa#=l+kq-&v4fT==Eou- z3o3Yt`}S2e7MI(Z3mCj1XJ^~O;LDYX=f{WLh~P?zgTn{YQJYQO_XK#8^%~f7EY5ir ze9In$B#rQ`JyCU%r^E|xX^P8}My`dg9s1*%^Crmp?x+c+-8gKlAX=ud3Sf(Dw}cjs zbbQi!MTG`=Vdy!$1t5zePLN_ciKxBRo@JlM2K>oxK>cN7>=g`~t!}r3xja#EK?uv~ z|ICdBu>Idv(#dk?bm`?6U*vMaIh{~Bw&%c$J61M|FY(VMi>2mNq3~blPjP2=%uLQa zct`CdH+g(WilBzyS#%52PZ#hD7RJKxdNpLTDuD4xBS$|JM2G^>NUPiCCY+BvzfdDy z)&ka|tPU5T1PeiI0m;6OSL>jID3O(eKrQg*NSq-@uL7AD^_)8F!RR9V#OO?kIfki5 z2Vx}}UA&)4ga_f@dczOTIwc|yO7dsr@+?vjQil-kT@HcoHyjr&w9B!b7hWyz)umWZdApZ)XQ;$^& zgO&4`G0lbwShgxeNy(|lD`BC5H&=-u5mM!7aih<%i9s~8l`T^AlvXwremK@4chTRh z5;2J=EImeNNz#cV75UiUh(QxD$q4tkzy_;Cx&_Q;B*m0mMF=wRd65W*M8ie6lbZWp zcG#vGyVdcnclC}x|GOEjknD~gD5z`ys1Z+bJ8pfbEw!%y^S!ky6|+UTI#i3jLbEq;Ki;sXQHVcy?O0Q^!+2`HN$lwi_gjV9 zUy36C?Bot<_Hbe2ym8@nC{*`L?!Mu#^Amyl|7gZ;u!;QVZ_>FExu9>~$>Z04L#c6F z&@)JrSn##$5PS7N`X_*z9Z;-SJXuYYO3la`@OrNq8yPMQl={27nv2Z^#I6&egg4*~ zAm81nQ8T`RRZWcmMX>rvYF?p*@z&t532NlcQ{62tfP{abd6cv;NM{4DDbvld z_K=N)Wdglg;M*bHAbx3_njtId#x^RSB0UE~hZ8V&W2vaDHWN+GH)VVW~e|0I) z14@JV%C_cPtdpVGwSO0EHTQ{r^PVM|+Kv+^c3j^-FwlQ}W_38(9!H`pFN)lD7);6X zpy2b3ru*O^dtg(iIXQN4b28QM?$eEDI@j^sua+rl#S`0ioLD({-L}D%oTsrRU)8`p zE$FOFMJE)O7{sD#Q+Yly*QIS(vGaSfvHJEXzLti8;fKkY6B#e#X`m#sBQwRie-) z`H$X;1^fEH)!(;s(C@Sd?zwo+)BMS&a2VVfv^fJ01ngXSb1(jFu7pArr@zTjdu8)I z_u$9=2>srCJPyXJ>wAU*?R*eVelVT9#eo)a%`NGwn&Qdyw$|ydYMbZffYo)3kfhNp0@=n19Ch!r~3I$kE#Enpp zaP`RTC7%!OhYA>^uF5{NDDwp}$L8NcPb~qoc>mT^lbN52)9V_YI%=`Mm=xn<4^yE_zL1Rdq+E zbwy?1M>F}=!=L`l*t(YVt#cE9{O3&vlM&*HLapj_w?^iT`NqiM%;F**VkD}G_UC_2CTkVD4mXl{F1fl&gGNj;juoeM&t2^#NmmGS)=m= zhRbD_E79EBwbqmK6{4v^Bv#%@^8|6}uOKd+Ktx6p2>~|+@)WRM!RvpI5$34F3*iA?a&e_ zCVozN7yPG2?9$D!QTxHC1!nvHzG_dU3-1~TI}S??#T*sA-AJluq5wlK0DKN1kG{dA zhvi0Aj!c9Hxd%lZLZ2)(GD~QmjL{ybXt!a|Dc;6nZuBmDMRNenCX0?7knHa+sv&@R zA4d}Xr#XK2#0~%XiG6!G#l_d3{*6ZzJ?}i~GMGI(Kl`}@O=`)1>;F6Mvs#n3;BDXd z)4gD;)yKCy_UH|v+5@*2`kQaLq3c?uvsU}o-1Ft9L~-BYa~CP&SHQJ}463?Uxs^7AzMgugp|fa_FoQFZ{iNSkcRwu~o}4YFS~5Nnd3c27!{dhnON(`Jd@wz4yLQjz^M?RweD!QPI7xvh zoK}#3nDcDzGs7S%`|f04770gq_aGD15DxUCL=rYu2i)`6L-9j2j2H*xYO>lxxS5H?-y~%9$c+5b4*t|Ybha5AC`^Yuw z96)BYA@*~r8bZ&&yH?0&Tav`q37?HK;tE^Lk`Cc22WMw?HF=uXRdPH+#z6q7HCnL+ z8jVV);&kvv0ox-YGYP92imwXh0Xehv3LQ^bk}~IrEagbM14a;=9kZ;*mfcZP_&48@ zW(Z>@X|F3pq@-aq#bg*humY^j&0j|&(^WA4sOclgr@cAZjZb6^^P&+=FyIm z`}Upe7(J&o;X-%qJTYGNZ`sn%_KQb=&j3ReR+n7gu(c}Ej!GC4sz9<3@iZY~$oORK ziY8bCGUGS{oq+R{D>+or0x5b_->E&vsiP%j)TIhWO8{KAyi|MVW&Y8*+NIj1gQBPt zPM;BwiJyL{_V&v!bN**)mushMm!IK$=asS37thYWG@5zgh0G`~+;fk>SI=HNJ^$-x z@4I;MzGqL8ye7T>F{Mr!z}_~c*oGVua1n2wo){k*>_Oc;7yt>%A8|Plq3!4Tf%vN4 zv;|hrwoSYqHX~|CVHH~`Rtx3;xC->h)KP2k(+-Oh0YAV!DPKf{tqF&OjxYzr6Mz{^ zIx?mR5E~HhM2dm=#WV>>6YvG#HR@|%po!lB8IVwa$W2GuFztrz;O{3D(6!*}j)O6l z{z(5JhnUo99HBZAywO&t#=r}5Fp$a(Hv_2gN}_}RT2m*iWTU8RjKm6GZuV%nNGKc< z%y2>o>B*Q*$gcFZ__TuLZrQrc4r8c1H_9hAHJRPK@S-N=^+W`f3cO%umBnaD>QrLQ z?R)UNz&{`HaptJ)`MY-CpKJHn_r>itevtP>0aUpg6%f^iyk*X0vWITHB^DBfG~2Y} zMs>($tex}afF}b$3VlG-@FJISK7FTG*4JbnBa@^L60}-5V?z2nGR3*sQD{G!(Ljh6d3F{|C)S%wd`8f{-yTS%a@pmW@yaJp&7#7b`&+e|E;79OrZwH6b;HEclY6u`T+nAur!|zK2s0Z;H4YMBgw=vyCIskUC<~*g1?*8pxs03Tz zzuPxfZ~8sHOu_4sz3|D~C+o6os@CU~3~-Fl+@EJ- z^B8xCVH)Fqx)A?kewV<_(w=~`#H|xGRN`4@PEN+-5y@&xgeoSLK|tDe#2L^@_`6%c>h4+M=bI}}n8gQox~-u^*9N=r;7myJ$2hygB^nyRZKTr~ z9St2utJ59uMxBxgt|t(4i#n4{r?Y}{-)=OTwKkJVlX3dsDwE*2GphN9WgbBL#;!2R zea=42J2!U4qo(le!vaDPv_GDLzMMep#sUv|V^uI<3{q7K5tEpWdYzyphUj{|5U8bO z%AsGuC`ztS8=3A924HUGzQBzP-??w!-mh`VT|d9!*<>>GwY~fH-2O+r@TE`R*?srd zo;rG0<9p{iIu1PZ4EG8bYP{>{Q(r$?{uRSdku7>&`9Hx)<-z>z1Z%}c#XVJP0`i9Z&LC)J&Tv(SKw=qXbAGoVr`DX+Wh z=wE#G@TX5+yKQR1r!tsP5-`&sIuF)fI-}8Vntpfe8olNM_lVgZK`mpa&khR{Oi3Y) zrZbAtmf_9~^Y3NHG`dD#d$>0&Hu!RH?Ap3*boAh7{%~_NJTPhGLo0i&Vzl=CV`F34 zth={&Y-~IKL%TV&apPg$Xtx`|y!>NR11|((9U65oqLC(R@9J9ONS4;99W50EPi6UL z#<=XpxHQ3!zs`)>Nb{FWvmA|-ZP9D+DN=enf-<$e3+&+!5C9*a#lrAz*J zO?x>KcG>sc^u2&Nz~=;q$v2tJRx6>TF2L_d-HtwNFg=!Ru(-XZ#}Z@f@9L;UOx^|y ze>|ZvI9%~qnO*Uw*&PX0ktTZr&0mO3sS($+V^)V1tHBHh&$``)eE>aHa)Pg%)I{`B z1A4&gq7Y|Lwm|WkSYhR8D1|^}zluWkt7yrQiq)r*3(GQ0R~V`XA`++t0zH!A4NunQ zr2F?8xns49E7r_R4^Hj(JTw3B&RYx8+x%$ZI|nBwEBtKjR5p9!haKkkxP&v2sJwE= zy4A_ewNqoqq}p#@*?vsYgL0qhP|NiM^a=;&K{Gsks}(1!{?)7cd!a=a!84tLeoBRK zT#SVsRVT5}Rrh4E#n~;qq^xqBRA`PgdB8t{${%_Kmq~^L4NlA+WY9yk*6K?Nt%^|? z!S(_{GtCaAM7nbfDOsqYha*&M%o>_Ml0+7{0f1>Z;pv>EW0&Ae26s2sr=y9pqZ$hQ zxF2;!SK8=Gn=8Zu6J$-($|eOR+NKi)qfrob=1I3Cxs6jkoz5ps2Q-?1ZnM?GiK5D2 z)l`jAv~}v3#^r5aJCd(G`m7)f4hh209*t(tIgRGrZk=Y&k94{p?bhjb|5&H{@vJ1x za=-+>ub4A(_vr-VZR>TzP+`cC4vyV>m6uT)!>s64Tu1oP-f9P{Rve2)P`Sa|fWMeX z)9VfS5GLi zoKEAkqR{SOM(b^g_ob&cH(G3t28T55bSCVy6$;bRUq1TOiO)B9c1_g&*V9K{vF>?` zvxhCV|F5`fkB_R#@Ar2nnLNoPnPd`X^0<@9i_9Z4XI^(gh}ldsA%Fq_Apswgkc5B; z2mu6Hmfdx&&8i>kwk*4~twmZbwd@VLu2O65*8QwYt+lResa2L*YptbfUDqPT(C@kT zoZzeN`d0&I&hMW4JKyvA{q8;YJbuTC?YJRFSxIx-@RKa+jTo@(as~ z&f{Uk<9KA$Fi?qAzI@5&z~I2(@++@^Z5}*~Bixn-KlcJB z<#{SutdwJPdxBJuo20yA)w>E~*&2pyuuZ}dSBeYr3X1cvtWGK8oBf*= z96XDo(NLZI>#hXb0U(7=>~DvFn*;w9Sl(Aq?7*sX?7}Wo=b>YUIbqm~%R}SxphB37 z!^T|h%`Nmh@Y&=s+lgaca~%ba+=5>{8;adw!{X)zUnmzPp6>sz@>@MNQ`a2aA{iEn z@SX+4*&On2$6OdDz%ea^m2H?V!;2H=qM_eag{M1S2zWy{@DecXd*XW+tmgNx11*ak z*tBxx^{sW)shT=!*5b+8W2Dztm6sMi@sPk*<>j=tHMhoAIW@a_+w5#(YjulFz59t) zR~4xb(bi;Ou&*wm!%}l;`Cyx$)b`fJi`-3P3+F$ec^wtZT}|CrUB%^BtXHVOIyb(5 zbrGJUH+MJ2qV?!v4R9NKT`4`fD26r6L4SQ?edGKpygQtQ`TS|Rtf`^a%|{cwuu@1c zu*2W3*c_r@%nAeXuw8+9ZhWV65?frw@yB?wd`@?WuTQYfc!2ivVFgpsq3WW`r8ktWn0mVGo7ZiIy^w`#$slKEn}1Pfz`yXmhUzMB zgR8oM_fzRmDypz6gDVq_(z8uI?Z?fTZ;v_$F1n}eFBB=;X^}1iN6>Q z?`jgdEs%HKfob8+y5!?SeCW@Mx;O1lTMTiln^P85edUcEvBwR z%tv?gUmCo+bwRebFZ$Bc6w03}ENQIYRpzNFu}{#W;JGI=HkMJhW=1~>wD}iiE7Ypx z;m%oZM~>UHowhv=JBA*Nho`#jfi}FAA%*_-Dr_tz!n}ATc9dzw*rN->XXv$>C+r6YOy`ru?@4IA>rVbvq+q+y^`?appj^!h_akjfV zs|zcfmF348U~5xztGCrfas!LEUUi_cV{={D-mvh>p~G$QfmlVMs?K+o+}BpFf$bxl zrve$mnfKpPr_q4e|A{`^GGUtLS8an> zP49y}P+e*6&Z&lVcYn7#9`L(O<5Y!(N_~=>(#W;e?($@sN(k-U;M5AT+;&^RYKoSlgAezq_-yInel+MiYh28S2d}SV$8MCma3CmiDgAT}Q`H z+Z*dV?Wy%OXW;4Axtq_-J_o9)H(uVDbGnhY9(OC>JGSt8N9Qk0 z?Wj^=I|6^7aCgDy?v&!l!TW+c+k2*8#%ZkAaF{rcD{gup_a@-FKI`*zY>z4qddu={ z+uGv~ELKugPQK0TcIlnr6B@k0vkAYqdq2*KLS*TB#R#w0Nwz z9H-QF7UjDi8y}E&?$mOD!^?XH*FT2ug3t4U)7MNM(EQrWE7BA99JXJ;5+P3?{Ibku z9%Op_-d?21ec{yK1$__PLrR&A)2I{mDigER#1;ACd9fwncszl}vFGX!@pxP_{wTtC z!tGGJyq@V@rG{E?0&xrcw-t}=ApCc5{f<&&JD_;f6G-p8aG*IR9T$$bkL&V%lSUo! z`s9w+7ncRo4Z=sBcw3s~MqbVO0*@&dVfei6c3aMrBghix`M5J>?LB3#{AOALOUj*; zA6{N)b}8DhjMtIG;rWLqI3V>7+YjJQ;XZiPUIk5P7Uh%hG>rHr?;+198lDiQQPD=)U6 zg9my4l^xG7=SQ8G{{6xi;bxog`}^RKuF2DC`DUI!9#_OM%Ybm{n{~$d&^~7T$CRLn z$HQviy5M9!;A5?wCEJ|lb(kaSMe^|a=lO+G@m#>8C#*~=FXBLOoxVn=Rb9PD{h`fm z%h>L*y_;j?vfKyl754k`)V$HW!+CGyhw=~PKU>gPkS#b`s24t4lqhG$Jy;X<~&z1R#{Rx>1uV2x}L7; zsk*DWyZY4p@ce!AUvZbX?{%N|9P+$e(_AxI^HOcB_W8Q~bRsqVB4gBD9{r)8aN#+4sH%!4BZ^Mw_R-? zYkxZ28eSc~JA5kqL8Lm;5xFLEAaXQvD)KIKu?{xL4zVZLOYALnG3ttTL^nqdN1u$I zjTOiA*t*z>cz!$^KOBEPF_w5F@jD1<&K_?`xbUD ze7du<^UR{rMbCC=UHh+?xZS>a_eN$gb0BlF zKfix<{}YQ_7w=#E1I?_Pdk#ng&tS5B=w zHP}4(@~VBS2UnlGdgAKW)?BmZ?Ap7AV#C_->2(L!cdb8mP1m)p*IvBt(Dj+Dmc1@} za-@3Xj!|_3+pv4XOJm`2ePi*)gOkeS(B%7@4sH%_zIek6TQXaoyK(Tw*S2uBe|-PhFR$TmFsfDd zDcISFKNTxnmSc`xQSQkbFhgJvT{%AYp5xtb{`Ta*cBMhkI2iyd#;kCDnpH^9=szhd z1gqLFZtAh@Y9oD;(XF60<2!b4+O}1TfNh~RH8nA^Yiir%rmaY3cQ~Xk z(1H_MupjA#wcrjdj^CZ*W;#k**`{n)_QEdsCS@aZbV}jtqtp1>(C|eV#(D+@Y7`tA zYJNo73chh=3$A()XP2@8SA7Vb0>^e>U5V>4@a)1`mkh4?9q#V{`nZW10;-19UD_w& zSs@Xm7lh-#GWCz6uK~smWhd}CvIag2Z6W9>{F^`ycfsF=l58^5=B3$<6hr7Q@-hX% z!+-nn%TwTa2?A5YH=0H7L>iaN!zIrAyYzMNf6gCdlMfi%AcYu|!9oo?cDLp8MdCQT zxfmNGxN(P44Cj=S6Fu2V%)eG)EA)JFlZR@k7RGGqNh2>ckdGRviJGZ}T4@2bVQ4u( zL0H6Sr!Yl`VUP=38L&euiKl3aI&l1QCoRGh`xVqpU!opLQ!n*VhWcTWae$W4Qd&k= z(p9vaR?tcsq*b(8p6If1PfnZ_sV@YjivPI!)6z z=?=P+?!rvW-SiuD4}FV%lYR?lvVEHl(|72*^xJeV{SGFae~<2?-=`yVKOLn%pg*Js z=t24;I!1p?57D2{arz#hJH?G z=^yDe`X_pw{+ZsOf1x+&U+FFSH#$fEPH)pY^e+7ey+^;G_vx4P0sSYPrw{1@{TE%N zk7$-IVd*)(urOYZi7=ePjyY}Ey2nBtj0MkAOH>CxN*6W*oG^b`sk+oEwOXC8x>b*T z2Ufl8$mY1|V-SSd-7D`z75#cl8M};2~eq8to;U|T!3t!+d zfyV?M6F5xZFcF`L_)NrSB7RiFkBazFk)No*i3*&k$WK(@MFn0|;6(*qOyI=?UQFP{ z1YS(w#ROhV;Kc-9OyI=?UR>bC1zud>#RXnm;Kc%~ zo-Xiofu{>RUEt{gPZxN)z)K0dl)y^~yp+I83A~iRO9{M`z)K0dR4QM{Z&(r$Nia!7 zB@vTEToMUMBqgCsLLx^bazr9WByvO|M+v9F@pXi5!*4QHdOt$T5i=lgKfN9Fxc~i5!#2 zF^L?L$T5i=lgM$29GA#(i5!>6afuw4$Z?4rm&kF69GA!miJXwg35lGL$O(y@kjM#% zjD2lZ)g>fyLLw(6a#A8EC2~?CCna)HA}1wsQX(fMa#AAe5?PnXx@9BGO2@1 z>L8Ok$fOQ3se?@FAd@=Cqz*EvgG}lmlRC(x4l=2OOzI$$I>@9BGO2@1>L8Ok$fOQ3 zse?@FAd@=Cqz*EvgG}lmlRC(x4l=2OOzI$$I*7HJqAjEjGO2@1>L8Ok$fOQ3se?@F zAd@=Cqz>Yo5P>XpkVze6QU{sTK_+#ONgZTT2bt7CCUuZW9b{4mG4CMKk~+ww4l=2O zOzI$$I>@9BGM%re#K`oeOXm2*<(@re_zWeWjTu^Y!e=NehQ^131|dK*o?2n#G_Nyq z`qmG5eO}M>kY=n{G2}J6hdr91b4ni`){OkVkujr{U*`8|2G;j`jWFkUYQ>N?p-oSZ zXxy+$79kqHSIDUZrxICD7N$5nhT1hr5Fzjt!9}N^ZsnhIwxo>GdO({;Y9u^m4go6vgT5H7IQ; zT_<2O3sn#_@&hwUZr|!5Mau&5;UX+O?3vEaD@S1fF^jTV8ac}H zY~cUOA)nTXOP^6bUgPn40aO$)3jIc5ThJ&DXj(t&L{_(NME7aaR}XzU(8Ht6li5R6 z7{zS{Rff%cm3%B;pStA;XmO++G)e=CVYAv4q9N1cq~dzSl-%AfpFV>_&5^6JfYyl| zNUkiZbVLst<$+LDXV7qd&RtaG25?jaW>m%1q=mG8Q_j?;f$8afU%zi;)F5Ba2`6Fy z6cVUJE4$E!_;21d$UiXJ>mhK9fmZ4Z3O8;DSep{ z-QxpJhPPXO+Qb5c1%(tK{El1RxXc=cu+;?1ki2xg$jCn-r>uLXtpX|91UsY zHjCo;I#J4MGuX)Y_26E{hYItf^@GNIv-HTBD(XgAJrvS9@vyMuNmj_^%Ck2iGXuCg zVo=`(->9XzK2e{@PS9|R#L}`tv;5hF0?0b|1bkYEmplVq>>Liw6k)Y}(5RUUM!jDS zuAK`uxn9J$S6bG*Z&XU8l;;loJKi#ya9uYNprxc^h@wh7%DJ$v!l)C;e zCTP5Rxq^54bdUFAjqV*58Sw?UJQxkgb`vkVY=%}|TrY1co+Xj*xe_+;6851Evi4lT|k<&YChQ&e{>c&qDEpGdkv;Zg@5=eN1^~aA=0;uHnb? lF;%%i!^5N(?*iZU6FIGUeS^J^(WP$~Id{(FVt`Rm{u|TLsu}!AXP+E zv})s8XszXKwXa&S)7IKescosX*7l99R$G?_w7v?NC%^Bx&rC7|(E7f=|L^lpa-Zk9 z`?>d?d+s^so_oVMW6Z|VOlEVZPMtq{)pOIHX3~v25n48F@|3AkA5-983xDXec_W** zHg8HX#uvihecqa7Yb`$*a~)&Wy^KjmE?joS+JOO-B;B|Y@umw`Uvs>da>d0W;5qQ!4Qz zJxL+bkEIe8*8}j>Q>BETG1+ht-^o+}utRA<*p2#Ix&jHe=hB??wf3sZuV5(_`d1DH zgI+ncCI1s*Tuw6@6DFOB@-mE3%l-{_4z<*f9!g8!dcoz@f1eyoO9;V5yN|*Pk0}XYPFk z!g(%@Qka**;2iW8;b{R|Dg0FbU_E9^hd3H%a#EV5;HVvgVS_k;c*=`1YN*`2lhZm3 zqOTF2Pfz8N%lA<(eJUSDWevumUJ;MocT>zZ5W08%2JkP2szU{CP(((>LmzOmB>ZOpelu zIw>A5mu@gGU}>QA1RKFi-$*aQL_KL1GNuOxs0@)VEz%g?77_AY_{e55-&2X`IC z!*9krPH>;hA+4QUe(ZB_4Z@L!DgUN;`X-m}3;G6(Mf9flyest6ciunvokm)?oZmzF z@?{e2C{v;^ys6AQy_IN=B99>#C*fPn3ra`%a_!FN6aIXi^rn1ymrrZ@gw3bA$$zqb zqOxiHDSsYDDkGmZpD$nT@HfSi%fmt6l*S0Iupll)-&7{*yFioy4w3x%GVEpx@jWf@QO?itTs?#7)d3a-Ug&FLt_)FMnmOp5gGJy@z7B*(^RVW^e1dkQ zkMHw*dK%Ayu_({yrG6RifN!GjP=|nt${60CMrjDAK)0HZCYpnJB&8QF&0_TaoF9-S zu?&_mPAU0&@X=Qpc>I^~UdvKIk0usk``F{`3HAbeHC$CyQPtgN@2lwR?3>fKwC|F> zYx{2LyT9-8zVGxM?E7=y2YuRM`{9bijfXoA&pEvG@Fj<@J$%dI`wu^U__@Oe5C8e_ z2ZyyI_9GQXI*-gbvh>I$N3K0`%aQw!JbvW4BL|QC`N#+Vf_#9QLu~J`8d;ySFWi^v zo7>mjx3(|cx3jOOZ+~B=@8!PUzP`iku=8-}aMR(`;kk#q53fC(KD_gA&*A-tGlyS3 z+m)8@1~El#u3as^j;LR~)}{9CG~D_9MNw(aQga zKO~TeK}MY%7{tgG{veXj;r|am2GwFztR{2O|5v~?px`g+cB0=PQ}aFOx^-}vA95F5 zA7=4<%*Y5_FJ|j%P>qdnh_@iTs0Qv3Shg)-OV0=S+zU1vekc4cfZ>81?nWLD;PJf5 zm^TgA&zNr~$ZdkLfD=nH@)f_xSjk$*;M3uDgT;zqnj*X$`6@snD%LSpiMm2N;QAN~ z_kcBPVyrp@Qi?Q@UdCdRu{^&CvWYrt=QCD^e09&FD^N$nM_`>%e`5*`?~&bbh->n~ zJ(9*nTC4`EGNEOm%t%U8(?hP3%1b;hjQAV0Nc?8hxeG3 zaPKiTHp5uQTE@n~b#}l3uJMQ)kGfOHpF%kkn&43O#D#F5Fg6KwPr4VR9c4{M`YDK; z3jZ{uoAx?m(^2k>9gNLvXKdDEjCCQ+Y~-2K00%hd9AfOW{fx~8OmhL>=?SSyfsZaC!Gt-z(=`WU+-&Dfn0#_n3e*q()q-CYLpelpxsjC~b#-P^<1eJJmK#NGc1 zV_&XPb2-)pD^|e^5@<6_cHeE7RC;w7<*1(><1_>^E_ievcm0P?8kubdDQj%vyA=3 z3HKCZFYIRQXH9UujQt#S{T$`}0_FTN4TrE7KVs}9q&bK>55B|Lul6(cGRpdO1Kd`| zeq(~e`?pp&g#Y$EXw}*o`yJwccQ0eFbi*Ov?^iSS>U6j#82bal{s6dMn-2#V{#Xo$ zI$lq~{fx0cA?=^g&OdKq?7tBAUym`?3z*+P_+QpC_SX>Hn~c4gX6!Ab|67K!w~_Ac z_ZWKz;eUUXv46n53-{h3#@>IKu@7En?4O7`qA>R1M~r=hy#Got_OTNVaQ-*)f3gq` zWqlf9>?rCwhC2Ie;GSYEYlZ8Edx9~|1c$Hz6P6|~v_elnBK`=R&nMuzUuN8VKI0ZA z+#be@iW#>ma1S$XYhc_CQta5uxC`H|9>(1-GVW=IdlO`OC*!^vIHdJ2gzINKkYT)d z3*#jl84q5~c0(mMGIK+jJFO2k6NLvlqs#h}}L0klN#8)z2^A6*6 zU5q!Nj7Gdit%LiB@#bE}TbkhZGoIMXcoN~QNYfU9dezGK=;@4)al-X6K6WSL9b4dD zWqdqfOo0cRfI27sjPXfulka7G3er!7o3@tm>3GioJTpUZZ!$jX5aV4vjL$A+d`^n- zxp1e$e?~9k^CmMsKg9T%fbFbqIHX;GIu<72kYZMzEPZ`#55myqXbyss&PdzkU-kng%ZaGx-qUd{ORDE9`W-<*I${1)W@@_xo| z#P?RjZA0Ge?Tp_{4)ER51-F;+Tjw*r6ZPHZW&C#J-;MVj3S2+qccSdOkoNAY8NUbR z-HUYhnc!Y!{C@9;sxqIIma{CrC z{*4;OzZrsik@3eKWBglt8Gju9$G0;6ZPfp5`1hya;Q!vUjQ{6qsNQ=S2c6;1ApV)% zjDJ4@_b}tnn&43HfiA|MBZsgbpsdVv#(xMHfA~D(KUU!0Wc>La#(y%O@fT{~-ede{ zR>pr0_Y2hXOT@kS3F8L=^RH0;%c~jx_4$nd=5@w@I~NXdzuUt2E2!)DYvKACfAu5A zUwe%4KcdXn;r@iOKr8s4QQm)bG5$uH@xLJ7o5hU3g}A?UF#a~+dV4S9??m7ZG5+_} zjQ<05{sZ6d0><|ea8JQ~#Q6It>z^jLhZ*lv;9g|>Fxqwm@O+4TAHKu*zfkVS4R9I8 z{~NIVcQ50g0KQKVb`<_&>lp7xn*Q?{2i@S=9gJ(JgXqP;%S_@4CSmVFk{g($tYngU z2omdDCYcd#!MC-SNwz*FIf|L&M40PMCV4uTQXRtTUT0GMZYDM0-H5Up z-(yk}+^8)~YEHrRGpXe%CMDJ}DT(-2W~^` zjDf-D4fq2U%2=tnQ*LW*>*Q@NeQ=U48Xk01IuzADy1ym0rit^WHK~^SwU449k4??k zJX|$cO-EBU&+R{a*)XQ6t~;?kuP)y%}DA(=%g4sNM$ z8a1k^e#^m%NS4_=9;HTdn_VW0>ap!zx91UcR50pxM}wo(NA}d;)_n~5mQGZt41J8L zZE5Hkn1U{CRFZ(Oxk3tb${0}UQ~92RJG;|T-PJKt>+QV$(z%hy+)Jz~xmNJS#48TFsM{-?LHd-bxvg|X{pRq&u74~nC4i>i16LEAiprfpGA zYjeP(qECX_9cOW$*W=U1YvVDXKItrNcS$?{_zh2o=MDaGyL^>DsNJtwjW%Do^}YA3 z3HS=f@249Yh{jnme5ZRV>tcdeh+=o(;eXg_-64c@tJ&As=oIrFZ& z*Gx&Lr>wdAF8POg_#5blBAP!&nm-O!$wspA>@;>RyOdqWZe?F%--gC9nTXZ%DnmK< z`p0sh@aOosD-jbIoje0ec`&&fWsK?xPdf*L)Qp(MwKKIOtB+EDn(3w-9Ns9O~i z7MwnG8-?RZlv&XIJZUK*;)r!1@Bh4bnRO*JmgwqANa8v4EvHWvBQYYGT?tN4>BRz1 zf1&5N7@@!g89ym5LO{@=9>;Y8=^ExA9{+#aKfFGPwby8wn)db@o}%Z_x0EjQWsmb6 zA9uX(vr-n8$U~x9dhk~VKeI!h^3Z2NXu;>n6BHB%6e2u2VJ!ZykHWv-t19}tU-Yz$ zHXl2#_m7V&O!q(RtK+(Yads868*Wm*!~EzJtW!oq)kw}`iSZl@lNpanZn&u|+px84 zZrN7t&ayK4;4x_@`Q;;XMO4{VelhvW%CtX7w;>J6y=346)vfGe)zJBQ9o$eAhcOPy zjwRa6$CvN-8qHjFi;}h1wAb{Kcnn{;+ITEi`fCUk^_(hJ&q1Z=yo*jRs<94E#yX67 zRj)s)V&gd0VVZGcLALQ|_Lp<4{XEBIF-*yma#;%V*m^xSuqeG?H-7=M0Cq%%W9`2Oe>Ov)OMv8yKrI^mZ$ql{A!!3mw_27Y zE=V#cA@HopguAWPAMhKDb__-Z_(TN7;*A`XxrMefxoz4{Seu)$%$=sPf{vT@Pf_T`RlrC#CPDl$#FnvU|VBC$0(E>+3EG z&3xsml}L_UE3bNGX6T~2dV6S%_M9{`E9kgHPa+9mas{tj$S<&{z?nRzH2b4~4m^Wc zVF+o4`w9BO_!IohZO_=<;=$8j?7KUk(S5llK6wfy9m$GsiN5*e{q(ZS6vU4l6&{s5 zXrJJ@giK>(m%yKhRT;egW||O~pGJ&`7b8-QIchNCms)}88aL8Jh{cIp1uu`FMo!ZP z1fne;+5#%k3SM7Kqe|`%w1JI=6hJJrog4j?5Iq!j=b=0AJS5%ev_9?eR!_H>OLzLM z_U#QLoi=0npY1+gHmde37Kgp)+PKl=nC>pM|EJCAEPBRXQZvb74&LUs*^WCT5Q%L-{O+y zQKgd4Cek)Gjy~OLwb&xJT2>V%wrprI+4aOtWs*;<9pGE>o8u|RvPtYh;P$XlhlqF_ z77X`$AlrH?NJj1CJdEBA8;q*JG-T8nm>hL#38U9ZYO3UTNWdO3rg-pEe5d= zw3Xi@nV)1`P%F?Y4s9yVPgPYT9d#3SLD{*L0U{ z;TtVh?Wb0Lp4MH{o@L6GvhJE=Y2u>{DI_hMtZgl~^3m3#ZUrkn?-5E3A!m!Z>183- zpkovvg1$mQawcNKoQ*tW=gtZqYGqCd)D#K;$p113iB1uE#USvWT}QQ7kM7!al-C^P zmmk!=rY+UJcJLry#vkO%BuM>pb)46x!{DkRYY7wGNK$v=np_sv7nfHZO_=eyqLSK zA6ebf$Bo&P&CR_C*7^|cA>zl^hJ7z0?xu#wFzN=D8 zxm(>@s?z1E;|!Py8HuyHM}_W5*Ff>m5U0Jhy?txDx{jjLGNXs}(CVxgu9Q4tPgE+Hm z*9ll7bz80456xzta(cX+@W!t7xTWR-OgnG_>YM~t&_#5vzC`Mp5aKlXsbO7O0HKAC z2iQF2_|0d6y4$Pu5P-bfZMRzac(Yl{IQgfa0V>u;BJRL(o0$1wD7WOWjKwP)2-6y$ zlPcRhIyDY>{PFLvIr0!VoCe;c_}dp>U-X z`pii$Ju=g+Wy~f|R7yuZZjYAv4AYJT}Ct-OfF$ZUBa> zOiKl0HSvn=+j1=4%5yD}dAq5^vgI~n>UcXZJGkl671v`D74kC?HVsgEVUZNBihyAm zQUE~mz%na<71JU=u_51}DT92@IPPX)0eiDweVeDWmD&fpw12L;-h=5Gq?za0HtmUJ zH@-8qs1E38^OR8g5Q^sI0)J}rOyKu$&o1s=bpx{TURBaQ(!P7i1=oA@B4P>8wu#ek zxZHJqz$1GoJ3_W^(*tZqZsoJlG*66B5j&D6kx@x^m6KxfD?_tCIgCRc?kD~(zmgCm zLGhpE_YBio<-2T9r;^qM0TO{u_N5@cU&P7is8f9-5vh4~t?zMqUEV!d@P{Y)%APE6 zC@k9|i%k6)6t2uJRQQTHt`P5Lgg%h*Fr*Hst8>_$J{ZI{mNBjN$^2t?KP8*6_xXu5xx8ufMp5R?P(R-t`{n6c{!t+*z zh;|Ek#vYp1VLf;GZf>~uUhU}a<>y*ErioacK@F{%7aq0y(Ytu@OPe;mq`jlJD+HtQ zUhr^&Zeh93@tZASEHr)@YqdxFu69(=VFRCysjBoGqZ!U;W1gn5D$myEAmK|$NsF>Z zoV+w>31}eE0iAN9QAY2O+;g%zc>2t#7Dq5vTvb&}E*5lHrkrj!I1b0=@+&c(qJcmok6 zSZAuQ496j<&@a6?K6ox1vRks+RqYD< zT9On_zdVf}IStW^#13*WV8wHQWz$L;0cm)|JDbh|f~*LV8N$;2oL|R99**#AT1smo zob=4dB_WB-D3}~I!ATFHzdW%WacH{qwv5Go2WzQzwRrv)ZajWMp{13T_u;Rz^V-VF z@#62k@#FD#t@v9ye*A%@ODWm-@oM_$_3Cy1BS+(+ujzNF@8a7?`$B^{iX2A-2_nA? zfi2=05XV^;D_2G}Up$eFW|Ofb^zuE)bWHkXR4Jm!Sz0O?)x6QD^kOufR`*v0=|sS?#*ZCvvr^VkV!zhLF3}FHf%+=#@ae1Qq<4~Y1EGYK$Ib1 zg!s~&&u27X&4Ks^(L3%}Npx!_-A)We=0v#yzv03fzxKZ8iV6KIX5U&?>^E?%iIUZ4 z2sD^vRg%kOU!B5@iV{&gBNc9vB)i{Wa@joIa2#4=oAl|-xqj_~$h33%zgk*UWGUV# zf3>{T#2buK?AZH?)h>10N)#VHvOV}%c|wR%HF|pgm8k`*=1l5P8ttZ1Ly@=C5?d9s z)R>B@43V`}=0??4tp?Y}Ox0$SH)yg(!|@V7H^}C-GyAXHFva04omv@`|LCuFRM2`U zxCM>41^p9U3cR>W>`h`{m^VWSL0SNz27{ske7TN1dTpM|P6Hn!^*}+fr>rJ*+GQN{ ziKp9Zda}CgnbNv#9^^&{MChK=E|Wr}tk?tP#Q?iZ%$2k;Eo9~}^tmv?g~PW^C$`N)|awe=5m{Xqd!M=ST?2~(mWjdOsXK#yVMN(qP6`q#tg+rQexf|*BeIU)a z^WuJyPR4WVsATp2E{*y77*kZ9 zEB{*SRHSVGm8ThtES`9!v{E``H)^3d+TG_?{b|eytE1cy^QbPxY3KFTWh&NZi`C?O z;777FMti@+U+IRl7B{=SCc93nKp`>jeW38muw(9T3AqySM#x@9G|p?N;IiNy(KN7? zMz3hIS5SaXrGqD(NIR0ZMnJT%%^~}|cG(Ez!3#)*o{{QjPUIVFOQ%dccgC0*WnAJW zL*1k^HZ5-%bN;%C&2vpW`=;dB5iu4SR48yF$;K8{SY`7mu6c z@q{10W=zwHuav3wid&;5tHCUlUgeVf&>wKuUfEVuUsS%XZ2RPvr>;HI=<(RACmN-M zR8(DJD^lePC9|rUrFgR?>hO#VkFo8}zA@jt{ERalZl$!LP4-GTT`1w}QNUcvuEFRv z`)NyzRG!e-04~~Y1DK>70lGq9rD4J}>V(1*UxcCtBUmyi-Y8Q$NOTQ&VfJIlBRI;7 z5Dr6QNIl|8NTfO>Jf|kZVh7n>hL^)`@3r1BaPIKjxrLrjf8A>RDaI{wYlKG)6-7R~ zsZQ}Kk{T~BDVLo#Zm@cc<&x{X<~boVS5(zfvp1s3RbASf6EKpp>+IFV9s`#Yx#+I& zMz5zL9IUgaqrnG*_=_qm|JBcwfl`bw=c=uU^R>Nm%k4_TeDjy|&K2eKwx!u8 z9&lbdJ?yJ@)>!NgE_vN8+*}$8+Uxk4EBNje>!s2_nOCtE+ie>zl!9&!!I)?QPMD&P zm$5sb#Le|%L<#tZbz%~WWv&yUZH6NLl>OK#CBOp{e~$&fuqQd03DJfLrcWa}IvMu* zy;z7L)WxyINd`m}Fh=l&6EWmHUGLkeP{6Vc;Xq->+AS`1T*b9>SJ#<2Cf!N<)o7Ms z!Gj)CiteiY$f@_OT4C*IODVyil4|R)+8nCf&tw%_BEv!z3RSN|pG(k%hYGrU_Ec^& zNRpzS-nJ*v_QHeHPu}Iub>F_}G1*vdGR~ZSdaG(JEwXM{Df;~AK)j(<_O<)u)`qw* zQduoY)s+$7NdtxaGEAo-cGn7Z5yN#ApXWD1&-5uowpb7bR54QcA7kWG@gybdQQa&cxCKxup2Av3_#{04Z^J#@M&a}P$M<((Zx{A8 z!Ue=%xTpWEzWzKIhsO_xc?e$$ai{S63-$76>gtB?9usV&`qp=Kn*GE5C&Tx`^uyza zw{^ImGi-hkYkP`^0r5vgoSL$EjuxaoKBh2L;dk#~x%`TgefEDi7^(~cmE)UEw*l#i+5f-;!v^P%ZowUbhH*3Av)CifOJX7KS6#d|_83fqJ#8VL=h2KMI zGYTbGm=Q=0lfc{$IDTn;IxIgLZ(Z?)#!mln$0r3A(um zzBIGw6?zmj=H#CkvRoT+C{T=_kfQQ!%8T;loQ5;tH?lZ%M{aG+z75&bhJE`sNSO`$ z`0eget1V7SqB@uA;kQ4UkJ-235xxryG*uzwDPikrWOi1;8WASslh$U4RY{JHgggsL zMaZ|PI2Ise8dMEpuPnW`XYJY^W$n>4PxVOPCO#DnHKfqe+Y7BA6(=QJn}un5MkM7S zkL?&Gvnj|DI!4xt6BV*t)Zv0YV-+(%$}7QcBMZ01jlLEiPk>A3;M^g%K=cNDF6d!7 z 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 d0m^$ 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�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 Date: Wed, 7 Oct 2026 00:41:34 +0200 Subject: [PATCH 002/281] =?UTF-8?q?key.ui.fx:=20M1a=20foundations=20?= =?UTF-8?q?=E2=80=94=20threading,=20fonts=20config,=20theme=20system?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - FxUtil (org.key_project.util.javafx): JavaFX Application Thread marshalling (runLater/runAndWait/callAndWait/assertFxThread), the FX analogue of the Swing module's SwingUtil/ThreadUtilities - ConfigF: font sizes derived from the core ViewSettings (sizeIndex and UI font size factor), mirroring Config - Theme (LIGHT/DARK) + ThemeManager: CSS-based theming with per-scene tracking so docking float stages follow switches; replaces FlatLaf - key-light.css / key-dark.css with -key-* custom properties --- .../key/gui/fx/configuration/ConfigF.java | 67 +++++++++++ .../de/uka/ilkd/key/gui/fx/theme/Theme.java | 39 ++++++ .../ilkd/key/gui/fx/theme/ThemeManager.java | 108 +++++++++++++++++ .../org/key_project/util/javafx/FxUtil.java | 112 ++++++++++++++++++ .../de/uka/ilkd/key/gui/fx/theme/key-dark.css | 66 +++++++++++ .../uka/ilkd/key/gui/fx/theme/key-light.css | 62 ++++++++++ 6 files changed, 454 insertions(+) create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/configuration/ConfigF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/theme/Theme.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/theme/ThemeManager.java create mode 100644 key.ui.fx/src/main/java/org/key_project/util/javafx/FxUtil.java create mode 100644 key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css create mode 100644 key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css 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..7c89d8d2faf --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/configuration/ConfigF.java @@ -0,0 +1,67 @@ +/* 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. + */ +public final class ConfigF { + + /** 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 */ + public Font proofTreeFont() { + return Font.font("System", baseSize()); + } + + /** @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/theme/Theme.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/theme/Theme.java new file mode 100644 index 00000000000..78e87d2e7e8 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/theme/Theme.java @@ -0,0 +1,39 @@ +/* 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.theme; + +/** + * The visual themes of the JavaFX UI, replacing the FlatLaf look and feels of the Swing module + * {@code key.ui}. + *

+ * Each theme refers to a CSS file living in this package; the stylesheet is applied as an + * additional user agent stylesheet by {@link ThemeManager}. The JavaFX CSS engine is the theming + * mechanism of key.ui.fx (no FXML). + */ +public enum Theme { + /** Light theme, close to the FlatLightLaf look of the Swing UI. */ + LIGHT("key-light.css"), + + /** Dark theme, close to the FlatDarkLaf look of the Swing UI. */ + DARK("key-dark.css"); + + private final String stylesheet; + + Theme(String stylesheet) { + this.stylesheet = stylesheet; + } + + /** + * @return the resource URL of the theme stylesheet (relative to this class) + */ + public String stylesheetUrl() { + return Theme.class.getResource(stylesheet).toExternalForm(); + } + + /** @return whether this is a dark theme */ + public boolean isDark() { + return this == DARK; + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/theme/ThemeManager.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/theme/ThemeManager.java new file mode 100644 index 00000000000..5f90a3e86ce --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/theme/ThemeManager.java @@ -0,0 +1,108 @@ +/* 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.theme; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +import javafx.beans.property.ReadOnlyObjectProperty; +import javafx.beans.property.ReadOnlyObjectWrapper; +import javafx.collections.ObservableList; +import javafx.scene.Scene; + +/** + * Applies and switches the {@link Theme} of all registered {@link Scene}s of the JavaFX UI. + *

+ * Counter-part of the FlatLaf look and feel handling in {@code MainWindow.updateLookAndFeel()} + * of the Swing module {@code key.ui}. The theme stylesheet is tracked on each scene and replaced + * when the theme changes, so every open window (including docking float stages) follows the + * switch. + */ +public final class ThemeManager { + + private static final ThemeManager INSTANCE = new ThemeManager(); + + /** Property key under which the currently applied stylesheet URL is stored per scene. */ + private static final String SCENE_KEY = "key-ui.theme-stylesheet"; + + private final List scenes = new CopyOnWriteArrayList<>(); + + private final ReadOnlyObjectWrapper currentTheme = + new ReadOnlyObjectWrapper<>(this, "theme", Theme.LIGHT); + + private ThemeManager() { + } + + /** + * @return the global {@link ThemeManager} instance + */ + public static ThemeManager getInstance() { + return INSTANCE; + } + + /** + * Registers the given {@link Scene} with the theme manager and applies the current theme to + * it. Use this for every top-level scene of the application (main window and floating + * windows). + * + * @param scene the scene to manage + */ + public void manage(Scene scene) { + if (!scenes.contains(scene)) { + scenes.add(scene); + } + updateScene(scene); + } + + /** + * Applies the current theme to the given scene without registering it for later updates. + * Handy for one-off windows (e.g. dialogs) that should follow the next programmatic theme + * switch but are not tracked. + * + * @param scene the scene to style + */ + public void style(Scene scene) { + updateScene(scene); + } + + /** + * Switches the theme and updates all managed scenes. + * + * @param theme the theme to activate + */ + public void setTheme(Theme theme) { + currentTheme.set(theme); + for (Scene scene : scenes) { + updateScene(scene); + } + } + + /** + * @return the currently active theme + */ + public Theme getTheme() { + return currentTheme.get(); + } + + /** + * @return an observable property of the currently active theme + */ + public ReadOnlyObjectProperty themeProperty() { + return currentTheme.getReadOnlyProperty(); + } + + private void updateScene(Scene scene) { + ObservableList stylesheets = scene.getStylesheets(); + Object previous = scene.getProperties().get(SCENE_KEY); + if (previous instanceof String previousUrl) { + stylesheets.remove(previousUrl); + } + String url = currentTheme.get().stylesheetUrl(); + if (!stylesheets.contains(url)) { + stylesheets.add(url); + } + scene.getProperties().put(SCENE_KEY, url); + } +} diff --git a/key.ui.fx/src/main/java/org/key_project/util/javafx/FxUtil.java b/key.ui.fx/src/main/java/org/key_project/util/javafx/FxUtil.java new file mode 100644 index 00000000000..679a5525dcc --- /dev/null +++ b/key.ui.fx/src/main/java/org/key_project/util/javafx/FxUtil.java @@ -0,0 +1,112 @@ +/* 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 org.key_project.util.javafx; + +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.FutureTask; + +import javafx.application.Platform; + +/** + * Utilities for working with the JavaFX Application Thread (the FX analogue of + * {@code org.key_project.util.java.SwingUtil} in the Swing module {@code key.ui}). + *

+ * All UI state must be mutated on the JavaFX Application Thread. Use these helpers to marshal + * between background threads (prover, SMT, I/O) and the UI. + */ +public final class FxUtil { + + private FxUtil() { + } + + /** + * @return whether the calling thread is the JavaFX Application Thread + */ + public static boolean isFxThread() { + return Platform.isFxApplicationThread(); + } + + /** + * Runs the given {@link Runnable} on the JavaFX Application Thread. If the calling thread + * already is the JavaFX Application Thread, the runnable is executed immediately. + * + * @param runnable the code to run on the FX thread + */ + public static void runLater(Runnable runnable) { + if (isFxThread()) { + runnable.run(); + } else { + Platform.runLater(runnable); + } + } + + /** + * Runs the given {@link Runnable} on the JavaFX Application Thread and blocks until it has + * been executed. + * + * @param runnable the code to run on the FX thread + * @throws IllegalStateException if the runnable throws an exception + */ + public static void runAndWait(Runnable runnable) { + if (isFxThread()) { + runnable.run(); + return; + } + FutureTask task = new FutureTask<>(runnable, null); + Platform.runLater(task); + try { + task.get(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IllegalStateException("Interrupted while waiting for the FX thread", e); + } catch (ExecutionException e) { + throw new IllegalStateException("Exception on the FX thread", e.getCause()); + } + } + + /** + * Runs the given {@link Callable} on the JavaFX Application Thread, blocks until the result + * is available and returns it. + * + * @param callable the computation to run on the FX thread + * @param the result type + * @return the computed value + * @throws IllegalStateException if the callable throws an exception + */ + public static T callAndWait(Callable callable) { + if (isFxThread()) { + try { + return callable.call(); + } catch (Exception e) { + throw new IllegalStateException("Exception on the FX thread", e); + } + } + FutureTask task = new FutureTask<>(callable); + Platform.runLater(task); + try { + return task.get(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IllegalStateException("Interrupted while waiting for the FX thread", e); + } catch (ExecutionException e) { + throw new IllegalStateException("Exception on the FX thread", e.getCause()); + } + } + + /** + * Asserts that the calling thread is the JavaFX Application Thread. Use this as a defensive + * check at the entry points of UI-updating methods. + * + * @throws IllegalStateException if called from a non-FX thread + */ + public static void assertFxThread() { + if (!isFxThread()) { + throw new IllegalStateException( + "Must be called on the JavaFX Application Thread, but was called on " + + Thread.currentThread().getName()); + } + } +} diff --git a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css new file mode 100644 index 00000000000..e483d6b20d8 --- /dev/null +++ b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css @@ -0,0 +1,66 @@ +/* + * Dark theme for the JavaFX UI of KeY (key.ui.fx). + * + * This stylesheet is applied on top of the default Modena theme. It + * re-defines the -key-* custom properties and adjusts the standard Modena + * look-up colors, such that both our own components and the built-in + * controls render in a dark color scheme. + */ +.root { + -key-background: #2b2b2b; + -key-surface: #3c3f41; + -key-text: #dedede; + -key-text-secondary: #9d9d9d; + -key-accent: #4a90e2; + -key-accent-text: #ffffff; + -key-success: #6fc46f; + -key-warning: #e3a94f; + -key-error: #e06c62; + -key-proved: #6fc46f; + -key-linked: #4a90e2; + -key-interactive: #e3a94f; + -key-border: #555555; + + /* restyle the built-in Modena controls for the dark theme */ + -fx-base: #3c3f41; + -fx-background: #2b2b2b; + -fx-control-inner-background: #2f3134; + -fx-text-background-color: -key-text; + -fx-text-fill: -key-text; + -fx-selection-bar: -key-accent; + -fx-selection-bar-non-focused: derive(-key-accent, 60%); + -fx-accent: -key-accent; +} + +/* status bar */ +.status-bar { + -fx-background-color: -key-surface; + -fx-border-color: -key-border transparent transparent transparent; + -fx-padding: 2 6 2 6; +} + +.status-bar .label { + -fx-text-fill: -key-text-secondary; +} + +/* notifications (toast) */ +.notification-toast { + -fx-background-color: -key-surface; + -fx-background-radius: 4; + -fx-border-color: -key-border; + -fx-border-radius: 4; + -fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.5), 10, 0.2, 0, 2); +} + +.notification-toast.error { + -fx-border-color: -key-error; +} + +.notification-toast.warning { + -fx-border-color: -key-warning; +} + +/* dock tab pane */ +.key-dock-tab-pane > .tab-header-area { + -fx-padding: 2 4 0 4; +} diff --git a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css new file mode 100644 index 00000000000..671c2f6a954 --- /dev/null +++ b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css @@ -0,0 +1,62 @@ +/* + * Light theme for the JavaFX UI of KeY (key.ui.fx). + * + * This stylesheet is applied on top of the default Modena theme and + * defines the KeY-specific color palette via CSS custom properties. + * Components should reference the -key-* properties instead of hard-coding + * colors, so that switching to the dark theme (key-dark.css) restyles the + * whole UI automatically. + */ +.root { + /* + * KeY palette. Modena look-up colors keep the default control styling; + * -key-* properties are used by KeY-specific components (status bar, + * sequent view, proof tree, notifications, ...). + */ + -key-background: #f4f4f4; + -key-surface: #ffffff; + -key-text: #1c1c1c; + -key-text-secondary: #555555; + -key-accent: #1a73e8; + -key-accent-text: #ffffff; + -key-success: #188038; + -key-warning: #b06000; + -key-error: #d93025; + -key-proved: #188038; + -key-linked: #1a73e8; + -key-interactive: #b06000; + -key-border: #d0d0d0; +} + +/* status bar */ +.status-bar { + -fx-background-color: -key-surface; + -fx-border-color: -key-border transparent transparent transparent; + -fx-padding: 2 6 2 6; +} + +.status-bar .label { + -fx-text-fill: -key-text-secondary; +} + +/* notifications (toast) */ +.notification-toast { + -fx-background-color: -key-surface; + -fx-background-radius: 4; + -fx-border-color: -key-border; + -fx-border-radius: 4; + -fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.3), 10, 0.2, 0, 2); +} + +.notification-toast.error { + -fx-border-color: -key-error; +} + +.notification-toast.warning { + -fx-border-color: -key-warning; +} + +/* dock tab pane */ +.key-dock-tab-pane > .tab-header-area { + -fx-padding: 2 4 0 4; +} From 323d4f24478f83863b93733457801d7f16f1ccbc Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Wed, 7 Oct 2026 00:42:59 +0200 Subject: [PATCH 003/281] =?UTF-8?q?key.ui.fx:=20M1b=20icon=20pipeline=20?= =?UTF-8?q?=E2=80=94=20icon=20fonts=20+=20IconFactoryF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - port the six glyph tables (Entypo, FontAwesomeBrands/Regular/Solid, MaterialDesignRegular, Typicons) from key.ui as new files in the fx package; java.awt.Font replaced by JavaFX Font.loadFont - IconFont (FX interface) mirroring the Swing IconFont - IconFactoryF: semantic Key enum (subset, glyph assignments mirroring the Swing IconFactory) and createIcon() rendering theme-aware Text nodes (style class 'key-icon' filled from the theme stylesheets) - .key-icon rule in key-light.css/key-dark.css --- .../uka/ilkd/key/gui/fx/fonticons/Entypo.java | 102 + .../gui/fx/fonticons/FontAwesomeBrands.java | 142 ++ .../gui/fx/fonticons/FontAwesomeRegular.java | 73 + .../gui/fx/fonticons/FontAwesomeSolid.java | 278 +++ .../key/gui/fx/fonticons/IconFactoryF.java | 128 ++ .../ilkd/key/gui/fx/fonticons/IconFont.java | 28 + .../fx/fonticons/MaterialDesignRegular.java | 1850 +++++++++++++++++ .../ilkd/key/gui/fx/fonticons/Typicons.java | 144 ++ .../de/uka/ilkd/key/gui/fx/theme/key-dark.css | 5 + .../uka/ilkd/key/gui/fx/theme/key-light.css | 5 + 10 files changed, 2755 insertions(+) create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/fonticons/Entypo.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/fonticons/FontAwesomeBrands.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/fonticons/FontAwesomeRegular.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/fonticons/FontAwesomeSolid.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/fonticons/IconFactoryF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/fonticons/IconFont.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/fonticons/MaterialDesignRegular.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/fonticons/Typicons.java diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/fonticons/Entypo.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/fonticons/Entypo.java new file mode 100644 index 00000000000..f6bb92b64b5 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/fonticons/Entypo.java @@ -0,0 +1,102 @@ +/* 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.fonticons; + + +/** + * Copyright (c) 2016 jIconFont
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +import javafx.scene.text.Font; + +public enum Entypo implements IconFont { + ADDRESS('\uE723'), ADJUST('\u25D1'), AIR('\uE753'), ARCHIVE('\uE738'), ARROW_COMBO('\uE74F'), + ATTENTION('\u26A0'), BACK_IN_TIME('\uE771'), BASKET('\uE73D'), BEHANCE('\uF34E'), + BRUSH('\uE79A'), BUCKET('\uE756'), CANCEL('\u2715'), CANCEL_CIRCLED('\u2716'), + CANCEL_SQUARED('\u274E'), CC('\uE7A5'), CCW('\u27F2'), CC_BY('\uE7A6'), CC_NC('\uE7A7'), + CC_NC_EU('\uE7A8'), CC_NC_JP('\uE7A9'), CC_ND('\uE7AB'), CC_PD('\uE7AC'), CC_REMIX('\uE7AF'), + CC_SA('\uE7AA'), CC_SHARE('\uE7AE'), CC_ZERO('\uE7AD'), CHART_PIE('\uE751'), CHAT('\uE720'), + CHECK('\u2713'), CLOUD('\u2601'), CLOUD_THUNDER('\u26C8'), CODE('\uE714'), COG('\u2699'), + COMMENT('\uE718'), COMPASS('\uE728'), CUP('\u2615'), CW('\u27F3'), DATABASE('\uE754'), + DB_SHAPE('\uF600'), DIRECTION('\u27A2'), DOC('\uE730'), DOCS('\uE736'), DOC_LANDSCAPE('\uE737'), + DOC_TEXT_INV('\uE731'), DOT('\uE78B'), DOT_2('\uE78C'), DOT_3('\uE78D'), DOWN('\u2B07'), + DOWN_BOLD('\uE4B0'), DOWN_CIRCLED('\uE758'), DOWN_DIR('\u25BE'), DOWN_OPEN('\uE75C'), + DOWN_OPEN_BIG('\uE764'), DOWN_OPEN_MINI('\uE760'), DOWN_THIN('\u2193'), DRIBBBLE('\uF31B'), + DRIBBBLE_CIRCLED('\uF31C'), DRIVE('\uE755'), DROPBOX('\uF330'), ERASE('\u232B'), + EVERNOTE('\uF333'), EXPORT('\uE715'), EYE('\uE70A'), FACEBOOK('\uF30C'), + FACEBOOK_CIRCLED('\uF30D'), FACEBOOK_SQUARED('\uF30E'), FAST_BACKWARD('\u23EA'), + FAST_FORWARD('\u23E9'), FEATHER('\u2712'), FLAG('\u2691'), FLASH('\u26A1'), FLATTR('\uF336'), + FLICKR('\uF303'), FLICKR_CIRCLED('\uF304'), FLIGHT('\u2708'), FLOW_BRANCH('\uE791'), + FLOW_CASCADE('\uE790'), FLOW_LINE('\uE793'), FLOW_PARALLEL('\uE794'), FLOW_TREE('\uE792'), + FORWARD('\u27A6'), GAUGE('\uE7A2'), GITHUB('\uF300'), GITHUB_CIRCLED('\uF301'), + GOOGLE_CIRCLES('\uF351'), GPLUS('\uF30F'), GPLUS_CIRCLED('\uF310'), HEART('\u2665'), + HEART_EMPTY('\u2661'), HELP('\u2753'), HELP_CIRCLED('\uE704'), HOME('\u2302'), + HOURGLASS('\u23F3'), INBOX('\uE777'), INFINITY('\u221E'), INFO('\u2139'), + INFO_CIRCLED('\uE705'), INSTAGREM('\uF32D'), INSTALL('\uE778'), KEYBOARD('\u2328'), + LANGUAGE('\uE752'), LASTFM('\uF321'), LASTFM_CIRCLED('\uF322'), LAYOUT('\u268F'), + LEFT('\u2B05'), LEFT_BOLD('\uE4AD'), LEFT_CIRCLED('\uE759'), LEFT_DIR('\u25C2'), + LEFT_OPEN('\uE75D'), LEFT_OPEN_BIG('\uE765'), LEFT_OPEN_MINI('\uE761'), LEFT_THIN('\u2190'), + LEVEL_DOWN('\u21B3'), LEVEL_UP('\u21B0'), LIFEBUOY('\uE788'), LINKEDIN('\uF318'), + LINKEDIN_CIRCLED('\uF319'), LIST('\uE005'), LIST_ADD('\uE003'), LOCATION('\uE724'), + LOGIN('\uE740'), LOGOUT('\uE741'), LOGO_DB('\uF603'), MAGNET('\uE7A1'), MAIL('\u2709'), + MAP('\uE727'), MENU('\u2630'), MINUS_CIRCLED('\u2796'), MINUS_SQUARED('\u229F'), MIXI('\uF34B'), + MOON('\u263D'), MOUSE('\uE789'), NETWORK('\uE776'), NOTE('\u266A'), NOTE_BEAMED('\u266B'), + PAPER_PLANE('\uE79B'), PAUSE('\u2389'), PAYPAL('\uF342'), PENCIL('\u270E'), PICASA('\uF345'), + PINTEREST('\uF312'), PINTEREST_CIRCLED('\uF313'), PLAY('\u25B6'), PLUS_CIRCLED('\u2795'), + PLUS_SQUARED('\u229E'), POPUP('\uE74C'), PRINT('\uE716'), PROGRESS_0('\uE768'), + PROGRESS_1('\uE769'), PROGRESS_2('\uE76A'), PROGRESS_3('\uE76B'), PUBLISH('\uE74D'), + QQ('\uF32A'), QUOTE('\u275E'), RDIO('\uF324'), RDIO_CIRCLED('\uF325'), RECORD('\u26AB'), + RENREN('\uF33C'), REPLY('\uE712'), REPLY_ALL('\uE713'), RESIZE_FULL('\uE744'), + RESIZE_SMALL('\uE746'), RETWEET('\uE717'), RIGHT('\u27A1'), RIGHT_BOLD('\uE4AE'), + RIGHT_CIRCLED('\uE75A'), RIGHT_DIR('\u25B8'), RIGHT_OPEN('\uE75E'), RIGHT_OPEN_BIG('\uE766'), + RIGHT_OPEN_MINI('\uE762'), RIGHT_THIN('\u2192'), RSS('\uE73A'), SHARE('\uE73C'), + SHAREABLE('\uE73E'), SINA_WEIBO('\uF33F'), SKYPE('\uF339'), SKYPE_CIRCLED('\uF33A'), + SMASHING('\uF357'), SOUNDCLOUD('\uF348'), SPOTIFY('\uF327'), SPOTIFY_CIRCLED('\uF328'), + STAR('\u2605'), STAR_EMPTY('\u2606'), STOP('\u25A0'), STUMBLEUPON('\uF31E'), + STUMBLEUPON_CIRCLED('\uF31F'), SUITCASE('\uE78E'), SWEDEN('\uF601'), SWITCH('\u21C6'), + TAG('\uE70C'), TAPE('\u2707'), THERMOMETER('\uE757'), TOOLS('\u2692'), TO_END('\u23ED'), + TO_START('\u23EE'), TRAFFIC_CONE('\uE7A3'), TRASH('\uE729'), TUMBLR('\uF315'), + TUMBLR_CIRCLED('\uF316'), TWITTER('\uF309'), TWITTER_CIRCLED('\uF30A'), UP('\u2B06'), + UPLOAD_CLOUD('\uE711'), UP_BOLD('\uE4AF'), UP_CIRCLED('\uE75B'), UP_DIR('\u25B4'), + UP_OPEN('\uE75F'), UP_OPEN_BIG('\uE767'), UP_OPEN_MINI('\uE763'), UP_THIN('\u2191'), + USER_ADD('\uE700'), VCARD('\uE722'), VIMEO('\uF306'), VIMEO_CIRCLED('\uF307'), + VKONTAKTE('\uF354'), VOLUME('\uE742'), WINDOW('\uE74E'); + + private final char character; + + Entypo(char character) { + this.character = character; + } + + @Override + public char getUnicode() { + return character; + } + + private static Font font; + + @Override + public Font getFont() { + if (font == null) { + font = Font.loadFont(Entypo.class.getResourceAsStream("/fonts/entypo.ttf"), 16.0); + } + return font; + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/fonticons/FontAwesomeBrands.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/fonticons/FontAwesomeBrands.java new file mode 100644 index 00000000000..01c1121a40a --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/fonticons/FontAwesomeBrands.java @@ -0,0 +1,142 @@ +/* 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.fonticons; + + +/** + * @author Alexander Weigl + * @version 1 (22.07.19) + */ + +import javafx.scene.text.Font; + +public enum FontAwesomeBrands implements IconFont { + _500PX('\uf26e'), ACCESSIBLE_ICON('\uf368'), ACCUSOFT('\uf369'), + ACQUISITIONS_INCORPORATED('\uf6af'), ADN('\uf170'), ADOBE('\uf778'), ADVERSAL('\uf36a'), + AFFILIATETHEME('\uf36b'), AIRBNB('\uf834'), ALGOLIA('\uf36c'), ALIPAY('\uf642'), + AMAZON('\uf270'), AMAZON_PAY('\uf42c'), AMILIA('\uf36d'), ANDROID('\uf17b'), + ANGELLIST('\uf209'), ANGRYCREATIVE('\uf36e'), ANGULAR('\uf420'), APP_STORE('\uf36f'), + APP_STORE_IOS('\uf370'), APPER('\uf371'), APPLE('\uf179'), APPLE_PAY('\uf415'), + ARTSTATION('\uf77a'), ASYMMETRIK('\uf372'), ATLASSIAN('\uf77b'), AUDIBLE('\uf373'), + AUTOPREFIXER('\uf41c'), AVIANEX('\uf374'), AVIATO('\uf421'), AWS('\uf375'), BANDCAMP('\uf2d5'), + BATTLE_NET('\uf835'), BEHANCE('\uf1b4'), BEHANCE_SQUARE('\uf1b5'), BIMOBJECT('\uf378'), + BITBUCKET('\uf171'), BITCOIN('\uf379'), BITY('\uf37a'), BLACK_TIE('\uf27e'), + BLACKBERRY('\uf37b'), BLOGGER('\uf37c'), BLOGGER_B('\uf37d'), BLUETOOTH('\uf293'), + BLUETOOTH_B('\uf294'), BOOTSTRAP('\uf836'), BTC('\uf15a'), BUFFER('\uf837'), + BUROMOBELEXPERTE('\uf37f'), BUYSELLADS('\uf20d'), CANADIAN_MAPLE_LEAF('\uf785'), + CC_AMAZON_PAY('\uf42d'), CC_AMEX('\uf1f3'), CC_APPLE_PAY('\uf416'), CC_DINERS_CLUB('\uf24c'), + CC_DISCOVER('\uf1f2'), CC_JCB('\uf24b'), CC_MASTERCARD('\uf1f1'), CC_PAYPAL('\uf1f4'), + CC_STRIPE('\uf1f5'), CC_VISA('\uf1f0'), CENTERCODE('\uf380'), CENTOS('\uf789'), + CHROME('\uf268'), CHROMECAST('\uf838'), CLOUDSCALE('\uf383'), CLOUDSMITH('\uf384'), + CLOUDVERSIFY('\uf385'), CODEPEN('\uf1cb'), CODIEPIE('\uf284'), CONFLUENCE('\uf78d'), + CONNECTDEVELOP('\uf20e'), CONTAO('\uf26d'), CPANEL('\uf388'), CREATIVE_COMMONS('\uf25e'), + CREATIVE_COMMONS_BY('\uf4e7'), CREATIVE_COMMONS_NC('\uf4e8'), CREATIVE_COMMONS_NC_EU('\uf4e9'), + CREATIVE_COMMONS_NC_JP('\uf4ea'), CREATIVE_COMMONS_ND('\uf4eb'), CREATIVE_COMMONS_PD('\uf4ec'), + CREATIVE_COMMONS_PD_ALT('\uf4ed'), CREATIVE_COMMONS_REMIX('\uf4ee'), + CREATIVE_COMMONS_SA('\uf4ef'), CREATIVE_COMMONS_SAMPLING('\uf4f0'), + CREATIVE_COMMONS_SAMPLING_PLUS('\uf4f1'), CREATIVE_COMMONS_SHARE('\uf4f2'), + CREATIVE_COMMONS_ZERO('\uf4f3'), CRITICAL_ROLE('\uf6c9'), CSS3('\uf13c'), CSS3_ALT('\uf38b'), + CUTTLEFISH('\uf38c'), D_AND_D('\uf38d'), D_AND_D_BEYOND('\uf6ca'), DASHCUBE('\uf210'), + DELICIOUS('\uf1a5'), DEPLOYDOG('\uf38e'), DESKPRO('\uf38f'), DEV('\uf6cc'), + DEVIANTART('\uf1bd'), DHL('\uf790'), DIASPORA('\uf791'), DIGG('\uf1a6'), + DIGITAL_OCEAN('\uf391'), DISCORD('\uf392'), DISCOURSE('\uf393'), DOCHUB('\uf394'), + DOCKER('\uf395'), DRAFT2DIGITAL('\uf396'), DRIBBBLE('\uf17d'), DRIBBBLE_SQUARE('\uf397'), + DROPBOX('\uf16b'), DRUPAL('\uf1a9'), DYALOG('\uf399'), EARLYBIRDS('\uf39a'), EBAY('\uf4f4'), + EDGE('\uf282'), ELEMENTOR('\uf430'), ELLO('\uf5f1'), EMBER('\uf423'), EMPIRE('\uf1d1'), + ENVIRA('\uf299'), ERLANG('\uf39d'), ETHEREUM('\uf42e'), ETSY('\uf2d7'), EVERNOTE('\uf839'), + EXPEDITEDSSL('\uf23e'), FACEBOOK('\uf09a'), FACEBOOK_F('\uf39e'), FACEBOOK_MESSENGER('\uf39f'), + FACEBOOK_SQUARE('\uf082'), FANTASY_FLIGHT_GAMES('\uf6dc'), FEDEX('\uf797'), FEDORA('\uf798'), + FIGMA('\uf799'), FIREFOX('\uf269'), FIRST_ORDER('\uf2b0'), FIRST_ORDER_ALT('\uf50a'), + FIRSTDRAFT('\uf3a1'), FLICKR('\uf16e'), FLIPBOARD('\uf44d'), FLY('\uf417'), + FONT_AWESOME('\uf2b4'), FONT_AWESOME_ALT('\uf35c'), FONT_AWESOME_FLAG('\uf425'), + FONT_AWESOME_LOGO_FULL('\uf4e6'), FONTICONS('\uf280'), FONTICONS_FI('\uf3a2'), + FORT_AWESOME('\uf286'), FORT_AWESOME_ALT('\uf3a3'), FORUMBEE('\uf211'), FOURSQUARE('\uf180'), + FREE_CODE_CAMP('\uf2c5'), FREEBSD('\uf3a4'), FULCRUM('\uf50b'), GALACTIC_REPUBLIC('\uf50c'), + GALACTIC_SENATE('\uf50d'), GET_POCKET('\uf265'), GG('\uf260'), GG_CIRCLE('\uf261'), + GIT('\uf1d3'), GIT_ALT('\uf841'), GIT_SQUARE('\uf1d2'), GITHUB('\uf09b'), GITHUB_ALT('\uf113'), + GITHUB_SQUARE('\uf092'), GITKRAKEN('\uf3a6'), GITLAB('\uf296'), GITTER('\uf426'), + GLIDE('\uf2a5'), GLIDE_G('\uf2a6'), GOFORE('\uf3a7'), GOODREADS('\uf3a8'), + GOODREADS_G('\uf3a9'), GOOGLE('\uf1a0'), GOOGLE_DRIVE('\uf3aa'), GOOGLE_PLAY('\uf3ab'), + GOOGLE_PLUS('\uf2b3'), GOOGLE_PLUS_G('\uf0d5'), GOOGLE_PLUS_SQUARE('\uf0d4'), + GOOGLE_WALLET('\uf1ee'), GRATIPAY('\uf184'), GRAV('\uf2d6'), GRIPFIRE('\uf3ac'), + GRUNT('\uf3ad'), GULP('\uf3ae'), HACKER_NEWS('\uf1d4'), HACKER_NEWS_SQUARE('\uf3af'), + HACKERRANK('\uf5f7'), HIPS('\uf452'), HIRE_A_HELPER('\uf3b0'), HOOLI('\uf427'), + HORNBILL('\uf592'), HOTJAR('\uf3b1'), HOUZZ('\uf27c'), HTML5('\uf13b'), HUBSPOT('\uf3b2'), + IMDB('\uf2d8'), INSTAGRAM('\uf16d'), INTERCOM('\uf7af'), INTERNET_EXPLORER('\uf26b'), + INVISION('\uf7b0'), IOXHOST('\uf208'), ITCH_IO('\uf83a'), ITUNES('\uf3b4'), + ITUNES_NOTE('\uf3b5'), JAVA('\uf4e4'), JEDI_ORDER('\uf50e'), JENKINS('\uf3b6'), JIRA('\uf7b1'), + JOGET('\uf3b7'), JOOMLA('\uf1aa'), JS('\uf3b8'), JS_SQUARE('\uf3b9'), JSFIDDLE('\uf1cc'), + KAGGLE('\uf5fa'), KEYBASE('\uf4f5'), KEYCDN('\uf3ba'), KICKSTARTER('\uf3bb'), + KICKSTARTER_K('\uf3bc'), KORVUE('\uf42f'), LARAVEL('\uf3bd'), LASTFM('\uf202'), + LASTFM_SQUARE('\uf203'), LEANPUB('\uf212'), LESS('\uf41d'), LINE('\uf3c0'), LINKEDIN('\uf08c'), + LINKEDIN_IN('\uf0e1'), LINODE('\uf2b8'), LINUX('\uf17c'), LYFT('\uf3c3'), MAGENTO('\uf3c4'), + MAILCHIMP('\uf59e'), MANDALORIAN('\uf50f'), MARKDOWN('\uf60f'), MASTODON('\uf4f6'), + MAXCDN('\uf136'), MEDAPPS('\uf3c6'), MEDIUM('\uf23a'), MEDIUM_M('\uf3c7'), MEDRT('\uf3c8'), + MEETUP('\uf2e0'), MEGAPORT('\uf5a3'), MENDELEY('\uf7b3'), MICROSOFT('\uf3ca'), MIX('\uf3cb'), + MIXCLOUD('\uf289'), MIZUNI('\uf3cc'), MODX('\uf285'), MONERO('\uf3d0'), NAPSTER('\uf3d2'), + NEOS('\uf612'), NIMBLR('\uf5a8'), NODE('\uf419'), NODE_JS('\uf3d3'), NPM('\uf3d4'), + NS8('\uf3d5'), NUTRITIONIX('\uf3d6'), ODNOKLASSNIKI('\uf263'), ODNOKLASSNIKI_SQUARE('\uf264'), + OLD_REPUBLIC('\uf510'), OPENCART('\uf23d'), OPENID('\uf19b'), OPERA('\uf26a'), + OPTIN_MONSTER('\uf23c'), OSI('\uf41a'), PAGE4('\uf3d7'), PAGELINES('\uf18c'), PALFED('\uf3d8'), + PATREON('\uf3d9'), PAYPAL('\uf1ed'), PENNY_ARCADE('\uf704'), PERISCOPE('\uf3da'), + PHABRICATOR('\uf3db'), PHOENIX_FRAMEWORK('\uf3dc'), PHOENIX_SQUADRON('\uf511'), PHP('\uf457'), + PIED_PIPER('\uf2ae'), PIED_PIPER_ALT('\uf1a8'), PIED_PIPER_HAT('\uf4e5'), + PIED_PIPER_PP('\uf1a7'), PINTEREST('\uf0d2'), PINTEREST_P('\uf231'), PINTEREST_SQUARE('\uf0d3'), + PLAYSTATION('\uf3df'), PRODUCT_HUNT('\uf288'), PUSHED('\uf3e1'), PYTHON('\uf3e2'), QQ('\uf1d6'), + QUINSCAPE('\uf459'), QUORA('\uf2c4'), R_PROJECT('\uf4f7'), RASPBERRY_PI('\uf7bb'), + RAVELRY('\uf2d9'), REACT('\uf41b'), REACTEUROPE('\uf75d'), README('\uf4d5'), REBEL('\uf1d0'), + RED_RIVER('\uf3e3'), REDDIT('\uf1a1'), REDDIT_ALIEN('\uf281'), REDDIT_SQUARE('\uf1a2'), + REDHAT('\uf7bc'), RENREN('\uf18b'), REPLYD('\uf3e6'), RESEARCHGATE('\uf4f8'), + RESOLVING('\uf3e7'), REV('\uf5b2'), ROCKETCHAT('\uf3e8'), ROCKRMS('\uf3e9'), SAFARI('\uf267'), + SALESFORCE('\uf83b'), SASS('\uf41e'), SCHLIX('\uf3ea'), SCRIBD('\uf28a'), SEARCHENGIN('\uf3eb'), + SELLCAST('\uf2da'), SELLSY('\uf213'), SERVICESTACK('\uf3ec'), SHIRTSINBULK('\uf214'), + SHOPWARE('\uf5b5'), SIMPLYBUILT('\uf215'), SISTRIX('\uf3ee'), SITH('\uf512'), SKETCH('\uf7c6'), + SKYATLAS('\uf216'), SKYPE('\uf17e'), SLACK('\uf198'), SLACK_HASH('\uf3ef'), + SLIDESHARE('\uf1e7'), SNAPCHAT('\uf2ab'), SNAPCHAT_GHOST('\uf2ac'), SNAPCHAT_SQUARE('\uf2ad'), + SOUNDCLOUD('\uf1be'), SOURCETREE('\uf7d3'), SPEAKAP('\uf3f3'), SPEAKER_DECK('\uf83c'), + SPOTIFY('\uf1bc'), SQUARESPACE('\uf5be'), STACK_EXCHANGE('\uf18d'), STACK_OVERFLOW('\uf16c'), + STACKPATH('\uf842'), STAYLINKED('\uf3f5'), STEAM('\uf1b6'), STEAM_SQUARE('\uf1b7'), + STEAM_SYMBOL('\uf3f6'), STICKER_MULE('\uf3f7'), STRAVA('\uf428'), STRIPE('\uf429'), + STRIPE_S('\uf42a'), STUDIOVINARI('\uf3f8'), STUMBLEUPON('\uf1a4'), STUMBLEUPON_CIRCLE('\uf1a3'), + SUPERPOWERS('\uf2dd'), SUPPLE('\uf3f9'), SUSE('\uf7d6'), SYMFONY('\uf83d'), TEAMSPEAK('\uf4f9'), + TELEGRAM('\uf2c6'), TELEGRAM_PLANE('\uf3fe'), TENCENT_WEIBO('\uf1d5'), THE_RED_YETI('\uf69d'), + THEMECO('\uf5c6'), THEMEISLE('\uf2b2'), THINK_PEAKS('\uf731'), TRADE_FEDERATION('\uf513'), + TRELLO('\uf181'), TRIPADVISOR('\uf262'), TUMBLR('\uf173'), TUMBLR_SQUARE('\uf174'), + TWITCH('\uf1e8'), TWITTER('\uf099'), TWITTER_SQUARE('\uf081'), TYPO3('\uf42b'), UBER('\uf402'), + UBUNTU('\uf7df'), UIKIT('\uf403'), UNIREGISTRY('\uf404'), UNTAPPD('\uf405'), UPS('\uf7e0'), + USB('\uf287'), USPS('\uf7e1'), USSUNNAH('\uf407'), VAADIN('\uf408'), VIACOIN('\uf237'), + VIADEO('\uf2a9'), VIADEO_SQUARE('\uf2aa'), VIBER('\uf409'), VIMEO('\uf40a'), + VIMEO_SQUARE('\uf194'), VIMEO_V('\uf27d'), VINE('\uf1ca'), VK('\uf189'), VNV('\uf40b'), + VUEJS('\uf41f'), WAZE('\uf83f'), WEEBLY('\uf5cc'), WEIBO('\uf18a'), WEIXIN('\uf1d7'), + WHATSAPP('\uf232'), WHATSAPP_SQUARE('\uf40c'), WHMCS('\uf40d'), WIKIPEDIA_W('\uf266'), + WINDOWS('\uf17a'), WIX('\uf5cf'), WIZARDS_OF_THE_COAST('\uf730'), WOLF_PACK_BATTALION('\uf514'), + WORDPRESS('\uf19a'), WORDPRESS_SIMPLE('\uf411'), WPBEGINNER('\uf297'), WPEXPLORER('\uf2de'), + WPFORMS('\uf298'), WPRESSR('\uf3e4'), XBOX('\uf412'), XING('\uf168'), XING_SQUARE('\uf169'), + Y_COMBINATOR('\uf23b'), YAHOO('\uf19e'), YAMMER('\uf840'), YANDEX('\uf413'), + YANDEX_INTERNATIONAL('\uf414'), YARN('\uf7e3'), YELP('\uf1e9'), YOAST('\uf2b1'), + YOUTUBE('\uf167'), YOUTUBE_SQUARE('\uf431'), ZHIHU('\uf63f'); + + private static Font font; + private final char unicode; + + FontAwesomeBrands(char c) { + unicode = c; + } + + @Override + public Font getFont() { + if (font == null) { + font = Font.loadFont( + getClass().getResourceAsStream("/fonts/Font Awesome 6 Brands-Regular-400.otf"), + 16.0); + } + return font; + } + + @Override + public char getUnicode() { + return unicode; + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/fonticons/FontAwesomeRegular.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/fonticons/FontAwesomeRegular.java new file mode 100644 index 00000000000..b356745ef96 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/fonticons/FontAwesomeRegular.java @@ -0,0 +1,73 @@ +/* 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.fonticons; + + + +import javafx.scene.text.Font; + +public enum FontAwesomeRegular implements IconFont { + ADDRESS_BOOK('\uf2b9'), ADDRESS_CARD('\uf2bb'), ANGRY('\uf556'), + ARROW_ALT_CIRCLE_DOWN('\uf358'), ARROW_ALT_CIRCLE_LEFT('\uf359'), + ARROW_ALT_CIRCLE_RIGHT('\uf35a'), ARROW_ALT_CIRCLE_UP('\uf35b'), BELL('\uf0f3'), + BELL_SLASH('\uf1f6'), BOOKMARK('\uf02e'), BUILDING('\uf1ad'), CALENDAR('\uf133'), + CALENDAR_ALT('\uf073'), CALENDAR_CHECK('\uf274'), CALENDAR_MINUS('\uf272'), + CALENDAR_PLUS('\uf271'), CALENDAR_TIMES('\uf273'), CARET_SQUARE_DOWN('\uf150'), + CARET_SQUARE_LEFT('\uf191'), CARET_SQUARE_RIGHT('\uf152'), CARET_SQUARE_UP('\uf151'), + CHART_BAR('\uf080'), CHECK_CIRCLE('\uf058'), CHECK_SQUARE('\uf14a'), CIRCLE('\uf111'), + CLIPBOARD('\uf328'), CLOCK('\uf017'), CLONE('\uf24d'), CLOSED_CAPTIONING('\uf20a'), + COMMENT('\uf075'), COMMENT_ALT('\uf27a'), COMMENT_DOTS('\uf4ad'), COMMENTS('\uf086'), + COMPASS('\uf14e'), COPY('\uf0c5'), COPYRIGHT('\uf1f9'), CREDIT_CARD('\uf09d'), DIZZY('\uf567'), + DOT_CIRCLE('\uf192'), EDIT('\uf044'), ENVELOPE('\uf0e0'), ENVELOPE_OPEN('\uf2b6'), + EYE('\uf06e'), EYE_SLASH('\uf070'), FILE('\uf15b'), FILE_ALT('\uf15c'), FILE_ARCHIVE('\uf1c6'), + FILE_AUDIO('\uf1c7'), FILE_CODE('\uf1c9'), FILE_EXCEL('\uf1c3'), FILE_IMAGE('\uf1c5'), + FILE_PDF('\uf1c1'), FILE_POWERPOINT('\uf1c4'), FILE_VIDEO('\uf1c8'), FILE_WORD('\uf1c2'), + FLAG('\uf024'), FLUSHED('\uf579'), FOLDER('\uf07b'), FOLDER_OPEN('\uf07c'), + FONT_AWESOME_LOGO_FULL('\uf4e6'), FROWN('\uf119'), FROWN_OPEN('\uf57a'), FUTBOL('\uf1e3'), + GEM('\uf3a5'), GRIMACE('\uf57f'), GRIN('\uf580'), GRIN_ALT('\uf581'), GRIN_BEAM('\uf582'), + GRIN_BEAM_SWEAT('\uf583'), GRIN_HEARTS('\uf584'), GRIN_SQUINT('\uf585'), + GRIN_SQUINT_TEARS('\uf586'), GRIN_STARS('\uf587'), GRIN_TEARS('\uf588'), GRIN_TONGUE('\uf589'), + GRIN_TONGUE_SQUINT('\uf58a'), GRIN_TONGUE_WINK('\uf58b'), GRIN_WINK('\uf58c'), + HAND_LIZARD('\uf258'), HAND_PAPER('\uf256'), HAND_PEACE('\uf25b'), HAND_POINT_DOWN('\uf0a7'), + HAND_POINT_LEFT('\uf0a5'), HAND_POINT_RIGHT('\uf0a4'), HAND_POINT_UP('\uf0a6'), + HAND_POINTER('\uf25a'), HAND_ROCK('\uf255'), HAND_SCISSORS('\uf257'), HAND_SPOCK('\uf259'), + HANDSHAKE('\uf2b5'), HDD('\uf0a0'), HEART('\uf004'), HOSPITAL('\uf0f8'), HOURGLASS('\uf254'), + ID_BADGE('\uf2c1'), ID_CARD('\uf2c2'), IMAGE('\uf03e'), IMAGES('\uf302'), KEYBOARD('\uf11c'), + KISS('\uf596'), KISS_BEAM('\uf597'), KISS_WINK_HEART('\uf598'), LAUGH('\uf599'), + LAUGH_BEAM('\uf59a'), LAUGH_SQUINT('\uf59b'), LAUGH_WINK('\uf59c'), LEMON('\uf094'), + LIFE_RING('\uf1cd'), LIGHTBULB('\uf0eb'), LIST_ALT('\uf022'), MAP('\uf279'), MEH('\uf11a'), + MEH_BLANK('\uf5a4'), MEH_ROLLING_EYES('\uf5a5'), MINUS_SQUARE('\uf146'), + MONEY_BILL_ALT('\uf3d1'), MOON('\uf186'), NEWSPAPER('\uf1ea'), OBJECT_GROUP('\uf247'), + OBJECT_UNGROUP('\uf248'), PAPER_PLANE('\uf1d8'), PAUSE_CIRCLE('\uf28b'), PLAY_CIRCLE('\uf144'), + PLUS_SQUARE('\uf0fe'), QUESTION_CIRCLE('\uf059'), REGISTERED('\uf25d'), SAD_CRY('\uf5b3'), + SAD_TEAR('\uf5b4'), SAVE('\uf0c7'), SHARE_SQUARE('\uf14d'), SMILE('\uf118'), + SMILE_BEAM('\uf5b8'), SMILE_WINK('\uf4da'), SNOWFLAKE('\uf2dc'), SQUARE('\uf0c8'), + STAR('\uf005'), STAR_HALF('\uf089'), STICKY_NOTE('\uf249'), STOP_CIRCLE('\uf28d'), + SUN('\uf185'), SURPRISE('\uf5c2'), THUMBS_DOWN('\uf165'), THUMBS_UP('\uf164'), + TIMES_CIRCLE('\uf057'), TIRED('\uf5c8'), TRASH_ALT('\uf2ed'), USER('\uf007'), + USER_CIRCLE('\uf2bd'), WINDOW_CLOSE('\uf410'), WINDOW_MAXIMIZE('\uf2d0'), + WINDOW_MINIMIZE('\uf2d1'), WINDOW_RESTORE('\uf2d2'); + + private static Font font; + private final char unicode; + + FontAwesomeRegular(char c) { + unicode = c; + } + + @Override + public Font getFont() { + if (font == null) { + font = Font.loadFont( + getClass().getResourceAsStream("/fonts/Font Awesome 6 Free-Regular-400.otf"), 16.0); + } + return font; + } + + @Override + public char getUnicode() { + return unicode; + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/fonticons/FontAwesomeSolid.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/fonticons/FontAwesomeSolid.java new file mode 100644 index 00000000000..227184d360b --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/fonticons/FontAwesomeSolid.java @@ -0,0 +1,278 @@ +/* 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.fonticons; + + +/** + * @author Alexander Weigl + * @version 1 (08.04.19) + */ + +import javafx.scene.text.Font; + +public enum FontAwesomeSolid implements IconFont { + AD('\uf641'), ADDRESS_BOOK('\uf2b9'), ADDRESS_CARD('\uf2bb'), ADJUST('\uf042'), + AIR_FRESHENER('\uf5d0'), ALIGN_CENTER('\uf037'), ALIGN_JUSTIFY('\uf039'), ALIGN_LEFT('\uf036'), + ALIGN_RIGHT('\uf038'), ALLERGIES('\uf461'), AMBULANCE('\uf0f9'), + AMERICAN_SIGN_LANGUAGE_INTERPRETING('\uf2a3'), ANCHOR('\uf13d'), ANGLE_DOUBLE_DOWN('\uf103'), + ANGLE_DOUBLE_LEFT('\uf100'), ANGLE_DOUBLE_RIGHT('\uf101'), ANGLE_DOUBLE_UP('\uf102'), + ANGLE_DOWN('\uf107'), ANGLE_LEFT('\uf104'), ANGLE_RIGHT('\uf105'), ANGLE_UP('\uf106'), + ANGRY('\uf556'), ANKH('\uf644'), APPLE_ALT('\uf5d1'), ARCHIVE('\uf187'), ARCHWAY('\uf557'), + ARROW_ALT_CIRCLE_DOWN('\uf358'), ARROW_ALT_CIRCLE_LEFT('\uf359'), + ARROW_ALT_CIRCLE_RIGHT('\uf35a'), ARROW_ALT_CIRCLE_UP('\uf35b'), ARROW_CIRCLE_DOWN('\uf0ab'), + ARROW_CIRCLE_LEFT('\uf0a8'), ARROW_CIRCLE_RIGHT('\uf0a9'), ARROW_CIRCLE_UP('\uf0aa'), + ARROW_DOWN('\uf063'), ARROW_LEFT('\uf060'), ARROW_RIGHT('\uf061'), ARROW_UP('\uf062'), + ARROWS_ALT('\uf0b2'), ARROWS_ALT_H('\uf337'), ARROWS_ALT_V('\uf338'), + ARROWS_TO_EYE('\ue4bf'), + ASSISTIVE_LISTENING_SYSTEMS('\uf2a2'), ASTERISK('\uf069'), AT('\uf1fa'), ATLAS('\uf558'), + ATOM('\uf5d2'), AUDIO_DESCRIPTION('\uf29e'), AWARD('\uf559'), BABY('\uf77c'), + BABY_CARRIAGE('\uf77d'), BACKSPACE('\uf55a'), BACKWARD('\uf04a'), BACON('\uf7e5'), + BALANCE_SCALE('\uf24e'), BALANCE_SCALE_LEFT('\uf515'), BALANCE_SCALE_RIGHT('\uf516'), + BAN('\uf05e'), BAND_AID('\uf462'), BARCODE('\uf02a'), BARS('\uf0c9'), BASEBALL_BALL('\uf433'), + BASKETBALL_BALL('\uf434'), BATH('\uf2cd'), BATTERY_EMPTY('\uf244'), BATTERY_FULL('\uf240'), + BATTERY_HALF('\uf242'), BATTERY_QUARTER('\uf243'), BATTERY_THREE_QUARTERS('\uf241'), + BED('\uf236'), BEER('\uf0fc'), BELL('\uf0f3'), BELL_SLASH('\uf1f6'), BEZIER_CURVE('\uf55b'), + BIBLE('\uf647'), BICYCLE('\uf206'), BIKING('\uf84a'), BINOCULARS('\uf1e5'), BIOHAZARD('\uf780'), + BIRTHDAY_CAKE('\uf1fd'), BLENDER('\uf517'), BLENDER_PHONE('\uf6b6'), BLIND('\uf29d'), + BLOG('\uf781'), BOLD('\uf032'), BOLT('\uf0e7'), BOMB('\uf1e2'), BONE('\uf5d7'), BONG('\uf55c'), + BOOK('\uf02d'), BOOK_DEAD('\uf6b7'), BOOK_MEDICAL('\uf7e6'), BOOK_OPEN('\uf518'), + BOOK_READER('\uf5da'), BOOKMARK('\uf02e'), BORDER_ALL('\uf84c'), BORDER_NONE('\uf850'), + BORDER_STYLE('\uf853'), BOWLING_BALL('\uf436'), BOX('\uf466'), BOX_OPEN('\uf49e'), + BOXES('\uf468'), BRAILLE('\uf2a1'), BRAIN('\uf5dc'), BREAD_SLICE('\uf7ec'), BRIEFCASE('\uf0b1'), + BRIEFCASE_MEDICAL('\uf469'), BROADCAST_TOWER('\uf519'), BROOM('\uf51a'), BRUSH('\uf55d'), + BUG('\uf188'), BUILDING('\uf1ad'), BULLHORN('\uf0a1'), BULLSEYE('\uf140'), BURN('\uf46a'), + BUS('\uf207'), BUS_ALT('\uf55e'), BUSINESS_TIME('\uf64a'), CALCULATOR('\uf1ec'), + CALENDAR('\uf133'), CALENDAR_ALT('\uf073'), CALENDAR_CHECK('\uf274'), CALENDAR_DAY('\uf783'), + CALENDAR_MINUS('\uf272'), CALENDAR_PLUS('\uf271'), CALENDAR_TIMES('\uf273'), + CALENDAR_WEEK('\uf784'), CAMERA('\uf030'), CAMERA_RETRO('\uf083'), CAMPGROUND('\uf6bb'), + CANDY_CANE('\uf786'), CANNABIS('\uf55f'), CAPSULES('\uf46b'), CAR('\uf1b9'), CAR_ALT('\uf5de'), + CAR_BATTERY('\uf5df'), CAR_CRASH('\uf5e1'), CAR_SIDE('\uf5e4'), CARET_DOWN('\uf0d7'), + CARET_LEFT('\uf0d9'), CARET_RIGHT('\uf0da'), CARET_SQUARE_DOWN('\uf150'), + CARET_SQUARE_LEFT('\uf191'), CARET_SQUARE_RIGHT('\uf152'), CARET_SQUARE_UP('\uf151'), + CARET_UP('\uf0d8'), CARROT('\uf787'), CART_ARROW_DOWN('\uf218'), CART_PLUS('\uf217'), + CASH_REGISTER('\uf788'), CAT('\uf6be'), CERTIFICATE('\uf0a3'), CHAIR('\uf6c0'), + CHALKBOARD('\uf51b'), CHALKBOARD_TEACHER('\uf51c'), CHARGING_STATION('\uf5e7'), + CHART_AREA('\uf1fe'), CHART_BAR('\uf080'), CHART_LINE('\uf201'), CHART_PIE('\uf200'), + CHECK('\uf00c'), CHECK_CIRCLE('\uf058'), CHECK_DOUBLE('\uf560'), CHECK_SQUARE('\uf14a'), + CHEESE('\uf7ef'), CHESS('\uf439'), CHESS_BISHOP('\uf43a'), CHESS_BOARD('\uf43c'), + CHESS_KING('\uf43f'), CHESS_KNIGHT('\uf441'), CHESS_PAWN('\uf443'), CHESS_QUEEN('\uf445'), + CHESS_ROOK('\uf447'), CHEVRON_CIRCLE_DOWN('\uf13a'), CHEVRON_CIRCLE_LEFT('\uf137'), + CHEVRON_CIRCLE_RIGHT('\uf138'), CHEVRON_CIRCLE_UP('\uf139'), CHEVRON_DOWN('\uf078'), + CHEVRON_LEFT('\uf053'), CHEVRON_RIGHT('\uf054'), CHEVRON_UP('\uf077'), CHILD('\uf1ae'), + CHURCH('\uf51d'), CIRCLE('\uf111'), CIRCLE_NOTCH('\uf1ce'), CITY('\uf64f'), + CLINIC_MEDICAL('\uf7f2'), CLIPBOARD('\uf328'), CLIPBOARD_CHECK('\uf46c'), + CLIPBOARD_LIST('\uf46d'), CLOCK('\uf017'), CLONE('\uf24d'), CLOSED_CAPTIONING('\uf20a'), + CLOUD('\uf0c2'), CLOUD_DOWNLOAD_ALT('\uf381'), CLOUD_MEATBALL('\uf73b'), CLOUD_MOON('\uf6c3'), + CLOUD_MOON_RAIN('\uf73c'), CLOUD_RAIN('\uf73d'), CLOUD_SHOWERS_HEAVY('\uf740'), + CLOUD_SUN('\uf6c4'), CLOUD_SUN_RAIN('\uf743'), CLOUD_UPLOAD_ALT('\uf382'), COCKTAIL('\uf561'), + CODE('\uf121'), CODE_BRANCH('\uf126'), COFFEE('\uf0f4'), COG('\uf013'), COGS('\uf085'), + COINS('\uf51e'), COLUMNS('\uf0db'), COMMENT('\uf075'), COMMENT_ALT('\uf27a'), + COMMENT_DOLLAR('\uf651'), COMMENT_DOTS('\uf4ad'), COMMENT_MEDICAL('\uf7f5'), + COMMENT_SLASH('\uf4b3'), COMMENTS('\uf086'), COMMENTS_DOLLAR('\uf653'), COMPACT_DISC('\uf51f'), + COMPASS('\uf14e'), COMPRESS('\uf066'), COMPRESS_ARROWS_ALT('\uf78c'), CONCIERGE_BELL('\uf562'), + COOKIE('\uf563'), COOKIE_BITE('\uf564'), COPY('\uf0c5'), COPYRIGHT('\uf1f9'), COUCH('\uf4b8'), + CREDIT_CARD('\uf09d'), CROP('\uf125'), CROP_ALT('\uf565'), CROSS('\uf654'), + CROSSHAIRS('\uf05b'), CROW('\uf520'), CROWN('\uf521'), CRUTCH('\uf7f7'), CUBE('\uf1b2'), + CUBES('\uf1b3'), CUT('\uf0c4'), DATABASE('\uf1c0'), DEAF('\uf2a4'), DEMOCRAT('\uf747'), + DESKTOP('\uf108'), DHARMACHAKRA('\uf655'), DIAGNOSES('\uf470'), DICE('\uf522'), + DICE_D20('\uf6cf'), DICE_D6('\uf6d1'), DICE_FIVE('\uf523'), DICE_FOUR('\uf524'), + DICE_ONE('\uf525'), DICE_SIX('\uf526'), DICE_THREE('\uf527'), DICE_TWO('\uf528'), + DIGITAL_TACHOGRAPH('\uf566'), DIRECTIONS('\uf5eb'), DIVIDE('\uf529'), DIZZY('\uf567'), + DNA('\uf471'), DOG('\uf6d3'), DOLLAR_SIGN('\uf155'), DOLLY('\uf472'), DOLLY_FLATBED('\uf474'), + DONATE('\uf4b9'), DOOR_CLOSED('\uf52a'), DOOR_OPEN('\uf52b'), DOT_CIRCLE('\uf192'), + DOVE('\uf4ba'), DOWNLOAD('\uf019'), DRAFTING_COMPASS('\uf568'), DRAGON('\uf6d5'), + DRAW_POLYGON('\uf5ee'), DRUM('\uf569'), DRUM_STEELPAN('\uf56a'), DRUMSTICK_BITE('\uf6d7'), + DUMBBELL('\uf44b'), DUMPSTER('\uf793'), DUMPSTER_FIRE('\uf794'), DUNGEON('\uf6d9'), + EDIT('\uf044'), EGG('\uf7fb'), EJECT('\uf052'), ELLIPSIS_H('\uf141'), ELLIPSIS_V('\uf142'), + ENVELOPE('\uf0e0'), ENVELOPE_OPEN('\uf2b6'), ENVELOPE_OPEN_TEXT('\uf658'), + ENVELOPE_SQUARE('\uf199'), EQUALS('\uf52c'), ERASER('\uf12d'), ETHERNET('\uf796'), + EURO_SIGN('\uf153'), EXCHANGE_ALT('\uf362'), EXCLAMATION('\uf12a'), + EXCLAMATION_CIRCLE('\uf06a'), EXCLAMATION_TRIANGLE('\uf071'), EXPAND('\uf065'), + EXPAND_ARROWS_ALT('\uf31e'), EXTERNAL_LINK_ALT('\uf35d'), EXTERNAL_LINK_SQUARE_ALT('\uf360'), + EYE('\uf06e'), EYE_DROPPER('\uf1fb'), EYE_SLASH('\uf070'), FAN('\uf863'), + FAST_BACKWARD('\uf049'), FAST_FORWARD('\uf050'), FAX('\uf1ac'), FEATHER('\uf52d'), + FEATHER_ALT('\uf56b'), FEMALE('\uf182'), FIGHTER_JET('\uf0fb'), FILE('\uf15b'), + FILE_ALT('\uf15c'), FILE_ARCHIVE('\uf1c6'), FILE_AUDIO('\uf1c7'), FILE_CODE('\uf1c9'), + FILE_CONTRACT('\uf56c'), FILE_CSV('\uf6dd'), FILE_DOWNLOAD('\uf56d'), FILE_EXCEL('\uf1c3'), + FILE_EXPORT('\uf56e'), FILE_IMAGE('\uf1c5'), FILE_IMPORT('\uf56f'), FILE_INVOICE('\uf570'), + FILE_INVOICE_DOLLAR('\uf571'), FILE_MEDICAL('\uf477'), FILE_MEDICAL_ALT('\uf478'), + FILE_PDF('\uf1c1'), FILE_POWERPOINT('\uf1c4'), FILE_PRESCRIPTION('\uf572'), + FILE_SIGNATURE('\uf573'), FILE_UPLOAD('\uf574'), FILE_VIDEO('\uf1c8'), FILE_WORD('\uf1c2'), + FILL('\uf575'), FILL_DRIP('\uf576'), FILM('\uf008'), FILTER('\uf0b0'), FINGERPRINT('\uf577'), + FIRE('\uf06d'), FIRE_ALT('\uf7e4'), FIRE_EXTINGUISHER('\uf134'), FIRST_AID('\uf479'), + FISH('\uf578'), FIST_RAISED('\uf6de'), FLAG('\uf024'), FLAG_CHECKERED('\uf11e'), + FLAG_USA('\uf74d'), FLASK('\uf0c3'), FLUSHED('\uf579'), FOLDER('\uf07b'), + FOLDER_MINUS('\uf65d'), FOLDER_OPEN('\uf07c'), FOLDER_PLUS('\uf65e'), FONT('\uf031'), + FONT_AWESOME_LOGO_FULL('\uf4e6'), FOOTBALL_BALL('\uf44e'), FORWARD('\uf04e'), FROG('\uf52e'), + FROWN('\uf119'), FROWN_OPEN('\uf57a'), FUNNEL_DOLLAR('\uf662'), FUTBOL('\uf1e3'), + GAMEPAD('\uf11b'), GAS_PUMP('\uf52f'), GAVEL('\uf0e3'), GEM('\uf3a5'), GENDERLESS('\uf22d'), + GHOST('\uf6e2'), GIFT('\uf06b'), GIFTS('\uf79c'), GLASS_CHEERS('\uf79f'), + GLASS_MARTINI('\uf000'), GLASS_MARTINI_ALT('\uf57b'), GLASS_WHISKEY('\uf7a0'), + GLASSES('\uf530'), GLOBE('\uf0ac'), GLOBE_AFRICA('\uf57c'), GLOBE_AMERICAS('\uf57d'), + GLOBE_ASIA('\uf57e'), GLOBE_EUROPE('\uf7a2'), GOLF_BALL('\uf450'), GOPURAM('\uf664'), + GRADUATION_CAP('\uf19d'), GREATER_THAN('\uf531'), GREATER_THAN_EQUAL('\uf532'), + GRIMACE('\uf57f'), GRIN('\uf580'), GRIN_ALT('\uf581'), GRIN_BEAM('\uf582'), + GRIN_BEAM_SWEAT('\uf583'), GRIN_HEARTS('\uf584'), GRIN_SQUINT('\uf585'), + GRIN_SQUINT_TEARS('\uf586'), GRIN_STARS('\uf587'), GRIN_TEARS('\uf588'), GRIN_TONGUE('\uf589'), + GRIN_TONGUE_SQUINT('\uf58a'), GRIN_TONGUE_WINK('\uf58b'), GRIN_WINK('\uf58c'), + GRIP_HORIZONTAL('\uf58d'), GRIP_LINES('\uf7a4'), GRIP_LINES_VERTICAL('\uf7a5'), + GRIP_VERTICAL('\uf58e'), GUITAR('\uf7a6'), H_SQUARE('\uf0fd'), HAMBURGER('\uf805'), + HAMMER('\uf6e3'), HAMSA('\uf665'), HAND_HOLDING('\uf4bd'), HAND_HOLDING_HEART('\uf4be'), + HAND_HOLDING_USD('\uf4c0'), HAND_LIZARD('\uf258'), HAND_MIDDLE_FINGER('\uf806'), + HAND_PAPER('\uf256'), HAND_PEACE('\uf25b'), HAND_POINT_DOWN('\uf0a7'), + HAND_POINT_LEFT('\uf0a5'), HAND_POINT_RIGHT('\uf0a4'), HAND_POINT_UP('\uf0a6'), + HAND_POINTER('\uf25a'), HAND_ROCK('\uf255'), HAND_SCISSORS('\uf257'), HAND_SPOCK('\uf259'), + HANDS('\uf4c2'), HANDS_HELPING('\uf4c4'), HANDSHAKE('\uf2b5'), HANUKIAH('\uf6e6'), + HARD_HAT('\uf807'), HASHTAG('\uf292'), HAT_WIZARD('\uf6e8'), HAYKAL('\uf666'), HDD('\uf0a0'), + HEADING('\uf1dc'), HEADPHONES('\uf025'), HEADPHONES_ALT('\uf58f'), HEADSET('\uf590'), + HEART('\uf004'), HEART_BROKEN('\uf7a9'), HEARTBEAT('\uf21e'), HELICOPTER('\uf533'), + HIGHLIGHTER('\uf591'), HIKING('\uf6ec'), HIPPO('\uf6ed'), HISTORY('\uf1da'), + HOCKEY_PUCK('\uf453'), HOLLY_BERRY('\uf7aa'), HOME('\uf015'), HORSE('\uf6f0'), + HORSE_HEAD('\uf7ab'), HOSPITAL('\uf0f8'), HOSPITAL_ALT('\uf47d'), HOSPITAL_SYMBOL('\uf47e'), + HOT_TUB('\uf593'), HOTDOG('\uf80f'), HOTEL('\uf594'), HOURGLASS('\uf254'), + HOURGLASS_END('\uf253'), HOURGLASS_HALF('\uf252'), HOURGLASS_START('\uf251'), + HOUSE_DAMAGE('\uf6f1'), HRYVNIA('\uf6f2'), I_CURSOR('\uf246'), ICE_CREAM('\uf810'), + ICICLES('\uf7ad'), ICONS('\uf86d'), ID_BADGE('\uf2c1'), ID_CARD('\uf2c2'), + ID_CARD_ALT('\uf47f'), IGLOO('\uf7ae'), IMAGE('\uf03e'), IMAGES('\uf302'), INBOX('\uf01c'), + INDENT('\uf03c'), INDUSTRY('\uf275'), INFINITY('\uf534'), INFO('\uf129'), INFO_CIRCLE('\uf05a'), + ITALIC('\uf033'), JEDI('\uf669'), JOINT('\uf595'), JOURNAL_WHILLS('\uf66a'), KAABA('\uf66b'), + KEY('\uf084'), KEYBOARD('\uf11c'), KHANDA('\uf66d'), KISS('\uf596'), KISS_BEAM('\uf597'), + KISS_WINK_HEART('\uf598'), KIWI_BIRD('\uf535'), LANDMARK('\uf66f'), LANGUAGE('\uf1ab'), + LAPTOP('\uf109'), LAPTOP_CODE('\uf5fc'), LAPTOP_MEDICAL('\uf812'), LAUGH('\uf599'), + LAUGH_BEAM('\uf59a'), LAUGH_SQUINT('\uf59b'), LAUGH_WINK('\uf59c'), LAYER_GROUP('\uf5fd'), + LEAF('\uf06c'), LEMON('\uf094'), LESS_THAN('\uf536'), LESS_THAN_EQUAL('\uf537'), + LEVEL_DOWN_ALT('\uf3be'), LEVEL_UP_ALT('\uf3bf'), LIFE_RING('\uf1cd'), LIGHTBULB('\uf0eb'), + LINK('\uf0c1'), LIRA_SIGN('\uf195'), LIST('\uf03a'), LIST_ALT('\uf022'), LIST_OL('\uf0cb'), + LIST_UL('\uf0ca'), LOCATION_ARROW('\uf124'), LOCK('\uf023'), LOCK_OPEN('\uf3c1'), + LONG_ARROW_ALT_DOWN('\uf309'), LONG_ARROW_ALT_LEFT('\uf30a'), LONG_ARROW_ALT_RIGHT('\uf30b'), + LONG_ARROW_ALT_UP('\uf30c'), LOW_VISION('\uf2a8'), LUGGAGE_CART('\uf59d'), MAGIC('\uf0d0'), + MAGNET('\uf076'), MAIL_BULK('\uf674'), MALE('\uf183'), MAP('\uf279'), MAP_MARKED('\uf59f'), + MAP_MARKED_ALT('\uf5a0'), MAP_MARKER('\uf041'), MAP_MARKER_ALT('\uf3c5'), MAP_PIN('\uf276'), + MAP_SIGNS('\uf277'), MARKER('\uf5a1'), MARS('\uf222'), MARS_DOUBLE('\uf227'), + MARS_STROKE('\uf229'), MARS_STROKE_H('\uf22b'), MARS_STROKE_V('\uf22a'), MASK('\uf6fa'), + MEDAL('\uf5a2'), MEDKIT('\uf0fa'), MEH('\uf11a'), MEH_BLANK('\uf5a4'), + MEH_ROLLING_EYES('\uf5a5'), MEMORY('\uf538'), MENORAH('\uf676'), MERCURY('\uf223'), + METEOR('\uf753'), MICROCHIP('\uf2db'), MICROPHONE('\uf130'), MICROPHONE_ALT('\uf3c9'), + MICROPHONE_ALT_SLASH('\uf539'), MICROPHONE_SLASH('\uf131'), MICROSCOPE('\uf610'), + MINUS('\uf068'), MINUS_CIRCLE('\uf056'), MINUS_SQUARE('\uf146'), MITTEN('\uf7b5'), + MOBILE('\uf10b'), MOBILE_ALT('\uf3cd'), MONEY_BILL('\uf0d6'), MONEY_BILL_ALT('\uf3d1'), + MONEY_BILL_WAVE('\uf53a'), MONEY_BILL_WAVE_ALT('\uf53b'), MONEY_CHECK('\uf53c'), + MONEY_CHECK_ALT('\uf53d'), MONUMENT('\uf5a6'), MOON('\uf186'), MORTAR_PESTLE('\uf5a7'), + MOSQUE('\uf678'), MOTORCYCLE('\uf21c'), MOUNTAIN('\uf6fc'), MOUSE_POINTER('\uf245'), + MUG_HOT('\uf7b6'), MUSIC('\uf001'), NETWORK_WIRED('\uf6ff'), NEUTER('\uf22c'), + NEWSPAPER('\uf1ea'), NOT_EQUAL('\uf53e'), NOTES_MEDICAL('\uf481'), OBJECT_GROUP('\uf247'), + OBJECT_UNGROUP('\uf248'), OIL_CAN('\uf613'), OM('\uf679'), OTTER('\uf700'), OUTDENT('\uf03b'), + PAGER('\uf815'), PAINT_BRUSH('\uf1fc'), PAINT_ROLLER('\uf5aa'), PALETTE('\uf53f'), + PALLET('\uf482'), PAPER_PLANE('\uf1d8'), PAPERCLIP('\uf0c6'), PARACHUTE_BOX('\uf4cd'), + PARAGRAPH('\uf1dd'), PARKING('\uf540'), PASSPORT('\uf5ab'), PASTAFARIANISM('\uf67b'), + PASTE('\uf0ea'), PAUSE('\uf04c'), PAUSE_CIRCLE('\uf28b'), PAW('\uf1b0'), PEACE('\uf67c'), + PEN('\uf304'), PEN_ALT('\uf305'), PEN_FANCY('\uf5ac'), PEN_NIB('\uf5ad'), PEN_SQUARE('\uf14b'), + PENCIL_ALT('\uf303'), PENCIL_RULER('\uf5ae'), PEOPLE_CARRY('\uf4ce'), PEPPER_HOT('\uf816'), + PERCENT('\uf295'), PERCENTAGE('\uf541'), PERSON_BOOTH('\uf756'), PHONE('\uf095'), + PHONE_ALT('\uf879'), PHONE_SLASH('\uf3dd'), PHONE_SQUARE('\uf098'), PHONE_SQUARE_ALT('\uf87b'), + PHONE_VOLUME('\uf2a0'), PHOTO_VIDEO('\uf87c'), PIGGY_BANK('\uf4d3'), PILLS('\uf484'), + PIZZA_SLICE('\uf818'), PLACE_OF_WORSHIP('\uf67f'), PLANE('\uf072'), PLANE_ARRIVAL('\uf5af'), + PLANE_DEPARTURE('\uf5b0'), PLAY('\uf04b'), PLAY_CIRCLE('\uf144'), PLUG('\uf1e6'), + PLUS('\uf067'), PLUS_CIRCLE('\uf055'), PLUS_SQUARE('\uf0fe'), PODCAST('\uf2ce'), POLL('\uf681'), + POLL_H('\uf682'), POO('\uf2fe'), POO_STORM('\uf75a'), POOP('\uf619'), PORTRAIT('\uf3e0'), + POUND_SIGN('\uf154'), POWER_OFF('\uf011'), PRAY('\uf683'), PRAYING_HANDS('\uf684'), + PRESCRIPTION('\uf5b1'), PRESCRIPTION_BOTTLE('\uf485'), PRESCRIPTION_BOTTLE_ALT('\uf486'), + PRINT('\uf02f'), PROCEDURES('\uf487'), PROJECT_DIAGRAM('\uf542'), PUZZLE_PIECE('\uf12e'), + QRCODE('\uf029'), QUESTION('\uf128'), QUESTION_CIRCLE('\uf059'), QUIDDITCH('\uf458'), + QUOTE_LEFT('\uf10d'), QUOTE_RIGHT('\uf10e'), QURAN('\uf687'), RADIATION('\uf7b9'), + RADIATION_ALT('\uf7ba'), RAINBOW('\uf75b'), RANDOM('\uf074'), RECEIPT('\uf543'), + RECYCLE('\uf1b8'), REDO('\uf01e'), REDO_ALT('\uf2f9'), REGISTERED('\uf25d'), + REMOVE_FORMAT('\uf87d'), REPLY('\uf3e5'), REPLY_ALL('\uf122'), REPUBLICAN('\uf75e'), + RESTROOM('\uf7bd'), RETWEET('\uf079'), RIBBON('\uf4d6'), RING('\uf70b'), ROAD('\uf018'), + ROBOT('\uf544'), ROCKET('\uf135'), ROUTE('\uf4d7'), RSS('\uf09e'), RSS_SQUARE('\uf143'), + RUBLE_SIGN('\uf158'), RULER('\uf545'), RULER_COMBINED('\uf546'), RULER_HORIZONTAL('\uf547'), + RULER_VERTICAL('\uf548'), RUNNING('\uf70c'), RUPEE_SIGN('\uf156'), SAD_CRY('\uf5b3'), + SAD_TEAR('\uf5b4'), SATELLITE('\uf7bf'), SATELLITE_DISH('\uf7c0'), SAVE('\uf0c7'), + SCHOOL('\uf549'), SCREWDRIVER('\uf54a'), SCROLL('\uf70e'), SD_CARD('\uf7c2'), SEARCH('\uf002'), + SEARCH_DOLLAR('\uf688'), SEARCH_LOCATION('\uf689'), SEARCH_MINUS('\uf010'), + SEARCH_PLUS('\uf00e'), SEEDLING('\uf4d8'), SERVER('\uf233'), SHAPES('\uf61f'), SHARE('\uf064'), + SHARE_ALT('\uf1e0'), SHARE_ALT_SQUARE('\uf1e1'), SHARE_SQUARE('\uf14d'), SHEKEL_SIGN('\uf20b'), + SHIELD_ALT('\uf3ed'), SHIP('\uf21a'), SHIPPING_FAST('\uf48b'), SHOE_PRINTS('\uf54b'), + SHOPPING_BAG('\uf290'), SHOPPING_BASKET('\uf291'), SHOPPING_CART('\uf07a'), SHOWER('\uf2cc'), + SHUTTLE_VAN('\uf5b6'), SIGN('\uf4d9'), SIGN_IN_ALT('\uf2f6'), SIGN_LANGUAGE('\uf2a7'), + SIGN_OUT_ALT('\uf2f5'), SIGNAL('\uf012'), SIGNATURE('\uf5b7'), SIM_CARD('\uf7c4'), + SITEMAP('\uf0e8'), SKATING('\uf7c5'), SKIING('\uf7c9'), SKIING_NORDIC('\uf7ca'), + SKULL('\uf54c'), SKULL_CROSSBONES('\uf714'), SLASH('\uf715'), SLEIGH('\uf7cc'), + SLIDERS_H('\uf1de'), SMILE('\uf118'), SMILE_BEAM('\uf5b8'), SMILE_WINK('\uf4da'), + SMOG('\uf75f'), SMOKING('\uf48d'), SMOKING_BAN('\uf54d'), SMS('\uf7cd'), SNOWBOARDING('\uf7ce'), + SNOWFLAKE('\uf2dc'), SNOWMAN('\uf7d0'), SNOWPLOW('\uf7d2'), SOCKS('\uf696'), + SOLAR_PANEL('\uf5ba'), SORT('\uf0dc'), SORT_ALPHA_DOWN('\uf15d'), SORT_ALPHA_DOWN_ALT('\uf881'), + SORT_ALPHA_UP('\uf15e'), SORT_ALPHA_UP_ALT('\uf882'), SORT_AMOUNT_DOWN('\uf160'), + SORT_AMOUNT_DOWN_ALT('\uf884'), SORT_AMOUNT_UP('\uf161'), SORT_AMOUNT_UP_ALT('\uf885'), + SORT_DOWN('\uf0dd'), SORT_NUMERIC_DOWN('\uf162'), SORT_NUMERIC_DOWN_ALT('\uf886'), + SORT_NUMERIC_UP('\uf163'), SORT_NUMERIC_UP_ALT('\uf887'), SORT_UP('\uf0de'), SPA('\uf5bb'), + SPACE_SHUTTLE('\uf197'), SPELL_CHECK('\uf891'), SPIDER('\uf717'), SPINNER('\uf110'), + SPLOTCH('\uf5bc'), SPRAY_CAN('\uf5bd'), SQUARE('\uf0c8'), SQUARE_FULL('\uf45c'), + SQUARE_ROOT_ALT('\uf698'), STAMP('\uf5bf'), STAR('\uf005'), STAR_AND_CRESCENT('\uf699'), + STAR_HALF('\uf089'), STAR_HALF_ALT('\uf5c0'), STAR_OF_DAVID('\uf69a'), STAR_OF_LIFE('\uf621'), + STEP_BACKWARD('\uf048'), STEP_FORWARD('\uf051'), STETHOSCOPE('\uf0f1'), STICKY_NOTE('\uf249'), + STOP('\uf04d'), STOP_CIRCLE('\uf28d'), STOPWATCH('\uf2f2'), STORE('\uf54e'), + STORE_ALT('\uf54f'), STREAM('\uf550'), STREET_VIEW('\uf21d'), STRIKETHROUGH('\uf0cc'), + STROOPWAFEL('\uf551'), SUBSCRIPT('\uf12c'), SUBWAY('\uf239'), SUITCASE('\uf0f2'), + SUITCASE_ROLLING('\uf5c1'), SUN('\uf185'), SUPERSCRIPT('\uf12b'), SURPRISE('\uf5c2'), + SWATCHBOOK('\uf5c3'), SWIMMER('\uf5c4'), SWIMMING_POOL('\uf5c5'), SYNAGOGUE('\uf69b'), + SYNC('\uf021'), SYNC_ALT('\uf2f1'), SYRINGE('\uf48e'), TABLE('\uf0ce'), TABLE_TENNIS('\uf45d'), + TABLET('\uf10a'), TABLET_ALT('\uf3fa'), TABLETS('\uf490'), TACHOMETER_ALT('\uf3fd'), + TAG('\uf02b'), TAGS('\uf02c'), TAPE('\uf4db'), TASKS('\uf0ae'), TAXI('\uf1ba'), TEETH('\uf62e'), + TEETH_OPEN('\uf62f'), TEMPERATURE_HIGH('\uf769'), TEMPERATURE_LOW('\uf76b'), TENGE('\uf7d7'), + TERMINAL('\uf120'), TEXT_HEIGHT('\uf034'), TEXT_WIDTH('\uf035'), TH('\uf00a'), + TH_LARGE('\uf009'), TH_LIST('\uf00b'), THEATER_MASKS('\uf630'), THERMOMETER('\uf491'), + THERMOMETER_EMPTY('\uf2cb'), THERMOMETER_FULL('\uf2c7'), THERMOMETER_HALF('\uf2c9'), + THERMOMETER_QUARTER('\uf2ca'), THERMOMETER_THREE_QUARTERS('\uf2c8'), THUMBS_DOWN('\uf165'), + THUMBS_UP('\uf164'), THUMBTACK('\uf08d'), TICKET_ALT('\uf3ff'), TIMES('\uf00d'), + TIMES_CIRCLE('\uf057'), TINT('\uf043'), TINT_SLASH('\uf5c7'), TIRED('\uf5c8'), + TOGGLE_OFF('\uf204'), TOGGLE_ON('\uf205'), TOILET('\uf7d8'), TOILET_PAPER('\uf71e'), + TOOLBOX('\uf552'), TOOLS('\uf7d9'), TOOTH('\uf5c9'), TORAH('\uf6a0'), TORII_GATE('\uf6a1'), + TRACTOR('\uf722'), TRADEMARK('\uf25c'), TRAFFIC_LIGHT('\uf637'), TRAIN('\uf238'), + TRAM('\uf7da'), TRANSGENDER('\uf224'), TRANSGENDER_ALT('\uf225'), TRASH('\uf1f8'), + TRASH_ALT('\uf2ed'), TRASH_RESTORE('\uf829'), TRASH_RESTORE_ALT('\uf82a'), TREE('\uf1bb'), + TROPHY('\uf091'), TRUCK('\uf0d1'), TRUCK_LOADING('\uf4de'), TRUCK_MONSTER('\uf63b'), + TRUCK_MOVING('\uf4df'), TRUCK_PICKUP('\uf63c'), TSHIRT('\uf553'), TTY('\uf1e4'), TV('\uf26c'), + UMBRELLA('\uf0e9'), UMBRELLA_BEACH('\uf5ca'), UNDERLINE('\uf0cd'), UNDO('\uf0e2'), + UNDO_ALT('\uf2ea'), UNIVERSAL_ACCESS('\uf29a'), UNIVERSITY('\uf19c'), UNLINK('\uf127'), + UNLOCK('\uf09c'), UNLOCK_ALT('\uf13e'), UPLOAD('\uf093'), USER('\uf007'), USER_ALT('\uf406'), + USER_ALT_SLASH('\uf4fa'), USER_ASTRONAUT('\uf4fb'), USER_CHECK('\uf4fc'), USER_CIRCLE('\uf2bd'), + USER_CLOCK('\uf4fd'), USER_COG('\uf4fe'), USER_EDIT('\uf4ff'), USER_FRIENDS('\uf500'), + USER_GRADUATE('\uf501'), USER_INJURED('\uf728'), USER_LOCK('\uf502'), USER_MD('\uf0f0'), + USER_MINUS('\uf503'), USER_NINJA('\uf504'), USER_NURSE('\uf82f'), USER_PLUS('\uf234'), + USER_SECRET('\uf21b'), USER_SHIELD('\uf505'), USER_SLASH('\uf506'), USER_TAG('\uf507'), + USER_TIE('\uf508'), USER_TIMES('\uf235'), USERS('\uf0c0'), USERS_COG('\uf509'), + UTENSIL_SPOON('\uf2e5'), UTENSILS('\uf2e7'), VECTOR_SQUARE('\uf5cb'), VENUS('\uf221'), + VENUS_DOUBLE('\uf226'), VENUS_MARS('\uf228'), VIAL('\uf492'), VIALS('\uf493'), VIDEO('\uf03d'), + VIDEO_SLASH('\uf4e2'), VIHARA('\uf6a7'), VOICEMAIL('\uf897'), VOLLEYBALL_BALL('\uf45f'), + VOLUME_DOWN('\uf027'), VOLUME_MUTE('\uf6a9'), VOLUME_OFF('\uf026'), VOLUME_UP('\uf028'), + VOTE_YEA('\uf772'), VR_CARDBOARD('\uf729'), WALKING('\uf554'), WALLET('\uf555'), + WAREHOUSE('\uf494'), WATER('\uf773'), WAVE_SQUARE('\uf83e'), WEIGHT('\uf496'), + WEIGHT_HANGING('\uf5cd'), WHEELCHAIR('\uf193'), WIFI('\uf1eb'), WIND('\uf72e'), + WINDOW_CLOSE('\uf410'), WINDOW_MAXIMIZE('\uf2d0'), WINDOW_MINIMIZE('\uf2d1'), + WINDOW_RESTORE('\uf2d2'), WINE_BOTTLE('\uf72f'), WINE_GLASS('\uf4e3'), WINE_GLASS_ALT('\uf5ce'), + WON_SIGN('\uf159'), WRENCH('\uf0ad'), X_RAY('\uf497'), YEN_SIGN('\uf157'), YIN_YANG('\uf6ad'); + + private static Font font; + private final char unicode; + + FontAwesomeSolid(char c) { + unicode = c; + } + + @Override + public Font getFont() { + if (font == null) { + font = Font.loadFont( + getClass().getResourceAsStream("/fonts/Font Awesome 6 Free-Solid-900.otf"), 16.0); + } + return font; + } + + @Override + public char getUnicode() { + return unicode; + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/fonticons/IconFactoryF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/fonticons/IconFactoryF.java new file mode 100644 index 00000000000..cfb1fdc6b65 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/fonticons/IconFactoryF.java @@ -0,0 +1,128 @@ +/* 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.fonticons; + +import javafx.scene.Node; +import javafx.scene.text.Font; +import javafx.scene.text.Text; + +/** + * Factory for the icons of the JavaFX UI, the counter-part of + * {@code de.uka.ilkd.key.gui.fonticons.IconFactory} in the Swing module {@code key.ui}. + *

+ * Icons are rendered as pure JavaFX {@link Text} nodes using the icon fonts bundled with the + * module. The text color is controlled by the {@code key-icon} style class, which is themed via + * {@code key-light.css}/{\code key-dark.css} — no AWT/ImageIcon involved. + *

+ * Only a subset of the semantic keys of the Swing {@code IconFactory} is defined yet; the + * remaining keys are ported together with the actions that use them (milestone M3). + */ +public final class IconFactoryF { + + /** The default size of icons created via the convenience factory methods. */ + public static final double DEFAULT_SIZE = 14.0; + + /** + * Style class of icon text nodes; the fill is defined by the theme stylesheets + * ({@code .key-icon { -fx-fill: -key-text; }}). + */ + public static final String ICON_STYLE_CLASS = "key-icon"; + + /** + * The semantic icon keys of the JavaFX UI. The glyph assignments mirror the constants of the + * Swing {@code IconFactory}. + */ + public enum Key { + QUIT(FontAwesomeSolid.WINDOW_CLOSE), + RECENT_FILES(FontAwesomeSolid.CLOCK), + SEARCH(FontAwesomeSolid.SEARCH), + STATISTICS(FontAwesomeSolid.THERMOMETER_HALF), + TOOLBOX(FontAwesomeSolid.TOOLBOX), + PLUS(FontAwesomeSolid.PLUS_CIRCLE), + MINUS(FontAwesomeSolid.MINUS_CIRCLE), + NEXT(FontAwesomeSolid.ARROW_RIGHT), + PREVIOUS(FontAwesomeSolid.ARROW_LEFT), + START(FontAwesomeSolid.PLAY), + STOP(FontAwesomeSolid.STOP), + CLOSE(FontAwesomeSolid.TIMES), + OPEN_MOST_RECENT(FontAwesomeSolid.REDO_ALT), + OPEN_KEY_FILE(FontAwesomeSolid.FOLDER_OPEN), + SAVE_FILE(FontAwesomeSolid.SAVE), + EDIT(FontAwesomeSolid.EDIT), + PRUNE(FontAwesomeSolid.CUT), + GOAL_BACK(FontAwesomeSolid.BACKSPACE), + EXPAND_GOALS(FontAwesomeSolid.EXPAND_ARROWS_ALT), + CONFIGURE(FontAwesomeSolid.COG), + HELP(FontAwesomeSolid.QUESTION_CIRCLE), + PROOF_MANAGEMENT(FontAwesomeSolid.TASKS), + AUTO_MODE_START(FontAwesomeSolid.PLAY_CIRCLE), + AUTO_MODE_STOP(FontAwesomeSolid.STOP_CIRCLE), + PROOF_TREE(FontAwesomeSolid.SITEMAP), + INFO_VIEW(FontAwesomeSolid.INFO_CIRCLE), + PROOF_SEARCH_STRATEGY(FontAwesomeSolid.COG); + + private final IconFont glyph; + + Key(IconFont glyph) { + this.glyph = glyph; + } + + /** + * @return the glyph backing this key + */ + public IconFont glyph() { + return glyph; + } + } + + private IconFactoryF() { + } + + /** + * Creates an icon for the given semantic key at {@link #DEFAULT_SIZE}. + * + * @param key the semantic key + * @return a new icon node (may be used as a graphic of buttons, menu items and tabs) + */ + public static Node createIcon(Key key) { + return createIcon(key, DEFAULT_SIZE); + } + + /** + * Creates an icon for the given semantic key at the given size. + * + * @param key the semantic key + * @param size the font size of the icon + * @return a new icon node + */ + public static Node createIcon(Key key, double size) { + return createIcon(key.glyph(), size); + } + + /** + * Creates an icon rendering the given glyph at {@link #DEFAULT_SIZE}. + * + * @param glyph the glyph + * @return a new icon node + */ + public static Node createIcon(IconFont glyph) { + return createIcon(glyph, DEFAULT_SIZE); + } + + /** + * Creates an icon rendering the given glyph at the given size. + * + * @param glyph the glyph + * @param size the font size of the icon + * @return a new icon node + */ + public static Node createIcon(IconFont glyph, double size) { + Text text = new Text(String.valueOf(glyph.getUnicode())); + text.getStyleClass().add(ICON_STYLE_CLASS); + javafx.scene.text.Font base = glyph.getFont(); + text.setFont(Font.font(base.getFamily(), size)); + return text; + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/fonticons/IconFont.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/fonticons/IconFont.java new file mode 100644 index 00000000000..5290df2a1b8 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/fonticons/IconFont.java @@ -0,0 +1,28 @@ +/* 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.fonticons; + +import javafx.scene.text.Font; + +/** + * An icon font that provides glyphs for a set of icons, the JavaFX counter-part of + * {@code de.uka.ilkd.key.gui.fonticons.IconFont} in the Swing module {@code key.ui}. + *

+ * Implementations are enums whose constants each represent one glyph (see + * {@link #getUnicode()}). The font is loaded lazily from the module resources via + * {@link javafx.scene.text.Font#loadFont}. + */ +public interface IconFont { + + /** + * @return the (cached) {@link Font} carrying the glyphs + */ + Font getFont(); + + /** + * @return the unicode code point of the glyph in {@link #getFont()} + */ + char getUnicode(); +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/fonticons/MaterialDesignRegular.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/fonticons/MaterialDesignRegular.java new file mode 100644 index 00000000000..a00f659606e --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/fonticons/MaterialDesignRegular.java @@ -0,0 +1,1850 @@ +/* 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.fonticons; + + +/** + * @author Alexander Weigl + * @version 1 (22.07.19) + */ + +import javafx.scene.text.Font; + +public enum MaterialDesignRegular implements IconFont { + _3D_ROTATION('\ue84d'), AC_UNIT('\ueb3b'), + + ACCESS_ALARM('\ue190'), + + ACCESS_ALARMS('\ue191'), + + ACCESS_TIME('\ue192'), + + ACCESSIBILITY('\ue84e'), + + ACCESSIBLE('\ue914'), + + ACCOUNT_BALANCE('\ue84f'), + + ACCOUNT_BALANCE_WALLET('\ue850'), + + ACCOUNT_BOX('\ue851'), + + ACCOUNT_CIRCLE('\ue853'), + + ADB('\ue60e'), + + ADD('\ue145'), ADD_A_PHOTO('\ue439'), ADD_ALARM('\ue193'), ADD_ALERT('\ue003'), + ADD_BOX('\ue146'), ADD_CIRCLE('\ue147'), ADD_CIRCLE_OUTLINE('\ue148'), ADD_LOCATION('\ue567'), + ADD_SHOPPING_CART('\ue854'), ADD_TO_PHOTOS('\ue39d'), ADD_TO_QUEUE('\ue05c'), ADJUST('\ue39e'), + AIRLINE_SEAT_FLAT('\ue630'), AIRLINE_SEAT_FLAT_ANGLED('\ue631'), + + AIRLINE_SEAT_INDIVIDUAL_SUITE('\ue632'), + + AIRLINE_SEAT_LEGROOM_EXTRA('\ue633'), + + AIRLINE_SEAT_LEGROOM_NORMAL('\ue634'), + + AIRLINE_SEAT_LEGROOM_REDUCED('\ue635'), + + AIRLINE_SEAT_RECLINE_EXTRA('\ue636'), + + AIRLINE_SEAT_RECLINE_NORMAL('\ue637'), + + AIRPLANEMODE_ACTIVE('\ue195'), + + AIRPLANEMODE_INACTIVE('\ue194'), + + AIRPLAY('\ue055'), + + AIRPORT_SHUTTLE('\ueb3c'), + + ALARM('\ue855'), + + ALARM_ADD('\ue856'), + + ALARM_OFF('\ue857'), + + ALARM_ON('\ue858'), + + ALBUM('\ue019'), + + ALL_INCLUSIVE('\ueb3d'), + + ALL_OUT('\ue90b'), + + ANDROID('\ue859'), + + ANNOUNCEMENT('\ue85a'), + + APPS('\ue5c3'), + + ARCHIVE('\ue149'), + + ARROW_BACK('\ue5c4'), + + ARROW_DOWNWARD('\ue5db'), + + ARROW_DROP_DOWN('\ue5c5'), + + ARROW_DROP_DOWN_CIRCLE('\ue5c6'), + + ARROW_DROP_UP('\ue5c7'), + + ARROW_FORWARD('\ue5c8'), + + ARROW_UPWARD('\ue5d8'), + + ART_TRACK('\ue060'), + + ASPECT_RATIO('\ue85b'), + + ASSESSMENT('\ue85c'), + + ASSIGNMENT('\ue85d'), + + ASSIGNMENT_IND('\ue85e'), + + ASSIGNMENT_LATE('\ue85f'), + + ASSIGNMENT_RETURN('\ue860'), + + ASSIGNMENT_RETURNED('\ue861'), + + ASSIGNMENT_TURNED_IN('\ue862'), + + ASSISTANT('\ue39f'), + + ASSISTANT_PHOTO('\ue3a0'), + + ATTACH_FILE('\ue226'), + + ATTACH_MONEY('\ue227'), + + ATTACHMENT('\ue2bc'), + + AUDIOTRACK('\ue3a1'), + + AUTORENEW('\ue863'), + + AV_TIMER('\ue01b'), + + BACKSPACE('\ue14a'), + + BACKUP('\ue864'), + + BATTERY_ALERT('\ue19c'), + + BATTERY_CHARGING_FULL('\ue1a3'), + + BATTERY_FULL('\ue1a4'), + + BATTERY_STD('\ue1a5'), + + BATTERY_UNKNOWN('\ue1a6'), + + BEACH_ACCESS('\ueb3e'), + + BEENHERE('\ue52d'), + + BLOCK('\ue14b'), + + BLUETOOTH('\ue1a7'), + + BLUETOOTH_AUDIO('\ue60f'), + + BLUETOOTH_CONNECTED('\ue1a8'), + + BLUETOOTH_DISABLED('\ue1a9'), + + BLUETOOTH_SEARCHING('\ue1aa'), + + BLUR_CIRCULAR('\ue3a2'), + + BLUR_LINEAR('\ue3a3'), + + BLUR_OFF('\ue3a4'), + + BLUR_ON('\ue3a5'), + + BOOK('\ue865'), + + BOOKMARK('\ue866'), + + BOOKMARK_BORDER('\ue867'), + + BORDER_ALL('\ue228'), + + BORDER_BOTTOM('\ue229'), + + BORDER_CLEAR('\ue22a'), + + BORDER_COLOR('\ue22b'), + + BORDER_HORIZONTAL('\ue22c'), + + BORDER_INNER('\ue22d'), + + BORDER_LEFT('\ue22e'), + + BORDER_OUTER('\ue22f'), + + BORDER_RIGHT('\ue230'), + + BORDER_STYLE('\ue231'), + + BORDER_TOP('\ue232'), + + BORDER_VERTICAL('\ue233'), + + BRANDING_WATERMARK('\ue06b'), + + BRIGHTNESS_1('\ue3a6'), + + BRIGHTNESS_2('\ue3a7'), + + BRIGHTNESS_3('\ue3a8'), + + BRIGHTNESS_4('\ue3a9'), + + BRIGHTNESS_5('\ue3aa'), + + BRIGHTNESS_6('\ue3ab'), + + BRIGHTNESS_7('\ue3ac'), + + BRIGHTNESS_AUTO('\ue1ab'), + + BRIGHTNESS_HIGH('\ue1ac'), + + BRIGHTNESS_LOW('\ue1ad'), + + BRIGHTNESS_MEDIUM('\ue1ae'), + + BROKEN_IMAGE('\ue3ad'), + + BRUSH('\ue3ae'), + + BUBBLE_CHART('\ue6dd'), + + BUG_REPORT('\ue868'), + + BUILD('\ue869'), + + BURST_MODE('\ue43c'), + + BUSINESS('\ue0af'), + + BUSINESS_CENTER('\ueb3f'), + + CACHED('\ue86a'), + + CAKE('\ue7e9'), + + CALL('\ue0b0'), + + CALL_END('\ue0b1'), + + CALL_MADE('\ue0b2'), + + CALL_MERGE('\ue0b3'), + + CALL_MISSED('\ue0b4'), + + CALL_MISSED_OUTGOING('\ue0e4'), + + CALL_RECEIVED('\ue0b5'), + + CALL_SPLIT('\ue0b6'), + + CALL_TO_ACTION('\ue06c'), + + CAMERA('\ue3af'), + + CAMERA_ALT('\ue3b0'), + + CAMERA_ENHANCE('\ue8fc'), + + CAMERA_FRONT('\ue3b1'), + + CAMERA_REAR('\ue3b2'), + + CAMERA_ROLL('\ue3b3'), + + CANCEL('\ue5c9'), + + CARD_GIFTCARD('\ue8f6'), + + CARD_MEMBERSHIP('\ue8f7'), + + CARD_TRAVEL('\ue8f8'), + + CASINO('\ueb40'), + + CAST('\ue307'), + + CAST_CONNECTED('\ue308'), + + CENTER_FOCUS_STRONG('\ue3b4'), + + CENTER_FOCUS_WEAK('\ue3b5'), + + CHANGE_HISTORY('\ue86b'), + + CHAT('\ue0b7'), + + CHAT_BUBBLE('\ue0ca'), + + CHAT_BUBBLE_OUTLINE('\ue0cb'), + + CHECK('\ue5ca'), + + CHECK_BOX('\ue834'), + + CHECK_BOX_OUTLINE_BLANK('\ue835'), + + CHECK_CIRCLE('\ue86c'), + + CHEVRON_LEFT('\ue5cb'), + + CHEVRON_RIGHT('\ue5cc'), + + CHILD_CARE('\ueb41'), + + CHILD_FRIENDLY('\ueb42'), + + CHROME_READER_MODE('\ue86d'), + + CLASS('\ue86e'), + + CLEAR('\ue14c'), + + CLEAR_ALL('\ue0b8'), + + CLOSE('\ue5cd'), + + CLOSED_CAPTION('\ue01c'), + + CLOUD('\ue2bd'), + + CLOUD_CIRCLE('\ue2be'), + + CLOUD_DONE('\ue2bf'), + + CLOUD_DOWNLOAD('\ue2c0'), + + CLOUD_OFF('\ue2c1'), + + CLOUD_QUEUE('\ue2c2'), + + CLOUD_UPLOAD('\ue2c3'), + + CODE('\ue86f'), + + COLLECTIONS('\ue3b6'), + + COLLECTIONS_BOOKMARK('\ue431'), + + COLOR_LENS('\ue3b7'), + + COLORIZE('\ue3b8'), + + COMMENT('\ue0b9'), + + COMPARE('\ue3b9'), + + COMPARE_ARROWS('\ue915'), + + COMPUTER('\ue30a'), + + CONFIRMATION_NUMBER('\ue638'), + + CONTACT_MAIL('\ue0d0'), + + CONTACT_PHONE('\ue0cf'), + + CONTACTS('\ue0ba'), + + CONTENT_COPY('\ue14d'), + + CONTENT_CUT('\ue14e'), + + CONTENT_PASTE('\ue14f'), + + CONTROL_POINT('\ue3ba'), + + CONTROL_POINT_DUPLICATE('\ue3bb'), + + COPYRIGHT('\ue90c'), + + CREATE('\ue150'), + + CREATE_NEW_FOLDER('\ue2cc'), + + CREDIT_CARD('\ue870'), + + CROP('\ue3be'), + + CROP_16_9('\ue3bc'), + + CROP_3_2('\ue3bd'), + + CROP_5_4('\ue3bf'), + + CROP_7_5('\ue3c0'), + + CROP_DIN('\ue3c1'), + + CROP_FREE('\ue3c2'), + + CROP_LANDSCAPE('\ue3c3'), + + CROP_ORIGINAL('\ue3c4'), + + CROP_PORTRAIT('\ue3c5'), + + CROP_ROTATE('\ue437'), + + CROP_SQUARE('\ue3c6'), + + DASHBOARD('\ue871'), + + DATA_USAGE('\ue1af'), + + DATE_RANGE('\ue916'), + + DEHAZE('\ue3c7'), + + DELETE('\ue872'), + + DELETE_FOREVER('\ue92b'), + + DELETE_SWEEP('\ue16c'), + + DESCRIPTION('\ue873'), + + DESKTOP_MAC('\ue30b'), + + DESKTOP_WINDOWS('\ue30c'), + + DETAILS('\ue3c8'), + + DEVELOPER_BOARD('\ue30d'), + + DEVELOPER_MODE('\ue1b0'), + + DEVICE_HUB('\ue335'), + + DEVICES('\ue1b1'), + + DEVICES_OTHER('\ue337'), + + DIALER_SIP('\ue0bb'), + + DIALPAD('\ue0bc'), + + DIRECTIONS('\ue52e'), + + DIRECTIONS_BIKE('\ue52f'), + + DIRECTIONS_BOAT('\ue532'), + + DIRECTIONS_BUS('\ue530'), + + DIRECTIONS_CAR('\ue531'), + + DIRECTIONS_RAILWAY('\ue534'), + + DIRECTIONS_RUN('\ue566'), + + DIRECTIONS_SUBWAY('\ue533'), + + DIRECTIONS_TRANSIT('\ue535'), + + DIRECTIONS_WALK('\ue536'), + + DISC_FULL('\ue610'), + + DNS('\ue875'), + + DO_NOT_DISTURB('\ue612'), + + DO_NOT_DISTURB_ALT('\ue611'), + + DO_NOT_DISTURB_OFF('\ue643'), + + DO_NOT_DISTURB_ON('\ue644'), + + DOCK('\ue30e'), + + DOMAIN('\ue7ee'), + + DONE('\ue876'), + + DONE_ALL('\ue877'), + + DONUT_LARGE('\ue917'), + + DONUT_SMALL('\ue918'), + + DRAFTS('\ue151'), + + DRAG_HANDLE('\ue25d'), + + DRIVE_ETA('\ue613'), + + DVR('\ue1b2'), + + EDIT('\ue3c9'), + + EDIT_LOCATION('\ue568'), + + EJECT('\ue8fb'), + + EMAIL('\ue0be'), + + ENHANCED_ENCRYPTION('\ue63f'), + + EQUALIZER('\ue01d'), + + ERROR('\ue000'), + + ERROR_OUTLINE('\ue001'), + + EURO_SYMBOL('\ue926'), + + EV_STATION('\ue56d'), + + EVENT('\ue878'), + + EVENT_AVAILABLE('\ue614'), + + EVENT_BUSY('\ue615'), + + EVENT_NOTE('\ue616'), + + EVENT_SEAT('\ue903'), + + EXIT_TO_APP('\ue879'), + + EXPAND_LESS('\ue5ce'), + + EXPAND_MORE('\ue5cf'), + + EXPLICIT('\ue01e'), + + EXPLORE('\ue87a'), + + EXPOSURE('\ue3ca'), + + EXPOSURE_NEG_1('\ue3cb'), + + EXPOSURE_NEG_2('\ue3cc'), + + EXPOSURE_PLUS_1('\ue3cd'), + + EXPOSURE_PLUS_2('\ue3ce'), + + EXPOSURE_ZERO('\ue3cf'), + + EXTENSION('\ue87b'), + + FACE('\ue87c'), + + FAST_FORWARD('\ue01f'), + + FAST_REWIND('\ue020'), + + FAVORITE('\ue87d'), + + FAVORITE_BORDER('\ue87e'), + + FEATURED_PLAY_LIST('\ue06d'), + + FEATURED_VIDEO('\ue06e'), + + FEEDBACK('\ue87f'), + + FIBER_DVR('\ue05d'), + + FIBER_MANUAL_RECORD('\ue061'), + + FIBER_NEW('\ue05e'), + + FIBER_PIN('\ue06a'), + + FIBER_SMART_RECORD('\ue062'), + + FILE_DOWNLOAD('\ue2c4'), + + FILE_UPLOAD('\ue2c6'), + + FILTER('\ue3d3'), + + FILTER_1('\ue3d0'), + + FILTER_2('\ue3d1'), + + FILTER_3('\ue3d2'), + + FILTER_4('\ue3d4'), + + FILTER_5('\ue3d5'), + + FILTER_6('\ue3d6'), + + FILTER_7('\ue3d7'), + + FILTER_8('\ue3d8'), + + FILTER_9('\ue3d9'), + + FILTER_9_PLUS('\ue3da'), + + FILTER_B_AND_W('\ue3db'), + + FILTER_CENTER_FOCUS('\ue3dc'), + + FILTER_DRAMA('\ue3dd'), + + FILTER_FRAMES('\ue3de'), + + FILTER_HDR('\ue3df'), + + FILTER_LIST('\ue152'), + + FILTER_NONE('\ue3e0'), + + FILTER_TILT_SHIFT('\ue3e2'), + + FILTER_VINTAGE('\ue3e3'), + + FIND_IN_PAGE('\ue880'), + + FIND_REPLACE('\ue881'), + + FINGERPRINT('\ue90d'), + + FIRST_PAGE('\ue5dc'), + + FITNESS_CENTER('\ueb43'), + + FLAG('\ue153'), + + FLARE('\ue3e4'), + + FLASH_AUTO('\ue3e5'), + + FLASH_OFF('\ue3e6'), + + FLASH_ON('\ue3e7'), + + FLIGHT('\ue539'), + + FLIGHT_LAND('\ue904'), + + FLIGHT_TAKEOFF('\ue905'), + + FLIP('\ue3e8'), + + FLIP_TO_BACK('\ue882'), + + FLIP_TO_FRONT('\ue883'), + + FOLDER('\ue2c7'), + + FOLDER_OPEN('\ue2c8'), + + FOLDER_SHARED('\ue2c9'), + + FOLDER_SPECIAL('\ue617'), + + FONT_DOWNLOAD('\ue167'), + + FORMAT_ALIGN_CENTER('\ue234'), + + FORMAT_ALIGN_JUSTIFY('\ue235'), + + FORMAT_ALIGN_LEFT('\ue236'), + + FORMAT_ALIGN_RIGHT('\ue237'), + + FORMAT_BOLD('\ue238'), + + FORMAT_CLEAR('\ue239'), + + FORMAT_COLOR_FILL('\ue23a'), + + FORMAT_COLOR_RESET('\ue23b'), + + FORMAT_COLOR_TEXT('\ue23c'), + + FORMAT_INDENT_DECREASE('\ue23d'), + + FORMAT_INDENT_INCREASE('\ue23e'), + + FORMAT_ITALIC('\ue23f'), + + FORMAT_LINE_SPACING('\ue240'), + + FORMAT_LIST_BULLETED('\ue241'), + + FORMAT_LIST_NUMBERED('\ue242'), + + FORMAT_PAINT('\ue243'), + + FORMAT_QUOTE('\ue244'), + + FORMAT_SHAPES('\ue25e'), + + FORMAT_SIZE('\ue245'), + + FORMAT_STRIKETHROUGH('\ue246'), + + FORMAT_TEXTDIRECTION_L_TO_R('\ue247'), + + FORMAT_TEXTDIRECTION_R_TO_L('\ue248'), + + FORMAT_UNDERLINED('\ue249'), + + FORUM('\ue0bf'), + + FORWARD('\ue154'), + + FORWARD_10('\ue056'), + + FORWARD_30('\ue057'), + + FORWARD_5('\ue058'), + + FREE_BREAKFAST('\ueb44'), + + FULLSCREEN('\ue5d0'), + + FULLSCREEN_EXIT('\ue5d1'), + + FUNCTIONS('\ue24a'), + + G_TRANSLATE('\ue927'), + + GAMEPAD('\ue30f'), + + GAMES('\ue021'), + + GAVEL('\ue90e'), + + GESTURE('\ue155'), + + GET_APP('\ue884'), + + GIF('\ue908'), + + GOLF_COURSE('\ueb45'), + + GPS_FIXED('\ue1b3'), + + GPS_NOT_FIXED('\ue1b4'), + + GPS_OFF('\ue1b5'), + + GRADE('\ue885'), + + GRADIENT('\ue3e9'), + + GRAIN('\ue3ea'), + + GRAPHIC_EQ('\ue1b8'), + + GRID_OFF('\ue3eb'), + + GRID_ON('\ue3ec'), + + GROUP('\ue7ef'), + + GROUP_ADD('\ue7f0'), + + GROUP_WORK('\ue886'), + + HD('\ue052'), + + HDR_OFF('\ue3ed'), + + HDR_ON('\ue3ee'), + + HDR_STRONG('\ue3f1'), + + HDR_WEAK('\ue3f2'), + + HEADSET('\ue310'), + + HEADSET_MIC('\ue311'), + + HEALING('\ue3f3'), + + HEARING('\ue023'), + + HELP('\ue887'), + + HELP_OUTLINE('\ue8fd'), + + HIGH_QUALITY('\ue024'), + + HIGHLIGHT('\ue25f'), + + HIGHLIGHT_OFF('\ue888'), + + HISTORY('\ue889'), + + HOME('\ue88a'), + + HOT_TUB('\ueb46'), + + HOTEL('\ue53a'), + + HOURGLASS_EMPTY('\ue88b'), + + HOURGLASS_FULL('\ue88c'), + + HTTP('\ue902'), + + HTTPS('\ue88d'), + + IMAGE('\ue3f4'), + + IMAGE_ASPECT_RATIO('\ue3f5'), + + IMPORT_CONTACTS('\ue0e0'), + + IMPORT_EXPORT('\ue0c3'), + + IMPORTANT_DEVICES('\ue912'), + + INBOX('\ue156'), + + INDETERMINATE_CHECK_BOX('\ue909'), + + INFO('\ue88e'), + + INFO_OUTLINE('\ue88f'), + + INPUT('\ue890'), + + INSERT_CHART('\ue24b'), + + INSERT_COMMENT('\ue24c'), + + INSERT_DRIVE_FILE('\ue24d'), + + INSERT_EMOTICON('\ue24e'), + + INSERT_INVITATION('\ue24f'), + + INSERT_LINK('\ue250'), + + INSERT_PHOTO('\ue251'), + + INVERT_COLORS('\ue891'), + + INVERT_COLORS_OFF('\ue0c4'), + + ISO('\ue3f6'), + + KEYBOARD('\ue312'), + + KEYBOARD_ARROW_DOWN('\ue313'), + + KEYBOARD_ARROW_LEFT('\ue314'), + + KEYBOARD_ARROW_RIGHT('\ue315'), + + KEYBOARD_ARROW_UP('\ue316'), + + KEYBOARD_BACKSPACE('\ue317'), + + KEYBOARD_CAPSLOCK('\ue318'), + + KEYBOARD_HIDE('\ue31a'), + + KEYBOARD_RETURN('\ue31b'), + + KEYBOARD_TAB('\ue31c'), + + KEYBOARD_VOICE('\ue31d'), + + KITCHEN('\ueb47'), + + LABEL('\ue892'), + + LABEL_OUTLINE('\ue893'), + + LANDSCAPE('\ue3f7'), + + LANGUAGE('\ue894'), + + LAPTOP('\ue31e'), + + LAPTOP_CHROMEBOOK('\ue31f'), + + LAPTOP_MAC('\ue320'), + + LAPTOP_WINDOWS('\ue321'), + + LAST_PAGE('\ue5dd'), + + LAUNCH('\ue895'), + + LAYERS('\ue53b'), + + LAYERS_CLEAR('\ue53c'), + + LEAK_ADD('\ue3f8'), + + LEAK_REMOVE('\ue3f9'), + + LENS('\ue3fa'), + + LIBRARY_ADD('\ue02e'), + + LIBRARY_BOOKS('\ue02f'), + + LIBRARY_MUSIC('\ue030'), + + LIGHTBULB_OUTLINE('\ue90f'), + + LINE_STYLE('\ue919'), + + LINE_WEIGHT('\ue91a'), + + LINEAR_SCALE('\ue260'), + + LINK('\ue157'), + + LINKED_CAMERA('\ue438'), + + LIST('\ue896'), + + LIVE_HELP('\ue0c6'), + + LIVE_TV('\ue639'), + + LOCAL_ACTIVITY('\ue53f'), + + LOCAL_AIRPORT('\ue53d'), + + LOCAL_ATM('\ue53e'), + + LOCAL_BAR('\ue540'), + + LOCAL_CAFE('\ue541'), + + LOCAL_CAR_WASH('\ue542'), + + LOCAL_CONVENIENCE_STORE('\ue543'), + + LOCAL_DINING('\ue556'), + + LOCAL_DRINK('\ue544'), + + LOCAL_FLORIST('\ue545'), + + LOCAL_GAS_STATION('\ue546'), + + LOCAL_GROCERY_STORE('\ue547'), + + LOCAL_HOSPITAL('\ue548'), + + LOCAL_HOTEL('\ue549'), + + LOCAL_LAUNDRY_SERVICE('\ue54a'), + + LOCAL_LIBRARY('\ue54b'), + + LOCAL_MALL('\ue54c'), + + LOCAL_MOVIES('\ue54d'), + + LOCAL_OFFER('\ue54e'), + + LOCAL_PARKING('\ue54f'), + + LOCAL_PHARMACY('\ue550'), + + LOCAL_PHONE('\ue551'), + + LOCAL_PIZZA('\ue552'), + + LOCAL_PLAY('\ue553'), + + LOCAL_POST_OFFICE('\ue554'), + + LOCAL_PRINTSHOP('\ue555'), + + LOCAL_SEE('\ue557'), + + LOCAL_SHIPPING('\ue558'), + + LOCAL_TAXI('\ue559'), + + LOCATION_CITY('\ue7f1'), + + LOCATION_DISABLED('\ue1b6'), + + LOCATION_OFF('\ue0c7'), + + LOCATION_ON('\ue0c8'), + + LOCATION_SEARCHING('\ue1b7'), + + LOCK('\ue897'), + + LOCK_OPEN('\ue898'), + + LOCK_OUTLINE('\ue899'), + + LOOKS('\ue3fc'), + + LOOKS_3('\ue3fb'), + + LOOKS_4('\ue3fd'), + + LOOKS_5('\ue3fe'), + + LOOKS_6('\ue3ff'), + + LOOKS_ONE('\ue400'), + + LOOKS_TWO('\ue401'), + + LOOP('\ue028'), + + LOUPE('\ue402'), + + LOW_PRIORITY('\ue16d'), + + LOYALTY('\ue89a'), + + MAIL('\ue158'), + + MAIL_OUTLINE('\ue0e1'), + + MAP('\ue55b'), + + MARKUNREAD('\ue159'), + + MARKUNREAD_MAILBOX('\ue89b'), + + MEMORY('\ue322'), + + MENU('\ue5d2'), + + MERGE_TYPE('\ue252'), + + MESSAGE('\ue0c9'), + + MIC('\ue029'), + + MIC_NONE('\ue02a'), + + MIC_OFF('\ue02b'), + + MMS('\ue618'), + + MODE_COMMENT('\ue253'), + + MODE_EDIT('\ue254'), + + MONETIZATION_ON('\ue263'), + + MONEY_OFF('\ue25c'), + + MONOCHROME_PHOTOS('\ue403'), + + MOOD('\ue7f2'), + + MOOD_BAD('\ue7f3'), + + MORE('\ue619'), + + MORE_HORIZ('\ue5d3'), + + MORE_VERT('\ue5d4'), + + MOTORCYCLE('\ue91b'), + + MOUSE('\ue323'), + + MOVE_TO_INBOX('\ue168'), + + MOVIE('\ue02c'), + + MOVIE_CREATION('\ue404'), + + MOVIE_FILTER('\ue43a'), + + MULTILINE_CHART('\ue6df'), + + MUSIC_NOTE('\ue405'), + + MUSIC_VIDEO('\ue063'), + + MY_LOCATION('\ue55c'), + + NATURE('\ue406'), + + NATURE_PEOPLE('\ue407'), + + NAVIGATE_BEFORE('\ue408'), + + NAVIGATE_NEXT('\ue409'), + + NAVIGATION('\ue55d'), + + NEAR_ME('\ue569'), + + NETWORK_CELL('\ue1b9'), + + NETWORK_CHECK('\ue640'), + + NETWORK_LOCKED('\ue61a'), + + NETWORK_WIFI('\ue1ba'), + + NEW_RELEASES('\ue031'), + + NEXT_WEEK('\ue16a'), + + NFC('\ue1bb'), + + NO_ENCRYPTION('\ue641'), + + NO_SIM('\ue0cc'), + + NOT_INTERESTED('\ue033'), + + NOTE('\ue06f'), + + NOTE_ADD('\ue89c'), + + NOTIFICATIONS('\ue7f4'), + + NOTIFICATIONS_ACTIVE('\ue7f7'), + + NOTIFICATIONS_NONE('\ue7f5'), + + NOTIFICATIONS_OFF('\ue7f6'), + + NOTIFICATIONS_PAUSED('\ue7f8'), + + OFFLINE_PIN('\ue90a'), + + ONDEMAND_VIDEO('\ue63a'), + + OPACITY('\ue91c'), + + OPEN_IN_BROWSER('\ue89d'), + + OPEN_IN_NEW('\ue89e'), + + OPEN_WITH('\ue89f'), + + PAGES('\ue7f9'), + + PAGEVIEW('\ue8a0'), + + PALETTE('\ue40a'), + + PAN_TOOL('\ue925'), + + PANORAMA('\ue40b'), + + PANORAMA_FISH_EYE('\ue40c'), + + PANORAMA_HORIZONTAL('\ue40d'), + + PANORAMA_VERTICAL('\ue40e'), + + PANORAMA_WIDE_ANGLE('\ue40f'), + + PARTY_MODE('\ue7fa'), + + PAUSE('\ue034'), + + PAUSE_CIRCLE_FILLED('\ue035'), + + PAUSE_CIRCLE_OUTLINE('\ue036'), + + PAYMENT('\ue8a1'), + + PEOPLE('\ue7fb'), + + PEOPLE_OUTLINE('\ue7fc'), + + PERM_CAMERA_MIC('\ue8a2'), + + PERM_CONTACT_CALENDAR('\ue8a3'), + + PERM_DATA_SETTING('\ue8a4'), + + PERM_DEVICE_INFORMATION('\ue8a5'), + + PERM_IDENTITY('\ue8a6'), + + PERM_MEDIA('\ue8a7'), + + PERM_PHONE_MSG('\ue8a8'), + + PERM_SCAN_WIFI('\ue8a9'), + + PERSON('\ue7fd'), + + PERSON_ADD('\ue7fe'), + + PERSON_OUTLINE('\ue7ff'), + + PERSON_PIN('\ue55a'), + + PERSON_PIN_CIRCLE('\ue56a'), + + PERSONAL_VIDEO('\ue63b'), + + PETS('\ue91d'), + + PHONE('\ue0cd'), + + PHONE_ANDROID('\ue324'), + + PHONE_BLUETOOTH_SPEAKER('\ue61b'), + + PHONE_FORWARDED('\ue61c'), + + PHONE_IN_TALK('\ue61d'), + + PHONE_IPHONE('\ue325'), + + PHONE_LOCKED('\ue61e'), + + PHONE_MISSED('\ue61f'), + + PHONE_PAUSED('\ue620'), + + PHONELINK('\ue326'), + + PHONELINK_ERASE('\ue0db'), + + PHONELINK_LOCK('\ue0dc'), + + PHONELINK_OFF('\ue327'), + + PHONELINK_RING('\ue0dd'), + + PHONELINK_SETUP('\ue0de'), + + PHOTO('\ue410'), + + PHOTO_ALBUM('\ue411'), + + PHOTO_CAMERA('\ue412'), + + PHOTO_FILTER('\ue43b'), + + PHOTO_LIBRARY('\ue413'), + + PHOTO_SIZE_SELECT_ACTUAL('\ue432'), + + PHOTO_SIZE_SELECT_LARGE('\ue433'), + + PHOTO_SIZE_SELECT_SMALL('\ue434'), + + PICTURE_AS_PDF('\ue415'), + + PICTURE_IN_PICTURE('\ue8aa'), + + PICTURE_IN_PICTURE_ALT('\ue911'), + + PIE_CHART('\ue6c4'), + + PIE_CHART_OUTLINED('\ue6c5'), + + PIN_DROP('\ue55e'), + + PLACE('\ue55f'), + + PLAY_ARROW('\ue037'), + + PLAY_CIRCLE_FILLED('\ue038'), + + PLAY_CIRCLE_OUTLINE('\ue039'), + + PLAY_FOR_WORK('\ue906'), + + PLAYLIST_ADD('\ue03b'), + + PLAYLIST_ADD_CHECK('\ue065'), + + PLAYLIST_PLAY('\ue05f'), + + PLUS_ONE('\ue800'), + + POLL('\ue801'), + + POLYMER('\ue8ab'), + + POOL('\ueb48'), + + PORTABLE_WIFI_OFF('\ue0ce'), + + PORTRAIT('\ue416'), + + POWER('\ue63c'), + + POWER_INPUT('\ue336'), + + POWER_SETTINGS_NEW('\ue8ac'), + + PREGNANT_WOMAN('\ue91e'), + + PRESENT_TO_ALL('\ue0df'), + + PRINT('\ue8ad'), + + PRIORITY_HIGH('\ue645'), + + PUBLIC('\ue80b'), + + PUBLISH('\ue255'), + + QUERY_BUILDER('\ue8ae'), + + QUESTION_ANSWER('\ue8af'), + + QUEUE('\ue03c'), + + QUEUE_MUSIC('\ue03d'), + + QUEUE_PLAY_NEXT('\ue066'), + + RADIO('\ue03e'), + + RADIO_BUTTON_CHECKED('\ue837'), + + RADIO_BUTTON_UNCHECKED('\ue836'), + + RATE_REVIEW('\ue560'), + + RECEIPT('\ue8b0'), + + RECENT_ACTORS('\ue03f'), + + RECORD_VOICE_OVER('\ue91f'), + + REDEEM('\ue8b1'), + + REDO('\ue15a'), + + REFRESH('\ue5d5'), + + REMOVE('\ue15b'), + + REMOVE_CIRCLE('\ue15c'), + + REMOVE_CIRCLE_OUTLINE('\ue15d'), + + REMOVE_FROM_QUEUE('\ue067'), + + REMOVE_RED_EYE('\ue417'), + + REMOVE_SHOPPING_CART('\ue928'), + + REORDER('\ue8fe'), + + REPEAT('\ue040'), + + REPEAT_ONE('\ue041'), + + REPLAY('\ue042'), + + REPLAY_10('\ue059'), + + REPLAY_30('\ue05a'), + + REPLAY_5('\ue05b'), + + REPLY('\ue15e'), + + REPLY_ALL('\ue15f'), + + REPORT('\ue160'), + + REPORT_PROBLEM('\ue8b2'), + + RESTAURANT('\ue56c'), + + RESTAURANT_MENU('\ue561'), + + RESTORE('\ue8b3'), + + RESTORE_PAGE('\ue929'), + + RING_VOLUME('\ue0d1'), + + ROOM('\ue8b4'), + + ROOM_SERVICE('\ueb49'), + + ROTATE_90_DEGREES_CCW('\ue418'), + + ROTATE_LEFT('\ue419'), + + ROTATE_RIGHT('\ue41a'), + + ROUNDED_CORNER('\ue920'), + + ROUTER('\ue328'), + + ROWING('\ue921'), + + RSS_FEED('\ue0e5'), + + RV_HOOKUP('\ue642'), + + SATELLITE('\ue562'), + + SAVE('\ue161'), + + SCANNER('\ue329'), + + SCHEDULE('\ue8b5'), + + SCHOOL('\ue80c'), + + SCREEN_LOCK_LANDSCAPE('\ue1be'), + + SCREEN_LOCK_PORTRAIT('\ue1bf'), + + SCREEN_LOCK_ROTATION('\ue1c0'), + + SCREEN_ROTATION('\ue1c1'), + + SCREEN_SHARE('\ue0e2'), + + SD_CARD('\ue623'), + + SD_STORAGE('\ue1c2'), + + SEARCH('\ue8b6'), + + SECURITY('\ue32a'), + + SELECT_ALL('\ue162'), + + SEND('\ue163'), + + SENTIMENT_DISSATISFIED('\ue811'), + + SENTIMENT_NEUTRAL('\ue812'), + + SENTIMENT_SATISFIED('\ue813'), + + SENTIMENT_VERY_DISSATISFIED('\ue814'), + + SENTIMENT_VERY_SATISFIED('\ue815'), + + SETTINGS('\ue8b8'), + + SETTINGS_APPLICATIONS('\ue8b9'), + + SETTINGS_BACKUP_RESTORE('\ue8ba'), + + SETTINGS_BLUETOOTH('\ue8bb'), + + SETTINGS_BRIGHTNESS('\ue8bd'), + + SETTINGS_CELL('\ue8bc'), + + SETTINGS_ETHERNET('\ue8be'), + + SETTINGS_INPUT_ANTENNA('\ue8bf'), + + SETTINGS_INPUT_COMPONENT('\ue8c0'), + + SETTINGS_INPUT_COMPOSITE('\ue8c1'), + + SETTINGS_INPUT_HDMI('\ue8c2'), + + SETTINGS_INPUT_SVIDEO('\ue8c3'), + + SETTINGS_OVERSCAN('\ue8c4'), + + SETTINGS_PHONE('\ue8c5'), + + SETTINGS_POWER('\ue8c6'), + + SETTINGS_REMOTE('\ue8c7'), + + SETTINGS_SYSTEM_DAYDREAM('\ue1c3'), + + SETTINGS_VOICE('\ue8c8'), + + SHARE('\ue80d'), + + SHOP('\ue8c9'), + + SHOP_TWO('\ue8ca'), + + SHOPPING_BASKET('\ue8cb'), + + SHOPPING_CART('\ue8cc'), + + SHORT_TEXT('\ue261'), + + SHOW_CHART('\ue6e1'), + + SHUFFLE('\ue043'), + + SIGNAL_CELLULAR_4_BAR('\ue1c8'), + + SIGNAL_CELLULAR_CONNECTED_NO_INTERNET_4_BAR('\ue1cd'), + + SIGNAL_CELLULAR_NO_SIM('\ue1ce'), + + SIGNAL_CELLULAR_NULL('\ue1cf'), + + SIGNAL_CELLULAR_OFF('\ue1d0'), + + SIGNAL_WIFI_4_BAR('\ue1d8'), + + SIGNAL_WIFI_4_BAR_LOCK('\ue1d9'), + + SIGNAL_WIFI_OFF('\ue1da'), + + SIM_CARD('\ue32b'), + + SIM_CARD_ALERT('\ue624'), + + SKIP_NEXT('\ue044'), + + SKIP_PREVIOUS('\ue045'), + + SLIDESHOW('\ue41b'), + + SLOW_MOTION_VIDEO('\ue068'), + + SMARTPHONE('\ue32c'), + + SMOKE_FREE('\ueb4a'), + + SMOKING_ROOMS('\ueb4b'), + + SMS('\ue625'), + + SMS_FAILED('\ue626'), + + SNOOZE('\ue046'), + + SORT('\ue164'), + + SORT_BY_ALPHA('\ue053'), + + SPA('\ueb4c'), + + SPACE_BAR('\ue256'), + + SPEAKER('\ue32d'), + + SPEAKER_GROUP('\ue32e'), + + SPEAKER_NOTES('\ue8cd'), + + SPEAKER_NOTES_OFF('\ue92a'), + + SPEAKER_PHONE('\ue0d2'), + + SPELLCHECK('\ue8ce'), + + STAR('\ue838'), + + STAR_BORDER('\ue83a'), + + STAR_HALF('\ue839'), + + STARS('\ue8d0'), + + STAY_CURRENT_LANDSCAPE('\ue0d3'), + + STAY_CURRENT_PORTRAIT('\ue0d4'), + + STAY_PRIMARY_LANDSCAPE('\ue0d5'), + + STAY_PRIMARY_PORTRAIT('\ue0d6'), + + STOP('\ue047'), + + STOP_SCREEN_SHARE('\ue0e3'), + + STORAGE('\ue1db'), + + STORE('\ue8d1'), + + STORE_MALL_DIRECTORY('\ue563'), + + STRAIGHTEN('\ue41c'), + + STREETVIEW('\ue56e'), + + STRIKETHROUGH_S('\ue257'), + + STYLE('\ue41d'), + + SUBDIRECTORY_ARROW_LEFT('\ue5d9'), + + SUBDIRECTORY_ARROW_RIGHT('\ue5da'), + + SUBJECT('\ue8d2'), + + SUBSCRIPTIONS('\ue064'), + + SUBTITLES('\ue048'), + + SUBWAY('\ue56f'), + + SUPERVISOR_ACCOUNT('\ue8d3'), + + SURROUND_SOUND('\ue049'), + + SWAP_CALLS('\ue0d7'), + + SWAP_HORIZ('\ue8d4'), + + SWAP_VERT('\ue8d5'), + + SWAP_VERTICAL_CIRCLE('\ue8d6'), + + SWITCH_CAMERA('\ue41e'), + + SWITCH_VIDEO('\ue41f'), + + SYNC('\ue627'), + + SYNC_DISABLED('\ue628'), + + SYNC_PROBLEM('\ue629'), + + SYSTEM_UPDATE('\ue62a'), + + SYSTEM_UPDATE_ALT('\ue8d7'), + + TAB('\ue8d8'), + + TAB_UNSELECTED('\ue8d9'), + + TABLET('\ue32f'), + + TABLET_ANDROID('\ue330'), + + TABLET_MAC('\ue331'), + + TAG_FACES('\ue420'), + + TAP_AND_PLAY('\ue62b'), + + TERRAIN('\ue564'), + + TEXT_FIELDS('\ue262'), + + TEXT_FORMAT('\ue165'), + + TEXTSMS('\ue0d8'), + + TEXTURE('\ue421'), + + THEATERS('\ue8da'), + + THUMB_DOWN('\ue8db'), + + THUMB_UP('\ue8dc'), + + THUMBS_UP_DOWN('\ue8dd'), + + TIME_TO_LEAVE('\ue62c'), + + TIMELAPSE('\ue422'), + + TIMELINE('\ue922'), + + TIMER('\ue425'), + + TIMER_10('\ue423'), + + TIMER_3('\ue424'), + + TIMER_OFF('\ue426'), + + TITLE('\ue264'), + + TOC('\ue8de'), + + TODAY('\ue8df'), + + TOLL('\ue8e0'), + + TONALITY('\ue427'), + + TOUCH_APP('\ue913'), + + TOYS('\ue332'), + + TRACK_CHANGES('\ue8e1'), + + TRAFFIC('\ue565'), + + TRAIN('\ue570'), + + TRAM('\ue571'), + + TRANSFER_WITHIN_A_STATION('\ue572'), + + TRANSFORM('\ue428'), + + TRANSLATE('\ue8e2'), + + TRENDING_DOWN('\ue8e3'), + + TRENDING_FLAT('\ue8e4'), + + TRENDING_UP('\ue8e5'), + + TUNE('\ue429'), + + TURNED_IN('\ue8e6'), + + TURNED_IN_NOT('\ue8e7'), + + TV('\ue333'), + + UNARCHIVE('\ue169'), + + UNDO('\ue166'), + + UNFOLD_LESS('\ue5d6'), + + UNFOLD_MORE('\ue5d7'), + + UPDATE('\ue923'), + + USB('\ue1e0'), + + VERIFIED_USER('\ue8e8'), + + VERTICAL_ALIGN_BOTTOM('\ue258'), + + VERTICAL_ALIGN_CENTER('\ue259'), + + VERTICAL_ALIGN_TOP('\ue25a'), + + VIBRATION('\ue62d'), + + VIDEO_CALL('\ue070'), + + VIDEO_LABEL('\ue071'), + + VIDEO_LIBRARY('\ue04a'), + + VIDEOCAM('\ue04b'), + + VIDEOCAM_OFF('\ue04c'), + + VIDEOGAME_ASSET('\ue338'), + + VIEW_AGENDA('\ue8e9'), + + VIEW_ARRAY('\ue8ea'), + + VIEW_CAROUSEL('\ue8eb'), + + VIEW_COLUMN('\ue8ec'), + + VIEW_COMFY('\ue42a'), + + VIEW_COMPACT('\ue42b'), + + VIEW_DAY('\ue8ed'), + + VIEW_HEADLINE('\ue8ee'), + + VIEW_LIST('\ue8ef'), + + VIEW_MODULE('\ue8f0'), + + VIEW_QUILT('\ue8f1'), + + VIEW_STREAM('\ue8f2'), + + VIEW_WEEK('\ue8f3'), + + VIGNETTE('\ue435'), + + VISIBILITY('\ue8f4'), + + VISIBILITY_OFF('\ue8f5'), + + VOICE_CHAT('\ue62e'), + + VOICEMAIL('\ue0d9'), + + VOLUME_DOWN('\ue04d'), + + VOLUME_MUTE('\ue04e'), + + VOLUME_OFF('\ue04f'), + + VOLUME_UP('\ue050'), + + VPN_KEY('\ue0da'), + + VPN_LOCK('\ue62f'), + + WALLPAPER('\ue1bc'), + + WARNING('\ue002'), + + WATCH('\ue334'), + + WATCH_LATER('\ue924'), + + WB_AUTO('\ue42c'), + + WB_CLOUDY('\ue42d'), + + WB_INCANDESCENT('\ue42e'), + + WB_IRIDESCENT('\ue436'), + + WB_SUNNY('\ue430'), + + WC('\ue63d'), WEB('\ue051'), WEB_ASSET('\ue069'), WEEKEND('\ue16b'), WHATSHOT('\ue80e'), + WIDGETS('\ue1bd'), WIFI('\ue63e'), WIFI_LOCK('\ue1e1'), WIFI_TETHERING('\ue1e2'), + WORK('\ue8f9'), WRAP_TEXT('\ue25b'), YOUTUBE_SEARCHED_FOR('\ue8fa'), ZOOM_IN('\ue8ff'), + ZOOM_OUT('\ue900'), ZOOM_OUT_MAP('\ue56b'); + + private final char codepoint; + private Font font; + + MaterialDesignRegular(char c) { + codepoint = c; + } + + @Override + public Font getFont() { + if (font == null) { + font = Font.loadFont(getClass().getResourceAsStream("/fonts/MaterialIcons-Regular.ttf"), + 16.0); + } + return font; + } + + @Override + public char getUnicode() { + return codepoint; + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/fonticons/Typicons.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/fonticons/Typicons.java new file mode 100644 index 00000000000..3529a36a9c7 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/fonticons/Typicons.java @@ -0,0 +1,144 @@ +/* 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.fonticons; + + +/* + * Copyright (c) 2016 jIconFont

Permission is hereby granted, free of charge, to any + * person obtaining a copy of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the rights to use, copy, + * modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit + * persons to whom the Software is furnished to do so, subject to the following conditions:

+ * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY + * OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +import javafx.scene.text.Font; + +public enum Typicons implements IconFont { + + ADJUST_BRIGHTNESS('\uE000'), ADJUST_CONTRAST('\uE001'), ANCHOR('\uE003'), + ANCHOR_OUTLINE('\uE002'), ARCHIVE('\uE004'), ARROW_BACK('\uE006'), ARROW_BACK_OUTLINE('\uE005'), + ARROW_DOWN('\uE009'), ARROW_DOWN_OUTLINE('\uE007'), ARROW_DOWN_THICK('\uE008'), + ARROW_FORWARD('\uE00B'), ARROW_FORWARD_OUTLINE('\uE00A'), ARROW_LEFT('\uE00E'), + ARROW_LEFT_OUTLINE('\uE00C'), ARROW_LEFT_THICK('\uE00D'), ARROW_LOOP('\uE010'), + ARROW_LOOP_OUTLINE('\uE00F'), ARROW_MAXIMISE('\uE012'), ARROW_MAXIMISE_OUTLINE('\uE011'), + ARROW_MINIMISE('\uE014'), ARROW_MINIMISE_OUTLINE('\uE013'), ARROW_MOVE('\uE016'), + ARROW_MOVE_OUTLINE('\uE015'), ARROW_REPEAT('\uE018'), ARROW_REPEAT_OUTLINE('\uE017'), + ARROW_RIGHT('\uE01B'), ARROW_RIGHT_OUTLINE('\uE019'), ARROW_RIGHT_THICK('\uE01A'), + ARROW_SHUFFLE('\uE01C'), ARROW_SORTED_DOWN('\uE01D'), ARROW_SORTED_UP('\uE01E'), + ARROW_SYNC('\uE020'), ARROW_SYNC_OUTLINE('\uE01F'), ARROW_UNSORTED('\uE021'), + ARROW_UP('\uE024'), ARROW_UP_OUTLINE('\uE022'), ARROW_UP_THICK('\uE023'), AT('\uE025'), + ATTACHMENT('\uE027'), ATTACHMENT_OUTLINE('\uE026'), BACKSPACE('\uE029'), + BACKSPACE_OUTLINE('\uE028'), BATTERY_CHARGE('\uE02A'), BATTERY_FULL('\uE02B'), + BATTERY_HIGH('\uE02C'), BATTERY_LOW('\uE02D'), BATTERY_MID('\uE02E'), BEAKER('\uE02F'), + BEER('\uE030'), BELL('\uE031'), BOOK('\uE032'), BOOKMARK('\uE033'), BRIEFCASE('\uE034'), + BRUSH('\uE035'), BUSINESS_CARD('\uE036'), CALCULATOR('\uE037'), CALENDAR('\uE039'), + CALENDAR_OUTLINE('\uE038'), CAMERA('\uE03B'), CAMERA_OUTLINE('\uE03A'), CANCEL('\uE03D'), + CANCEL_OUTLINE('\uE03C'), CHART_AREA('\uE03F'), CHART_AREA_OUTLINE('\uE03E'), + CHART_BAR('\uE041'), CHART_BAR_OUTLINE('\uE040'), CHART_LINE('\uE043'), + CHART_LINE_OUTLINE('\uE042'), CHART_PIE('\uE045'), CHART_PIE_OUTLINE('\uE044'), + CHEVRON_LEFT('\uE047'), CHEVRON_LEFT_OUTLINE('\uE046'), CHEVRON_RIGHT('\uE049'), + CHEVRON_RIGHT_OUTLINE('\uE048'), CLIPBOARD('\uE04A'), CLOUD_STORAGE('\uE04B'), + CLOUD_STORAGE_OUTLINE('\uE054'), CODE('\uE04D'), CODE_OUTLINE('\uE04C'), COFFEE('\uE04E'), + COG('\uE050'), COG_OUTLINE('\uE04F'), COMPASS('\uE051'), CONTACTS('\uE052'), + CREDIT_CARD('\uE053'), CSS3('\uE055'), DATABASE('\uE056'), DELETE('\uE058'), + DELETE_OUTLINE('\uE057'), DEVICE_DESKTOP('\uE059'), DEVICE_LAPTOP('\uE05A'), + DEVICE_PHONE('\uE05B'), DEVICE_TABLET('\uE05C'), DIRECTIONS('\uE05D'), DIVIDE('\uE05F'), + DIVIDE_OUTLINE('\uE05E'), DOCUMENT('\uE063'), DOCUMENT_ADD('\uE060'), DOCUMENT_DELETE('\uE061'), + DOCUMENT_TEXT('\uE062'), DOWNLOAD('\uE065'), DOWNLOAD_OUTLINE('\uE064'), DROPBOX('\uE066'), + EDIT('\uE067'), EJECT('\uE069'), EJECT_OUTLINE('\uE068'), EQUALS('\uE06B'), + EQUALS_OUTLINE('\uE06A'), EXPORT('\uE06D'), EXPORT_OUTLINE('\uE06C'), EYE('\uE06F'), + EYE_OUTLINE('\uE06E'), FEATHER('\uE070'), FILM('\uE071'), FILTER('\uE072'), FLAG('\uE074'), + FLAG_OUTLINE('\uE073'), FLASH('\uE076'), FLASH_OUTLINE('\uE075'), FLOW_CHILDREN('\uE077'), + FLOW_MERGE('\uE078'), FLOW_PARALLEL('\uE079'), FLOW_SWITCH('\uE07A'), FOLDER('\uE07E'), + FOLDER_ADD('\uE07B'), FOLDER_DELETE('\uE07C'), FOLDER_OPEN('\uE07D'), GIFT('\uE07F'), + GLOBE('\uE081'), GLOBE_OUTLINE('\uE080'), GROUP('\uE083'), GROUP_OUTLINE('\uE082'), + HEADPHONES('\uE084'), HEART('\uE088'), HEART_FULL_OUTLINE('\uE085'), + HEART_HALF_OUTLINE('\uE086'), HEART_OUTLINE('\uE087'), HOME('\uE08A'), HOME_OUTLINE('\uE089'), + HTML5('\uE08B'), IMAGE('\uE08D'), IMAGE_OUTLINE('\uE08C'), INFINITY('\uE08F'), + INFINITY_OUTLINE('\uE08E'), INFO('\uE093'), INFO_LARGE('\uE091'), INFO_LARGE_OUTLINE('\uE090'), + INFO_OUTLINE('\uE092'), INPUT_CHECKED('\uE095'), INPUT_CHECKED_OUTLINE('\uE094'), KEY('\uE097'), + KEYBOARD('\uE098'), KEY_OUTLINE('\uE096'), LEAF('\uE099'), LIGHTBULB('\uE09A'), LINK('\uE09C'), + LINK_OUTLINE('\uE09B'), LOCATION('\uE0A0'), LOCATION_ARROW('\uE09E'), + LOCATION_ARROW_OUTLINE('\uE09D'), LOCATION_OUTLINE('\uE09F'), LOCK_CLOSED('\uE0A2'), + LOCK_CLOSED_OUTLINE('\uE0A1'), LOCK_OPEN('\uE0A4'), LOCK_OPEN_OUTLINE('\uE0A3'), MAIL('\uE0A5'), + MAP('\uE0A6'), MEDIA_EJECT('\uE0A8'), MEDIA_EJECT_OUTLINE('\uE0A7'), + MEDIA_FAST_FORWARD('\uE0AA'), MEDIA_FAST_FORWARD_OUTLINE('\uE0A9'), MEDIA_PAUSE('\uE0AC'), + MEDIA_PAUSE_OUTLINE('\uE0AB'), MEDIA_PLAY('\uE0B0'), MEDIA_PLAY_OUTLINE('\uE0AD'), + MEDIA_PLAY_REVERSE('\uE0AF'), MEDIA_PLAY_REVERSE_OUTLINE('\uE0AE'), MEDIA_RECORD('\uE0B2'), + MEDIA_RECORD_OUTLINE('\uE0B1'), MEDIA_REWIND('\uE0B4'), MEDIA_REWIND_OUTLINE('\uE0B3'), + MEDIA_STOP('\uE0B6'), MEDIA_STOP_OUTLINE('\uE0B5'), MESSAGE('\uE0B8'), MESSAGES('\uE0B9'), + MESSAGE_TYPING('\uE0B7'), MICROPHONE('\uE0BB'), MICROPHONE_OUTLINE('\uE0BA'), MINUS('\uE0BD'), + MINUS_OUTLINE('\uE0BC'), MORTAR_BOARD('\uE0BE'), NEWS('\uE0BF'), NOTES('\uE0C1'), + NOTES_OUTLINE('\uE0C0'), PEN('\uE0C2'), PENCIL('\uE0C3'), PHONE('\uE0C5'), + PHONE_OUTLINE('\uE0C4'), PI('\uE0C7'), PIN('\uE0C9'), PIN_OUTLINE('\uE0C8'), PIPETTE('\uE0CA'), + PI_OUTLINE('\uE0C6'), PLANE('\uE0CC'), PLANE_OUTLINE('\uE0CB'), PLUG('\uE0CD'), PLUS('\uE0CF'), + PLUS_OUTLINE('\uE0CE'), POINT_OF_INTEREST('\uE0D1'), POINT_OF_INTEREST_OUTLINE('\uE0D0'), + POWER('\uE0D3'), POWER_OUTLINE('\uE0D2'), PRINTER('\uE0D4'), PUZZLE('\uE0D6'), + PUZZLE_OUTLINE('\uE0D5'), RADAR('\uE0D8'), RADAR_OUTLINE('\uE0D7'), REFRESH('\uE0DA'), + REFRESH_OUTLINE('\uE0D9'), RSS('\uE0DC'), RSS_OUTLINE('\uE0DB'), SCISSORS('\uE0DE'), + SCISSORS_OUTLINE('\uE0DD'), SHOPPING_BAG('\uE0DF'), SHOPPING_CART('\uE0E0'), + SOCIAL_AT_CIRCULAR('\uE0E1'), SOCIAL_DRIBBBLE('\uE0E3'), SOCIAL_DRIBBBLE_CIRCULAR('\uE0E2'), + SOCIAL_FACEBOOK('\uE0E5'), SOCIAL_FACEBOOK_CIRCULAR('\uE0E4'), SOCIAL_FLICKR('\uE0E7'), + SOCIAL_FLICKR_CIRCULAR('\uE0E6'), SOCIAL_GITHUB('\uE0E9'), SOCIAL_GITHUB_CIRCULAR('\uE0E8'), + SOCIAL_GOOGLE_PLUS('\uE0EB'), SOCIAL_GOOGLE_PLUS_CIRCULAR('\uE0EA'), SOCIAL_INSTAGRAM('\uE0ED'), + SOCIAL_INSTAGRAM_CIRCULAR('\uE0EC'), SOCIAL_LAST_FM('\uE0EF'), + SOCIAL_LAST_FM_CIRCULAR('\uE0EE'), SOCIAL_LINKEDIN('\uE0F1'), + SOCIAL_LINKEDIN_CIRCULAR('\uE0F0'), SOCIAL_PINTEREST('\uE0F3'), + SOCIAL_PINTEREST_CIRCULAR('\uE0F2'), SOCIAL_SKYPE('\uE0F5'), SOCIAL_SKYPE_OUTLINE('\uE0F4'), + SOCIAL_TUMBLER('\uE0F7'), SOCIAL_TUMBLER_CIRCULAR('\uE0F6'), SOCIAL_TWITTER('\uE0F9'), + SOCIAL_TWITTER_CIRCULAR('\uE0F8'), SOCIAL_VIMEO('\uE0FB'), SOCIAL_VIMEO_CIRCULAR('\uE0FA'), + SOCIAL_YOUTUBE('\uE0FD'), SOCIAL_YOUTUBE_CIRCULAR('\uE0FC'), SORT_ALPHABETICALLY('\uE0FF'), + SORT_ALPHABETICALLY_OUTLINE('\uE0FE'), SORT_NUMERICALLY('\uE101'), + SORT_NUMERICALLY_OUTLINE('\uE100'), SPANNER('\uE103'), SPANNER_OUTLINE('\uE102'), + SPIRAL('\uE104'), STAR('\uE109'), STARBURST('\uE10B'), STARBURST_OUTLINE('\uE10A'), + STAR_FULL_OUTLINE('\uE105'), STAR_HALF('\uE107'), STAR_HALF_OUTLINE('\uE106'), + STAR_OUTLINE('\uE108'), STOPWATCH('\uE10C'), SUPPORT('\uE10D'), TABS_OUTLINE('\uE10E'), + TAG('\uE10F'), TAGS('\uE110'), THERMOMETER('\uE119'), THUMBS_DOWN('\uE11A'), + THUMBS_OK('\uE11B'), THUMBS_UP('\uE11C'), TH_LARGE('\uE112'), TH_LARGE_OUTLINE('\uE111'), + TH_LIST('\uE114'), TH_LIST_OUTLINE('\uE113'), TH_MENU('\uE116'), TH_MENU_OUTLINE('\uE115'), + TH_SMALL('\uE118'), TH_SMALL_OUTLINE('\uE117'), TICK('\uE11E'), TICKET('\uE11F'), + TICK_OUTLINE('\uE11D'), TIME('\uE120'), TIMES('\uE122'), TIMES_OUTLINE('\uE121'), + TRASH('\uE123'), TREE('\uE124'), UPLOAD('\uE126'), UPLOAD_OUTLINE('\uE125'), USER('\uE12C'), + USER_ADD('\uE128'), USER_ADD_OUTLINE('\uE127'), USER_DELETE('\uE12A'), + USER_DELETE_OUTLINE('\uE129'), USER_OUTLINE('\uE12B'), VENDOR_ANDROID('\uE12D'), + VENDOR_APPLE('\uE12E'), VENDOR_MICROSOFT('\uE12F'), VIDEO('\uE131'), VIDEO_OUTLINE('\uE130'), + VOLUME('\uE135'), VOLUME_DOWN('\uE132'), VOLUME_MUTE('\uE133'), VOLUME_UP('\uE134'), + WARNING('\uE137'), WARNING_OUTLINE('\uE136'), WATCH('\uE138'), WAVES('\uE13A'), + WAVES_OUTLINE('\uE139'), WEATHER_CLOUDY('\uE13B'), WEATHER_DOWNPOUR('\uE13C'), + WEATHER_NIGHT('\uE13D'), WEATHER_PARTLY_SUNNY('\uE13E'), WEATHER_SHOWER('\uE13F'), + WEATHER_SNOW('\uE140'), WEATHER_STORMY('\uE141'), WEATHER_SUNNY('\uE142'), + WEATHER_WINDY('\uE144'), WEATHER_WINDY_CLOUDY('\uE143'), WINE('\uE147'), WI_FI('\uE146'), + WI_FI_OUTLINE('\uE145'), WORLD('\uE149'), WORLD_OUTLINE('\uE148'), ZOOM('\uE14F'), + ZOOM_IN('\uE14B'), ZOOM_IN_OUTLINE('\uE14A'), ZOOM_OUT('\uE14D'), ZOOM_OUTLINE('\uE14E'), + ZOOM_OUT_OUTLINE('\uE14C'); + + private static Font font; + private final char character; + + Typicons(char character) { + this.character = character; + } + + @Override + public Font getFont() { + if (font == null) { + font = Font.loadFont(getClass().getResourceAsStream("/fonts/typicons.ttf"), 16.0); + } + return font; + + } + + @Override + public char getUnicode() { + return character; + } +} diff --git a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css index e483d6b20d8..dd2f019bf24 100644 --- a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css +++ b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css @@ -64,3 +64,8 @@ .key-dock-tab-pane > .tab-header-area { -fx-padding: 2 4 0 4; } + +/* icon text nodes (see IconFactoryF) follow the theme text color */ +.key-icon { + -fx-fill: -key-text; +} diff --git a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css index 671c2f6a954..4b1ae6ca393 100644 --- a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css +++ b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css @@ -60,3 +60,8 @@ .key-dock-tab-pane > .tab-header-area { -fx-padding: 2 4 0 4; } + +/* icon text nodes (see IconFactoryF) follow the theme text color */ +.key-icon { + -fx-fill: -key-text; +} From 60d3193b3b1d3c4f116042595b2423dc006992d8 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Wed, 7 Oct 2026 00:45:28 +0200 Subject: [PATCH 004/281] =?UTF-8?q?key.ui.fx:=20M1c=20docking=20framework?= =?UTF-8?q?=20=E2=80=94=20DockWorkspace/DockTabPane/layout=20store?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Custom lightweight docking layer replacing the Docking Frames (bibliothek) dependency of key.ui; no external library, built with plain JavaFX widgets: - Dockable (property-based interface: id, title, content, icon, closable) + SimpleDockable implementation - DockLocation (LEFT/MAIN/RIGHT) mirroring DockingHelper's CGrid roles - DockTabPane: TabPane mirroring the workspace model, close-handler for user-closed tabs, reorderable tabs, bound tab label/graphic/closability - DockWorkspace: observable per-role dockable lists as single source of truth, open/close/move/select, floating dockables in own Stage (themed), restoreFactoryDefault, save/restore layout, empty roles collapse - DockLayoutStore: java.util.Properties persistence (layout.properties in the KeY config dir) replacing layout.xml; divider positions/floating not persisted yet Verified: Scene constructors take Parent (not Node) in JavaFX 25 — content is wrapped before use as scene root. --- .../key/gui/fx/docking/DockLayoutStore.java | 106 +++++ .../ilkd/key/gui/fx/docking/DockLocation.java | 18 + .../ilkd/key/gui/fx/docking/DockTabPane.java | 144 +++++++ .../key/gui/fx/docking/DockWorkspace.java | 369 ++++++++++++++++++ .../uka/ilkd/key/gui/fx/docking/Dockable.java | 93 +++++ .../key/gui/fx/docking/SimpleDockable.java | 81 ++++ 6 files changed, 811 insertions(+) create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/docking/DockLayoutStore.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/docking/DockLocation.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/docking/DockTabPane.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/docking/DockWorkspace.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/docking/Dockable.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/docking/SimpleDockable.java diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/docking/DockLayoutStore.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/docking/DockLayoutStore.java new file mode 100644 index 00000000000..5164031052b --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/docking/DockLayoutStore.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.docking; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; + +/** + * Persists the docking layout (which dockables are open in which {@link DockLocation role}) in a + * plain {@link Properties} file inside the KeY configuration directory. + *

+ * Counter-part of the Docking Frames {@code layout.xml} persistence of the Swing module + * {@code key.ui}; the file format is deliberately simple and self-contained. The actual + * sequence of "which dockable belongs to which role" is the persisted content; divider positions + * and floating windows are not persisted (yet). + */ +public final class DockLayoutStore { + + /** The name of the properties file inside the config directory. */ + public static final String FILE_NAME = "layout.properties"; + + private static final String KEY_PREFIX = "dock."; + + private final Path file; + + /** + * Creates a store writing to the given config directory. + * + * @param configDir the KeY configuration directory + */ + public DockLayoutStore(Path configDir) { + this.file = configDir.resolve(FILE_NAME); + } + + /** + * @return the properties file backing this store + */ + public Path file() { + return file; + } + + /** + * Stores the open dockables per role. + * + * @param openDockables the ordered dockable ids per role + * @throws IOException on I/O errors + */ + public void save(Map> openDockables) throws IOException { + if (file.getParent() != null) { + Files.createDirectories(file.getParent()); + } + Properties properties = new Properties(); + for (DockLocation location : DockLocation.values()) { + List ids = openDockables.getOrDefault(location, List.of()); + properties.setProperty(KEY_PREFIX + location.name(), String.join(",", ids)); + } + try (var out = Files.newOutputStream(file)) { + properties.store(out, "KeY JavaFX UI docking layout"); + } + } + + /** + * Loads the stored layout. + * + * @return the ordered dockable ids per role; roles absent from the file (or missing file) + * yield empty lists + * @throws IOException on I/O errors + */ + public Map> load() throws IOException { + EnumMap> result = new EnumMap<>(DockLocation.class); + for (DockLocation location : DockLocation.values()) { + result.put(location, List.of()); + } + if (!Files.isRegularFile(file)) { + return result; + } + Properties properties = new Properties(); + try (var in = Files.newInputStream(file)) { + properties.load(in); + } + for (DockLocation location : DockLocation.values()) { + String value = properties.getProperty(KEY_PREFIX + location.name(), ""); + List ids = Arrays.stream(value.split(",")).map(String::trim) + .filter(s -> !s.isEmpty()).toList(); + result.put(location, ids); + } + return result; + } + + /** + * Deletes the persisted layout, if present. + * + * @throws IOException on I/O errors + */ + public void clear() throws IOException { + Files.deleteIfExists(file); + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/docking/DockLocation.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/docking/DockLocation.java new file mode 100644 index 00000000000..53b45a7b93d --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/docking/DockLocation.java @@ -0,0 +1,18 @@ +/* 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.docking; + +/** + * The role areas of the {@link DockWorkspace}, mirroring the {@code CGrid} roles of + * {@code DockingHelper} in the Swing module {@code key.ui}. + */ +public enum DockLocation { + /** The left panel (proof tree, info view, strategy selection, ...). */ + LEFT, + /** The central panel (goal list, sequent view, ...). */ + MAIN, + /** The right panel (source view, ...). */ + RIGHT +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/docking/DockTabPane.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/docking/DockTabPane.java new file mode 100644 index 00000000000..107db85de28 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/docking/DockTabPane.java @@ -0,0 +1,144 @@ +/* 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.docking; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.function.Consumer; + +import javafx.scene.Node; +import javafx.scene.control.Tab; +import javafx.scene.control.TabPane; +import javafx.scene.layout.StackPane; + +/** + * A tab pane that hosts one tab per {@link Dockable}, keeping tab label, graphic, content and + * closability in sync with the dockable's properties. + *

+ * Replaces the Docking Frames {@code DockTabbedPane} of the Swing module {@code key.ui}. The + * visibility (which dockables are open in which role) is owned by the {@link DockWorkspace}; a + * panel simply mirrors its part of that model. + */ +public class DockTabPane extends TabPane { + + private final Map tabByDockableId = new HashMap<>(); + private Consumer onDockableClosed = dockable -> { + }; + + public DockTabPane() { + getStyleClass().add("key-dock-tab-pane"); + setTabDragPolicy(TabDragPolicy.REORDER); + } + + /** + * Registers the handler invoked when the user closes a tab via its close button. The handler + * is responsible for removing the dockable from the workspace model. + * + * @param handler the handler, or {@code null} to disable + */ + public void setOnDockableClosed(Consumer handler) { + this.onDockableClosed = handler == null ? dockable -> { + } : handler; + } + + /** + * @param id the dockable id + * @return whether a tab for the given dockable is present + */ + public boolean contains(String id) { + return tabByDockableId.containsKey(id); + } + + /** + * @return whether no tabs are shown + */ + public boolean isEmpty() { + return tabByDockableId.isEmpty(); + } + + /** + * Adds a tab for the given dockable if it is not present yet. + * + * @param dockable the dockable to show + * @return {@code true} if a tab was added, {@code false} if it was already present + */ + public boolean addDockable(Dockable dockable) { + if (tabByDockableId.containsKey(dockable.getId())) { + return false; + } + DockTab tab = new DockTab(dockable); + tab.setOnClosed(ignored -> onDockableClosed.accept(dockable)); + tabByDockableId.put(dockable.getId(), tab); + getTabs().add(tab); + if (getTabs().size() == 1) { + getSelectionModel().select(0); + } + return true; + } + + /** + * Removes the tab of the given dockable. + * + * @param dockable the dockable to remove + * @return {@code true} if a tab was present and has been removed + */ + public boolean removeDockable(Dockable dockable) { + return removeDockable(dockable.getId()); + } + + /** + * Removes the tab with the given dockable id. + * + * @param id the dockable id + * @return {@code true} if a tab was present and has been removed + */ + public boolean removeDockable(String id) { + Tab tab = tabByDockableId.remove(id); + if (tab != null) { + getTabs().remove(tab); + return true; + } + return false; + } + + /** + * Selects the tab of the given dockable. + * + * @param id the dockable id + */ + public void select(String id) { + Tab tab = tabByDockableId.get(id); + if (tab != null) { + getSelectionModel().select(tab); + } + } + + /** + * @return the dockable of the currently selected tab, if any + */ + public Optional getSelectedDockable() { + Tab selected = getSelectionModel().getSelectedItem(); + if (selected instanceof DockTab dockTab) { + return Optional.of(dockTab.dockable); + } + return Optional.empty(); + } + + private static final class DockTab extends Tab { + + private final Dockable dockable; + + DockTab(Dockable dockable) { + this.dockable = dockable; + Node content = dockable.getContent(); + setContent(content != null ? content : new StackPane()); + setGraphic(dockable.getIcon()); + textProperty().bind(dockable.titleProperty()); + graphicProperty().bind(dockable.iconProperty()); + closableProperty().bind(dockable.closableProperty()); + } + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/docking/DockWorkspace.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/docking/DockWorkspace.java new file mode 100644 index 00000000000..c4f5f1f5381 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/docking/DockWorkspace.java @@ -0,0 +1,369 @@ +/* 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.docking; + +import java.io.IOException; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import javafx.collections.FXCollections; +import javafx.collections.ListChangeListener; +import javafx.collections.ObservableList; +import javafx.scene.Node; +import javafx.scene.Parent; +import javafx.scene.Scene; +import javafx.scene.control.SplitPane; +import javafx.scene.layout.StackPane; +import javafx.stage.Stage; + +import de.uka.ilkd.key.gui.fx.theme.ThemeManager; + +/** + * The docking workspace of the JavaFX UI: three role areas (LEFT / MAIN / RIGHT) à la + * {@code DockingHelper}'s {@code CGrid} roles of the Swing module {@code key.ui}, realized with + * plain JavaFX {@link SplitPane} and {@link DockTabPane} widgets. + *

+ * The workspace owns the model: an observable list of open {@link Dockable}s per role. The + * {@link DockTabPane}s merely mirror that model. Dockables can be opened, closed, floated into + * their own {@link Stage}, and the layout can be saved to / restored from a + * {@link DockLayoutStore}. + *

+ * Calling style (all methods) is the FX Application Thread via {@code Platform.runLater}, like + * any UI mutation. + */ +public final class DockWorkspace { + + /** + * An entry of the factory-default layout: open the given dockable in the given role. + * + * @param location the role area + * @param dockable the dockable + */ + public record Default(DockLocation location, Dockable dockable) { + } + + /** + * Recreates a dockable from its persisted id, e.g. a built-in view or an SPI panel. + */ + @FunctionalInterface + public interface DockableFactory { + + /** + * @param id the persisted dockable id + * @return the recreated dockable, or empty if the id is unknown + */ + Optional create(String id); + } + + private final EnumMap panes = new EnumMap<>(DockLocation.class); + private final EnumMap> dockables = + new EnumMap<>(DockLocation.class); + private final Map floatStages = new HashMap<>(); + + private final SplitPane root = new SplitPane(); + + private List defaultLayout = List.of(); + + /** + * Creates an empty workspace with three role areas. + */ + public DockWorkspace() { + root.getStyleClass().add("key-dock-workspace"); + for (DockLocation location : DockLocation.values()) { + DockTabPane pane = new DockTabPane(); + pane.setOnDockableClosed(this::close); + panes.put(location, pane); + ObservableList list = FXCollections.observableArrayList(); + list.addListener(dockablesChanged(location)); + dockables.put(location, list); + } + root.getItems().addAll(panes.get(DockLocation.LEFT), panes.get(DockLocation.MAIN), + panes.get(DockLocation.RIGHT)); + updateVisibility(DockLocation.LEFT); + updateVisibility(DockLocation.RIGHT); + } + + /** + * @return the root node to embed in the application scene + */ + public Node getRoot() { + return root; + } + + /** + * @param location the role area + * @return the tab pane presenting that role + */ + public DockTabPane pane(DockLocation location) { + return panes.get(location); + } + + /** + * @param location the role area + * @return the observable model of the dockables open in that role + */ + public ObservableList dockables(DockLocation location) { + return dockables.get(location); + } + + /** + * @param id the dockable id + * @return whether the dockable is currently open (in any role or floating) + */ + public boolean isOpen(String id) { + return find(id).isPresent(); + } + + /** + * @param id the dockable id + * @return the open dockable with the given id, if any + */ + public Optional find(String id) { + for (ObservableList list : dockables.values()) { + for (Dockable dockable : list) { + if (dockable.getId().equals(id)) { + return Optional.of(dockable); + } + } + } + return Optional.empty(); + } + + /** + * @param id the dockable id + * @return the role the dockable currently lives in + */ + public Optional locationOf(String id) { + for (DockLocation location : DockLocation.values()) { + for (Dockable dockable : dockables.get(location)) { + if (dockable.getId().equals(id)) { + return Optional.of(location); + } + } + } + return Optional.empty(); + } + + /** + * Opens the given dockable in the given role. If it is already open elsewhere (another role + * or floating), it is moved; if it is already open in the requested role it is selected. + * + * @param dockable the dockable to open + * @param location the target role + */ + public void open(Dockable dockable, DockLocation location) { + String id = dockable.getId(); + + Stage floatStage = floatStages.remove(id); + if (floatStage != null) { + floatStage.close(); + } + + Optional current = locationOf(id); + if (current.isPresent()) { + if (current.get() == location) { + select(dockable); + return; + } + dockables.get(current.get()).remove(dockable); + } + dockables.get(location).add(dockable); + select(dockable); + } + + /** + * Closes the given dockable wherever it is open. + * + * @param dockable the dockable to close + */ + public void close(Dockable dockable) { + close(dockable.getId()); + } + + /** + * Closes the dockable with the given id wherever it is open. + * + * @param id the dockable id + */ + public void close(String id) { + Stage floatStage = floatStages.remove(id); + if (floatStage != null) { + floatStage.close(); + } + for (ObservableList list : dockables.values()) { + list.removeIf(dockable -> dockable.getId().equals(id)); + } + } + + /** + * Selects the given dockable in its role. + * + * @param dockable the dockable to select + */ + public void select(Dockable dockable) { + locationOf(dockable.getId()) + .ifPresent(location -> panes.get(location).select(dockable.getId())); + } + + /** + * Floats the given dockable into its own undecorated-framed window. Closing the float window + * closes the dockable. To re-dock, call {@link #open(Dockable, DockLocation)}. + * + * @param dockable the dockable to float + */ + public void floatDockable(Dockable dockable) { + String id = dockable.getId(); + locationOf(id).ifPresent(location -> dockables.get(location).remove(dockable)); + + Stage floatStage = floatStages.get(id); + if (floatStage != null) { + floatStage.close(); + } + Node content = dockable.getContent(); + Parent sceneRoot; + if (content instanceof Parent parent) { + sceneRoot = parent; + } else { + StackPane wrapper = new StackPane(); + if (content != null) { + wrapper.getChildren().add(content); + } + sceneRoot = wrapper; + } + Scene scene = new Scene(sceneRoot); + ThemeManager.getInstance().manage(scene); + Stage stage = new Stage(); + stage.titleProperty().bind(dockable.titleProperty()); + stage.setScene(scene); + stage.setOnCloseRequest(ignored -> close(id)); + floatStages.put(id, stage); + stage.show(); + } + + /** + * Sets the factory-default layout used by {@link #restoreFactoryDefault()}. + * + * @param layout the default dockables per role + */ + public void setDefaultLayout(List layout) { + this.defaultLayout = List.copyOf(layout); + } + + /** + * @return the factory-default layout currently registered + */ + public List getDefaultLayout() { + return defaultLayout; + } + + /** + * Closes everything and re-opens the factory-default layout. + */ + public void restoreFactoryDefault() { + closeAllFloating(); + for (ObservableList list : dockables.values()) { + list.clear(); + } + for (Default entry : defaultLayout) { + open(entry.dockable(), entry.location()); + } + } + + /** + * Persists the current layout through the given store. + * + * @param store the store to write to + * @throws IOException on I/O errors + */ + public void saveLayout(DockLayoutStore store) throws IOException { + EnumMap> ids = new EnumMap<>(DockLocation.class); + for (DockLocation location : DockLocation.values()) { + ids.put(location, + dockables.get(location).stream().map(Dockable::getId).toList()); + } + store.save(ids); + } + + /** + * Restores a previously persisted layout. If the store contains no open dockables, the + * factory-default layout is opened instead. + * + * @param store the store to read from + * @param factory recreates dockables from their persisted ids + * @throws IOException on I/O errors + */ + public void restoreLayout(DockLayoutStore store, DockableFactory factory) throws IOException { + Map> saved = store.load(); + boolean anyOpen = saved.values().stream().anyMatch(list -> !list.isEmpty()); + + closeAllFloating(); + for (ObservableList list : dockables.values()) { + list.clear(); + } + + if (!anyOpen) { + for (Default entry : defaultLayout) { + open(entry.dockable(), entry.location()); + } + return; + } + + for (DockLocation location : DockLocation.values()) { + for (String id : saved.getOrDefault(location, List.of())) { + if (isOpen(id)) { + continue; + } + factory.create(id).ifPresent(dockable -> open(dockable, location)); + } + } + } + + private void closeAllFloating() { + for (Stage stage : floatStages.values()) { + stage.close(); + } + floatStages.clear(); + } + + private ListChangeListener dockablesChanged(DockLocation location) { + return change -> { + while (change.next()) { + for (Dockable dockable : change.getRemoved()) { + panes.get(location).removeDockable(dockable); + } + for (Dockable dockable : change.getAddedSubList()) { + panes.get(location).addDockable(dockable); + } + } + updateVisibility(location); + }; + } + + private void updateVisibility(DockLocation location) { + if (location == DockLocation.MAIN) { + return; + } + DockTabPane pane = panes.get(location); + boolean empty = dockables.get(location).isEmpty(); + boolean visible = root.getItems().contains(pane); + if (empty && visible) { + root.getItems().remove(pane); + } else if (!empty && !visible) { + int index = location == DockLocation.LEFT ? 0 : root.getItems().size(); + root.getItems().add(Math.min(index, root.getItems().size()), pane); + } + applySensibleDividers(); + } + + private void applySensibleDividers() { + int count = root.getItems().size(); + if (count >= 2) { + double[] positions = count == 3 ? new double[] { 0.2, 0.8 } : new double[] { 0.25 }; + root.setDividerPositions(positions); + } + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/docking/Dockable.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/docking/Dockable.java new file mode 100644 index 00000000000..326bf5c1c1b --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/docking/Dockable.java @@ -0,0 +1,93 @@ +/* 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.docking; + +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.StringProperty; +import javafx.scene.Node; + +/** + * A dockable unit of the JavaFX UI: a titled, optionally closable piece of {@link Node} content + * that can be placed into a dock role area of the {@link DockWorkspace}. + *

+ * Counter-part of a Docking Frames {@code Dockable} in the Swing module {@code key.ui}. The + * title, icon, content and closability are exposed as JavaFX properties so that the tab + * presentation stays in sync when a view updates its title or icon. + */ +public interface Dockable { + + /** + * @return an identifier that is unique within a {@link DockWorkspace}; used for layout + * persistence + */ + String getId(); + + /** + * @return the title of this dockable (bound to the tab label) + */ + StringProperty titleProperty(); + + /** + * @return the current title + */ + default String getTitle() { + return titleProperty().get(); + } + + /** + * Sets the title. + * + * @param title the new title + */ + default void setTitle(String title) { + titleProperty().set(title); + } + + /** + * @return the content shown when this dockable is open + */ + ObjectProperty contentProperty(); + + /** + * @return the content shown when this dockable is open + */ + default Node getContent() { + return contentProperty().get(); + } + + /** + * @return an optional icon node shown in the tab header (may be {@code null}) + */ + ObjectProperty iconProperty(); + + /** + * @return the icon shown in the tab header, or {@code null} if none + */ + default Node getIcon() { + return iconProperty().get(); + } + + /** + * @return whether the user may close this dockable (tab close button) + */ + BooleanProperty closableProperty(); + + /** + * @return whether the user may close this dockable + */ + default boolean isClosable() { + return closableProperty().get(); + } + + /** + * Sets whether the user may close this dockable. + * + * @param closable {@code true} to make it closable + */ + default void setClosable(boolean closable) { + closableProperty().set(closable); + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/docking/SimpleDockable.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/docking/SimpleDockable.java new file mode 100644 index 00000000000..5e4425d2d06 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/docking/SimpleDockable.java @@ -0,0 +1,81 @@ +/* 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.docking; + +import java.util.Objects; + +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; +import javafx.scene.Node; + +/** + * Straightforward implementation of a {@link Dockable} with plain JavaFX properties. + *

+ * Most views (proof tree, sequent, source view, ...) are wrapped in a {@code SimpleDockable} + * before being opened in the {@link DockWorkspace}. + */ +public class SimpleDockable implements Dockable { + + private final String id; + private final StringProperty title = new SimpleStringProperty(this, "title"); + private final ObjectProperty content = new SimpleObjectProperty<>(this, "content"); + private final ObjectProperty icon = new SimpleObjectProperty<>(this, "icon"); + private final BooleanProperty closable = new SimpleBooleanProperty(this, "closable", true); + + /** + * Creates a closable dockable without an icon. + * + * @param id unique identifier used for layout persistence + * @param title initial title + * @param content the content node + */ + public SimpleDockable(String id, String title, Node content) { + this(id, title, content, null); + } + + /** + * Creates a closable dockable. + * + * @param id unique identifier used for layout persistence + * @param title initial title + * @param content the content node + * @param icon optional icon shown in the tab header (may be {@code null}) + */ + public SimpleDockable(String id, String title, Node content, Node icon) { + this.id = Objects.requireNonNull(id, "id"); + this.title.set(title); + this.content.set(content); + this.icon.set(icon); + } + + @Override + public String getId() { + return id; + } + + @Override + public StringProperty titleProperty() { + return title; + } + + @Override + public ObjectProperty contentProperty() { + return content; + } + + @Override + public ObjectProperty iconProperty() { + return icon; + } + + @Override + public BooleanProperty closableProperty() { + return closable; + } +} From dfacc5c8dd9f1b1e3b767abf910a600890f0cc1c Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Wed, 7 Oct 2026 09:17:12 +0200 Subject: [PATCH 005/281] =?UTF-8?q?key.ui.fx:=20M1d=20main=20window=20?= =?UTF-8?q?=E2=80=94=20menus,=20toolbars,=20status=20bar,=20notifications,?= =?UTF-8?q?=20settings=20shell?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - MainWindowF (counter-part of key.ui's MainWindow): builds the themed scene around the DockWorkspace with the default layout mirroring DockingHelper's roles (LEFT: loaded proofs, goal list, proof tree, info, strategy; MAIN: sequent; RIGHT: source view — placeholder contents until M2), menu bar (File/View/Proof/Options/About) and two icon toolbars, status bar with copyright + theme/font info - accelerators resolved through the new KeyStrokeManagerF: registry of KeyCombination bindings keyed by the Swing action class names so the persisted keystrokes.json can be adopted later; defaults mirror KeyStrokeSettings (F5/F6, SHORTCUT+S/O/N/Q/SPACE, macros, ...) - NotificationManagerF: themed toast notifications (info/warning/error), auto-dismiss, click-to-dismiss, overlay is click-transparent outside the toasts - settings shell: SettingsProviderF/SettingsManagerF with a code-built dialog (category tree + panel area + OK/Apply/Cancel) and the first provider ThemeSettingsProviderF (light/dark theme, base font size via the core ViewSettings) - ThemeManager: initial theme selectable via key.fx.theme system property - MainApplication delegates to MainWindowF Verified on screen (light + dark theme, startup toast, menu/toolbar icons, default docking layout, status bar): screenshots captured via X11. --- .../uka/ilkd/key/gui/fx/MainApplication.java | 43 +- .../de/uka/ilkd/key/gui/fx/MainWindowF.java | 448 ++++++++++++++++++ .../fx/keyshortcuts/KeyStrokeManagerF.java | 134 ++++++ .../fx/notification/NotificationManagerF.java | 129 +++++ .../key/gui/fx/settings/SettingsManagerF.java | 175 +++++++ .../gui/fx/settings/SettingsProviderF.java | 58 +++ .../fx/settings/ThemeSettingsProviderF.java | 101 ++++ .../ilkd/key/gui/fx/theme/ThemeManager.java | 17 +- .../de/uka/ilkd/key/gui/fx/theme/key-dark.css | 17 + .../uka/ilkd/key/gui/fx/theme/key-light.css | 17 + 10 files changed, 1098 insertions(+), 41 deletions(-) create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/MainWindowF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/keyshortcuts/KeyStrokeManagerF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/NotificationManagerF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/SettingsManagerF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/SettingsProviderF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/ThemeSettingsProviderF.java 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 index 07b032505b3..97c58243d30 100644 --- 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 @@ -5,56 +5,19 @@ package de.uka.ilkd.key.gui.fx; import javafx.application.Application; -import javafx.application.Platform; -import javafx.geometry.Pos; -import javafx.scene.Scene; -import javafx.scene.control.Label; -import javafx.scene.control.Menu; -import javafx.scene.control.MenuBar; -import javafx.scene.control.MenuItem; -import javafx.scene.control.SeparatorMenuItem; -import javafx.scene.layout.BorderPane; import javafx.stage.Stage; /** * The JavaFX application of KeY, counter-part of {@code de.uka.ilkd.key.gui.MainWindow} in the * Swing module {@code key.ui}. *

- * Milestone M0: this is a deliberately minimal, runnable shell. The docking workspace (M1), the - * menus/toolbars, and the views (M2) replace the placeholder content stage by stage. + * 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. */ public final class MainApplication extends Application { @Override public void start(final Stage stage) { - BorderPane root = new BorderPane(); - - root.setTop(buildMenuBar()); - - Label placeholder = new Label("KeY UI (JavaFX)\n\nThe docking workspace arrives in " - + "milestone M1."); - placeholder.setAlignment(Pos.CENTER); - root.setCenter(placeholder); - - Label statusLine = new Label("Ready"); - BorderPane.setAlignment(statusLine, Pos.CENTER_LEFT); - root.setBottom(statusLine); - - stage.setTitle("KeY (JavaFX)"); - stage.setScene(new Scene(root, 1024, 768)); - stage.show(); - } - - private MenuBar buildMenuBar() { - MenuBar menuBar = new MenuBar(); - - Menu fileMenu = new Menu("File"); - MenuItem quitItem = new MenuItem("Quit"); - quitItem.setOnAction(e -> Platform.exit()); - fileMenu.getItems().add(new SeparatorMenuItem()); - fileMenu.getItems().add(quitItem); - - menuBar.getMenus().add(fileMenu); - return menuBar; + 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..3870d69b820 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/MainWindowF.java @@ -0,0 +1,448 @@ +/* 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.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import javafx.application.Platform; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Node; +import javafx.scene.Scene; +import javafx.scene.control.CheckMenuItem; +import javafx.scene.control.Label; +import javafx.scene.control.Menu; +import javafx.scene.control.MenuBar; +import javafx.scene.control.MenuItem; +import javafx.scene.control.RadioMenuItem; +import javafx.scene.control.SeparatorMenuItem; +import javafx.scene.control.ToggleGroup; +import javafx.scene.control.ToolBar; +import javafx.scene.control.Tooltip; +import javafx.scene.image.Image; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.Region; +import javafx.scene.layout.StackPane; +import javafx.scene.layout.VBox; +import javafx.stage.Stage; + +import de.uka.ilkd.key.gui.fx.configuration.ConfigF; +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.SimpleDockable; +import de.uka.ilkd.key.gui.fx.fonticons.IconFactoryF; +import de.uka.ilkd.key.gui.fx.keyshortcuts.KeyStrokeManagerF; +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.settings.SettingsManagerF; +import de.uka.ilkd.key.gui.fx.settings.ThemeSettingsProviderF; +import de.uka.ilkd.key.gui.fx.theme.Theme; +import de.uka.ilkd.key.gui.fx.theme.ThemeManager; +import de.uka.ilkd.key.settings.PathConfig; +import de.uka.ilkd.key.settings.ProofIndependentSettings; +import de.uka.ilkd.key.settings.ViewSettings; +import de.uka.ilkd.key.util.KeYConstants; +import de.uka.ilkd.key.util.KeYResourceManager; + +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/"; + + /** 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<>(); + + private final Label statusLeft = new Label(); + private final Label statusRight = new Label(); + + /** + * Creates the main window bound to the given stage. + * + * @param stage the primary stage of the JavaFX application + */ + public MainWindowF(Stage stage) { + this.stage = stage; + this.layoutStore = new DockLayoutStore(PathConfig.currentPaths.keyConfigDir); + } + + /** + * Builds and shows the main window. + */ + public void initialize() { + stage.setTitle(KeYResourceManager.getManager().getUserInterfaceTitle()); + setWindowIcons(); + + registerSettingsProviders(); + buildDockables(); + workspace.setDefaultLayout(defaultLayout()); + + BorderPane root = new BorderPane(); + root.setTop(buildTop()); + StackPane center = new StackPane(workspace.getRoot()); + NotificationManagerF.getInstance().attach(center); + root.setCenter(center); + root.setBottom(buildStatusBar()); + + Scene scene = new Scene(root, 1100, 800); + ThemeManager.getInstance().manage(scene); + stage.setScene(scene); + stage.show(); + + restoreLayout(); + + ThemeManager.getInstance().themeProperty().addListener((obs, old, theme) -> updateStatus()); + updateStatus(); + + 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 registered dockables (id → dockable) + */ + public Map getDockables() { + return dockables; + } + + 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() { + registerDockable(ID_LOADED_PROOFS, "Loaded Proofs"); // TaskTree + registerDockable(ID_GOAL_LIST, "Goal List"); + registerDockable(ID_PROOF_TREE, "Proof Tree"); + registerDockable(ID_INFO_VIEW, "Info"); + registerDockable(ID_STRATEGY, "Strategy"); + registerDockable(ID_SEQUENT, "Sequent"); + registerDockable(ID_SOURCE_VIEW, "Source"); + } + + private void registerDockable(String id, String title) { + dockables.put(id, + new SimpleDockable(id, title, placeholderContent(title))); + } + + private List defaultLayout() { + return List.of(new DockWorkspace.Default(DockLocation.LEFT, + requireDockable(ID_LOADED_PROOFS)), + new DockWorkspace.Default(DockLocation.LEFT, requireDockable(ID_GOAL_LIST)), + new DockWorkspace.Default(DockLocation.LEFT, requireDockable(ID_PROOF_TREE)), + new DockWorkspace.Default(DockLocation.LEFT, requireDockable(ID_INFO_VIEW)), + new DockWorkspace.Default(DockLocation.LEFT, requireDockable(ID_STRATEGY)), + new DockWorkspace.Default(DockLocation.MAIN, requireDockable(ID_SEQUENT)), + new DockWorkspace.Default(DockLocation.RIGHT, requireDockable(ID_SOURCE_VIEW))); + } + + 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()); + 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()); + return menuBar; + } + + private Menu buildFileMenu() { + Menu file = new Menu("File"); + file.getItems().addAll( + menuItem("Open Example…", IconFactoryF.Key.OPEN_KEY_FILE, this::notYetImplemented), + menuItem("Open File…", "de.uka.ilkd.key.gui.actions.OpenFileAction", + IconFactoryF.Key.OPEN_KEY_FILE, this::notYetImplemented), + menuItem("Open Most Recent…", "de.uka.ilkd.key.gui.actions.OpenMostRecentFileAction", + this::notYetImplemented), + new SeparatorMenuItem(), + menuItem("Save File…", "de.uka.ilkd.key.gui.actions.SaveFileAction", + IconFactoryF.Key.SAVE_FILE, this::notYetImplemented), + menuItem("Save Bundle…", "de.uka.ilkd.key.gui.actions.SaveBundleAction", + this::notYetImplemented), + menuItem("Quick Save", "de.uka.ilkd.key.gui.actions.QuickSaveAction", + this::notYetImplemented), + menuItem("Quick Load", "de.uka.ilkd.key.gui.actions.QuickLoadAction", + this::notYetImplemented), + new SeparatorMenuItem(), + menuItem("Exit", "de.uka.ilkd.key.gui.actions.ExitMainAction", + IconFactoryF.Key.QUIT, Platform::exit)); + return file; + } + + private Menu buildViewMenu() { + Menu view = new Menu("View"); + // placeholder toggles; wired to the real views in M2 + CheckMenuItem prettyPrint = new CheckMenuItem("Pretty Print"); + CheckMenuItem unicode = new CheckMenuItem("Unicode Symbols"); + CheckMenuItem syntaxHighlighting = new CheckMenuItem("Syntax Highlighting"); + + 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)); + + Menu themeMenu = new Menu("Theme"); + themeMenu.getItems().addAll(lightTheme, darkTheme); + + Menu fontSize = new Menu("Font Size"); + fontSize.getItems().addAll( + menuItem("Increase", "de.uka.ilkd.key.gui.actions.IncreaseFontSizeAction", + IconFactoryF.Key.PLUS, () -> changeFontSize(1)), + menuItem("Decrease", "de.uka.ilkd.key.gui.actions.DecreaseFontSizeAction", + IconFactoryF.Key.MINUS, () -> changeFontSize(-1))); + + view.getItems().addAll(prettyPrint, unicode, syntaxHighlighting, new SeparatorMenuItem(), + themeMenu, fontSize); + return view; + } + + private Menu buildProofMenu() { + Menu proof = new Menu("Proof"); + Menu automation = new Menu("Automation"); + automation.getItems().addAll( + menuItem("Start Automatic Proof", "de.uka.ilkd.key.gui.actions.AutoModeAction", + IconFactoryF.Key.AUTO_MODE_START, this::notYetImplemented), + menuItem("Stop Automatic Proof", IconFactoryF.Key.AUTO_MODE_STOP, + this::notYetImplemented)); + proof.getItems().addAll(automation, new SeparatorMenuItem(), + menuItem("Goal Back", "de.uka.ilkd.key.gui.actions.GoalBackAction", + IconFactoryF.Key.GOAL_BACK, this::notYetImplemented), + menuItem("Prune Proof", "de.uka.ilkd.key.gui.actions.PruneProofAction", + IconFactoryF.Key.PRUNE, this::notYetImplemented)); + return proof; + } + + private Menu buildOptionsMenu() { + Menu options = new Menu("Options"); + options.getItems().addAll( + menuItem("Preferences…", + "de.uka.ilkd.key.gui.settings.SettingsManager$ShowSettingsAction", + IconFactoryF.Key.CONFIGURE, this::openSettings), + new SeparatorMenuItem(), + menuItem("Reset Dock Layout", this::resetLayout), + menuItem("SMT Solvers…", IconFactoryF.Key.TOOLBOX, this::notYetImplemented)); + return options; + } + + private Menu buildAboutMenu() { + Menu about = new Menu("About"); + about.getItems().addAll( + menuItem("License…", IconFactoryF.Key.INFO_VIEW, this::showLicense), + menuItem("About KeY…", this::showAbout)); + return about; + } + + private ToolBar buildFileToolBar() { + ToolBar bar = new ToolBar(); + bar.getStyleClass().add("key-file-tool-bar"); + bar.getItems().addAll( + toolbarButton("Open File", IconFactoryF.Key.OPEN_KEY_FILE, this::notYetImplemented), + toolbarButton("Open Most Recent", IconFactoryF.Key.OPEN_MOST_RECENT, + this::notYetImplemented), + toolbarButton("Save File", IconFactoryF.Key.SAVE_FILE, this::notYetImplemented)); + return bar; + } + + private ToolBar buildProofToolBar() { + ToolBar bar = new ToolBar(); + bar.getStyleClass().add("key-proof-tool-bar"); + bar.getItems().addAll( + toolbarButton("Start Automatic Proof", IconFactoryF.Key.AUTO_MODE_START, + this::notYetImplemented), + toolbarButton("Stop Automatic Proof", IconFactoryF.Key.AUTO_MODE_STOP, + this::notYetImplemented), + toolbarButton("Goal Back", IconFactoryF.Key.GOAL_BACK, this::notYetImplemented), + toolbarButton("Prune Proof", IconFactoryF.Key.PRUNE, this::notYetImplemented)); + return bar; + } + + private Node toolbarButton(String tooltip, IconFactoryF.Key icon, Runnable action) { + javafx.scene.control.Button button = + new javafx.scene.control.Button(null, IconFactoryF.createIcon(icon)); + 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}). + */ + 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.getInstance().binding(actionId).ifPresent(item::setAccelerator); + } + 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)); + statusLeft.setAlignment(Pos.CENTER_LEFT); + statusLeft.setMaxWidth(Double.MAX_VALUE); + HBox.setHgrow(statusLeft, Priority.ALWAYS); + Region spacer = new Region(); + HBox.setHgrow(spacer, Priority.ALWAYS); + statusRight.setAlignment(Pos.CENTER_RIGHT); + bar.getChildren().addAll(statusLeft, spacer, statusRight); + return bar; + } + + private void updateStatus() { + Theme theme = ThemeManager.getInstance().getTheme(); + int sizeIndex = ConfigF.DEFAULT.sizeIndex(); + statusLeft.setText(KeYConstants.COPYRIGHT); + statusRight.setText("Theme: " + theme.name().toLowerCase() + " · Font size: " + + ConfigF.SIZES[sizeIndex]); + } + + 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().openSettings(stage); + } + + private void resetLayout() { + workspace.restoreFactoryDefault(); + } + + private void showLicense() { + NotificationManagerF.getInstance().notify( + KeYConstants.COPYRIGHT + " KeY is free software and comes with ABSOLUTELY NO " + + "WARRANTY. See About | License.", + Kind.INFO); + } + + private void showAbout() { + NotificationManagerF.getInstance().notify( + KeYResourceManager.getManager().getUserInterfaceTitle() + " — JavaFX UI (key.ui.fx)."); + } + + private void notYetImplemented() { + NotificationManagerF.getInstance().notify( + "This action arrives in a later milestone of the key.ui.fx rewrite.", Kind.WARNING); + } + + private void registerSettingsProviders() { + SettingsManagerF.getInstance().addProvider(new ThemeSettingsProviderF()); + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/keyshortcuts/KeyStrokeManagerF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/keyshortcuts/KeyStrokeManagerF.java new file mode 100644 index 00000000000..22499ca45c8 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/keyshortcuts/KeyStrokeManagerF.java @@ -0,0 +1,134 @@ +/* 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.keyshortcuts; + +import java.util.Map; +import java.util.Optional; +import java.util.TreeMap; + +import javafx.scene.input.KeyCombination; + +/** + * Central registry of keyboard shortcuts of the JavaFX UI, counter-part of + * {@code KeyStrokeManager}/{@code KeyStrokeSettings} of the Swing module {@code key.ui}. + *

+ * Actions are keyed by the fully qualified class name of their Swing counterpart, so that the + * persisted {@code keystrokes.json} of the Swing UI can be adopted by a later milestone. The + * defaults mirror {@link javax.swing.KeyStroke KeyStrokeSettings}. Format parity (reading/writing + * {@code keystrokes.json}) is deferred; the registry itself is the single source of truth for + * accelerators. + */ +public final class KeyStrokeManagerF { + + private final Map bindings = new TreeMap<>(); + + private KeyStrokeManagerF() { + registerDefaults(); + } + + private static final KeyStrokeManagerF INSTANCE = new KeyStrokeManagerF(); + + /** + * @return the global shortcut manager instance + */ + public static KeyStrokeManagerF getInstance() { + return INSTANCE; + } + + private static KeyCombination combo(String spec) { + return KeyCombination.keyCombination(spec); + } + + private void defineDefault(String actionId, String spec) { + bindings.put(actionId, combo(spec)); + } + + /** + * @return the registered shortcut for the given action, if any + */ + public Optional binding(String actionId) { + return Optional.ofNullable(bindings.get(actionId)); + } + + /** + * Binds a shortcut to an action, overriding a possible default. + * + * @param actionId the action id + * @param combination the shortcut, or {@code null} to remove the binding + */ + public void bind(String actionId, KeyCombination combination) { + if (combination == null) { + bindings.remove(actionId); + } else { + bindings.put(actionId, combination); + } + } + + /** + * Clears all overrides and re-establishes the default shortcuts. + */ + public void resetToDefaults() { + bindings.clear(); + registerDefaults(); + } + + /** + * @return an immutable snapshot of the current bindings (action id → shortcut spec), ready for + * persistence + */ + public Map snapshot() { + TreeMap copy = new TreeMap<>(); + bindings.forEach((id, combo) -> copy.put(id, combo.getDisplayText())); + return copy; + } + + private void registerDefaults() { + defineDefault("de.uka.ilkd.key.macros.FullAutoPilotProofMacro", modifier() + "V"); + defineDefault("de.uka.ilkd.key.macros.AutoPilotPrepareProofMacro", modifier() + "D"); + defineDefault("de.uka.ilkd.key.macros.PropositionalExpansionMacro", modifier() + "A"); + defineDefault("de.uka.ilkd.key.macros.FullPropositionalExpansionMacro", modifier() + "S"); + defineDefault("de.uka.ilkd.key.macros.TryCloseMacro", modifier() + "C"); + defineDefault("de.uka.ilkd.key.macros.FinishSymbolicExecutionMacro", modifier() + "X"); + defineDefault("de.uka.ilkd.key.macros.OneStepProofMacro", modifier() + "SPACE"); + defineDefault("de.uka.ilkd.key.macros.HeapSimplificationMacro", modifier() + "H"); + defineDefault("de.uka.ilkd.key.macros.UpdateSimplificationMacro", modifier() + "L"); + defineDefault("de.uka.ilkd.key.macros.IntegerSimplificationMacro", modifier() + "I"); + defineDefault("de.uka.ilkd.key.macros.SMTPreparationMacro", modifier() + "Y"); + + defineDefault("de.uka.ilkd.key.gui.actions.SearchInProofTreeAction", modifier() + "F"); + defineDefault("de.uka.ilkd.key.gui.actions.PrettyPrintToggleAction", modifier() + "P"); + defineDefault("de.uka.ilkd.key.gui.actions.UnicodeToggleAction", modifier() + "U"); + defineDefault("de.uka.ilkd.key.gui.actions.ProofManagementAction", modifier() + "M"); + + defineDefault("de.uka.ilkd.key.gui.actions.QuickSaveAction", "F5"); + defineDefault("de.uka.ilkd.key.gui.actions.QuickLoadAction", "F6"); + + defineDefault("de.uka.ilkd.key.gui.actions.IncreaseFontSizeAction", modifier() + "PLUS"); + defineDefault("de.uka.ilkd.key.gui.actions.DecreaseFontSizeAction", modifier() + "MINUS"); + defineDefault("de.uka.ilkd.key.gui.actions.AbandonTaskAction", modifier() + "W"); + defineDefault("de.uka.ilkd.key.gui.actions.PruneProofAction", modifier() + "DELETE"); + defineDefault("de.uka.ilkd.key.gui.actions.GoalBackAction", modifier() + "Z"); + defineDefault("de.uka.ilkd.key.gui.actions.CopyToClipboardAction", modifier() + "C"); + defineDefault("de.uka.ilkd.key.gui.actions.ExitMainAction", modifier() + "Q"); + defineDefault("de.uka.ilkd.key.gui.actions.GoalSelectAboveAction", modifier() + "K"); + defineDefault("de.uka.ilkd.key.gui.actions.GoalSelectBelowAction", modifier() + "J"); + defineDefault("de.uka.ilkd.key.gui.actions.AutoModeAction", modifier() + "SPACE"); + defineDefault("de.uka.ilkd.key.gui.actions.OpenMostRecentFileAction", modifier() + "R"); + defineDefault("de.uka.ilkd.key.gui.actions.SaveBundleAction", modifier() + "B"); + defineDefault("de.uka.ilkd.key.gui.actions.SaveFileAction", modifier() + "S"); + defineDefault("de.uka.ilkd.key.gui.settings.SettingsManager$ShowSettingsAction", + modifier() + "N"); + defineDefault("de.uka.ilkd.key.gui.actions.OpenFileAction", modifier() + "O"); + defineDefault("de.uka.ilkd.key.gui.actions.SearchInSequentAction", "F"); + defineDefault("de.uka.ilkd.key.gui.actions.SearchNextAction", "F3"); + defineDefault("de.uka.ilkd.key.gui.actions.SearchPreviousAction", "SHIFT+F3"); + defineDefault("de.uka.ilkd.key.gui.actions.SelectionBackAction", "SHORTCUT+ALT+LEFT"); + defineDefault("de.uka.ilkd.key.gui.actions.SelectionForwardAction", "SHORTCUT+ALT+RIGHT"); + } + + private static String modifier() { + return "SHORTCUT+"; + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/NotificationManagerF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/NotificationManagerF.java new file mode 100644 index 00000000000..a1c341368e2 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/NotificationManagerF.java @@ -0,0 +1,129 @@ +/* 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.notification; + +import javafx.animation.FadeTransition; +import javafx.animation.PauseTransition; +import javafx.application.Platform; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.geometry.Pos; +import javafx.scene.control.Label; +import javafx.scene.layout.Pane; +import javafx.scene.layout.StackPane; +import javafx.scene.layout.VBox; +import javafx.util.Duration; + +/** + * Shows transient toast notifications in the top-right corner of the main window. + *

+ * Counter-part of the {@code NotificationManager} of the Swing module {@code key.ui}. Notifications + * are plain JavaFX labels with the {@code notification-toast} style class (see + * {@code key-light.css}/{@code key-dark.css}); they auto-dismiss after a configurable timeout and + * dismiss immediately when clicked. + */ +public final class NotificationManagerF { + + /** Severity of a notification, mapped to a CSS style class. */ + public enum Kind { + INFO("info"), + WARNING("warning"), + ERROR("error"); + + private final String styleClass; + + Kind(String styleClass) { + this.styleClass = styleClass; + } + + String styleClass() { + return styleClass; + } + } + + private static final NotificationManagerF INSTANCE = new NotificationManagerF(); + + private final VBox toastBox = new VBox(6); + private final SimpleObjectProperty timeout = + new SimpleObjectProperty<>(this, "timeout", Duration.seconds(5)); + + private NotificationManagerF() { + toastBox.setPickOnBounds(false); + toastBox.getStyleClass().add("notification-overlay"); + // prevent the stack pane from stretching the box to the full width, which would push + // the toasts to the left edge + toastBox.setMaxWidth(420); + toastBox.setAlignment(Pos.TOP_RIGHT); + } + + /** + * @return the global notification manager instance + */ + public static NotificationManagerF getInstance() { + return INSTANCE; + } + + /** + * Attaches the toast overlay to the given pane, which should cover the content area of the + * main window. The overlay is mouse-transparent outside of the toasts themselves, so normal + * interaction with the content below is unaffected. + * + * @param overlay the pane to place the toasts in + */ + public void attach(Pane overlay) { + if (!overlay.getChildren().contains(toastBox)) { + overlay.getChildren().add(toastBox); + StackPane.setAlignment(toastBox, Pos.TOP_RIGHT); + toastBox.setTranslateX(-12); + toastBox.setTranslateY(40); + } + } + + /** + * @return the auto-dismiss timeout of toasts + */ + public ObjectProperty timeoutProperty() { + return timeout; + } + + /** + * Shows an informational notification. + * + * @param message the message to display + */ + public void notify(String message) { + notify(message, Kind.INFO); + } + + /** + * Shows a notification with the given severity. + * + * @param message the message to display + * @param kind the severity + */ + public void notify(String message, Kind kind) { + Platform.runLater(() -> { + Label toast = new Label(message); + toast.getStyleClass().addAll("notification-toast", kind.styleClass()); + toast.setMaxWidth(420); + toast.setWrapText(true); + toast.setOnMouseClicked(ignored -> dismiss(toast)); + + toastBox.getChildren().add(toast); + PauseTransition pause = new PauseTransition(timeout.get()); + pause.setOnFinished(ignored -> dismiss(toast)); + pause.play(); + }); + } + + private void dismiss(Label toast) { + if (toastBox.getChildren().contains(toast)) { + FadeTransition fade = new FadeTransition(Duration.millis(200), toast); + fade.setOnFinished( + ignored -> Platform.runLater(() -> toastBox.getChildren().remove(toast))); + fade.play(); + } + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/SettingsManagerF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/SettingsManagerF.java new file mode 100644 index 00000000000..4077926648a --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/SettingsManagerF.java @@ -0,0 +1,175 @@ +/* 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.settings; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.control.Button; +import javafx.scene.control.ButtonBar; +import javafx.scene.control.ScrollPane; +import javafx.scene.control.TreeItem; +import javafx.scene.control.TreeView; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.StackPane; +import javafx.stage.Modality; +import javafx.stage.Stage; +import javafx.stage.Window; + +/** + * Aggregates the registered {@link SettingsProviderF}s and opens the settings dialog, counter-part + * of {@code SettingsManager}/{@code SettingsDialog} of the Swing module {@code key.ui}. + *

+ * The dialog is built in code (no FXML): a tree of categories on the left (implemented via + * {@link TreeView}), the selected provider's panel on the right, and OK / Apply / Cancel controls. + */ +public final class SettingsManagerF { + + private static final SettingsManagerF INSTANCE = new SettingsManagerF(); + + private final List providers = new ArrayList<>(); + private final TreeMap> providersByCategory = new TreeMap<>(); + + private SettingsManagerF() { + } + + /** + * @return the global settings manager instance + */ + public static SettingsManagerF getInstance() { + return INSTANCE; + } + + /** + * Registers a settings provider. + * + * @param provider the provider to register + */ + public void addProvider(SettingsProviderF provider) { + providers.add(provider); + providersByCategory + .computeIfAbsent(provider.getCategory() == null ? "" : provider.getCategory(), + key -> new ArrayList<>()) + .add(provider); + } + + /** + * Registers multiple settings providers. + * + * @param providers the providers to register + */ + public void addProviders(List providers) { + providers.forEach(this::addProvider); + } + + /** + * @return the currently registered providers, in registration order + */ + public List getProviders() { + return List.copyOf(providers); + } + + /** + * Applies all registered providers; used by auto mode and profile loading. + */ + public void applyAll() { + providers.forEach(SettingsProviderF::apply); + } + + /** + * Opens the settings dialog, modal to the given owner window. + * + * @param owner the owner window, or {@code null} for a standalone dialog + */ + public void openSettings(Window owner) { + Stage stage = new Stage(); + stage.initOwner(owner); + stage.initModality(Modality.WINDOW_MODAL); + stage.setTitle("KeY - Settings"); + + BorderPane content = new BorderPane(); + content.setPadding(new Insets(10)); + + Map, SettingsProviderF> providerByItem = new IdentityHashMap<>(); + TreeView tree = buildTree(providerByItem); + content.setLeft(tree); + BorderPane.setMargin(tree, new Insets(0, 10, 0, 0)); + + ScrollPane scrollPane = new ScrollPane(); + scrollPane.setFitToWidth(true); + StackPane panelArea = new StackPane(); + scrollPane.setContent(panelArea); + content.setCenter(scrollPane); + BorderPane.setAlignment(scrollPane, Pos.CENTER); + + tree.getSelectionModel().selectedItemProperty().addListener((obs, old, selected) -> { + SettingsProviderF provider = selected == null ? null : providerByItem.get(selected); + if (provider != null) { + panelArea.getChildren().setAll(provider.getPanel()); + } + }); + + ButtonBar buttons = new ButtonBar(); + Button okButton = new Button("OK"); + Button applyButton = new Button("Apply"); + Button cancelButton = new Button("Cancel"); + ButtonBar.setButtonData(okButton, ButtonBar.ButtonData.OK_DONE); + ButtonBar.setButtonData(applyButton, ButtonBar.ButtonData.APPLY); + ButtonBar.setButtonData(cancelButton, ButtonBar.ButtonData.CANCEL_CLOSE); + buttons.getButtons().addAll(okButton, applyButton, cancelButton); + + applyButton.setOnAction(ignored -> providers.forEach(SettingsProviderF::apply)); + okButton.setOnAction(ignored -> { + providers.forEach(SettingsProviderF::apply); + stage.close(); + }); + cancelButton.setOnAction(ignored -> stage.close()); + + content.setBottom(buttons); + + stage.setScene(new javafx.scene.Scene(content, 900, 600)); + selectFirstNonEmpty(tree); + stage.show(); + } + + private TreeView buildTree(Map, SettingsProviderF> providerByItem) { + TreeItem root = new TreeItem<>("Settings"); + root.setExpanded(true); + + for (var entry : providersByCategory.entrySet()) { + TreeItem categoryItem = new TreeItem<>(entry.getKey()); + categoryItem.setExpanded(true); + entry.getValue().stream() + .sorted(Comparator.comparing(SettingsProviderF::getDescription)) + .forEach(provider -> { + TreeItem item = new TreeItem<>(provider.getDescription()); + providerByItem.put(item, provider); + categoryItem.getChildren().add(item); + }); + root.getChildren().add(categoryItem); + } + + TreeView tree = new TreeView<>(root); + tree.setShowRoot(false); + tree.setPrefWidth(260); + return tree; + } + + private void selectFirstNonEmpty(TreeView tree) { + TreeItem root = tree.getRoot(); + if (root != null && !root.getChildren().isEmpty()) { + TreeItem firstCategory = root.getChildren().get(0); + if (!firstCategory.getChildren().isEmpty()) { + tree.getSelectionModel().select(firstCategory.getChildren().get(0)); + } + } + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/SettingsProviderF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/SettingsProviderF.java new file mode 100644 index 00000000000..53de9176a26 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/SettingsProviderF.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.gui.fx.settings; + +import java.util.List; + +import javafx.scene.Node; + +/** + * A single entry of the KeY settings dialog, counter-part of {@code SettingsProvider} of the + * Swing module {@code key.ui}. + *

+ * The provider supplies a human readable description (shown in the settings tree), a category + * (grouping in the tree), and a JavaFX panel building the input controls. When the user accepts + * the dialog, {@link #apply()} is invoked for every provider; implementors must transfer the + * values of their input components into the respective settings. + */ +public interface SettingsProviderF { + + /** + * @return a human readable description shown in the settings tree + */ + String getDescription(); + + /** + * @return the name of the category group in the settings tree + */ + String getCategory(); + + /** + * Provides the panel shown on the right side of the settings dialog when this provider is + * selected. + * + * @return a fresh panel; may also be reused, in which case it must be updated on selection + */ + Node getPanel(); + + /** + * Transfers the values of the panel's input components into the corresponding settings. + * Called for every provider when the user accepts or applies the settings. + */ + void apply(); + + /** + * Restores the values shown in the panel to the values currently stored in the settings. + */ + default void reset() { + } + + /** + * @return provider-specific sub categories/panels, empty if none + */ + default List getChildren() { + return List.of(); + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/ThemeSettingsProviderF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/ThemeSettingsProviderF.java new file mode 100644 index 00000000000..c1d8c71e9d8 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/ThemeSettingsProviderF.java @@ -0,0 +1,101 @@ +/* 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.settings; + +import javafx.collections.FXCollections; +import javafx.geometry.Insets; +import javafx.scene.Node; +import javafx.scene.control.ComboBox; +import javafx.scene.control.Label; +import javafx.scene.control.RadioButton; +import javafx.scene.control.ToggleGroup; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.VBox; + +import de.uka.ilkd.key.gui.fx.configuration.ConfigF; +import de.uka.ilkd.key.gui.fx.theme.Theme; +import de.uka.ilkd.key.gui.fx.theme.ThemeManager; +import de.uka.ilkd.key.settings.ProofIndependentSettings; +import de.uka.ilkd.key.settings.ViewSettings; + +/** + * Settings provider for the look of the JavaFX UI: light/dark theme and the base font size. The + * font size is persisted within the core {@link ViewSettings} (same {@code SIZES} as the Swing + * module's {@code Config}); the theme is managed by the {@link ThemeManager}. + */ +public final class ThemeSettingsProviderF implements SettingsProviderF { + + private final ToggleGroup themeGroup = new ToggleGroup(); + private final ComboBox fontSizeComboBox = new ComboBox<>(); + + private RadioButton lightRadio; + private RadioButton darkRadio; + + @Override + public String getDescription() { + return "Look & Feel"; + } + + @Override + public String getCategory() { + return "General"; + } + + @Override + public Node getPanel() { + lightRadio = createRadio("Light", Theme.LIGHT); + darkRadio = createRadio("Dark", Theme.DARK); + Theme current = ThemeManager.getInstance().getTheme(); + (current == Theme.DARK ? darkRadio : lightRadio).setSelected(true); + + fontSizeComboBox.setItems( + FXCollections.observableArrayList(ConfigF.SIZES[0], ConfigF.SIZES[1], ConfigF.SIZES[2], + ConfigF.SIZES[3], ConfigF.SIZES[4], ConfigF.SIZES[5])); + fontSizeComboBox.getSelectionModel().select(ConfigF.DEFAULT.sizeIndex()); + + VBox packet = new VBox(16); + packet.setPadding(new Insets(12)); + + GridPane themeRow = new GridPane(); + themeRow.setHgap(8); + themeRow.add(new Label("Theme:"), 0, 0); + themeRow.add(lightRadio, 1, 0); + themeRow.add(darkRadio, 2, 0); + + GridPane fontRow = new GridPane(); + fontRow.setHgap(8); + fontRow.add(new Label("Base font size:"), 0, 0); + fontRow.add(fontSizeComboBox, 1, 0); + + packet.getChildren().addAll(themeRow, fontRow); + return packet; + } + + @Override + public void apply() { + Theme selected = darkRadio.isSelected() ? Theme.DARK : Theme.LIGHT; + ThemeManager.getInstance().setTheme(selected); + int index = fontSizeComboBox.getSelectionModel().getSelectedIndex(); + if (index >= 0) { + ViewSettings viewSettings = + ProofIndependentSettings.DEFAULT_INSTANCE.getViewSettings(); + viewSettings.setFontIndex(index); + } + } + + @Override + public void reset() { + Theme current = ThemeManager.getInstance().getTheme(); + (current == Theme.DARK ? darkRadio : lightRadio).setSelected(true); + fontSizeComboBox.getSelectionModel().select(ConfigF.DEFAULT.sizeIndex()); + } + + private RadioButton createRadio(String label, Theme theme) { + RadioButton radio = new RadioButton(label); + radio.setToggleGroup(themeGroup); + radio.setUserData(theme); + return radio; + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/theme/ThemeManager.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/theme/ThemeManager.java index 5f90a3e86ce..8b1b4928524 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/theme/ThemeManager.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/theme/ThemeManager.java @@ -27,14 +27,29 @@ public final class ThemeManager { /** Property key under which the currently applied stylesheet URL is stored per scene. */ private static final String SCENE_KEY = "key-ui.theme-stylesheet"; + /** + * System property selecting the initial theme ({@code "light"} or {@code "dark"}, default + * light). Also handy for UI tests running headless against Xvfb. + */ + public static final String THEME_PROPERTY = "key.fx.theme"; + private final List scenes = new CopyOnWriteArrayList<>(); private final ReadOnlyObjectWrapper currentTheme = - new ReadOnlyObjectWrapper<>(this, "theme", Theme.LIGHT); + new ReadOnlyObjectWrapper<>(this, "theme", initialTheme()); private ThemeManager() { } + /** + * Determines the initial theme from the {@code key.fx.theme} system property ({@code "light"} + * or {@code "dark"}); defaults to light. + */ + private static Theme initialTheme() { + String property = System.getProperty(THEME_PROPERTY, ""); + return Theme.DARK.name().equalsIgnoreCase(property) ? Theme.DARK : Theme.LIGHT; + } + /** * @return the global {@link ThemeManager} instance */ diff --git a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css index dd2f019bf24..ae22f1a8d3d 100644 --- a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css +++ b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css @@ -43,15 +43,32 @@ -fx-text-fill: -key-text-secondary; } +/* placeholder views (milestone M1) */ +.view-title { + -fx-font-size: 1.4em; + -fx-text-fill: -key-text; + -fx-font-weight: bold; +} + +.view-hint { + -fx-text-fill: -key-text-secondary; +} + /* notifications (toast) */ .notification-toast { -fx-background-color: -key-surface; -fx-background-radius: 4; -fx-border-color: -key-border; -fx-border-radius: 4; + -fx-padding: 8 12 8 12; + -fx-text-fill: -key-text; -fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.5), 10, 0.2, 0, 2); } +.notification-toast.info { + -fx-border-color: -key-accent; +} + .notification-toast.error { -fx-border-color: -key-error; } diff --git a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css index 4b1ae6ca393..43c7b18aa33 100644 --- a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css +++ b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css @@ -39,15 +39,32 @@ -fx-text-fill: -key-text-secondary; } +/* placeholder views (milestone M1) */ +.view-title { + -fx-font-size: 1.4em; + -fx-text-fill: -key-text; + -fx-font-weight: bold; +} + +.view-hint { + -fx-text-fill: -key-text-secondary; +} + /* notifications (toast) */ .notification-toast { -fx-background-color: -key-surface; -fx-background-radius: 4; -fx-border-color: -key-border; -fx-border-radius: 4; + -fx-padding: 8 12 8 12; + -fx-text-fill: -key-text; -fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.3), 10, 0.2, 0, 2); } +.notification-toast.info { + -fx-border-color: -key-accent; +} + .notification-toast.error { -fx-border-color: -key-error; } From 032cb86e50bb53fa452c1ab1e7737213281469b3 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Wed, 7 Oct 2026 09:47:29 +0200 Subject: [PATCH 006/281] =?UTF-8?q?key.ui.fx:=20M2a=20sequent=20view=20spi?= =?UTF-8?q?ke=20=E2=80=94=20TextFlow=20rendering=20over=20the=20core=20pp?= =?UTF-8?q?=20pipeline?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit First real view of the JavaFX UI, counter-part of key.ui's SequentView (JEditorPane + HTML): renders the printed sequent as Text runs in a TextFlow inside a ScrollPane, using the UI-agnostic printer of key.core (SequentViewLogicPrinter + PosTableLayouter/InitialPositionTable). Mouse clicks map via TextFlow.getHitInfo to a character index and back to a PosInSequent — no HTML, no AWT. MainWindowF: the 'sequent' dockable now hosts the SequentViewF; a demo load (-Dkey.fx.demo.sequent=) fetches a proof via core KeYEnvironment on a background task; -Dkey.fx.verify.sequent=1 runs an in-app self test that round-trips every glyph through getRangeShape/ getHitInfo/InitialPositionTable (Agatha: 513/513 exact, PASS) proving the TextFlow character model matches the position table exactly (no Swing HTML +1 quirk). Clicked terms are highlighted and shown in the status bar. Verified on screen (light theme) and via the self test; JavaFX 25 API: getHitInfo replaced deprecated hitTest, getRangeShape(int,int,boolean) is the current shape API. --- key.ui.fx/build.gradle | 11 + .../de/uka/ilkd/key/gui/fx/MainWindowF.java | 83 ++++- .../key/gui/fx/nodeviews/SequentViewF.java | 347 ++++++++++++++++++ .../de/uka/ilkd/key/gui/fx/theme/key-dark.css | 19 + .../uka/ilkd/key/gui/fx/theme/key-light.css | 19 + 5 files changed, 478 insertions(+), 1 deletion(-) create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentViewF.java diff --git a/key.ui.fx/build.gradle b/key.ui.fx/build.gradle index bf586481ca9..937c16b295b 100644 --- a/key.ui.fx/build.gradle +++ b/key.ui.fx/build.gradle @@ -45,6 +45,17 @@ run { systemProperties["key.examples.dir"] = "$rootDir/key.ui/examples" systemProperties["KeyDebugFlag"] = "on" + // Forward selected system properties from the Gradle command line to the application: + // * key.fx.theme = light|dark (theme override, also used by headless UI tests) + // * key.fx.demo.sequent = (M2a spike: load a demo proof into the sequent view) + // * key.fx.verify.sequent = any (M2a spike: run the position-mapping self test afterwards) + ["key.fx.theme", "key.fx.demo.sequent", "key.fx.verify.sequent"].each { name -> + def value = System.getProperty(name) + if (value != null) { + systemProperties[name] = value + } + } + // enable assertions jvmArgs += "-ea" 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 index 3870d69b820..612f277135d 100644 --- 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 @@ -5,11 +5,13 @@ package de.uka.ilkd.key.gui.fx; import java.io.IOException; +import java.nio.file.Path; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Optional; import javafx.application.Platform; +import javafx.concurrent.Task; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.Node; @@ -33,6 +35,8 @@ import javafx.scene.layout.VBox; import javafx.stage.Stage; +import de.uka.ilkd.key.control.DefaultUserInterfaceControl; +import de.uka.ilkd.key.control.KeYEnvironment; import de.uka.ilkd.key.gui.fx.configuration.ConfigF; import de.uka.ilkd.key.gui.fx.docking.DockLayoutStore; import de.uka.ilkd.key.gui.fx.docking.DockLocation; @@ -41,6 +45,7 @@ import de.uka.ilkd.key.gui.fx.docking.SimpleDockable; import de.uka.ilkd.key.gui.fx.fonticons.IconFactoryF; import de.uka.ilkd.key.gui.fx.keyshortcuts.KeyStrokeManagerF; +import de.uka.ilkd.key.gui.fx.nodeviews.SequentViewF; 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.settings.SettingsManagerF; @@ -88,6 +93,12 @@ public final class MainWindowF { private final Label statusLeft = new Label(); private final Label statusRight = new Label(); + /** + * 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(); + /** * Creates the main window bound to the given stage. * @@ -126,6 +137,9 @@ public void initialize() { ThemeManager.getInstance().themeProperty().addListener((obs, old, theme) -> updateStatus()); updateStatus(); + wireSequentView(); + startDemoProofLoad(); + NotificationManagerF.getInstance() .notify("KeY (JavaFX) started. Docking layout restored from " + layoutStore.file() + "."); @@ -145,6 +159,73 @@ public Map getDockables() { return dockables; } + /** + * @return the sequent view docked in the main area + */ + public SequentViewF getSequentView() { + return sequentView; + } + + // ------------------------------------------------------------------ + // sequent view (M2a spike) + // ------------------------------------------------------------------ + + private void wireSequentView() { + sequentView.setOnPosSelected(pos -> { + if (pos == null) { + statusRight.setText(""); + return; + } + String text = sequentView.getHighlightedText(pos); + statusRight.setText(text.isBlank() ? String.valueOf(pos) : text); + 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; + } + Path location = Path.of(file); + Task> loadTask = new Task<>() { + @Override + protected KeYEnvironment call() throws Exception { + return KeYEnvironment.load(location); + } + }; + loadTask.setOnSucceeded(event -> { + KeYEnvironment env = loadTask.getValue(); + sequentView.setProof(env.getLoadedProof(), env.getServices()); + workspace.select(dockables.get(ID_SEQUENT)); + NotificationManagerF.getInstance() + .notify("Demo proof loaded: " + location, Kind.INFO); + statusLeft.setText("Proof: " + env.getLoadedProof().name()); + 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); + } + }); + loadTask.setOnFailed(event -> { + Throwable error = loadTask.getException(); + LOGGER.error("Demo proof loading failed", error); + NotificationManagerF.getInstance() + .notify("Demo proof loading failed: " + error.getMessage(), Kind.ERROR); + }); + Thread loader = new Thread(loadTask, "fx-demo-proof-loader"); + loader.setDaemon(true); + loader.start(); + } + private void setWindowIcons() { Image icon = new Image(MainWindowF.class.getResourceAsStream(IMAGE_DIR + "key-color-icon-square.png")); @@ -163,7 +244,7 @@ private void buildDockables() { registerDockable(ID_PROOF_TREE, "Proof Tree"); registerDockable(ID_INFO_VIEW, "Info"); registerDockable(ID_STRATEGY, "Strategy"); - registerDockable(ID_SEQUENT, "Sequent"); + dockables.put(ID_SEQUENT, new SimpleDockable(ID_SEQUENT, "Sequent", sequentView)); registerDockable(ID_SOURCE_VIEW, "Source"); } diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentViewF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentViewF.java new file mode 100644 index 00000000000..00203ba4ac3 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentViewF.java @@ -0,0 +1,347 @@ +/* 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.nodeviews; + +import java.util.Objects; +import java.util.function.Consumer; +import javafx.geometry.Insets; +import javafx.geometry.Point2D; +import javafx.scene.control.ScrollPane; +import javafx.scene.input.MouseEvent; +import javafx.scene.shape.HLineTo; +import javafx.scene.shape.LineTo; +import javafx.scene.shape.MoveTo; +import javafx.scene.shape.PathElement; +import javafx.scene.shape.VLineTo; +import javafx.scene.text.Font; +import javafx.scene.text.HitInfo; +import javafx.scene.text.Text; +import javafx.scene.text.TextFlow; + +import de.uka.ilkd.key.gui.fx.configuration.ConfigF; +import de.uka.ilkd.key.java.Services; +import de.uka.ilkd.key.logic.label.TermLabel; +import de.uka.ilkd.key.pp.IdentitySequentPrintFilter; +import de.uka.ilkd.key.pp.InitialPositionTable; +import de.uka.ilkd.key.pp.NotationInfo; +import de.uka.ilkd.key.pp.PosInSequent; +import de.uka.ilkd.key.pp.PosTableLayouter; +import de.uka.ilkd.key.pp.Range; +import de.uka.ilkd.key.pp.SequentViewLogicPrinter; +import de.uka.ilkd.key.pp.VisibleTermLabels; +import de.uka.ilkd.key.proof.Node; +import de.uka.ilkd.key.proof.Proof; + +import org.key_project.logic.Name; + +/** + * First JavaFX version of the sequent view, the counter-part of + * {@code de.uka.ilkd.key.gui.nodeviews.SequentView} (a Swing {@code JEditorPane} with HTML + * content) in the module {@code key.ui}. + *

+ * Milestone M2a spike. The rendering pipeline reuses the UI-agnostic pretty-printer of + * {@code key.core}: {@link SequentViewLogicPrinter} produces the printed sequent string together + * with an {@link InitialPositionTable} that maps character indexes to {@link PosInSequent}s. + * The string is rendered as {@link Text} runs inside a {@link TextFlow}; mouse clicks are mapped + * with {@link TextFlow#getHitInfo} back to a character index and hence to a {@link PosInSequent} + * -- no HTML, no AWT. + *

+ * The spike intentionally supports only a plain character-grid rendering (monospaced font like + * the Swing view) and click highlighting; syntax highlighting, hover tooltips, update + * highlighting, sequent hiding and mediator wiring arrive with the milestone M2 implementation. + */ +public class SequentViewF extends ScrollPane { + + /** + * Visible term labels of the spike: none. The real view feeds this from the mediator's term + * label visibility manager. + */ + private static final VisibleTermLabels NO_VISIBLE_TERM_LABELS = new VisibleTermLabels() { + @Override + public boolean contains(TermLabel label) { + return false; + } + + @Override + public boolean contains(Name name) { + return false; + } + }; + + private final TextFlow textFlow = new TextFlow(); + + private final IdentitySequentPrintFilter filter = new IdentitySequentPrintFilter(); + + private SequentViewLogicPrinter printer; + private Proof proof; + private Node selectedNode; + private String printed; + private Range highlightedRange; + + private Consumer onPosSelected = pos -> { + }; + + /** + * Creates an empty sequent view. + */ + public SequentViewF() { + getStyleClass().add("sequent-view"); + setFitToWidth(true); + setFitToHeight(true); + setContent(textFlow); + textFlow.getStyleClass().add("sequent-view-flow"); + textFlow.setPadding(new Insets(6)); + textFlow.setOnMouseClicked(this::handleMouseClick); + printPlaceholder(); + } + + /** + * Sets the proof whose root node is displayed. + * + * @param proof the loaded proof, may be {@code null} to reset the view + * @param services the services of the proof's environment + */ + public void setProof(Proof proof, Services services) { + this.proof = proof; + this.selectedNode = proof != null ? proof.root() : null; + this.printer = proof != null + ? SequentViewLogicPrinter.positionPrinter(new NotationInfo(), services, + NO_VISIBLE_TERM_LABELS) + : null; + printSequent(); + } + + /** + * @return the displayed proof, or {@code null} if none is loaded + */ + public Proof getProof() { + return proof; + } + + /** + * Re-prints the sequent of the currently selected node. + */ + public void printSequent() { + highlightedRange = null; + if (printer == null || selectedNode == null) { + printPlaceholder(); + return; + } + filter.setSequent(selectedNode.sequent()); + // TODO(M2): compute the line width from the font metrics and the viewport width + printer.update(filter, PosTableLayouter.DEFAULT_LINE_WIDTH); + printed = printer.result(); + rebuildRuns(); + } + + /** + * @return the initial position table of the current printing, or {@code null} + */ + public InitialPositionTable getInitialPositionTable() { + return printer == null ? null : printer.layouter().getInitialPositionTable(); + } + + /** + * Returns the text of the printed sequent covered by the given position; the JavaFX + * counterpart of {@code SequentView.getHighlightedText(PosInSequent)}. Unlike the Swing + * version, no offset correction is needed: the character indexes of the position table match + * the printed string exactly (there is no HTML document shift). + * + * @param pos the position, may be {@code null} + * @return the covered text, or the empty string + */ + public String getHighlightedText(PosInSequent pos) { + if (pos == null || printed == null || pos.getBounds() == null) { + return ""; + } + Range bounds = pos.getBounds(); + int start = Math.clamp(bounds.start(), 0, printed.length()); + int end = Math.clamp(bounds.start() + bounds.length(), start, printed.length()); + return printed.substring(start, end); + } + + /** + * Registers the handler invoked when the user clicks on a term. + * + * @param handler receives the position of the clicked term, or {@code null} when the click hit + * no position + */ + public void setOnPosSelected(Consumer handler) { + this.onPosSelected = handler == null ? pos -> { + } : handler; + } + + private void handleMouseClick(MouseEvent event) { + InitialPositionTable table = getInitialPositionTable(); + if (table == null || printed == null) { + return; + } + Point2D local = textFlow.sceneToLocal(event.getSceneX(), event.getSceneY()); + HitInfo hit = textFlow.getHitInfo(local); + int charIndex = hit.getCharIndex(); + if (charIndex < 0 || charIndex >= printed.length()) { + highlightedRange = null; + rebuildRuns(); + onPosSelected.accept(null); + return; + } + PosInSequent pos = table.getPosInSequent(charIndex, filter); + Range bounds = pos != null ? pos.getBounds() : null; + highlightedRange = bounds != null && bounds.length() > 0 ? bounds : null; + rebuildRuns(); + onPosSelected.accept(pos); + } + + private void rebuildRuns() { + textFlow.getChildren().clear(); + if (printed == null) { + return; + } + Font font = ConfigF.DEFAULT.monoFont(); + if (highlightedRange == null) { + addRun(printed, font, "sequent-text"); + return; + } + int start = Math.clamp(highlightedRange.start(), 0, printed.length()); + int end = Math.clamp(highlightedRange.end(), start, printed.length()); + addRun(printed.substring(0, start), font, "sequent-text"); + addRun(printed.substring(start, end), font, "sequent-text", "sequent-term-highlight"); + addRun(printed.substring(end), font, "sequent-text"); + } + + private void addRun(String content, Font font, String... styleClasses) { + if (content.isEmpty()) { + return; + } + Text run = new Text(content); + run.getStyleClass().addAll(styleClasses); + run.setFont(font); + textFlow.getChildren().add(run); + } + + private void printPlaceholder() { + textFlow.getChildren().clear(); + Text placeholder = new Text("No proof loaded.\n" + + "Start with -Dkey.fx.demo.sequent= to try the sequent view spike."); + placeholder.getStyleClass().add("sequent-placeholder"); + placeholder.setFont(ConfigF.DEFAULT.systemFont()); + textFlow.getChildren().add(placeholder); + } + + /** + * For tests: the current selection highlight range. + */ + Range highlightedRange() { + return highlightedRange; + } + + /** + * For tests: the printed sequent string. + */ + String printed() { + return printed; + } + + /** + * Development self-test (M2a): verifies that the character model of the {@link TextFlow} + * matches the {@link InitialPositionTable}. Every non-whitespace character of the printed + * sequent is round-tripped: the on-screen bounds of the character are computed via + * {@link TextFlow#getRangeShape(int, int, boolean)}, the center of the resulting shape is + * mapped back through {@link TextFlow#getHitInfo} to a character index, and the + * {@link PosInSequent} of both indexes must agree. This is the same mapping the mouse-click + * handler performs. + * + * @return a one-line report, {@code "... PASS"} if the mapping is consistent + */ + public String verifyPositionMapping() { + if (printed == null || textFlow.getChildren().isEmpty()) { + return "no printed sequent"; + } + // make sure the text flow is laid out before asking for glyph shapes + textFlow.applyCss(); + textFlow.layout(); + + InitialPositionTable table = getInitialPositionTable(); + if (table == null) { + return "no position table"; + } + int chars = printed.length(); + int sampled = 0; + int exact = 0; + int tolerant = 0; + int mismatches = 0; + int firstMismatch = -1; + for (int i = 0; i < chars; i++) { + char c = printed.charAt(i); + if (Character.isWhitespace(c)) { + continue; + } + sampled++; + HitInfo hit = textFlow.getHitInfo(centerOfRangeShape(i)); + if (hit == null) { + mismatches++; + if (firstMismatch < 0) { + firstMismatch = i; + } + continue; + } + int j = hit.getCharIndex(); + if (j == i) { + exact++; + continue; + } + PosInSequent posI = table.getPosInSequent(i, filter); + PosInSequent posJ = table.getPosInSequent(j, filter); + if (Math.abs(j - i) <= 1 && Objects.equals(posI, posJ)) { + tolerant++; + } else { + mismatches++; + if (firstMismatch < 0) { + firstMismatch = i; + } + } + } + String result = mismatches == 0 ? "PASS" : "FAIL"; + return "chars=" + chars + " sampled=" + sampled + " exact=" + exact + " tolerant=" + + tolerant + " mismatches=" + mismatches + + (firstMismatch >= 0 ? " firstMismatch=" + firstMismatch : "") + " " + result; + } + + /** + * @return the center of the on-screen shape of the character at {@code index} in the local + * coordinates of the text flow + */ + private Point2D centerOfRangeShape(int index) { + PathElement[] elements = textFlow.getRangeShape(index, index + 1, true); + double minX = Double.POSITIVE_INFINITY; + double minY = Double.POSITIVE_INFINITY; + double maxX = Double.NEGATIVE_INFINITY; + double maxY = Double.NEGATIVE_INFINITY; + for (PathElement element : elements) { + double x = Double.NaN; + double y = Double.NaN; + if (element instanceof MoveTo m) { + x = m.getX(); + y = m.getY(); + } else if (element instanceof LineTo l) { + x = l.getX(); + y = l.getY(); + } else if (element instanceof HLineTo h) { + x = h.getX(); + } else if (element instanceof VLineTo v) { + y = v.getY(); + } + if (!Double.isNaN(x)) { + minX = Math.min(minX, x); + maxX = Math.max(maxX, x); + } + if (!Double.isNaN(y)) { + minY = Math.min(minY, y); + maxY = Math.max(maxY, y); + } + } + return new Point2D((minX + maxX) / 2.0, (minY + maxY) / 2.0); + } +} diff --git a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css index ae22f1a8d3d..a5e79a0a0d3 100644 --- a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css +++ b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css @@ -86,3 +86,22 @@ .key-icon { -fx-fill: -key-text; } + +/* sequent view (M2a spike) */ +.sequent-view, +.sequent-view-flow { + -fx-background-color: transparent; +} + +.sequent-text { + -fx-fill: -key-text; +} + +.sequent-term-highlight { + -fx-fill: -key-accent; + -fx-font-weight: bold; +} + +.sequent-placeholder { + -fx-fill: -key-text-secondary; +} diff --git a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css index 43c7b18aa33..486b8724421 100644 --- a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css +++ b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css @@ -82,3 +82,22 @@ .key-icon { -fx-fill: -key-text; } + +/* sequent view (M2a spike) */ +.sequent-view, +.sequent-view-flow { + -fx-background-color: transparent; +} + +.sequent-text { + -fx-fill: -key-text; +} + +.sequent-term-highlight { + -fx-fill: -key-accent; + -fx-font-weight: bold; +} + +.sequent-placeholder { + -fx-fill: -key-text-secondary; +} From 7ab6b64e37c16bfa8ec7a531cd5e039b7b65384f Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Wed, 7 Oct 2026 09:56:30 +0200 Subject: [PATCH 007/281] =?UTF-8?q?key.ui.fx:=20M2b=20selection=20foundati?= =?UTF-8?q?on=20=E2=80=94=20KeYSelectionModel=20copy=20+=20selection-drive?= =?UTF-8?q?n=20sequent=20view?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Copies de.uka.ilkd.key.core's KeYSelectionEvent and KeYSelectionListener verbatim into de.uka.ilkd.key.core.fx (package line only) and ports KeYSelectionModel with a single seam refactor: the constructor of the original takes the Swing KeYMediator and calls setProof(proof, previous) on proof changes; here the same callback is expressed by the nested ProofBinder interface, which KeYMediatorF (M2c) will implement. All other logic identical. SequentViewF is no longer driven by a direct setProof call: it attaches to the KeYSelectionModel (as the Swing view observes the mediator's model) and re-displays on selectedProofChanged AND selectedNodeChanged (the Swing setSelectedProof fires only the proof event). display(Node) derives the printer's Services from node.proof(), re-creates the printer when the proof changes, and marshals to the FX thread. MainWindowF owns the selection model; the demo proof load routes the loaded proof through selectionModel.setSelectedProof (the same path the mediator will use), firing the events that drive the view. Verified on screen incl. the position-mapping self test (Agatha: 513/513 PASS). --- .../ilkd/key/core/fx/KeYSelectionEvent.java | 58 +++ .../key/core/fx/KeYSelectionListener.java | 40 ++ .../ilkd/key/core/fx/KeYSelectionModel.java | 368 ++++++++++++++++++ .../de/uka/ilkd/key/gui/fx/MainWindowF.java | 22 +- .../key/gui/fx/nodeviews/SequentViewF.java | 72 +++- 5 files changed, 548 insertions(+), 12 deletions(-) create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/core/fx/KeYSelectionEvent.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/core/fx/KeYSelectionListener.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/core/fx/KeYSelectionModel.java 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/gui/fx/MainWindowF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/MainWindowF.java index 612f277135d..a499f3e714c 100644 --- 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 @@ -37,6 +37,7 @@ import de.uka.ilkd.key.control.DefaultUserInterfaceControl; import de.uka.ilkd.key.control.KeYEnvironment; +import de.uka.ilkd.key.core.fx.KeYSelectionModel; import de.uka.ilkd.key.gui.fx.configuration.ConfigF; import de.uka.ilkd.key.gui.fx.docking.DockLayoutStore; import de.uka.ilkd.key.gui.fx.docking.DockLocation; @@ -99,6 +100,14 @@ public final class MainWindowF { */ private final SequentViewF sequentView = new SequentViewF(); + /** + * The selection model of the window, copy of the Swing mediator's model. Until the + * {@code KeYMediatorF} exists (M2c) the proof binding is a no-op. + */ + private final KeYSelectionModel selectionModel = new KeYSelectionModel((newProof, + previousProof) -> LOGGER.debug("Selected proof changed: {} -> {}", previousProof, + newProof)); + /** * Creates the main window bound to the given stage. * @@ -166,11 +175,19 @@ public SequentViewF getSequentView() { return sequentView; } + /** + * @return the selection model of the window + */ + public KeYSelectionModel getSelectionModel() { + return selectionModel; + } + // ------------------------------------------------------------------ // sequent view (M2a spike) // ------------------------------------------------------------------ private void wireSequentView() { + sequentView.attach(selectionModel); sequentView.setOnPosSelected(pos -> { if (pos == null) { statusRight.setText(""); @@ -201,7 +218,10 @@ protected KeYEnvironment call() throws Exception { }; loadTask.setOnSucceeded(event -> { KeYEnvironment env = loadTask.getValue(); - sequentView.setProof(env.getLoadedProof(), env.getServices()); + // route the proof through the selection model (as the mediator will do in M2c): + // fires selectedProofChanged + selects the first open goal or a leaf, which the + // sequent view observes and displays. + selectionModel.setSelectedProof(env.getLoadedProof()); workspace.select(dockables.get(ID_SEQUENT)); NotificationManagerF.getInstance() .notify("Demo proof loaded: " + location, Kind.INFO); diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentViewF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentViewF.java index 00203ba4ac3..4f900e75360 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentViewF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentViewF.java @@ -20,8 +20,10 @@ import javafx.scene.text.Text; import javafx.scene.text.TextFlow; +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.configuration.ConfigF; -import de.uka.ilkd.key.java.Services; import de.uka.ilkd.key.logic.label.TermLabel; import de.uka.ilkd.key.pp.IdentitySequentPrintFilter; import de.uka.ilkd.key.pp.InitialPositionTable; @@ -35,6 +37,7 @@ import de.uka.ilkd.key.proof.Proof; import org.key_project.logic.Name; +import org.key_project.util.javafx.FxUtil; /** * First JavaFX version of the sequent view, the counter-part of @@ -80,6 +83,21 @@ public boolean contains(Name name) { private String printed; private Range highlightedRange; + private KeYSelectionModel selectionModel; + private final KeYSelectionListener selectionListener = new KeYSelectionListener() { + @Override + public void selectedNodeChanged(KeYSelectionEvent event) { + display(selectionModel.getSelectedNode()); + } + + @Override + public void selectedProofChanged(KeYSelectionEvent event) { + // setSelectedProof fires only the proof event (no node event); re-display here, + // mirroring MainWindow.setSequentView of the Swing UI + display(selectionModel.getSelectedNode()); + } + }; + private Consumer onPosSelected = pos -> { }; @@ -98,18 +116,50 @@ public SequentViewF() { } /** - * Sets the proof whose root node is displayed. + * Registers this view as a selection listener on the given model and displays the currently + * selected node, mirroring how the Swing {@code SequentView} observes the mediator's + * selection model. + * + * @param model the selection model to observe + */ + public void attach(KeYSelectionModel model) { + Objects.requireNonNull(model); + if (selectionModel == model) { + return; + } + if (selectionModel != null) { + selectionModel.removeKeYSelectionListener(selectionListener); + } + selectionModel = model; + model.addKeYSelectionListenerChecked(selectionListener); + display(model.getSelectedNode()); + } + + /** + * Displays the sequent of the given node. * - * @param proof the loaded proof, may be {@code null} to reset the view - * @param services the services of the proof's environment + * @param node the node whose sequent is shown, may be {@code null} to reset the view */ - public void setProof(Proof proof, Services services) { - this.proof = proof; - this.selectedNode = proof != null ? proof.root() : null; - this.printer = proof != null - ? SequentViewLogicPrinter.positionPrinter(new NotationInfo(), services, - NO_VISIBLE_TERM_LABELS) - : null; + public void display(Node node) { + if (!FxUtil.isFxThread()) { + FxUtil.runLater(() -> display(node)); + return; + } + highlightedRange = null; + if (node == null) { + proof = null; + selectedNode = null; + printer = null; + printPlaceholder(); + return; + } + if (node.proof() != proof) { + proof = node.proof(); + // TODO(M2c): share the NotationInfo and the term label visibility with the mediator + printer = SequentViewLogicPrinter.positionPrinter(new NotationInfo(), + node.proof().getServices(), NO_VISIBLE_TERM_LABELS); + } + selectedNode = node; printSequent(); } From d656828ca187e47fd53e37bfd7acc4e2a828698d Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Wed, 7 Oct 2026 12:51:34 +0200 Subject: [PATCH 008/281] =?UTF-8?q?key.ui.fx:=20M2c=20proof=20tree=20view?= =?UTF-8?q?=20=E2=80=94=20JavaFX=20TreeView=20over=20the=20proof=20structu?= =?UTF-8?q?re?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit First version of the proof tree, counter-part of key.ui's ProofTreeView (Swing JTree + GUIProofTreeModel, ~5000 lines). The FX version builds the tree directly from the Proof using the Swing model's structure rules, reimplemented in ~330 lines: - each branch is a flat list of rule applications (the linear walk of GUIBranchNode.fillChildrenCache); at a branch point one labeled sub-branch per child is created; - branch labels come from NodeInfo.getBranchLabel(), falling back to 'Case ' (mirroring GUIAbstractTreeNode.ensureBranchLabelIsSet); the root branch is labeled 'Proof Tree'; - entries display 'serialNr:name()' whitespace-normalized like the Swing renderer; leaves are styled by goal state (open red / closed green / interactive orange) via theme CSS classes; - clicking a node drives KeYSelectionModel.setSelectedNode; selection and proof changes rebuild/reveal in the tree (bidirectional wiring with re-entrancy guards). MainWindowF hosts the view in the 'proofTree' dockable and wires it to the selection model. Demo extensions: -Dkey.fx.demo.autoprove=1 runs the core automatic prover after loading (startAndWaitForAutoMode) to obtain a real multi-branch tree; -Dkey.fx.show=sequent|prooftree selects the dockable; -Dkey.fx.verify.tree=1 runs a structure self test (Agatha auto-proved: proofNodes=149, nodeEntries=149, unique=149, branches=5 PASS — every node exactly once, 5 branch folders). Deferred (later M2/M3): search, ProofTreeViewFilter modes, heatmap, notes, one-step-simplifier protocol children, lazy population, live updates on rule applications (needs proof event wiring, M3). --- key.ui.fx/build.gradle | 6 +- .../de/uka/ilkd/key/gui/fx/MainWindowF.java | 36 +- .../key/gui/fx/prooftree/ProofTreeViewF.java | 343 ++++++++++++++++++ .../de/uka/ilkd/key/gui/fx/theme/key-dark.css | 29 ++ .../uka/ilkd/key/gui/fx/theme/key-light.css | 29 ++ 5 files changed, 437 insertions(+), 6 deletions(-) create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/prooftree/ProofTreeViewF.java diff --git a/key.ui.fx/build.gradle b/key.ui.fx/build.gradle index 937c16b295b..8731eb2b68b 100644 --- a/key.ui.fx/build.gradle +++ b/key.ui.fx/build.gradle @@ -49,7 +49,11 @@ run { // * key.fx.theme = light|dark (theme override, also used by headless UI tests) // * key.fx.demo.sequent = (M2a spike: load a demo proof into the sequent view) // * key.fx.verify.sequent = any (M2a spike: run the position-mapping self test afterwards) - ["key.fx.theme", "key.fx.demo.sequent", "key.fx.verify.sequent"].each { name -> + // * key.fx.show = sequent|prooftree (M2: dockable selected after the demo load) + // * key.fx.demo.autoprove = any (M2: run the automatic prover on the demo proof) + // * key.fx.verify.tree = any (M2: run the proof tree structure self test afterwards) + ["key.fx.theme", "key.fx.demo.sequent", "key.fx.verify.sequent", "key.fx.show", + "key.fx.demo.autoprove", "key.fx.verify.tree"].each { name -> def value = System.getProperty(name) if (value != null) { systemProperties[name] = value 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 index a499f3e714c..5a446718697 100644 --- 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 @@ -49,6 +49,7 @@ import de.uka.ilkd.key.gui.fx.nodeviews.SequentViewF; 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.prooftree.ProofTreeViewF; import de.uka.ilkd.key.gui.fx.settings.SettingsManagerF; import de.uka.ilkd.key.gui.fx.settings.ThemeSettingsProviderF; import de.uka.ilkd.key.gui.fx.theme.Theme; @@ -105,8 +106,15 @@ public final class MainWindowF { * {@code KeYMediatorF} exists (M2c) the proof binding is a no-op. */ private final KeYSelectionModel selectionModel = new KeYSelectionModel((newProof, - previousProof) -> LOGGER.debug("Selected proof changed: {} -> {}", previousProof, - newProof)); + previousProof) -> LOGGER.debug("Selected proof changed: {} -> {}", + previousProof == null ? null : previousProof.name(), + newProof == null ? null : newProof.name())); + + /** + * 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(); /** * Creates the main window bound to the given stage. @@ -188,6 +196,7 @@ public KeYSelectionModel getSelectionModel() { private void wireSequentView() { sequentView.attach(selectionModel); + proofTreeView.attach(selectionModel); sequentView.setOnPosSelected(pos -> { if (pos == null) { statusRight.setText(""); @@ -213,7 +222,13 @@ private void startDemoProofLoad() { Task> loadTask = new Task<>() { @Override protected KeYEnvironment call() throws Exception { - return KeYEnvironment.load(location); + KeYEnvironment env = KeYEnvironment.load(location); + 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"); + } + return env; } }; loadTask.setOnSucceeded(event -> { @@ -222,7 +237,10 @@ protected KeYEnvironment call() throws Exception { // fires selectedProofChanged + selects the first open goal or a leaf, which the // sequent view observes and displays. selectionModel.setSelectedProof(env.getLoadedProof()); - workspace.select(dockables.get(ID_SEQUENT)); + String show = System.getProperty("key.fx.show", ID_SEQUENT); + String target = ID_PROOF_TREE.equalsIgnoreCase(show) ? ID_PROOF_TREE : ID_SEQUENT; + LOGGER.info("Demo: selecting dockable '{}' (key.fx.show={})", target, show); + workspace.select(dockables.get(target)); NotificationManagerF.getInstance() .notify("Demo proof loaded: " + location, Kind.INFO); statusLeft.setText("Proof: " + env.getLoadedProof().name()); @@ -234,6 +252,13 @@ protected KeYEnvironment call() throws Exception { report.endsWith("PASS") ? Kind.INFO : Kind.ERROR); statusRight.setText(report); } + 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); + } }); loadTask.setOnFailed(event -> { Throwable error = loadTask.getException(); @@ -261,7 +286,8 @@ private void setWindowIcons() { private void buildDockables() { registerDockable(ID_LOADED_PROOFS, "Loaded Proofs"); // TaskTree registerDockable(ID_GOAL_LIST, "Goal List"); - registerDockable(ID_PROOF_TREE, "Proof Tree"); + dockables.put(ID_PROOF_TREE, + new SimpleDockable(ID_PROOF_TREE, "Proof Tree", proofTreeView)); registerDockable(ID_INFO_VIEW, "Info"); registerDockable(ID_STRATEGY, "Strategy"); dockables.put(ID_SEQUENT, new SimpleDockable(ID_SEQUENT, "Sequent", sequentView)); diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/prooftree/ProofTreeViewF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/prooftree/ProofTreeViewF.java new file mode 100644 index 00000000000..e1d3c87814f --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/prooftree/ProofTreeViewF.java @@ -0,0 +1,343 @@ +/* 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.prooftree; + +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.Iterator; +import java.util.Objects; +import java.util.Set; +import javafx.scene.control.Tooltip; +import javafx.scene.control.TreeCell; +import javafx.scene.control.TreeItem; +import javafx.scene.control.TreeView; + +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.Goal; +import de.uka.ilkd.key.proof.Node; +import de.uka.ilkd.key.proof.Proof; + +import org.key_project.util.javafx.FxUtil; + +/** + * First JavaFX version of the proof tree view, the counter-part of + * {@code de.uka.ilkd.key.gui.prooftree.ProofTreeView} (a Swing {@code JTree} driven by + * {@code GUIProofTreeModel}) in the module {@code key.ui}. + *

+ * Milestone M2, first version. The tree structure is built directly from the + * {@link Proof}: each branch is rendered as a flat list of rule applications (like the Swing + * model's linear walk), and at a branch point one sub-branch item per child is created, labeled + * with the branch label of the child (falling back to {@code Case }, mirroring + * {@code GUIAbstractTreeNode.ensureBranchLabelIsSet}). Leaves are styled by their goal state + * (open/closed/interactive). Clicking a node selects it in the {@link KeYSelectionModel}; + * selection changes (and proof changes) drive the view. + *

+ * Deliberately deferred to later M2/M3 chunks: search, filters (ProofTreeViewFilter), heatmap, + * notes/tooltips of the rich Swing renderer, one-step-simplifier protocol children, lazy + * population for very large proofs (currently built eagerly), and live updates on rule + * applications. + */ +public class ProofTreeViewF extends TreeView { + + /** + * One tree entry: either a proof node entry (a rule application) or a branch entry (labeled + * sub-tree; {@code node} is the root of the branch). + */ + public static final class Entry { + final Node node; + final String branchLabel; + + private Entry(Node node, String branchLabel) { + this.node = node; + this.branchLabel = branchLabel; + } + + static Entry node(Node node) { + return new Entry(node, null); + } + + static Entry branch(Node node, String label) { + return new Entry(node, Objects.requireNonNull(label)); + } + + public boolean isBranch() { + return branchLabel != null; + } + + public Node node() { + return node; + } + + /** + * @return the text shown for this entry, whitespace-normalized like the Swing renderer + */ + public String displayText() { + if (branchLabel != null) { + return branchLabel; + } + String text = node.serialNr() + ":" + node.name(); + return text.replaceAll("\\s+", " "); + } + } + + private KeYSelectionModel selectionModel; + private Proof proof; + private boolean updatingSelection; + + private final KeYSelectionListener selectionListener = new KeYSelectionListener() { + @Override + public void selectedNodeChanged(KeYSelectionEvent event) { + revealSelectedNode(); + } + + @Override + public void selectedProofChanged(KeYSelectionEvent event) { + setProof(event.getSource().getSelectedProof()); + } + }; + + /** + * Creates an empty proof tree view. + */ + public ProofTreeViewF() { + getStyleClass().add("proof-tree"); + setCellFactory(view -> new ProofTreeCell()); + getSelectionModel().selectedItemProperty() + .addListener((obs, oldItem, newItem) -> handleTreeSelection(newItem)); + } + + /** + * Registers this view as a selection listener on the given model and shows the currently + * selected proof, if any. + * + * @param model the selection model to observe + */ + public void attach(KeYSelectionModel model) { + Objects.requireNonNull(model); + if (selectionModel == model) { + return; + } + if (selectionModel != null) { + selectionModel.removeKeYSelectionListener(selectionListener); + } + selectionModel = model; + model.addKeYSelectionListenerChecked(selectionListener); + setProof(model.getSelectedProof()); + revealSelectedNode(); + } + + /** + * Shows the given proof, replacing any previously shown tree. + * + * @param newProof the proof to display, may be {@code null} + */ + public void setProof(Proof newProof) { + if (!FxUtil.isFxThread()) { + FxUtil.runLater(() -> setProof(newProof)); + return; + } + proof = newProof; + updatingSelection = true; + try { + if (proof == null) { + setRoot(null); + } else { + TreeItem root = buildBranch(proof.root(), "Proof Tree"); + setRoot(root); + root.setExpanded(true); + } + } finally { + updatingSelection = false; + } + } + + /** + * Rebuilds the tree from the current proof (used when rule applications changed it). + */ + public void refresh() { + setProof(proof); + } + + /** + * Development self-test (M2): verifies that every proof node appears exactly once as a node + * entry (each node is either on a branch's linear chain or the root of exactly one branch, + * whose folder is an additional labeled entry like the Swing model's branch nodes). + * + * @return a one-line report, {@code "... PASS"} if the structure is consistent + */ + public String verifyTreeStructure() { + if (proof == null || getRoot() == null) { + return "no proof"; + } + Set seen = Collections.newSetFromMap(new IdentityHashMap<>()); + int[] counters = { 0, 0 }; // node entries, branch entries + collect(getRoot(), seen, counters); + int proofNodeCount = 0; + for (Iterator it = proof.root().subtreeIterator(); it.hasNext();) { + it.next(); + proofNodeCount++; + } + boolean pass = seen.size() == proofNodeCount && counters[0] == proofNodeCount; + return "proofNodes=" + proofNodeCount + " nodeEntries=" + counters[0] + " unique=" + + seen.size() + " branches=" + counters[1] + " " + (pass ? "PASS" : "FAIL"); + } + + private static void collect(TreeItem item, Set seen, int[] counters) { + Entry entry = item.getValue(); + if (entry != null && entry.node != null) { + seen.add(entry.node); + if (entry.isBranch()) { + counters[1]++; + } else { + counters[0]++; + } + } + for (TreeItem child : item.getChildren()) { + collect(child, seen, counters); + } + } + + private TreeItem buildBranch(Node branchRoot, String label) { + TreeItem branchItem = new TreeItem<>(Entry.branch(branchRoot, label)); + + Node current = branchRoot; + // linear walk along single-child nodes, mirroring GUIBranchNode.fillChildrenCache + while (true) { + TreeItem item = new TreeItem<>(Entry.node(current)); + branchItem.getChildren().add(item); + if (current.childrenCount() == 1) { + current = current.child(0); + continue; + } + break; + } + // at a branch point (or a leaf): one branch item per child + for (Node child : current.children()) { + branchItem.getChildren().add(buildBranch(child, ensureBranchLabelIsSet(child))); + } + return branchItem; + } + + /** + * Returns the branch label of the given node, falling back to {@code Case } and storing it + * on the node like {@code GUIAbstractTreeNode.ensureBranchLabelIsSet}. + */ + private static String ensureBranchLabelIsSet(Node node) { + var nodeInfo = node.getNodeInfo(); + String label; + if (node.root()) { + label = "Proof Tree"; + } else { + synchronized (nodeInfo) { + label = nodeInfo.getBranchLabel(); + if (label == null) { + label = "Case " + (node.parent().getChildNr(node) + 1); + nodeInfo.setBranchLabel(label); + } + } + } + return label; + } + + private void handleTreeSelection(TreeItem item) { + if (updatingSelection || item == null || item.getValue() == null) { + return; + } + Node node = item.getValue().node(); + if (node != null && selectionModel != null && node != selectionModel.getSelectedNode()) { + selectionModel.setSelectedNode(node); + } + } + + /** + * Selects and reveals the tree item of the currently selected node, if present. + */ + private void revealSelectedNode() { + Node selected = selectionModel != null ? selectionModel.getSelectedNode() : null; + if (selected == null || getRoot() == null) { + return; + } + TreeItem item = findItem(getRoot(), selected); + if (item == null || getSelectionModel().getSelectedItem() == item) { + return; + } + updatingSelection = true; + try { + // expand the ancestors so the item is visible + TreeItem a = item.getParent(); + while (a != null) { + a.setExpanded(true); + a = a.getParent(); + } + getSelectionModel().select(item); + scrollTo(getRow(item)); + } finally { + updatingSelection = false; + } + } + + private static TreeItem findItem(TreeItem start, Node node) { + if (start.getValue() != null && start.getValue().node() == node) { + return start; + } + for (TreeItem child : start.getChildren()) { + TreeItem result = findItem(child, node); + if (result != null) { + return result; + } + } + return null; + } + + /** + * The tree cell rendering: text plus the goal-state style class (Swing colors map onto the + * theme's CSS properties). + */ + private final class ProofTreeCell extends TreeCell { + @Override + protected void updateItem(Entry item, boolean empty) { + super.updateItem(item, empty); + if (empty || item == null) { + setText(null); + setTooltip(null); + getStyleClass().clear(); + getStyleClass().add("proof-tree-cell"); + return; + } + setText(item.displayText()); + getStyleClass().clear(); + getStyleClass().add("proof-tree-cell"); + getStyleClass().add(styleClassOf(item)); + setTooltip(new Tooltip(tooltipText(item))); + } + + private String styleClassOf(Entry item) { + Node node = item.node; + if (item.isBranch()) { + return node.isClosed() ? "proof-tree-branch-closed" : "proof-tree-branch"; + } + if (node.leaf()) { + if (node.isClosed()) { + return "proof-tree-closed"; + } + Goal goal = proof != null ? proof.getOpenGoal(node) : null; + if (goal == null) { + return "proof-tree-closed"; + } + return goal.isAutomatic() ? "proof-tree-open" : "proof-tree-interactive"; + } + return "proof-tree-inner"; + } + + private String tooltipText(Entry item) { + if (item.isBranch()) { + return "Branch: " + item.branchLabel; + } + return "Node " + item.node.serialNr(); + } + } +} diff --git a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css index a5e79a0a0d3..d2a6b80f0ff 100644 --- a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css +++ b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css @@ -105,3 +105,32 @@ .sequent-placeholder { -fx-fill: -key-text-secondary; } + +/* proof tree (M2 first version) */ +.proof-tree { + -fx-background-color: -key-surface; +} + +.proof-tree-cell { + -fx-text-fill: -key-text; + -fx-fill: -key-text; + -fx-padding: 1 4 1 2; +} + +.proof-tree-branch { + -fx-text-fill: -key-text; + -fx-font-weight: bold; +} + +.proof-tree-branch-closed, +.proof-tree-closed { + -fx-text-fill: -key-proved; +} + +.proof-tree-open { + -fx-text-fill: -key-error; +} + +.proof-tree-interactive { + -fx-text-fill: -key-interactive; +} diff --git a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css index 486b8724421..40f53d1c00a 100644 --- a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css +++ b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css @@ -101,3 +101,32 @@ .sequent-placeholder { -fx-fill: -key-text-secondary; } + +/* proof tree (M2 first version) */ +.proof-tree { + -fx-background-color: -key-surface; +} + +.proof-tree-cell { + -fx-text-fill: -key-text; + -fx-fill: -key-text; + -fx-padding: 1 4 1 2; +} + +.proof-tree-branch { + -fx-text-fill: -key-text; + -fx-font-weight: bold; +} + +.proof-tree-branch-closed, +.proof-tree-closed { + -fx-text-fill: -key-proved; +} + +.proof-tree-open { + -fx-text-fill: -key-error; +} + +.proof-tree-interactive { + -fx-text-fill: -key-interactive; +} From bb054f6b473a94b74bb1900127c9f06c2f217382 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Wed, 7 Oct 2026 13:58:41 +0200 Subject: [PATCH 009/281] =?UTF-8?q?key.ui.fx:=20FxDriver=20hook=20?= =?UTF-8?q?=E2=80=94=20standalone=20view=20drivers=20for=20per-view=20veri?= =?UTF-8?q?fication?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MainApplication delegates the primary stage to the class named by -Dkey.fx.driver (implements FxDriver) instead of MainWindowF. Drivers show a single view with a local KeYSelectionModel and a demo proof, enabling per-view development and automated verification without the full main window (used by parallel M2 view tasks and UI tests). --- .../java/de/uka/ilkd/key/gui/fx/FxDriver.java | 29 +++++++++++++++++++ .../uka/ilkd/key/gui/fx/MainApplication.java | 12 +++++++- 2 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/FxDriver.java 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/MainApplication.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/MainApplication.java index 97c58243d30..da20fbed1f3 100644 --- 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 @@ -13,11 +13,21 @@ *

* 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) { + public void start(final Stage stage) throws Exception { + 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(); } } From 7c2a9f77a4ee7334bace0868ddf12a1702096295 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Wed, 7 Oct 2026 14:32:20 +0200 Subject: [PATCH 010/281] =?UTF-8?q?key.ui.fx:=20M2=20info=20view=20?= =?UTF-8?q?=E2=80=94=20proof=20details=20panel=20over=20the=20selection=20?= =?UTF-8?q?model?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit InfoViewF (counter-part of the Swing InfoView) shows the essential details of the selected proof as a labeled-row table: proof name, loaded problem file, open goals, closed goals (leaves minus open goals, as in the Swing loading report), total nodes (subtreeIterator walk), branch points (childrenCount() > 1) and closed status. The view attaches to a KeYSelectionModel (Fx-thread marshalled, like SequentViewF) and recomputes on selectedProofChanged; the Swing InfoNodeFactory symbol/rule browser is deferred. InfoViewDriver is a standalone FxDriver that loads the demo proof (KeYEnvironment.load) on a background thread, optionally runs auto mode, selects it and logs the self test verifyInfoView() (rendered fields non-empty and rendered node count == fresh subtreeIterator traversal). Theme CSS sections '/* info view */' added to key-light.css and key-dark.css. --- .../key/gui/fx/infoview/InfoViewDriver.java | 94 +++++++ .../ilkd/key/gui/fx/infoview/InfoViewF.java | 266 ++++++++++++++++++ .../de/uka/ilkd/key/gui/fx/theme/key-dark.css | 43 +++ .../uka/ilkd/key/gui/fx/theme/key-light.css | 43 +++ 4 files changed, 446 insertions(+) create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/infoview/InfoViewDriver.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/infoview/InfoViewF.java diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/infoview/InfoViewDriver.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/infoview/InfoViewDriver.java new file mode 100644 index 00000000000..cdaffe839c4 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/infoview/InfoViewDriver.java @@ -0,0 +1,94 @@ +/* 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.infoview; + +import java.nio.file.Path; +import javafx.scene.Scene; +import javafx.stage.Stage; + +import de.uka.ilkd.key.control.DefaultUserInterfaceControl; +import de.uka.ilkd.key.control.KeYEnvironment; +import de.uka.ilkd.key.core.fx.KeYSelectionModel; +import de.uka.ilkd.key.gui.fx.FxDriver; +import de.uka.ilkd.key.gui.fx.theme.ThemeManager; +import de.uka.ilkd.key.proof.Proof; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Standalone driver for the {@link InfoViewF} (development scaffolding, see {@link FxDriver}): + * shows the info view on the primary stage with its own {@link KeYSelectionModel} (no-op proof + * binder) and loads a demo proof in the background via + * {@link KeYEnvironment#load(Path)}. + *

+ * System properties: + *

    + *
  • {@code key.fx.demo.sequent} — the {@code .key} file to load; without it, the view stays + * empty,
  • + *
  • {@code key.fx.demo.autoprove} — if set, run the automatic prover on the demo proof + * (synchronously on the loader thread) before selecting it.
  • + *
+ * After the proof is selected, the self test {@link InfoViewF#verifyInfoView()} is executed and + * logged. + */ +public final class InfoViewDriver implements FxDriver { + + private static final Logger LOGGER = LoggerFactory.getLogger(InfoViewDriver.class); + + @Override + public void start(Stage stage) throws Exception { + InfoViewF view = new InfoViewF(); + KeYSelectionModel selectionModel = new KeYSelectionModel((newProof, previousProof) -> { + // no-op: this driver has no mediator behind the selection model + }); + view.attach(selectionModel); + + Scene scene = new Scene(view, 600, 460); + ThemeManager.getInstance().manage(scene); + stage.setTitle("KeY · Info"); + stage.setScene(scene); + stage.show(); + + String demoFile = System.getProperty("key.fx.demo.sequent"); + if (demoFile == null || demoFile.isBlank()) { + LOGGER.warn("No demo proof configured; set -Dkey.fx.demo.sequent= to " + + "populate the info view."); + return; + } + boolean autoprove = System.getProperty("key.fx.demo.autoprove") != null; + Thread loader = new Thread( + () -> loadDemoProof(selectionModel, view, Path.of(demoFile), autoprove), + "key-fx-infoview-demo-loader"); + loader.setDaemon(true); + loader.start(); + } + + /** + * Loads the demo proof, optionally runs the automatic prover, selects it in the model and + * runs the view's self test. + */ + private void loadDemoProof(KeYSelectionModel selectionModel, InfoViewF view, Path file, + boolean autoprove) { + try { + long start = System.currentTimeMillis(); + KeYEnvironment env = KeYEnvironment.load(file); + Proof proof = env.getLoadedProof(); + LOGGER.info("Loaded demo proof '{}' from {} ({} ms)", proof.name(), file, + System.currentTimeMillis() - start); + if (autoprove) { + long provingStart = System.currentTimeMillis(); + env.getProofControl().startAndWaitForAutoMode(proof); + LOGGER.info("Auto mode finished in {} ms: openGoals={} closed={}", + System.currentTimeMillis() - provingStart, proof.openGoals().size(), + proof.closed()); + } + selectionModel.setSelectedProof(proof); + LOGGER.info("Info self test: {}", view.verifyInfoView()); + } catch (Exception e) { + LOGGER.error("Failed to load demo proof {}", file, e); + } + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/infoview/InfoViewF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/infoview/InfoViewF.java new file mode 100644 index 00000000000..7a95a9d044e --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/infoview/InfoViewF.java @@ -0,0 +1,266 @@ +/* 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.infoview; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; +import javafx.geometry.Insets; +import javafx.scene.control.Label; +import javafx.scene.control.ScrollPane; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.VBox; + +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 org.key_project.util.javafx.FxUtil; + +/** + * First JavaFX version of the info view, the counter-part of + * {@code de.uka.ilkd.key.gui.InfoView} (a Swing {@code JSplitPane} with a rule/symbol browser + * tree) in the module {@code key.ui}. + *

+ * Milestone M2, first version. This version shows the essential details of the selected + * proof as a table of labeled rows (proof file, goal and node statistics, closed status), which + * the Swing UI displays in the statistics dialog + * ({@code de.uka.ilkd.key.gui.actions.ShowProofStatistics}) and in the loading report of + * {@code de.uka.ilkd.key.proof.io.ProblemLoader} ({@code countNodes()}, + * {@code countBranches() - openGoals().size()}). The statistics are recomputed on every + * {@code selectedProofChanged} notification; the data comes directly from the {@link Proof} + * (no mediator): + *

    + *
  • proof file: {@link Proof#getProofFile()} (the path of the loaded problem, may be + * {@code null} for programmatically created proofs),
  • + *
  • open goals: {@link Proof#openGoals()}.size(),
  • + *
  • closed goals: {@link Proof#countBranches()} minus the open goals — the formula the Swing + * loading report uses ({@code ProblemLoader.fireTaskFinished}); {@link Proof#closedGoals()} is + * deliberately not used, because it stays empty by default + * ({@code GeneralSettings.noPruningClosed = true}),
  • + *
  • total nodes: single pass over {@link Node#subtreeIterator()} from {@link Proof#root()},
  • + *
  • branches: number of nodes with {@link Node#childrenCount()} > 1 (branch points; note + * that {@link Proof#countBranches()} returns the number of leaves instead),
  • + *
  • status: {@link Proof#closed()}.
  • + *
+ *

+ * Deliberately deferred to later milestones: the rule/symbol/choice browser of the Swing + * {@code InfoNodeFactory}, node-level details, live refresh while the prover applies rules, and + * background computation of the statistics (currently a synchronous tree walk on the FX thread, + * which is fine for small demo proofs). + *

+ * Style classes (see {@code key-light.css}/{@code key-dark.css}, section "info view"): + * {@code .info-view} (this scroll pane), {@code .info-view-content}, {@code .info-view-title}, + * {@code .info-view-grid}, {@code .info-row-label}, {@code .info-row-value}, + * {@code .info-view-status-closed}, {@code .info-view-status-open}, + * {@code .info-view-placeholder}. + */ +public class InfoViewF extends ScrollPane { + + /** One labeled row of the info table. */ + private record Row(String key, Label label, Label value) { + } + + private final VBox content = new VBox(); + private final Label title = new Label(); + private final GridPane grid = new GridPane(); + private final Label placeholder = new Label("No proof selected."); + private final List rows = new ArrayList<>(); + + private Label fileValue; + private Label openGoalsValue; + private Label closedGoalsValue; + private Label nodesValue; + private Label branchesValue; + private Label statusValue; + + private KeYSelectionModel selectionModel; + private Proof proof; + /** number of proof nodes rendered in the nodes row (for {@link #verifyInfoView()}) */ + private int nodeCount; + /** number of branch points rendered in the branches row (for {@link #verifyInfoView()}) */ + private int branchCount; + + private final KeYSelectionListener selectionListener = new KeYSelectionListener() { + @Override + public void selectedProofChanged(KeYSelectionEvent event) { + display(event.getSource().getSelectedProof()); + } + }; + + /** + * Creates an empty info view. + */ + public InfoViewF() { + getStyleClass().add("info-view"); + setFitToWidth(true); + + content.getStyleClass().add("info-view-content"); + content.setPadding(new Insets(8)); + + title.getStyleClass().add("info-view-title"); + + grid.getStyleClass().add("info-view-grid"); + grid.setHgap(10); + grid.setVgap(4); + + fileValue = addRow("Proof File"); + openGoalsValue = addRow("Open Goals"); + closedGoalsValue = addRow("Closed Goals"); + nodesValue = addRow("Nodes"); + branchesValue = addRow("Branches"); + statusValue = addRow("Status"); + + placeholder.getStyleClass().add("info-view-placeholder"); + + content.getChildren().addAll(title, grid, placeholder); + setContent(content); + showPlaceholder(); + } + + /** + * Appends a labeled row to the grid. + * + * @param key the row's key (used by the self test to report empty fields) + * @return the value label of the created row + */ + private Label addRow(String key) { + Label label = new Label(key); + label.getStyleClass().add("info-row-label"); + Label value = new Label(); + value.getStyleClass().add("info-row-value"); + int rowIndex = grid.getRowCount(); + grid.add(label, 0, rowIndex); + grid.add(value, 1, rowIndex); + rows.add(new Row(key, label, value)); + return value; + } + + /** + * Registers this view as a selection listener on the given model and displays the currently + * selected proof, if any. Only {@code selectedProofChanged} is observed; this version is + * proof-level and shows no node details. + * + * @param model the selection model to observe + */ + public void attach(KeYSelectionModel model) { + Objects.requireNonNull(model); + if (selectionModel == model) { + return; + } + if (selectionModel != null) { + selectionModel.removeKeYSelectionListener(selectionListener); + } + selectionModel = model; + model.addKeYSelectionListenerChecked(selectionListener); + display(model.getSelectedProof()); + } + + /** + * Displays the details of the given proof. + * + * @param newProof the proof to display, may be {@code null} to clear the view + */ + public void display(Proof newProof) { + if (!FxUtil.isFxThread()) { + FxUtil.runLater(() -> display(newProof)); + return; + } + proof = newProof; + if (proof == null) { + showPlaceholder(); + return; + } + showTable(); + title.setText(proof.name().toString()); + fileValue.setText(proof.getProofFile() != null ? proof.getProofFile().toString() : "-"); + openGoalsValue.setText(Integer.toString(proof.openGoals().size())); + // "closed goals" as reported by the Swing loading report: leaves minus open goals + // (Proof.closedGoals() stays empty by default, GeneralSettings.noPruningClosed = true) + closedGoalsValue + .setText(Integer.toString(proof.countBranches() - proof.openGoals().size())); + nodeCount = 0; + branchCount = 0; + for (Iterator it = proof.root().subtreeIterator(); it.hasNext();) { + Node node = it.next(); + nodeCount++; + if (node.childrenCount() > 1) { + branchCount++; + } + } + nodesValue.setText(Integer.toString(nodeCount)); + branchesValue.setText(Integer.toString(branchCount)); + boolean closed = proof.closed(); + statusValue.setText(closed ? "closed" : "open"); + statusValue.getStyleClass().removeIf(c -> c.startsWith("info-view-status-")); + statusValue.getStyleClass() + .add(closed ? "info-view-status-closed" : "info-view-status-open"); + } + + /** + * @return the displayed proof, or {@code null} if none is selected + */ + public Proof getProof() { + return proof; + } + + private void showTable() { + title.setManaged(true); + title.setVisible(true); + grid.setManaged(true); + grid.setVisible(true); + placeholder.setManaged(false); + placeholder.setVisible(false); + } + + private void showPlaceholder() { + title.setText(""); + title.setManaged(false); + title.setVisible(false); + grid.setManaged(false); + grid.setVisible(false); + placeholder.setManaged(true); + placeholder.setVisible(true); + } + + /** + * Development self-test (M2): verifies that the info view is completely populated for the + * displayed proof and that the rendered node count matches a fresh traversal of + * {@code proof.root().subtreeIterator()}. May be called from any thread. + * + * @return a one-line report, {@code "... PASS"} if all fields are non-empty and the node + * counts agree + */ + public String verifyInfoView() { + if (!FxUtil.isFxThread()) { + return FxUtil.callAndWait(this::verifyInfoView); + } + if (proof == null) { + return "no proof selected FAIL"; + } + int independentNodeCount = 0; + for (Iterator it = proof.root().subtreeIterator(); it.hasNext();) { + it.next(); + independentNodeCount++; + } + String emptyFields = rows.stream() + .filter(row -> row.value().getText() == null || row.value().getText().isBlank()) + .map(Row::key) + .collect(Collectors.joining(",")); + boolean pass = emptyFields.isEmpty() && !title.getText().isBlank() + && nodeCount == independentNodeCount + && Integer.toString(independentNodeCount).equals(nodesValue.getText()); + return "proof=" + proof.name() + " openGoals=" + openGoalsValue.getText() + + " closedGoals=" + closedGoalsValue.getText() + " nodes=" + nodesValue.getText() + + " independentNodes=" + independentNodeCount + " branches=" + branchesValue.getText() + + " status=" + statusValue.getText() + + (emptyFields.isEmpty() ? "" : " emptyFields=" + emptyFields) + + " " + (pass ? "PASS" : "FAIL"); + } +} diff --git a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css index d2a6b80f0ff..b9911d7afa5 100644 --- a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css +++ b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css @@ -134,3 +134,46 @@ .proof-tree-interactive { -fx-text-fill: -key-interactive; } + +/* info view (M2 first version) */ +.info-view { + -fx-background: -key-surface; + -fx-background-color: -key-surface; +} + +.info-view-content { + -fx-padding: 8; +} + +.info-view-title { + -fx-font-size: 1.15em; + -fx-font-weight: bold; + -fx-text-fill: -key-text; + -fx-padding: 0 0 6 0; +} + +.info-view-grid { + -fx-hgap: 10; + -fx-vgap: 4; +} + +.info-row-label { + -fx-text-fill: -key-text-secondary; + -fx-font-weight: bold; +} + +.info-row-value { + -fx-text-fill: -key-text; +} + +.info-view-status-closed { + -fx-text-fill: -key-proved; +} + +.info-view-status-open { + -fx-text-fill: -key-error; +} + +.info-view-placeholder { + -fx-text-fill: -key-text-secondary; +} diff --git a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css index 40f53d1c00a..f5bac74459b 100644 --- a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css +++ b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css @@ -130,3 +130,46 @@ .proof-tree-interactive { -fx-text-fill: -key-interactive; } + +/* info view (M2 first version) */ +.info-view { + -fx-background: -key-surface; + -fx-background-color: -key-surface; +} + +.info-view-content { + -fx-padding: 8; +} + +.info-view-title { + -fx-font-size: 1.15em; + -fx-font-weight: bold; + -fx-text-fill: -key-text; + -fx-padding: 0 0 6 0; +} + +.info-view-grid { + -fx-hgap: 10; + -fx-vgap: 4; +} + +.info-row-label { + -fx-text-fill: -key-text-secondary; + -fx-font-weight: bold; +} + +.info-row-value { + -fx-text-fill: -key-text; +} + +.info-view-status-closed { + -fx-text-fill: -key-proved; +} + +.info-view-status-open { + -fx-text-fill: -key-error; +} + +.info-view-placeholder { + -fx-text-fill: -key-text-secondary; +} From 38f5976fbbe6a13ac403e0c0e66080f17e3d2298 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Wed, 7 Oct 2026 14:41:28 +0200 Subject: [PATCH 011/281] key.ui.fx: live proof tree updates via proof events + autoModeStopped MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ProofTreeViewF registers a ProofTreeListener on the displayed proof and coalesces structural events into one FX-thread rebuild, preserving branch expansion state and the selection. The rebuild reads nodes by count/index and retries on concurrent mutation (the prover thread may mutate the ArrayList-backed children while the tree is rebuilt). Core fact (verified by probe): during auto mode the parallel prover suspends the proof's non-essential tree listeners (Proof.suspendNonEssentialListeners), so per-application events never reach the views; the final state is delivered on autoModeStopped — the same contract the Swing MainWindow implements. MainWindowF therefore refreshes the tree from an AutoModeListener in the live demo flow. Demo affordances: key.fx.demo.autoprove.live=1 (auto mode after the proof is selected, ends with the structure self test) and key.fx.demo.maxsteps= (step-limited run, non-closed case). Verified both paths on the virtual display :99: closed 149/149 PASS (liveEvents=1 liveRefreshes=1 via re-delivered proofClosed), open 4/4 PASS (autoModeStopped refresh). Also forwards key.fx.driver. --- key.ui.fx/build.gradle | 10 +- .../de/uka/ilkd/key/gui/fx/MainWindowF.java | 56 ++++++ .../key/gui/fx/prooftree/ProofTreeViewF.java | 164 +++++++++++++++++- 3 files changed, 222 insertions(+), 8 deletions(-) diff --git a/key.ui.fx/build.gradle b/key.ui.fx/build.gradle index 8731eb2b68b..31e7b8c360b 100644 --- a/key.ui.fx/build.gradle +++ b/key.ui.fx/build.gradle @@ -46,14 +46,20 @@ run { systemProperties["KeyDebugFlag"] = "on" // Forward selected system properties from the Gradle command line to the application: + // * key.fx.driver = (M2: launch a standalone FxDriver view instead of MainWindowF) // * key.fx.theme = light|dark (theme override, also used by headless UI tests) // * key.fx.demo.sequent = (M2a spike: load a demo proof into the sequent view) // * key.fx.verify.sequent = any (M2a spike: run the position-mapping self test afterwards) // * key.fx.show = sequent|prooftree (M2: dockable selected after the demo load) // * key.fx.demo.autoprove = any (M2: run the automatic prover on the demo proof) // * key.fx.verify.tree = any (M2: run the proof tree structure self test afterwards) - ["key.fx.theme", "key.fx.demo.sequent", "key.fx.verify.sequent", "key.fx.show", - "key.fx.demo.autoprove", "key.fx.verify.tree"].each { name -> + // * key.fx.demo.autoprove.live = any (M2: auto mode AFTER the proof is selected — the + // views observe the final state via autoModeStopped; ends with the structure self test) + // * key.fx.demo.maxsteps = (M2: limit the auto mode strategy steps, e.g. 3 — tests + // the live-update refresh for the non-closed case) + ["key.fx.driver", "key.fx.theme", "key.fx.demo.sequent", "key.fx.verify.sequent", + "key.fx.show", "key.fx.demo.autoprove", "key.fx.verify.tree", + "key.fx.demo.autoprove.live", "key.fx.demo.maxsteps"].each { name -> def value = System.getProperty(name) if (value != null) { systemProperties[name] = value 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 index 5a446718697..0904ca71f9b 100644 --- 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 @@ -35,6 +35,7 @@ import javafx.scene.layout.VBox; import javafx.stage.Stage; +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.core.fx.KeYSelectionModel; @@ -54,12 +55,16 @@ import de.uka.ilkd.key.gui.fx.settings.ThemeSettingsProviderF; import de.uka.ilkd.key.gui.fx.theme.Theme; import de.uka.ilkd.key.gui.fx.theme.ThemeManager; +import de.uka.ilkd.key.proof.Proof; +import de.uka.ilkd.key.proof.ProofEvent; import de.uka.ilkd.key.settings.PathConfig; import de.uka.ilkd.key.settings.ProofIndependentSettings; import de.uka.ilkd.key.settings.ViewSettings; import de.uka.ilkd.key.util.KeYConstants; import de.uka.ilkd.key.util.KeYResourceManager; +import org.key_project.util.javafx.FxUtil; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -259,6 +264,9 @@ protected KeYEnvironment call() throws Exception { .notify("Tree structure verification: " + report, report.endsWith("PASS") ? Kind.INFO : Kind.ERROR); } + if (System.getProperty("key.fx.demo.autoprove.live") != null) { + startLiveAutoMode(env); + } }); loadTask.setOnFailed(event -> { Throwable error = loadTask.getException(); @@ -271,6 +279,54 @@ protected KeYEnvironment call() throws Exception { loader.start(); } + /** + * 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(() -> { + proofTreeView.refresh(); + 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(); + } + private void setWindowIcons() { Image icon = new Image(MainWindowF.class.getResourceAsStream(IMAGE_DIR + "key-color-icon-square.png")); diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/prooftree/ProofTreeViewF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/prooftree/ProofTreeViewF.java index e1d3c87814f..18161c047a7 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/prooftree/ProofTreeViewF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/prooftree/ProofTreeViewF.java @@ -5,10 +5,12 @@ package de.uka.ilkd.key.gui.fx.prooftree; import java.util.Collections; +import java.util.ConcurrentModificationException; import java.util.IdentityHashMap; import java.util.Iterator; import java.util.Objects; import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; import javafx.scene.control.Tooltip; import javafx.scene.control.TreeCell; import javafx.scene.control.TreeItem; @@ -20,9 +22,14 @@ 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.ProofTreeEvent; +import de.uka.ilkd.key.proof.ProofTreeListener; import org.key_project.util.javafx.FxUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + /** * First JavaFX version of the proof tree view, the counter-part of * {@code de.uka.ilkd.key.gui.prooftree.ProofTreeView} (a Swing {@code JTree} driven by @@ -38,11 +45,13 @@ *

* Deliberately deferred to later M2/M3 chunks: search, filters (ProofTreeViewFilter), heatmap, * notes/tooltips of the rich Swing renderer, one-step-simplifier protocol children, lazy - * population for very large proofs (currently built eagerly), and live updates on rule - * applications. + * population for very large proofs (currently built eagerly), and incremental per-event tree + * model updates (live updates currently coalesce into a full rebuild). */ public class ProofTreeViewF extends TreeView { + private static final Logger LOGGER = LoggerFactory.getLogger(ProofTreeViewF.class); + /** * One tree entry: either a proof node entry (a rule application) or a branch entry (labeled * sub-tree; {@code node} is the root of the branch). @@ -88,6 +97,64 @@ public String displayText() { private Proof proof; private boolean updatingSelection; + /** + * Coalescing flag for live updates: structural proof events (which can arrive in bursts from + * the prover thread) schedule at most one refresh. + */ + private volatile boolean refreshScheduled; + + /** Number of proof tree events observed since the current proof was set (verification). */ + private final AtomicInteger liveEventCount = new AtomicInteger(); + + /** Number of coalesced refreshes executed for live events (verification). */ + private final AtomicInteger liveRefreshCount = new AtomicInteger(); + + /** + * Listens to structural changes of the displayed proof and schedules a coalesced rebuild on + * the FX thread — the analogue of the Swing view's {@code GUIProofTreeModel} update calls. + */ + private final ProofTreeListener proofTreeListener = new ProofTreeListener() { + @Override + public void proofExpanded(ProofTreeEvent e) { + handleProofTreeEvent(); + } + + @Override + public void proofPruned(ProofTreeEvent e) { + handleProofTreeEvent(); + } + + @Override + public void proofStructureChanged(ProofTreeEvent e) { + handleProofTreeEvent(); + } + + @Override + public void proofClosed(ProofTreeEvent e) { + handleProofTreeEvent(); + } + + @Override + public void proofGoalRemoved(ProofTreeEvent e) { + handleProofTreeEvent(); + } + + @Override + public void proofGoalsAdded(ProofTreeEvent e) { + handleProofTreeEvent(); + } + + @Override + public void proofGoalsChanged(ProofTreeEvent e) { + handleProofTreeEvent(); + } + + @Override + public void notesChanged(ProofTreeEvent e) { + handleProofTreeEvent(); + } + }; + private final KeYSelectionListener selectionListener = new KeYSelectionListener() { @Override public void selectedNodeChanged(KeYSelectionEvent event) { @@ -140,7 +207,18 @@ public void setProof(Proof newProof) { FxUtil.runLater(() -> setProof(newProof)); return; } - proof = newProof; + Proof oldProof = this.proof; + this.proof = newProof; + if (oldProof != newProof) { + if (oldProof != null) { + oldProof.removeProofTreeListener(proofTreeListener); + } + if (newProof != null) { + newProof.addProofTreeListener(proofTreeListener); + } + liveEventCount.set(0); + liveRefreshCount.set(0); + } updatingSelection = true; try { if (proof == null) { @@ -156,10 +234,80 @@ public void setProof(Proof newProof) { } /** - * Rebuilds the tree from the current proof (used when rule applications changed it). + * Rebuilds the tree from the current proof, preserving the expansion state of the branches + * and the selection. Used when rule applications changed the proof (live updates) and by the + * self tests. */ public void refresh() { + if (proof == null) { + setProof(null); + return; + } + Set expandedBranches = Collections.newSetFromMap(new IdentityHashMap<>()); + collectExpandedBranches(getRoot(), expandedBranches); setProof(proof); + if (getRoot() != null) { + applyExpandedBranches(getRoot(), expandedBranches); + revealSelectedNode(); + } + } + + /** + * Coalesces structural proof events into at most one scheduled rebuild: events arrive in + * bursts from the prover thread and a full rebuild of the current proof state covers all of + * them. + */ + private void handleProofTreeEvent() { + liveEventCount.incrementAndGet(); + if (refreshScheduled) { + return; + } + refreshScheduled = true; + FxUtil.runLater(() -> { + refreshScheduled = false; + if (proof == null) { + return; + } + liveRefreshCount.incrementAndGet(); + try { + refresh(); + } catch (ConcurrentModificationException | IndexOutOfBoundsException e) { + // the proof was mutated concurrently while the tree was rebuilt; the + // corresponding structural event schedules another coalesced refresh + LOGGER.debug("Proof tree refresh raced with proof mutation, retrying", e); + handleProofTreeEvent(); + } + }); + } + + /** + * @return a one-line report about the observed live proof tree events and the executed + * coalesced refreshes (for the M2 verification) + */ + public String getLiveUpdateReport() { + return "liveEvents=" + liveEventCount.get() + " liveRefreshes=" + liveRefreshCount.get(); + } + + private static void collectExpandedBranches(TreeItem item, Set into) { + if (item == null) { + return; + } + if (item.isExpanded() && item.getValue() != null && item.getValue().isBranch()) { + into.add(item.getValue().node()); + } + for (TreeItem child : item.getChildren()) { + collectExpandedBranches(child, into); + } + } + + private static void applyExpandedBranches(TreeItem item, Set expandedBranches) { + if (item.getValue() != null && item.getValue().isBranch() + && expandedBranches.contains(item.getValue().node())) { + item.setExpanded(true); + } + for (TreeItem child : item.getChildren()) { + applyExpandedBranches(child, expandedBranches); + } } /** @@ -215,8 +363,12 @@ private TreeItem buildBranch(Node branchRoot, String label) { } break; } - // at a branch point (or a leaf): one branch item per child - for (Node child : current.children()) { + // at a branch point (or a leaf): one branch item per child. Read by index instead of + // iterating the live children list: the prover thread may add children concurrently + // (ArrayList view); a concurrent mutation is healed by the next structural event. + int branchCount = current.childrenCount(); + for (int i = 0; i < branchCount; i++) { + Node child = current.child(i); branchItem.getChildren().add(buildBranch(child, ensureBranchLabelIsSet(child))); } return branchItem; From 2e4fea13179a24eac5f0e9a38223e3227766f1b1 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Wed, 7 Oct 2026 14:52:03 +0200 Subject: [PATCH 012/281] key.ui.fx: integrate the info view into the main window MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wires InfoViewF (from the parallel view task) into the dockable layout: the Info dockable now shows the real view (proof name, file, goal/node/ branch counts, closed status) instead of the M1 placeholder, attached to the selection model. Integrator fix in InfoViewF: the GridPane gets column constraints — the label column keeps its preferred width (labels were ellipsized to '…' in the narrow left dock by the long proof file path) and the value column grows and wraps. Verified on the virtual display :99: Info tab shows Proof File (wrapped path), Open Goals 0, Closed Goals 3, Nodes 149, Branches 2, Status 'closed' (green) for the auto-closed Agatha proof. --- .../java/de/uka/ilkd/key/gui/fx/MainWindowF.java | 10 +++++++++- .../de/uka/ilkd/key/gui/fx/infoview/InfoViewF.java | 14 ++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) 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 index 0904ca71f9b..6fa18156fe4 100644 --- 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 @@ -46,6 +46,7 @@ import de.uka.ilkd.key.gui.fx.docking.Dockable; import de.uka.ilkd.key.gui.fx.docking.SimpleDockable; import de.uka.ilkd.key.gui.fx.fonticons.IconFactoryF; +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.nodeviews.SequentViewF; import de.uka.ilkd.key.gui.fx.notification.NotificationManagerF; @@ -121,6 +122,12 @@ public final class MainWindowF { */ private final ProofTreeViewF proofTreeView = new ProofTreeViewF(); + /** + * 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(); + /** * Creates the main window bound to the given stage. * @@ -202,6 +209,7 @@ public KeYSelectionModel getSelectionModel() { private void wireSequentView() { sequentView.attach(selectionModel); proofTreeView.attach(selectionModel); + infoView.attach(selectionModel); sequentView.setOnPosSelected(pos -> { if (pos == null) { statusRight.setText(""); @@ -344,7 +352,7 @@ private void buildDockables() { registerDockable(ID_GOAL_LIST, "Goal List"); dockables.put(ID_PROOF_TREE, new SimpleDockable(ID_PROOF_TREE, "Proof Tree", proofTreeView)); - registerDockable(ID_INFO_VIEW, "Info"); + dockables.put(ID_INFO_VIEW, new SimpleDockable(ID_INFO_VIEW, "Info", infoView)); registerDockable(ID_STRATEGY, "Strategy"); dockables.put(ID_SEQUENT, new SimpleDockable(ID_SEQUENT, "Sequent", sequentView)); registerDockable(ID_SOURCE_VIEW, "Source"); diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/infoview/InfoViewF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/infoview/InfoViewF.java index 7a95a9d044e..cddea22cea0 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/infoview/InfoViewF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/infoview/InfoViewF.java @@ -10,9 +10,13 @@ import java.util.Objects; import java.util.stream.Collectors; import javafx.geometry.Insets; +import javafx.geometry.Pos; import javafx.scene.control.Label; import javafx.scene.control.ScrollPane; +import javafx.scene.layout.ColumnConstraints; import javafx.scene.layout.GridPane; +import javafx.scene.layout.Priority; +import javafx.scene.layout.Region; import javafx.scene.layout.VBox; import de.uka.ilkd.key.core.fx.KeYSelectionEvent; @@ -109,6 +113,13 @@ public InfoViewF() { grid.getStyleClass().add("info-view-grid"); grid.setHgap(10); grid.setVgap(4); + // the label column keeps its preferred width (it must not be truncated by a long value, + // e.g. the proof file path); the value column takes the remaining space and wraps + ColumnConstraints labelColumn = new ColumnConstraints(); + labelColumn.setMinWidth(Region.USE_PREF_SIZE); + ColumnConstraints valueColumn = new ColumnConstraints(); + valueColumn.setHgrow(Priority.ALWAYS); + grid.getColumnConstraints().addAll(labelColumn, valueColumn); fileValue = addRow("Proof File"); openGoalsValue = addRow("Open Goals"); @@ -135,6 +146,9 @@ private Label addRow(String key) { label.getStyleClass().add("info-row-label"); Label value = new Label(); value.getStyleClass().add("info-row-value"); + value.setWrapText(true); + value.setAlignment(Pos.TOP_LEFT); + value.getStyleClass().add("info-row-value"); int rowIndex = grid.getRowCount(); grid.add(label, 0, rowIndex); grid.add(value, 1, rowIndex); From eed762fd83d3c0928cbea99267e1b8ae8108c08f Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Wed, 7 Oct 2026 14:57:43 +0200 Subject: [PATCH 013/281] key.ui.fx: KeYMediatorF skeleton (M2d) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The mediator (counter-part of the Swing KeYMediator) owns the selection model — it implements the model's ProofBinder seam, so setSelectedProof now routes through the real binding path: proof listener swap (ProofTreeListener + RuleAppListener), abbreviation rebind into the shared NotationInfo, OneStepSimplifier refresh. Ports the Swing mediator's essential listeners: proofGoalsAdded(empty) counts closed goals, proofPruned repairs the selection (FX-marshalled), proofClosed logs (notification events follow in M3), ruleApplied re-defaults the selection after interactive applications (outside auto mode), auto mode state tracking via attach(ProofControl). MainWindowF now obtains the selection model from the mediator; the demo load attaches the mediator to the environment's proof control. Verified on :99: binding null -> project.key, mediator receives the re-delivered proofClosed after the auto mode run, tree verification 149/149 PASS. --- .../de/uka/ilkd/key/core/fx/KeYMediatorF.java | 251 ++++++++++++++++++ .../de/uka/ilkd/key/gui/fx/MainWindowF.java | 24 +- 2 files changed, 266 insertions(+), 9 deletions(-) create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/core/fx/KeYMediatorF.java 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..86d1f0b12b9 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/core/fx/KeYMediatorF.java @@ -0,0 +1,251 @@ +/* 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.control.AutoModeListener; +import de.uka.ilkd.key.control.ProofControl; +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.rule.OneStepSimplifier; + +import org.key_project.util.javafx.FxUtil; + +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), the auto saver, user + * action listeners and the task/progress plumbing of 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; + + /** + * 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); + } + OneStepSimplifier.refreshOSS(newProof); + } + + /** + * @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(); + } + + /** + * @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; + } + + /** + * 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(); + } + + @Override + public void autoModeStopped(ProofEvent e) { + inAutoMode = false; + } + } +} 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 index 6fa18156fe4..7e256db5e65 100644 --- 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 @@ -38,6 +38,7 @@ 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.core.fx.KeYMediatorF; import de.uka.ilkd.key.core.fx.KeYSelectionModel; import de.uka.ilkd.key.gui.fx.configuration.ConfigF; import de.uka.ilkd.key.gui.fx.docking.DockLayoutStore; @@ -108,13 +109,16 @@ public final class MainWindowF { private final SequentViewF sequentView = new SequentViewF(); /** - * The selection model of the window, copy of the Swing mediator's model. Until the - * {@code KeYMediatorF} exists (M2c) the proof binding is a no-op. + * 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 KeYSelectionModel selectionModel = new KeYSelectionModel((newProof, - previousProof) -> LOGGER.debug("Selected proof changed: {} -> {}", - previousProof == null ? null : previousProof.name(), - newProof == null ? null : newProof.name())); + private final KeYMediatorF mediator = new KeYMediatorF(); + + /** + * The selection model of the window, owned by the mediator. + */ + private final KeYSelectionModel selectionModel = mediator.getSelectionModel(); /** * The proof tree view (first M2 version): displays the proof of the selection model, node @@ -246,9 +250,11 @@ protected KeYEnvironment call() throws Exception { }; loadTask.setOnSucceeded(event -> { KeYEnvironment env = loadTask.getValue(); - // route the proof through the selection model (as the mediator will do in M2c): - // fires selectedProofChanged + selects the first open goal or a leaf, which the - // sequent view observes and displays. + // the mediator observes the proof control (auto mode state, closed-goal counter) + mediator.attach(env.getProofControl()); + // 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()); String show = System.getProperty("key.fx.show", ID_SEQUENT); String target = ID_PROOF_TREE.equalsIgnoreCase(show) ? ID_PROOF_TREE : ID_SEQUENT; From 12d4b128cf8c1640ff72463d535a8efd5a800690 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Wed, 7 Oct 2026 14:59:40 +0200 Subject: [PATCH 014/281] =?UTF-8?q?key.ui.fx:=20M2=20strategy=20selection?= =?UTF-8?q?=20view=20=E2=80=94=20settings-definition-driven=20JavaFX=20con?= =?UTF-8?q?trol=20panel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ports key.ui's Swing StrategySelectionView to the JavaFX rewrite: - StrategySelectionViewF: builds radio groups per OneOfStrategyPropertyDefinition from JavaProfile.getDefault().getSettingsDefinition(), plus an editable Spinner for max rule applications (1..10,000,000, step 100). - Write-through mirrors the Swing view: radio/spinner changes overlay onto the proof's StrategyProperties and are written to both ProofSettings.DEFAULT_SETTINGS and the per-proof StrategySettings, then re-activate the strategy via the profile's supportedStrategies() name match with default-factory fallback. - StrategySelectionDriver: standalone FxDriver that loads a demo proof, refreshes on selection changes, runs a self-test (verifyStrategyView) and logs PASS/FAIL plus observable write-through events. - New .strategy-* CSS classes appended to key-light.css and key-dark.css. --- .../fx/strategy/StrategySelectionDriver.java | 154 +++++ .../fx/strategy/StrategySelectionViewF.java | 548 ++++++++++++++++++ .../de/uka/ilkd/key/gui/fx/theme/key-dark.css | 37 ++ .../uka/ilkd/key/gui/fx/theme/key-light.css | 37 ++ 4 files changed, 776 insertions(+) create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/strategy/StrategySelectionDriver.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/strategy/StrategySelectionViewF.java diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/strategy/StrategySelectionDriver.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/strategy/StrategySelectionDriver.java new file mode 100644 index 00000000000..4176e810691 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/strategy/StrategySelectionDriver.java @@ -0,0 +1,154 @@ +/* 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.strategy; + +import java.beans.PropertyChangeEvent; +import java.nio.file.Path; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.geometry.Rectangle2D; +import javafx.scene.Scene; +import javafx.scene.control.Label; +import javafx.scene.control.Separator; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.VBox; +import javafx.stage.Screen; +import javafx.stage.Stage; + +import de.uka.ilkd.key.control.DefaultUserInterfaceControl; +import de.uka.ilkd.key.control.KeYEnvironment; +import de.uka.ilkd.key.core.fx.KeYSelectionModel; +import de.uka.ilkd.key.gui.fx.FxDriver; +import de.uka.ilkd.key.gui.fx.theme.ThemeManager; +import de.uka.ilkd.key.proof.Proof; +import de.uka.ilkd.key.settings.StrategySettings; + +import org.key_project.util.javafx.FxUtil; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Standalone development driver for the {@link StrategySelectionViewF}: shows the strategy view + * together with a status label, backed by an own {@link KeYSelectionModel} without a mediator + * behind it. The demo proof named by the system property {@code key.fx.demo.sequent} is loaded on + * a background thread via the core {@link KeYEnvironment} and selected in the model, so the view + * displays and writes through the settings of a live proof. After the proof is displayed the + * driver runs the view's self test {@code verifyStrategyView()} and logs its report. + */ +public final class StrategySelectionDriver implements FxDriver { + + private static final Logger LOGGER = LoggerFactory.getLogger(StrategySelectionDriver.class); + + private final KeYSelectionModel selectionModel = + new KeYSelectionModel((newProof, previousProof) -> { + // no-op: the driver runs without a mediator behind its selection model + }); + private final StrategySelectionViewF view = new StrategySelectionViewF(); + private final Label status = new Label("Loading demo proof ..."); + private final Label settingsMirror = new Label(); + + @Override + public void start(Stage stage) throws Exception { + stage.setTitle("KeY · Strategy"); + + status.getStyleClass().add("strategy-status"); + status.setAlignment(Pos.CENTER_LEFT); + settingsMirror.getStyleClass().add("strategy-status"); + settingsMirror.setAlignment(Pos.CENTER_LEFT); + VBox bottom = new VBox(4, new Separator(), status, settingsMirror); + bottom.setPadding(new Insets(2, 8, 4, 8)); + + BorderPane root = new BorderPane(); + root.setCenter(view); + root.setBottom(bottom); + + Scene scene = new Scene(root, 900, 760); + ThemeManager.getInstance().manage(scene); + stage.setScene(scene); + stage.show(); + // Without a window manager (headless Xvnc verification) the platform places the stage + // off-screen; center it on the primary screen after the platform placement. + Rectangle2D bounds = Screen.getPrimary().getVisualBounds(); + stage.setX(bounds.getMinX() + Math.max(0, (bounds.getWidth() - scene.getWidth()) / 2)); + stage.setY(bounds.getMinY() + Math.max(0, (bounds.getHeight() - scene.getHeight()) / 2)); + + view.attach(selectionModel); + + String demoPath = System.getProperty("key.fx.demo.sequent"); + if (demoPath == null || demoPath.isBlank()) { + status.setText("No demo proof given; pass -Dkey.fx.demo.sequent="); + LOGGER.warn("No demo proof given (system property key.fx.demo.sequent is unset)"); + return; + } + Thread loader = + new Thread(() -> loadDemoProof(Path.of(demoPath)), "strategy-driver-loader"); + loader.setDaemon(true); + loader.start(); + } + + /** + * Loads the demo proof on the calling (background) thread and selects it in the model, then + * runs the view's self test. + * + * @param path the location of the demo problem + */ + private void loadDemoProof(Path path) { + try { + KeYEnvironment env = KeYEnvironment.load(path); + Proof proof = env.getLoadedProof(); + FxUtil.runLater(() -> { + selectionModel.setSelectedProof(proof); + status.setText("Proof: " + proof.name() + " · " + env.getServices().getProfile() + .displayName()); + observeStrategySettings(proof); + LOGGER.info("Strategy self test: {}", view.verifyStrategyView()); + }); + } catch (Exception e) { + LOGGER.error("Failed to load demo proof {}", path, e); + FxUtil.runLater(() -> status.setText("Failed to load demo proof: " + e)); + } + } + + /** + * Mirrors the strategy settings of the given proof into the second status line and the log, + * whenever a setting changes. This makes the write-through of the view observable from the + * outside (the mirror is updated on every user edit of the widgets). + * + * @param proof the proof whose strategy settings are observed + */ + private void observeStrategySettings(Proof proof) { + StrategySettings settings = proof.getSettings().getStrategySettings(); + settings.addPropertyChangeListener(this::logStrategySetting); + updateSettingsMirror(); + } + + /** + * Logs a strategy settings change and refreshes the settings mirror line. + * + * @param event the property change event of {@link StrategySettings} + */ + private void logStrategySetting(PropertyChangeEvent event) { + FxUtil.runLater(() -> { + LOGGER.info("Strategy setting changed: {} = {} (was {})", event.getPropertyName(), + event.getNewValue(), event.getOldValue()); + updateSettingsMirror(); + }); + } + + /** + * Refreshes the settings mirror line from the currently selected proof. + */ + private void updateSettingsMirror() { + Proof proof = selectionModel.getSelectedProof(); + if (proof == null) { + settingsMirror.setText(""); + return; + } + StrategySettings settings = proof.getSettings().getStrategySettings(); + settingsMirror.setText("Settings: maxSteps=" + settings.getMaxSteps() + " · strategy=" + + settings.getStrategy() + " · timeout=" + settings.getTimeout() + " ms"); + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/strategy/StrategySelectionViewF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/strategy/StrategySelectionViewF.java new file mode 100644 index 00000000000..7c11ddf89ee --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/strategy/StrategySelectionViewF.java @@ -0,0 +1,548 @@ +/* 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.strategy; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.control.Label; +import javafx.scene.control.RadioButton; +import javafx.scene.control.ScrollPane; +import javafx.scene.control.Spinner; +import javafx.scene.control.Toggle; +import javafx.scene.control.ToggleGroup; +import javafx.scene.control.Tooltip; +import javafx.scene.layout.FlowPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Pane; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; + +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.Goal; +import de.uka.ilkd.key.proof.Proof; +import de.uka.ilkd.key.proof.init.JavaProfile; +import de.uka.ilkd.key.proof.init.Profile; +import de.uka.ilkd.key.settings.ProofSettings; +import de.uka.ilkd.key.settings.StrategySettings; +import de.uka.ilkd.key.strategy.Strategy; +import de.uka.ilkd.key.strategy.StrategyFactory; +import de.uka.ilkd.key.strategy.StrategyProperties; +import de.uka.ilkd.key.strategy.definition.AbstractStrategyPropertyDefinition; +import de.uka.ilkd.key.strategy.definition.OneOfStrategyPropertyDefinition; +import de.uka.ilkd.key.strategy.definition.StrategyPropertyValueDefinition; +import de.uka.ilkd.key.strategy.definition.StrategySettingsDefinition; + +import org.key_project.util.javafx.FxUtil; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * First JavaFX version of the strategy selection view, the counter-part of + * {@code de.uka.ilkd.key.gui.StrategySelectionView} (a Swing {@code JPanel}) in the module + * {@code key.ui}. + *

+ * Milestone M2, first version. Like the original, the widgets are generated from the + * {@link StrategySettingsDefinition} of {@link JavaProfile#getDefault()}: every + * {@link OneOfStrategyPropertyDefinition} becomes a {@link ToggleGroup} of {@link RadioButton}s + * (the Swing view renders button groups, not a list of strategy factories), and the maximum + * number of rule applications ({@code MaxRuleAppSlider} in the original) is edited by an + * editable {@link Spinner} writing through to {@link StrategySettings#getMaxSteps()}. + *

+ * All changes write through to the live settings exactly as the Swing view does: the values + * shown by the widgets are overlaid on the active {@link StrategyProperties} of the selected + * proof and stored both in the global {@code ProofSettings.DEFAULT_SETTINGS} and in the proof's + * own settings, and the active strategy of the proof is recreated from the + * {@link StrategyFactory} matching the proof's active strategy name (falling back to the + * profile's default factory), mirroring the original's {@code updateStrategySettings} and + * {@code KeYMediator#setMaxAutomaticSteps(int)}. The maximum steps are written to both settings + * objects, like the mediator does. + *

+ * The view observes a {@link KeYSelectionModel} ({@link #attach(KeYSelectionModel)}) and re-reads + * the settings of the selected proof on every proof change ({@code refresh} of the original). + * A programmatic-update guard ({@code updating}) prevents feedback loops while the widgets are + * refreshed from the settings. + *

+ * Style classes (styled in {@code key-light.css}/{@code key-dark.css}): + * {@code .strategy-view} (this scroll pane), {@code .strategy-content} (inner VBox), + * {@code .strategy-maxsteps-row}, {@code .strategy-maxsteps-label}, + * {@code .strategy-maxsteps-spinner}, {@code .strategy-section-title}, + * {@code .strategy-property-label}, {@code .strategy-property-values} (FlowPane of the buttons), + * {@code .strategy-radio} (per value button), {@code .strategy-subproperty-row} and + * {@code .strategy-status} (driver status label, not part of this view). + *

+ * Deliberately deferred: the strategy preset combo box (built-in and user-defined presets with + * save/stash/rename), the parallel-prover merge lock, the auto-prove "go" button, keyboard + * shortcuts and the timeout setting (not shown by the Swing view either). + */ +public class StrategySelectionViewF extends ScrollPane { + + private static final Logger LOGGER = LoggerFactory.getLogger(StrategySelectionViewF.class); + + /** + * Upper bound of the max-steps spinner. The Swing slider spans 1 to 9,000,000 in log steps; a + * linear editable spinner covers the same range (the default is 10,000). + */ + private static final int MAX_STEPS_BOUND = 10_000_000; + + /** + * The max-steps value written through by {@link #verifyStrategyView()} (any in-bounds value + * unlikely to equal the live settings). + */ + private static final int VERIFY_MAX_STEPS = 4321; + + /** + * The always used {@link StrategyFactory} of the fixed widget definition, mirroring + * {@code StrategySelectionView.FACTORY}. + */ + private static final StrategyFactory FACTORY = JavaProfile.getDefault(); + + /** + * The {@link StrategySettingsDefinition} of {@link #FACTORY} which defines the UI controls to + * show, mirroring {@code StrategySelectionView.DEFINITION}. + */ + private static final StrategySettingsDefinition DEFINITION = FACTORY.getSettingsDefinition(); + + private final VBox content = new VBox(); + + /** + * Edits {@link StrategySettings#getMaxSteps()} ("Max. Rule Applications"), the + * {@code MaxRuleAppSlider} of the Swing view. + */ + private final Spinner maxStepsSpinner = new Spinner<>(1, MAX_STEPS_BOUND, 10_000, 100); + + /** + * Maps a strategy property key to the {@link ToggleGroup} which defines the value, mirroring + * {@code StrategySelectionComponents.propertyGroups}. + */ + private final Map propertyGroups = new LinkedHashMap<>(); + + /** + * Maps a strategy property key to the {@link RadioButton}s which define the values, mirroring + * {@code StrategySelectionComponents.propertyButtons}. The user data of each button is the + * API value ({@code StrategyPropertyValueDefinition.getApiValue()}). + */ + private final Map> propertyButtons = new LinkedHashMap<>(); + + private KeYSelectionModel selectionModel; + private Proof proof; + + /** + * Set while the widgets are updated programmatically from the settings ({@link #refresh}). + * Guards the widget listeners so that they do not write the freshly read values back and + * loop. + */ + private boolean updating; + + private final KeYSelectionListener selectionListener = new KeYSelectionListener() { + @Override + public void selectedProofChanged(KeYSelectionEvent event) { + refresh(event.getSource().getSelectedProof()); + } + }; + + /** + * Creates an empty strategy view; all controls are disabled until a proof is attached. + */ + public StrategySelectionViewF() { + getStyleClass().add("strategy-view"); + setFitToWidth(true); + content.getStyleClass().add("strategy-content"); + content.setPadding(new Insets(8)); + content.setSpacing(6); + setContent(content); + + // "Max. Rule Applications": label + spinner, the MaxRuleAppSlider of the Swing view + Label maxStepsLabel = new Label(DEFINITION.getMaxRuleApplicationsLabel()); + maxStepsLabel.getStyleClass().add("strategy-maxsteps-label"); + maxStepsSpinner.getStyleClass().add("strategy-maxsteps-spinner"); + maxStepsSpinner.setEditable(true); + maxStepsSpinner.valueProperty() + .addListener((obs, oldValue, newValue) -> handleMaxStepsChange()); + HBox maxStepsRow = new HBox(8, maxStepsLabel, maxStepsSpinner); + maxStepsRow.getStyleClass().add("strategy-maxsteps-row"); + maxStepsRow.setAlignment(Pos.CENTER_LEFT); + content.getChildren().add(maxStepsRow); + + // Generated strategy property groups ("JavaDL Options") + if (!DEFINITION.getProperties().isEmpty()) { + Label title = new Label(DEFINITION.getPropertiesTitle()); + title.getStyleClass().add("strategy-section-title"); + content.getChildren().add(title); + for (AbstractStrategyPropertyDefinition definition : DEFINITION.getProperties()) { + addPropertyControls(content, definition, true); + } + } + + enableAll(false); + } + + /** + * Registers this view as a selection listener on the given model and shows the settings of + * the currently selected proof, if any. + * + * @param model the selection model to observe + */ + public void attach(KeYSelectionModel model) { + Objects.requireNonNull(model); + if (selectionModel == model) { + return; + } + if (selectionModel != null) { + selectionModel.removeKeYSelectionListener(selectionListener); + } + selectionModel = model; + model.addKeYSelectionListenerChecked(selectionListener); + refresh(model.getSelectedProof()); + } + + /** + * Shows the strategy settings of the given proof, the counter-part of + * {@code StrategySelectionView.refresh(Proof)}: the radio buttons reflect the proof's active + * strategy properties and the spinner its maximum steps. + * + * @param newProof the proof whose settings are displayed, may be {@code null} + */ + public void refresh(Proof newProof) { + if (!FxUtil.isFxThread()) { + FxUtil.runLater(() -> refresh(newProof)); + return; + } + proof = newProof; + if (proof == null) { + enableAll(false); + return; + } + updating = true; + try { + StrategySettings settings = proof.getSettings().getStrategySettings(); + StrategyProperties active = settings.getActiveStrategyProperties(); + for (Map.Entry> entry : propertyButtons.entrySet()) { + String value = active.getProperty(entry.getKey()); + for (RadioButton button : entry.getValue()) { + button.setSelected(Objects.equals(button.getUserData(), value)); + } + } + int steps = + Math.clamp(settings.getMaxSteps(), 1, MAX_STEPS_BOUND); + maxStepsSpinner.getValueFactory().setValue(steps); + enableAll(true); + } finally { + updating = false; + } + } + + /** + * @return the proof whose settings are displayed, or {@code null} if none is attached + */ + public Proof getProof() { + return proof; + } + + /** + * Enables or disables all controls, mirroring {@code StrategySelectionView.enableAll}. + * + * @param enable {@code true} to enable the controls + */ + private void enableAll(boolean enable) { + content.setDisable(!enable); + } + + /** + * Adds the UI controls of the given strategy property definition (a radio button group), the + * JavaFX counter-part of {@code StrategySelectionView.createStrategyProperty}. Sub-properties + * are rendered after their parent, indented and with the label inline, like the original. + * + * @param parent the container to append to + * @param definition the property definition to render + * @param topLevel whether the label is rendered on its own line above the buttons + */ + private void addPropertyControls(Pane parent, AbstractStrategyPropertyDefinition definition, + boolean topLevel) { + if (!(definition instanceof OneOfStrategyPropertyDefinition oneOf)) { + LOGGER.warn("Unsupported strategy property definition: {}", definition); + return; + } + Label label = new Label(oneOf.getName()); + label.getStyleClass().add("strategy-property-label"); + if (oneOf.getTooltip() != null) { + label.setTooltip(new Tooltip(oneOf.getTooltip())); + } + ToggleGroup group = new ToggleGroup(); + FlowPane values = new FlowPane(); + values.getStyleClass().add("strategy-property-values"); + values.setHgap(10); + values.setVgap(2); + if (!oneOf.getValues().isEmpty()) { + propertyGroups.put(oneOf.getApiKey(), group); + group.selectedToggleProperty().addListener((obs, oldToggle, newToggle) -> { + if (newToggle != null) { + handleStrategyChange(); + } + }); + for (StrategyPropertyValueDefinition valueDefinition : oneOf.getValues()) { + RadioButton button = new RadioButton(valueDefinition.getValue()); + button.getStyleClass().add("strategy-radio"); + button.setToggleGroup(group); + button.setUserData(valueDefinition.getApiValue()); + if (valueDefinition.getTooltip() != null) { + button.setTooltip(new Tooltip(valueDefinition.getTooltip())); + } + propertyButtons.computeIfAbsent(oneOf.getApiKey(), k -> new ArrayList<>()) + .add(button); + values.getChildren().add(button); + } + } + if (topLevel) { + parent.getChildren().add(label); + parent.getChildren().add(values); + } else { + HBox row = new HBox(8, label, values); + row.getStyleClass().add("strategy-subproperty-row"); + row.setPadding(new Insets(0, 0, 0, 16)); + row.setAlignment(Pos.CENTER_LEFT); + HBox.setHgrow(values, Priority.ALWAYS); + parent.getChildren().add(row); + } + for (AbstractStrategyPropertyDefinition subProperty : definition.getSubProperties()) { + addPropertyControls(parent, subProperty, false); + } + } + + /** + * Called when the user edits the max-steps spinner: writes the new value to the settings of + * the selected proof and to the global default settings, mirroring + * {@code KeYMediator.setMaxAutomaticSteps(int)}. + */ + private void handleMaxStepsChange() { + if (updating) { + return; + } + Integer steps = maxStepsSpinner.getValue(); + if (steps == null) { + return; + } + LOGGER.debug("Maximum rule applications changed to {}", steps); + if (proof != null) { + proof.getSettings().getStrategySettings().setMaxSteps(steps); + } + ProofSettings.DEFAULT_SETTINGS.getStrategySettings().setMaxSteps(steps); + } + + /** + * Called when the user selects a strategy property value: updates the strategy settings of + * the selected proof, mirroring the action listener of the Swing radio buttons. + */ + private void handleStrategyChange() { + if (updating || proof == null) { + return; + } + updateStrategySettings(); + } + + /** + * Writes the values shown by the widgets through to the live strategy settings: the displayed + * properties are overlaid on the proof's active properties and stored in the global default + * settings as well as in the proof's settings, and the active strategy is recreated from the + * matching {@link StrategyFactory} (mirroring {@code StrategySelectionView + * #updateStrategySettings(String, StrategyProperties)}). + */ + private void updateStrategySettings() { + StrategyProperties properties = currentStrategyProperties(); + Strategy strategy = + getStrategy(proof.getActiveStrategy().name().toString(), properties); + LOGGER.debug("Writing strategy settings through ({} properties)", properties.size()); + ProofSettings.DEFAULT_SETTINGS.getStrategySettings().setStrategy(strategy.name()); + ProofSettings.DEFAULT_SETTINGS.getStrategySettings() + .setActiveStrategyProperties(properties); + proof.getSettings().getStrategySettings().setStrategy(strategy.name()); + proof.getSettings().getStrategySettings().setActiveStrategyProperties(properties); + proof.setActiveStrategy(strategy); + } + + /** + * Captures the strategy properties currently reflected by the widgets: starts from the + * complete active properties of the proof and overlays the values shown by the radio groups, + * so that properties without a control keep their value (mirroring + * {@code StrategySelectionView.currentStrategyProperties}). + * + * @return the merged properties + */ + private StrategyProperties currentStrategyProperties() { + StrategyProperties base = + proof.getSettings().getStrategySettings().getActiveStrategyProperties(); + StrategyProperties displayed = getDisplayedProperties(); + for (String key : displayed.stringPropertyNames()) { + base.setProperty(key, displayed.getProperty(key)); + } + return base; + } + + /** + * Builds the strategy properties reflected by the radio groups; a group without a selection + * falls back to the default value of the settings definition, mirroring + * {@code StrategySelectionView.getProperties()}. + * + * @return the displayed properties + */ + private StrategyProperties getDisplayedProperties() { + StrategyProperties properties = new StrategyProperties(); + for (Map.Entry entry : propertyGroups.entrySet()) { + Toggle selected = entry.getValue().getSelectedToggle(); + if (selected != null) { + properties.setProperty(entry.getKey(), (String) selected.getUserData()); + } else { + properties.setProperty(entry.getKey(), + DEFINITION.getDefaultPropertiesFactory().createDefaultStrategyProperties() + .getProperty(entry.getKey())); + } + } + return properties; + } + + /** + * Creates the strategy of the given name from the given properties, using the supported + * {@link StrategyFactory}s of the selected proof's profile and falling back to the profile's + * default factory (mirroring {@code StrategySelectionView.getStrategy}). + * + * @param strategyName the name of the strategy to create + * @param properties the strategy properties to configure the strategy with + * @return the strategy instance + */ + private Strategy getStrategy(String strategyName, StrategyProperties properties) { + Profile profile = proof.getServices().getProfile(); + for (StrategyFactory factory : profile.supportedStrategies()) { + if (strategyName.equals(factory.name().toString())) { + return factory.create(proof, properties); + } + } + LOGGER.info("Selected Strategy '{}' not found falling back to {}", strategyName, + profile.getDefaultStrategyFactory().name()); + return profile.getDefaultStrategyFactory().create(proof, properties); + } + + /** + * Development self-test (M2): verifies that the widgets were generated completely from the + * settings definition (one radio group per one-of property, including sub-properties) and + * that edits write through to the live settings objects: the maximum steps are driven through + * the spinner widget and read back from both the proof's and the global settings, and one + * radio group is toggled through the widget and read back from the proof's strategy + * properties. The original values are restored afterwards. + * + * @return a one-line report, {@code "... PASS"} if everything is consistent + */ + public String verifyStrategyView() { + if (!FxUtil.isFxThread()) { + return FxUtil.callAndWait(this::verifyStrategyView); + } + if (proof == null) { + return "no proof"; + } + int expectedGroups = 0; + for (AbstractStrategyPropertyDefinition definition : DEFINITION.getProperties()) { + expectedGroups += countOneOfDefinitions(definition); + } + int renderedGroups = propertyGroups.size(); + int renderedRadios = propertyButtons.values().stream().mapToInt(List::size).sum(); + + // write-through check: max steps via the spinner widget + StrategySettings settings = proof.getSettings().getStrategySettings(); + int originalSteps = settings.getMaxSteps(); + int probeSteps = + originalSteps == VERIFY_MAX_STEPS ? VERIFY_MAX_STEPS + 1 : VERIFY_MAX_STEPS; + maxStepsSpinner.getValueFactory().setValue(probeSteps); + int writtenSteps = settings.getMaxSteps(); + int globalSteps = ProofSettings.DEFAULT_SETTINGS.getStrategySettings().getMaxSteps(); + maxStepsSpinner.getValueFactory().setValue(originalSteps); + int restoredSteps = settings.getMaxSteps(); + + // write-through check: one radio group via the widget + RadioProbe probe = findRadioProbe(); + String radioReport = "skipped"; + boolean radioOk = true; + if (probe != null) { + String before = settings.getActiveStrategyProperty(probe.key()); + probe.alternative().setSelected(true); + String after = settings.getActiveStrategyProperty(probe.key()); + probe.current().setSelected(true); + String restored = settings.getActiveStrategyProperty(probe.key()); + radioReport = probe.key() + ":" + before + "->" + after + "->" + restored; + radioOk = Objects.equals(after, probe.alternative().getUserData()) + && Objects.equals(restored, before); + } + + boolean pass = renderedGroups == expectedGroups && renderedRadios > 0 + && writtenSteps == probeSteps && globalSteps == probeSteps + && restoredSteps == originalSteps && radioOk; + return "groups=" + renderedGroups + "/" + expectedGroups + " radios=" + renderedRadios + + " maxSteps=" + originalSteps + "->" + probeSteps + "->" + writtenSteps + "->" + + restoredSteps + " radio[" + radioReport + "] " + (pass ? "PASS" : "FAIL"); + } + + /** + * Counts the given definition and its sub-definitions which are one-of property definitions + * with at least one value; the expected number of rendered radio groups (the Swing original + * skips empty definitions as well, see {@code createStrategyProperty}). + * + * @param definition the definition to count, together with its sub-properties + * @return the number of rendered one-of definitions + */ + private static int countOneOfDefinitions(AbstractStrategyPropertyDefinition definition) { + int count = 0; + if (definition instanceof OneOfStrategyPropertyDefinition oneOf + && !oneOf.getValues().isEmpty()) { + count = 1; + } + for (AbstractStrategyPropertyDefinition subProperty : definition.getSubProperties()) { + count += countOneOfDefinitions(subProperty); + } + return count; + } + + /** + * Finds a radio group suitable for the write-through probe: at least two buttons where the + * currently active property value of the proof matches one of the buttons (the one to + * restore). + * + * @return the probe, or {@code null} if no group qualifies + */ + private RadioProbe findRadioProbe() { + StrategySettings settings = proof.getSettings().getStrategySettings(); + for (Map.Entry> entry : propertyButtons.entrySet()) { + if (entry.getValue().size() < 2) { + continue; + } + String value = settings.getActiveStrategyProperty(entry.getKey()); + RadioButton current = null; + RadioButton alternative = null; + for (RadioButton button : entry.getValue()) { + if (Objects.equals(button.getUserData(), value)) { + current = button; + } else if (alternative == null) { + alternative = button; + } + } + if (current != null && alternative != null) { + return new RadioProbe(entry.getKey(), current, alternative); + } + } + return null; + } + + /** + * A radio group probe of {@link #verifyStrategyView()}: the group key plus the button holding + * the currently active value (restored after the probe) and another button (written through + * during the probe). + */ + private record RadioProbe(String key, RadioButton current, RadioButton alternative) { + } +} diff --git a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css index d2a6b80f0ff..15a72548b17 100644 --- a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css +++ b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css @@ -134,3 +134,40 @@ .proof-tree-interactive { -fx-text-fill: -key-interactive; } + +/* strategy selection view */ +.strategy-view { + -fx-background-color: -key-surface; +} + +.strategy-content { + -fx-background-color: -key-surface; +} + +.strategy-maxsteps-row { + -fx-padding: 0 0 4 0; +} + +.strategy-maxsteps-label, +.strategy-property-label { + -fx-text-fill: -key-text; +} + +.strategy-section-title { + -fx-text-fill: -key-text; + -fx-font-weight: bold; + -fx-padding: 4 0 2 0; +} + +.strategy-radio { + -fx-text-fill: -key-text; +} + +.strategy-radio:selected { + -fx-text-fill: -key-accent; + -fx-font-weight: bold; +} + +.strategy-status { + -fx-text-fill: -key-text-secondary; +} diff --git a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css index 40f53d1c00a..9562b50d475 100644 --- a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css +++ b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css @@ -130,3 +130,40 @@ .proof-tree-interactive { -fx-text-fill: -key-interactive; } + +/* strategy selection view */ +.strategy-view { + -fx-background-color: -key-surface; +} + +.strategy-content { + -fx-background-color: -key-surface; +} + +.strategy-maxsteps-row { + -fx-padding: 0 0 4 0; +} + +.strategy-maxsteps-label, +.strategy-property-label { + -fx-text-fill: -key-text; +} + +.strategy-section-title { + -fx-text-fill: -key-text; + -fx-font-weight: bold; + -fx-padding: 4 0 2 0; +} + +.strategy-radio { + -fx-text-fill: -key-text; +} + +.strategy-radio:selected { + -fx-text-fill: -key-accent; + -fx-font-weight: bold; +} + +.strategy-status { + -fx-text-fill: -key-text-secondary; +} From 022a9eb4a278c4fee005d848bf9cecf734a0f657 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Wed, 7 Oct 2026 14:59:43 +0200 Subject: [PATCH 015/281] =?UTF-8?q?key.ui.fx:=20M2=20goal=20list=20view=20?= =?UTF-8?q?=E2=80=94=20JavaFX=20ListView=20over=20the=20open=20goals?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Counter-part of the Swing de.uka.ilkd.key.gui.GoalList (JList), ported to the essentials: - GoalListViewF (ListView): rebuilds on selectedProofChanged and on selectedNodeChanged only when the displayed goal set really differs (identity/size guard so a click-driven selection does not wipe the list under the cursor); click selects the goal in the KeYSelectionModel; the ListView selection is kept in sync with the model's selected goal. - Cells mirror the Swing IconCellRenderer: node serial (#n), automatic/interactive/linked marker (text analogue of the key-hole icons), and a one-line sequent printed by the core pretty printer (SequentViewLogicPrinter, no term labels, max 100 chars like MAX_DISPLAYED_SEQUENT_LENGTH), cached per goal node. - GoalListDriver (FxDriver): own KeYSelectionModel, loads the demo proof (key.fx.demo.sequent) on a background thread, optional limited auto mode (key.fx.goallist.autoprove) and root selection (key.fx.goallist.select.root) for interactive verification; status bar + log lines report selection changes; runs verifyGoalList() self-test (proofNodes/openGoals/rendered/ selectedInSync PASS). - CSS section 'goal list view' appended to key-light.css/key-dark.css (.goal-list, .goal-list-cell(:selected), .goal-list-label, .goal-list-marker, .goal-list-automatic/interactive/linked, .goal-list-sequent, .goal-list-empty, .goal-list-header). Verified on Xvnc :91 with the Agatha demo: rendered count equals proof.openGoals().size(), XTest click on a goal row fires setSelectedGoal (log/status evidence), theme honoured via ThemeManager.manage(scene). --- .../key/gui/fx/goallist/GoalListDriver.java | 189 ++++++++ .../key/gui/fx/goallist/GoalListViewF.java | 416 ++++++++++++++++++ .../de/uka/ilkd/key/gui/fx/theme/key-dark.css | 55 +++ .../uka/ilkd/key/gui/fx/theme/key-light.css | 55 +++ 4 files changed, 715 insertions(+) create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/goallist/GoalListDriver.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/goallist/GoalListViewF.java diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/goallist/GoalListDriver.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/goallist/GoalListDriver.java new file mode 100644 index 00000000000..79923f350b7 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/goallist/GoalListDriver.java @@ -0,0 +1,189 @@ +/* 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.goallist; + +import java.nio.file.Path; +import javafx.concurrent.Task; +import javafx.geometry.Insets; +import javafx.scene.Scene; +import javafx.scene.control.Label; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.HBox; +import javafx.stage.Stage; + +import de.uka.ilkd.key.control.DefaultUserInterfaceControl; +import de.uka.ilkd.key.control.KeYEnvironment; +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.FxDriver; +import de.uka.ilkd.key.gui.fx.theme.ThemeManager; +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.settings.StrategySettings; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Standalone {@link FxDriver} for the {@link GoalListViewF} (milestone M2): shows the goal list + * with its own {@link KeYSelectionModel} and loads the demo proof named by the system property + * {@code key.fx.demo.sequent} (a {@code .key} file) on a background thread via the core + * {@link KeYEnvironment}, mirroring how the future mediator will route a loaded proof through the + * selection model. If the system property {@code key.fx.goallist.autoprove} is set to a number, + * that many automatic rule applications are run first (so the proof contains several nodes; + * combined with {@code key.fx.goallist.select.root} the root becomes a closed inner node and a + * click on the goal row performs a real selection change). The self-test + * {@link GoalListViewF#verifyGoalList()} is run after the proof is displayed and reported on the + * console. + */ +public final class GoalListDriver implements FxDriver { + + private static final Logger LOGGER = LoggerFactory.getLogger(GoalListDriver.class); + + @Override + public void start(Stage stage) throws Exception { + GoalListViewF view = new GoalListViewF(); + KeYSelectionModel model = new KeYSelectionModel((newProof, previousProof) -> { + // no-op: a standalone driver has no mediator to bind the proof to + }); + view.attach(model); + + Label header = new Label("Proof: none"); + header.getStyleClass().add("goal-list-header"); + header.setPadding(new Insets(6, 10, 6, 10)); + + Label status = new Label("Ready."); + HBox statusBar = new HBox(status); + statusBar.getStyleClass().add("status-bar"); + + BorderPane root = new BorderPane(view); + root.setTop(header); + root.setBottom(statusBar); + + // reports every selection change in the status bar and the log; this is the evidence that + // clicking a goal row changes the selection + model.addKeYSelectionListenerChecked(new KeYSelectionListener() { + @Override + public void selectedNodeChanged(KeYSelectionEvent event) { + status.setText(selectionText(model)); + LOGGER.info("GoalListDriver: {}", status.getText()); + } + + @Override + public void selectedProofChanged(KeYSelectionEvent event) { + Proof proof = event.getSource().getSelectedProof(); + header.setText(proof == null ? "Proof: none" : "Proof: " + proof.name()); + // setSelectedProof fires only the proof event, but the model already auto-selects + // the first open goal; reflect that selection here as well + status.setText(selectionText(model)); + LOGGER.info("GoalListDriver: {}", status.getText()); + } + + private String selectionText(KeYSelectionModel model) { + Node node = model.getSelectedNode(); + Goal goal = model.getSelectedGoal(); + return node == null ? "No selection." + : goal == null ? "Selected node #" + node.serialNr() + " (inner node)" + : "Selected goal #" + goal.node().serialNr() + " (" + + (goal.isAutomatic() ? "automatic" : "interactive") + ")"; + } + }); + + stage.setTitle("KeY · Goal List"); + Scene scene = new Scene(root, 800, 500); + // apply the KeY theme (key-light.css/key-dark.css) like MainWindowF does + ThemeManager.getInstance().manage(scene); + stage.setScene(scene); + stage.show(); + + startDemoProofLoad(model, view); + } + + /** + * Loads the demo proof on a background thread and routes it through the selection model + * (whose handlers run on the FX thread), then runs the view's self-test. + */ + private void startDemoProofLoad(KeYSelectionModel model, GoalListViewF view) { + String file = System.getProperty("key.fx.demo.sequent"); + if (file == null || file.isBlank()) { + LOGGER.warn("GoalListDriver: no demo proof, set -Dkey.fx.demo.sequent="); + return; + } + // optional: number of automatic rule applications to run before the goal list is shown, + // so that the root becomes an inner node (verification affordance, like + // key.fx.demo.autoprove of MainWindowF) + final int autoSteps = readAutoSteps(); + Path location = Path.of(file); + Task> loadTask = new Task<>() { + @Override + protected KeYEnvironment call() throws Exception { + KeYEnvironment env = KeYEnvironment.load(location); + if (autoSteps > 0) { + Proof proof = env.getLoadedProof(); + StrategySettings strategySettings = + proof.getSettings().getStrategySettings(); + int originalSteps = strategySettings.getMaxSteps(); + try { + // transient: restored below. Note that any change writes the user's + // proof-settings.json, hence the restore in the finally block. + strategySettings.setMaxSteps(autoSteps); + LOGGER.info("GoalListDriver: running auto mode with max {} steps", + autoSteps); + env.getProofControl().startAndWaitForAutoMode(proof); + } finally { + strategySettings.setMaxSteps(originalSteps); + } + LOGGER.info("GoalListDriver: auto mode finished, {} open goals", + proof.openGoals().size()); + } + return env; + } + }; + loadTask.setOnSucceeded(event -> { + Proof proof = loadTask.getValue().getLoadedProof(); + LOGGER.info("GoalListDriver: demo proof loaded {} ({} open goals)", location, + proof.openGoals().size()); + // fires selectedProofChanged; onSucceeded runs on the FX thread like the future + // mediator will + model.setSelectedProof(proof); + LOGGER.info("GoalList self test: {}", view.verifyGoalList()); + // Verification affordance: select the root node. Combined with + // key.fx.goallist.autoprove>=1 the root is a closed inner node, so this clears the + // goal selection and a subsequent interactive click on a goal row performs a real + // selection change. + if (System.getProperty("key.fx.goallist.select.root") != null) { + model.setSelectedNode(proof.root()); + } + }); + loadTask.setOnFailed(event -> { + Throwable error = loadTask.getException(); + LOGGER.error("GoalListDriver: demo proof loading failed", error); + }); + Thread loader = new Thread(loadTask, "fx-goallist-demo-loader"); + loader.setDaemon(true); + loader.start(); + } + + /** + * @return the number of automatic rule applications for the + * {@code key.fx.goallist.autoprove} property, or {@code 0} to show the proof as + * loaded + */ + private static int readAutoSteps() { + String steps = System.getProperty("key.fx.goallist.autoprove"); + if (steps == null || steps.isBlank()) { + return 0; + } + try { + return Math.max(0, Integer.parseInt(steps.trim())); + } catch (NumberFormatException e) { + LOGGER.warn("GoalListDriver: ignoring invalid key.fx.goallist.autoprove value {}", + steps); + return 0; + } + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/goallist/GoalListViewF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/goallist/GoalListViewF.java new file mode 100644 index 00000000000..23dfbe55d62 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/goallist/GoalListViewF.java @@ -0,0 +1,416 @@ +/* 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.goallist; + +import java.util.ArrayList; +import java.util.IdentityHashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import javafx.scene.control.Label; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import javafx.scene.control.OverrunStyle; +import javafx.scene.control.Tooltip; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.Region; + +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.configuration.ConfigF; +import de.uka.ilkd.key.logic.label.TermLabel; +import de.uka.ilkd.key.pp.NotationInfo; +import de.uka.ilkd.key.pp.SequentViewLogicPrinter; +import de.uka.ilkd.key.pp.VisibleTermLabels; +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.logic.Name; +import org.key_project.util.javafx.FxUtil; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * First JavaFX version of the goal list, the counter-part of + * {@code de.uka.ilkd.key.gui.GoalList} (a Swing {@code JList}) in the module {@code key.ui}. + *

+ * Milestone M2, first version. The list shows the open goals ({@link Proof#openGoals()}) + * of the selected proof. Like the Swing original, the view observes the selection model: a proof + * change rebuilds the list; a node change re-highlights the selected goal and rebuilds the list + * only when the displayed goal set actually differs from the proof's open goals (compared by + * identity and size). This re-entrancy guard keeps a click-driven goal selection + * ({@code setSelectedGoal} fires a node change) from wiping and rebuilding the list under the + * user's cursor. Clicking a row selects the goal in the {@link KeYSelectionModel}; the ListView + * selection is kept in sync with the model's selected goal. + *

+ * A cell shows the node serial ({@code #n}), an automatic/interactive/linked marker (text + * analogue of the key-hole icons of the Swing {@code IconCellRenderer}), and a one-line printed + * sequent truncated to {@value #MAX_DISPLAYED_SEQUENT_LENGTH} characters, exactly like the Swing + * renderer. The sequent text is produced by the core pretty printer + * ({@link SequentViewLogicPrinter} with {@link NotationInfo} and the proof's {@code Services}, + * no term labels), cached per goal node. + *

+ * Style classes (see {@code key-light.css} / {@code key-dark.css}, section "goal list view"): + * {@code .goal-list} (the ListView), {@code .goal-list-cell} (every cell), the marker classes + * {@code .goal-list-automatic}, {@code .goal-list-interactive}, {@code .goal-list-linked}, + * {@code .goal-list-label}, {@code .goal-list-marker}, {@code .goal-list-sequent}, + * {@code .goal-list-empty} (placeholder) and {@code .goal-list-header} (driver header). The + * selected row is highlighted via the JavaFX pseudo class {@code .goal-list-cell:selected}. + *

+ * Deliberately deferred to later M2/M3 chunks: live goal updates via + * {@code ProofTreeListener}/{@code AutoModeListener} (currently updates piggyback on selection + * events), the popup menu with the {@code DisableGoal} actions, keyboard shortcuts, and the + * SelectingGoalListModel filtering. + */ +public class GoalListViewF extends ListView { + + private static final Logger LOGGER = LoggerFactory.getLogger(GoalListViewF.class); + + /** + * Maximum number of characters of the printed sequent shown in a cell, mirroring + * {@code GoalList.MAX_DISPLAYED_SEQUENT_LENGTH} of the Swing view. + */ + private static final int MAX_DISPLAYED_SEQUENT_LENGTH = 100; + + /** + * Visible term labels of the first version: none. The goal list prints the bare sequent like + * the Swing {@code GoalList.seqToString}. + */ + private static final VisibleTermLabels NO_VISIBLE_TERM_LABELS = new VisibleTermLabels() { + @Override + public boolean contains(TermLabel label) { + return false; + } + + @Override + public boolean contains(Name name) { + return false; + } + }; + + /** + * Cache of the printed one-line sequent texts, keyed by the goal's node. A node's sequent is + * immutable once created, and nodes are unique per identity, so an identity map is safe. + * Cleared whenever another proof is displayed. + */ + private final Map sequentTextCache = new IdentityHashMap<>(); + + private KeYSelectionModel selectionModel; + private Proof proof; + /** true while this view mutates the items or the ListView selection programmatically */ + private boolean updatingSelection; + + private final KeYSelectionListener selectionListener = new KeYSelectionListener() { + @Override + public void selectedNodeChanged(KeYSelectionEvent event) { + // A goal selection re-fires a node change: rebuild only when the displayed goal set + // really differs from the proof's open goals, otherwise just re-highlight. + if (displayedGoalsDifferFromProof()) { + rebuild(proof); + } else { + highlightSelectedGoal(); + } + } + + @Override + public void selectedProofChanged(KeYSelectionEvent event) { + rebuild(event.getSource().getSelectedProof()); + } + }; + + /** + * Creates an empty goal list. + */ + public GoalListViewF() { + getStyleClass().add("goal-list"); + setCellFactory(view -> new GoalListCell()); + setPlaceholder(emptyPlaceholder()); + getSelectionModel().selectedItemProperty() + .addListener((obs, oldItem, newItem) -> handleListSelection(newItem)); + } + + /** + * Registers this view as a selection listener on the given model and shows the open goals of + * the currently selected proof, if any. + * + * @param model the selection model to observe + */ + public void attach(KeYSelectionModel model) { + Objects.requireNonNull(model); + if (selectionModel == model) { + return; + } + if (selectionModel != null) { + selectionModel.removeKeYSelectionListener(selectionListener); + } + selectionModel = model; + model.addKeYSelectionListenerChecked(selectionListener); + setProof(model.getSelectedProof()); + } + + /** + * Shows the open goals of the given proof, replacing any previously shown list. + * + * @param newProof the proof to display, may be {@code null} + */ + public void setProof(Proof newProof) { + rebuild(newProof); + } + + /** + * Development self-test (M2): verifies that the rendered goal items are exactly the open + * goals of the displayed proof (identity and order) and that the ListView selection is in + * sync with the selection model's selected goal. + * + * @return a one-line report, {@code "... PASS"} if the list is consistent + */ + public String verifyGoalList() { + if (!FxUtil.isFxThread()) { + return FxUtil.callAndWait(this::verifyGoalList); + } + if (proof == null) { + return "no proof"; + } + int openGoals = proof.openGoals().size(); + int rendered = getItems().size(); + boolean sameGoals = rendered == openGoals; + int index = 0; + for (Goal goal : proof.openGoals()) { + if (index >= rendered || getItems().get(index) != goal) { + sameGoals = false; + break; + } + index++; + } + Goal selected = selectionModel != null ? selectionModel.getSelectedGoal() : null; + boolean selectedInSync = getSelectionModel().getSelectedItem() == selected; + boolean pass = sameGoals && selectedInSync; + return "proofNodes=" + countProofNodes() + " openGoals=" + openGoals + " rendered=" + + rendered + " selectedInSync=" + selectedInSync + " " + (pass ? "PASS" : "FAIL"); + } + + private int countProofNodes() { + int count = 0; + for (Iterator it = proof.root().subtreeIterator(); it.hasNext();) { + it.next(); + count++; + } + return count; + } + + private void rebuild(Proof newProof) { + if (!FxUtil.isFxThread()) { + FxUtil.runLater(() -> rebuild(newProof)); + return; + } + if (proof != newProof) { + sequentTextCache.clear(); + } + proof = newProof; + updatingSelection = true; + try { + if (proof == null || proof.isDisposed()) { + getItems().clear(); + } else { + List goals = new ArrayList<>(); + for (Goal goal : proof.openGoals()) { + goals.add(goal); + } + getItems().setAll(goals); + } + highlightSelectedGoal(); + } finally { + updatingSelection = false; + } + } + + /** + * @return whether the currently displayed goals differ from the open goals of the displayed + * proof (identity and size comparison, cheap enough to run on every selection event) + */ + private boolean displayedGoalsDifferFromProof() { + if (proof == null || proof.isDisposed()) { + return !getItems().isEmpty(); + } + var openGoals = proof.openGoals(); + if (openGoals.size() != getItems().size()) { + return true; + } + int index = 0; + for (Goal goal : openGoals) { + if (goal != getItems().get(index++)) { + return true; + } + } + return false; + } + + /** + * Selects the model's selected goal in the ListView (or clears the selection when no goal is + * selected, e.g. for an inner node). Mirrors {@code GoalList.selectSelectedGoal}. + */ + private void highlightSelectedGoal() { + Goal selected = selectionModel != null ? selectionModel.getSelectedGoal() : null; + if (getSelectionModel().getSelectedItem() == selected) { + return; + } + updatingSelection = true; + try { + if (selected == null) { + getSelectionModel().clearSelection(); + } else { + int index = getItems().indexOf(selected); + if (index >= 0) { + getSelectionModel().select(index); + scrollTo(index); + } else { + getSelectionModel().clearSelection(); + } + } + } finally { + updatingSelection = false; + } + } + + private void handleListSelection(Goal newItem) { + if (updatingSelection || newItem == null || selectionModel == null) { + return; + } + if (newItem != selectionModel.getSelectedGoal()) { + selectionModel.setSelectedGoal(newItem); + } + } + + /** + * @return the one-line printed sequent text of the goal (truncated, no term labels), cached + * per goal node + */ + private String sequentText(Goal goal) { + Node node = goal.node(); + String cached = sequentTextCache.get(node); + if (cached != null) { + return cached; + } + String res; + try { + SequentViewLogicPrinter printer = + SequentViewLogicPrinter.purePrinter(new NotationInfo(), + node.proof().getServices(), NO_VISIBLE_TERM_LABELS); + printer.setMaxChar(MAX_DISPLAYED_SEQUENT_LENGTH); + printer.printSequent(goal.sequent()); + // read the result only after printing has completed (see Swing GoalList.seqToString) + res = printer.result().replace('\n', ' '); + if (res.length() > MAX_DISPLAYED_SEQUENT_LENGTH) { + res = res.substring(0, MAX_DISPLAYED_SEQUENT_LENGTH) + "..."; + } + } catch (Exception ex) { + LOGGER.warn("Goal list: problem printing the sequent of node #{}", node.serialNr(), ex); + res = ""; + } + sequentTextCache.put(node, res); + return res; + } + + /** + * @return the marker text of the goal, the text analogue of the icon choice of the Swing + * {@code IconCellRenderer} (linked, key-hole for automatic, disabled key-hole for + * interactive) + */ + private static String markerText(Goal goal) { + if (goal.isLinked()) { + return "linked"; + } + return goal.isAutomatic() ? "automatic" : "interactive"; + } + + /** + * @return the style class highlighting the marker of the goal + */ + private static String markerStyleClass(Goal goal) { + if (goal.isLinked()) { + return "goal-list-linked"; + } + return goal.isAutomatic() ? "goal-list-automatic" : "goal-list-interactive"; + } + + private static Label emptyPlaceholder() { + Label placeholder = new Label("No proof loaded.\n" + + "Start with -Dkey.fx.demo.sequent= to try the goal list."); + placeholder.getStyleClass().add("goal-list-empty"); + placeholder.setWrapText(true); + placeholder.setFont(ConfigF.DEFAULT.systemFont()); + return placeholder; + } + + /** + * The cell rendering: node serial, goal marker, one-line sequent; selected state comes from + * the ListView selection (styled via {@code .goal-list-cell:selected}). + */ + private final class GoalListCell extends ListCell { + private final Label nameLabel = new Label(); + private final Label markerLabel = new Label(); + private final Label sequentLabel = new Label(); + private final HBox content; + + GoalListCell() { + nameLabel.getStyleClass().add("goal-list-label"); + markerLabel.getStyleClass().add("goal-list-marker"); + sequentLabel.getStyleClass().add("goal-list-sequent"); + nameLabel.setFont(ConfigF.DEFAULT.systemFont()); + markerLabel.setFont(ConfigF.DEFAULT.systemFont()); + sequentLabel.setFont(ConfigF.DEFAULT.monoFont()); + // keep the node serial and the marker at their preferred size: only the sequent + // label shrinks and ellipsizes when the cell is too narrow + nameLabel.setMinWidth(Region.USE_PREF_SIZE); + markerLabel.setMinWidth(Region.USE_PREF_SIZE); + sequentLabel.setMaxWidth(Double.MAX_VALUE); + sequentLabel.setTextOverrun(OverrunStyle.ELLIPSIS); + HBox.setHgrow(sequentLabel, Priority.ALWAYS); + content = new HBox(6, nameLabel, markerLabel, sequentLabel); + } + + /** + * Clamps the preferred cell width to the width of the ListView so that long sequents + * ellipsize (see the sequent label) instead of growing a horizontal scrollbar. + */ + @Override + protected double computePrefWidth(double height) { + double pref = super.computePrefWidth(height); + ListView listView = getListView(); + if (listView == null || listView.getWidth() <= 0) { + return pref; + } + return Math.min(pref, Math.max(listView.getWidth() - 12, 100)); + } + + @Override + protected void updateItem(Goal goal, boolean empty) { + super.updateItem(goal, empty); + getStyleClass().clear(); + getStyleClass().add("goal-list-cell"); + if (empty || goal == null) { + setGraphic(null); + setText(null); + setTooltip(null); + return; + } + String text = sequentText(goal); + String marker = markerText(goal); + markerLabel.getStyleClass().setAll("goal-list-marker", markerStyleClass(goal)); + nameLabel.setText("#" + goal.node().serialNr()); + markerLabel.setText(marker); + sequentLabel.setText(text); + setGraphic(content); + setText(null); + setTooltip(new Tooltip("Node #" + goal.node().serialNr() + " (" + marker + ")\n" + + text)); + } + } +} diff --git a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css index d2a6b80f0ff..0271e665a2c 100644 --- a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css +++ b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css @@ -134,3 +134,58 @@ .proof-tree-interactive { -fx-text-fill: -key-interactive; } + +/* goal list view */ +.goal-list { + -fx-background-color: -key-surface; +} + +.goal-list-cell { + -fx-background-color: transparent; + -fx-padding: 3 8 3 8; +} + +.goal-list-label { + -fx-text-fill: -key-text-secondary; +} + +.goal-list-marker { + -fx-font-size: 0.9em; +} + +.goal-list-automatic { + -fx-text-fill: -key-success; +} + +.goal-list-interactive { + -fx-text-fill: -key-interactive; +} + +.goal-list-linked { + -fx-text-fill: -key-linked; +} + +.goal-list-sequent { + -fx-text-fill: -key-text; +} + +/* selected row: darkened accent background, readable text for all labels */ +.goal-list-cell:selected { + -fx-background-color: derive(-key-accent, -65%); +} + +.goal-list-cell:selected .label { + -fx-text-fill: -key-text; +} + +.goal-list-empty { + -fx-text-fill: -key-text-secondary; +} + +.goal-list-header { + -fx-font-size: 1.1em; + -fx-font-weight: bold; + -fx-text-fill: -key-text; + -fx-background-color: -key-background; + -fx-border-color: transparent transparent -key-border transparent; +} diff --git a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css index 40f53d1c00a..2782cbb5c8c 100644 --- a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css +++ b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css @@ -130,3 +130,58 @@ .proof-tree-interactive { -fx-text-fill: -key-interactive; } + +/* goal list view */ +.goal-list { + -fx-background-color: -key-surface; +} + +.goal-list-cell { + -fx-background-color: transparent; + -fx-padding: 3 8 3 8; +} + +.goal-list-label { + -fx-text-fill: -key-text-secondary; +} + +.goal-list-marker { + -fx-font-size: 0.9em; +} + +.goal-list-automatic { + -fx-text-fill: -key-success; +} + +.goal-list-interactive { + -fx-text-fill: -key-interactive; +} + +.goal-list-linked { + -fx-text-fill: -key-linked; +} + +.goal-list-sequent { + -fx-text-fill: -key-text; +} + +/* selected row: subtle accent background, readable text for all labels */ +.goal-list-cell:selected { + -fx-background-color: derive(-key-accent, 85%); +} + +.goal-list-cell:selected .label { + -fx-text-fill: -key-text; +} + +.goal-list-empty { + -fx-text-fill: -key-text-secondary; +} + +.goal-list-header { + -fx-font-size: 1.1em; + -fx-font-weight: bold; + -fx-text-fill: -key-text; + -fx-background-color: -key-background; + -fx-border-color: transparent transparent -key-border transparent; +} From b2160f8557f22fa053f4c708a4e99f249b9ea6b8 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Wed, 7 Oct 2026 15:04:29 +0200 Subject: [PATCH 016/281] key.ui.fx: selection-driven status line MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The left status text follows the selection model (copyright when no proof is selected, otherwise the proof name plus its closed state or open-goal count), marshalled to the FX thread — the mediator's defaultSelection may fire from the prover thread. Because no selection event fires at auto mode stop (the proof suspends its listeners), the live demo flow refreshes the status from the final state in its autoModeStopped handler; the demo load no longer sets the status text manually (the listener owns it now). Verified on :99: 'Proof: project.key (closed)' after the auto-closed Agatha proof. --- .../de/uka/ilkd/key/gui/fx/MainWindowF.java | 49 ++++++++++++++++++- 1 file changed, 47 insertions(+), 2 deletions(-) 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 index 7e256db5e65..fa5efcefa1a 100644 --- 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 @@ -39,6 +39,8 @@ import de.uka.ilkd.key.control.DefaultUserInterfaceControl; import de.uka.ilkd.key.control.KeYEnvironment; 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.configuration.ConfigF; import de.uka.ilkd.key.gui.fx.docking.DockLayoutStore; @@ -132,6 +134,24 @@ public final class MainWindowF { */ private final InfoViewF infoView = new InfoViewF(); + /** + * 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(); + } + }; + /** * Creates the main window bound to the given stage. * @@ -214,6 +234,7 @@ private void wireSequentView() { sequentView.attach(selectionModel); proofTreeView.attach(selectionModel); infoView.attach(selectionModel); + selectionModel.addKeYSelectionListenerChecked(statusSelectionListener); sequentView.setOnPosSelected(pos -> { if (pos == null) { statusRight.setText(""); @@ -262,7 +283,6 @@ protected KeYEnvironment call() throws Exception { workspace.select(dockables.get(target)); NotificationManagerF.getInstance() .notify("Demo proof loaded: " + location, Kind.INFO); - statusLeft.setText("Proof: " + env.getLoadedProof().name()); if (System.getProperty("key.fx.verify.sequent") != null) { String report = sequentView.verifyPositionMapping(); LOGGER.info("Sequent position mapping verification: {}", report); @@ -323,6 +343,9 @@ public void autoModeStopped(ProofEvent e) { // MainWindow.autoModeStopped refreshes the views from the final state) FxUtil.runLater(() -> { proofTreeView.refresh(); + // no selection event fires at auto mode stop (the proof suspended its + // listeners), so the status line is refreshed from the final state here + updateProofStatus(); String report = proofTreeView.verifyTreeStructure() + " " + proofTreeView.getLiveUpdateReport(); LOGGER.info("Proof tree live update verification: {}", report); @@ -597,11 +620,33 @@ private HBox buildStatusBar() { private void updateStatus() { Theme theme = ThemeManager.getInstance().getTheme(); int sizeIndex = ConfigF.DEFAULT.sizeIndex(); - statusLeft.setText(KeYConstants.COPYRIGHT); + updateProofStatus(); statusRight.setText("Theme: " + theme.name().toLowerCase() + " · Font size: " + ConfigF.SIZES[sizeIndex]); } + /** + * 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(); + 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)"); + } + } + private void setTheme(Theme theme) { ThemeManager.getInstance().setTheme(theme); } From e0f98483f16481fb05d0ed689702cae0af57bfff Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Wed, 7 Oct 2026 15:18:34 +0200 Subject: [PATCH 017/281] key.ui.fx: integrate goal list + strategy views into the main window MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wires GoalListViewF and StrategySelectionViewF (parallel view tasks) into the dockable layout and the selection model, replacing the M1 placeholders. The goal list self test runs with key.fx.verify.goallist=1 (property forwarded in build.gradle). The views were stale after an auto mode run: the proof suspends its non-essential listeners, so no selection event fires at the stop. MainWindowF now refreshes all views from the final state in its autoModeStopped handler (refreshViewsFromFinalState: proof tree, goal list, info, sequent view, status) — the same contract the Swing MainWindow implements. GoalListViewF placeholder is state-aware now: a selected proof with zero open goals shows 'No open goals — the proof is closed.' instead of the misleading 'No proof loaded.' Verified on :99: goal list self test PASS at load, after the auto mode run the goal list shows the closed placeholder, the strategy view renders all settings-definition radio groups with the selected entries highlighted. --- key.ui.fx/build.gradle | 4 +- .../de/uka/ilkd/key/gui/fx/MainWindowF.java | 45 ++++++++++++++++--- .../key/gui/fx/goallist/GoalListViewF.java | 11 +++++ 3 files changed, 53 insertions(+), 7 deletions(-) diff --git a/key.ui.fx/build.gradle b/key.ui.fx/build.gradle index 31e7b8c360b..f49a57c36d3 100644 --- a/key.ui.fx/build.gradle +++ b/key.ui.fx/build.gradle @@ -57,9 +57,11 @@ run { // views observe the final state via autoModeStopped; ends with the structure self test) // * key.fx.demo.maxsteps = (M2: limit the auto mode strategy steps, e.g. 3 — tests // the live-update refresh for the non-closed case) + // * key.fx.verify.goallist = any (M2: run the goal list self test after the demo load) ["key.fx.driver", "key.fx.theme", "key.fx.demo.sequent", "key.fx.verify.sequent", "key.fx.show", "key.fx.demo.autoprove", "key.fx.verify.tree", - "key.fx.demo.autoprove.live", "key.fx.demo.maxsteps"].each { name -> + "key.fx.demo.autoprove.live", "key.fx.demo.maxsteps", "key.fx.verify.goallist"] + .each { name -> def value = System.getProperty(name) if (value != null) { systemProperties[name] = value 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 index fa5efcefa1a..31baaa21191 100644 --- 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 @@ -49,6 +49,7 @@ import de.uka.ilkd.key.gui.fx.docking.Dockable; import de.uka.ilkd.key.gui.fx.docking.SimpleDockable; 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.infoview.InfoViewF; import de.uka.ilkd.key.gui.fx.keyshortcuts.KeyStrokeManagerF; import de.uka.ilkd.key.gui.fx.nodeviews.SequentViewF; @@ -57,6 +58,7 @@ import de.uka.ilkd.key.gui.fx.prooftree.ProofTreeViewF; import de.uka.ilkd.key.gui.fx.settings.SettingsManagerF; import de.uka.ilkd.key.gui.fx.settings.ThemeSettingsProviderF; +import de.uka.ilkd.key.gui.fx.strategy.StrategySelectionViewF; import de.uka.ilkd.key.gui.fx.theme.Theme; import de.uka.ilkd.key.gui.fx.theme.ThemeManager; import de.uka.ilkd.key.proof.Proof; @@ -134,6 +136,18 @@ public final class MainWindowF { */ 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(); + + /** + * 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(); + /** * 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 @@ -234,6 +248,8 @@ private void wireSequentView() { sequentView.attach(selectionModel); proofTreeView.attach(selectionModel); infoView.attach(selectionModel); + goalListView.attach(selectionModel); + strategyView.attach(selectionModel); selectionModel.addKeYSelectionListenerChecked(statusSelectionListener); sequentView.setOnPosSelected(pos -> { if (pos == null) { @@ -298,6 +314,9 @@ protected KeYEnvironment call() throws Exception { .notify("Tree structure verification: " + report, report.endsWith("PASS") ? Kind.INFO : Kind.ERROR); } + if (System.getProperty("key.fx.verify.goallist") != null) { + LOGGER.info("Goal list verification: {}", goalListView.verifyGoalList()); + } if (System.getProperty("key.fx.demo.autoprove.live") != null) { startLiveAutoMode(env); } @@ -342,10 +361,7 @@ public void autoModeStopped(ProofEvent e) { // run, so the final tree state arrives only here (Swing parity: // MainWindow.autoModeStopped refreshes the views from the final state) FxUtil.runLater(() -> { - proofTreeView.refresh(); - // no selection event fires at auto mode stop (the proof suspended its - // listeners), so the status line is refreshed from the final state here - updateProofStatus(); + refreshViewsFromFinalState(); String report = proofTreeView.verifyTreeStructure() + " " + proofTreeView.getLiveUpdateReport(); LOGGER.info("Proof tree live update verification: {}", report); @@ -364,6 +380,21 @@ public void autoModeStopped(ProofEvent e) { worker.start(); } + /** + * 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(); + } + private void setWindowIcons() { Image icon = new Image(MainWindowF.class.getResourceAsStream(IMAGE_DIR + "key-color-icon-square.png")); @@ -378,11 +409,13 @@ private void setWindowIcons() { private void buildDockables() { registerDockable(ID_LOADED_PROOFS, "Loaded Proofs"); // TaskTree - registerDockable(ID_GOAL_LIST, "Goal List"); + dockables.put(ID_GOAL_LIST, + new SimpleDockable(ID_GOAL_LIST, "Goal List", goalListView)); dockables.put(ID_PROOF_TREE, new SimpleDockable(ID_PROOF_TREE, "Proof Tree", proofTreeView)); dockables.put(ID_INFO_VIEW, new SimpleDockable(ID_INFO_VIEW, "Info", infoView)); - registerDockable(ID_STRATEGY, "Strategy"); + dockables.put(ID_STRATEGY, + new SimpleDockable(ID_STRATEGY, "Strategy", strategyView)); dockables.put(ID_SEQUENT, new SimpleDockable(ID_SEQUENT, "Sequent", sequentView)); registerDockable(ID_SOURCE_VIEW, "Source"); } diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/goallist/GoalListViewF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/goallist/GoalListViewF.java index 23dfbe55d62..7f8b1075957 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/goallist/GoalListViewF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/goallist/GoalListViewF.java @@ -218,12 +218,14 @@ private void rebuild(Proof newProof) { try { if (proof == null || proof.isDisposed()) { getItems().clear(); + setPlaceholder(emptyPlaceholder()); } else { List goals = new ArrayList<>(); for (Goal goal : proof.openGoals()) { goals.add(goal); } getItems().setAll(goals); + setPlaceholder(goals.isEmpty() ? closedPlaceholder() : emptyPlaceholder()); } highlightSelectedGoal(); } finally { @@ -349,6 +351,15 @@ private static Label emptyPlaceholder() { return placeholder; } + /** placeholder shown while a proof is selected but all of its goals are closed. */ + private static Label closedPlaceholder() { + Label placeholder = new Label("No open goals — the proof is closed."); + placeholder.getStyleClass().add("goal-list-empty"); + placeholder.setWrapText(true); + placeholder.setFont(ConfigF.DEFAULT.systemFont()); + return placeholder; + } + /** * The cell rendering: node serial, goal marker, one-line sequent; selected state comes from * the ListView selection (styled via {@code .goal-list-cell:selected}). From 1737eda2c8c1b997f6ba62203c559cdeea253479 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Wed, 7 Oct 2026 15:50:48 +0200 Subject: [PATCH 018/281] key.ui.fx: proof tree search bar (Swing ProofTreeSearchBar parity) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Restructures ProofTreeViewF into a BorderPane (tree in the center, the search bar hidden at the bottom like the Swing view's bottomPanel) and ports the Swing search: a live query matched against the displayed text, a Collapse toggle that filters the tree to the matching subtrees (ProofTreeViewFilter.TreeSearchFilter semantics — subtrees without a match are hidden and non-matching intermediate steps within a matching branch are skipped; containsMatch is memoized per node), prev/next navigation that selects the matches in the selection model, an alert styling while there is no match, Ctrl+Shift+F to open (Swing WHEN_ANCESTOR_OF_FOCUSED_COMPONENT — the shortcut fires whenever the focus is inside the view) and Escape to close. Also fixes two latent M2c cell bugs found while testing: the cell wiped its style classes on every re-render (losing the tree-cell class, so selected rows had no selection styling and the disclosure arrows disappeared) — the default class is now kept and the state/highlight classes are swapped individually; and selected tree rows now use the theme's accent (light/dark) instead of the Modena default. The search self test runs with key.fx.verify.search= (property forwarded in build.gradle): it checks that the collapsed tree only shows matching entries plus branch folders and that the full tree is restored afterwards. Verified on :99 in both themes: collapsed search for andleft/goal, no-match alert field, next/prev navigation with the sequent view following, Escape restoring the full tree (149/149 PASS). --- key.ui.fx/build.gradle | 5 +- .../de/uka/ilkd/key/gui/fx/MainWindowF.java | 24 + .../key/gui/fx/prooftree/ProofTreeViewF.java | 445 ++++++++++++++++-- .../de/uka/ilkd/key/gui/fx/theme/key-dark.css | 48 ++ .../uka/ilkd/key/gui/fx/theme/key-light.css | 48 ++ 5 files changed, 533 insertions(+), 37 deletions(-) diff --git a/key.ui.fx/build.gradle b/key.ui.fx/build.gradle index f49a57c36d3..c1ea67fe443 100644 --- a/key.ui.fx/build.gradle +++ b/key.ui.fx/build.gradle @@ -58,10 +58,11 @@ run { // * key.fx.demo.maxsteps = (M2: limit the auto mode strategy steps, e.g. 3 — tests // the live-update refresh for the non-closed case) // * key.fx.verify.goallist = any (M2: run the goal list self test after the demo load) + // * key.fx.verify.search = any (M2: run the proof tree search self test after the demo load) ["key.fx.driver", "key.fx.theme", "key.fx.demo.sequent", "key.fx.verify.sequent", "key.fx.show", "key.fx.demo.autoprove", "key.fx.verify.tree", - "key.fx.demo.autoprove.live", "key.fx.demo.maxsteps", "key.fx.verify.goallist"] - .each { name -> + "key.fx.demo.autoprove.live", "key.fx.demo.maxsteps", "key.fx.verify.goallist", + "key.fx.verify.search"].each { name -> def value = System.getProperty(name) if (value != null) { systemProperties[name] = value 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 index 31baaa21191..0d8ad8f023e 100644 --- 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 @@ -314,6 +314,12 @@ protected KeYEnvironment call() throws Exception { .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()); } @@ -362,6 +368,9 @@ public void autoModeStopped(ProofEvent e) { // MainWindow.autoModeStopped refreshes the views from the final state) FxUtil.runLater(() -> { refreshViewsFromFinalState(); + if (System.getProperty("key.fx.verify.search") != null) { + runSearchVerification(); + } String report = proofTreeView.verifyTreeStructure() + " " + proofTreeView.getLiveUpdateReport(); LOGGER.info("Proof tree live update verification: {}", report); @@ -380,6 +389,21 @@ public void autoModeStopped(ProofEvent e) { worker.start(); } + /** + * 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 diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/prooftree/ProofTreeViewF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/prooftree/ProofTreeViewF.java index 18161c047a7..0c6a558f7d5 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/prooftree/ProofTreeViewF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/prooftree/ProofTreeViewF.java @@ -4,21 +4,38 @@ * SPDX-License-Identifier: GPL-2.0-or-later */ package de.uka.ilkd.key.gui.fx.prooftree; +import java.util.ArrayList; import java.util.Collections; import java.util.ConcurrentModificationException; +import java.util.HashMap; import java.util.IdentityHashMap; import java.util.Iterator; +import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; +import javafx.geometry.Insets; +import javafx.scene.control.Button; +import javafx.scene.control.TextField; +import javafx.scene.control.ToggleButton; import javafx.scene.control.Tooltip; import javafx.scene.control.TreeCell; import javafx.scene.control.TreeItem; import javafx.scene.control.TreeView; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyCodeCombination; +import javafx.scene.input.KeyCombination; +import javafx.scene.input.KeyEvent; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.Region; 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.fonticons.IconFactoryF; import de.uka.ilkd.key.proof.Goal; import de.uka.ilkd.key.proof.Node; import de.uka.ilkd.key.proof.Proof; @@ -31,27 +48,41 @@ import org.slf4j.LoggerFactory; /** - * First JavaFX version of the proof tree view, the counter-part of + * JavaFX version of the proof tree view, the counter-part of * {@code de.uka.ilkd.key.gui.prooftree.ProofTreeView} (a Swing {@code JTree} driven by * {@code GUIProofTreeModel}) in the module {@code key.ui}. *

- * Milestone M2, first version. The tree structure is built directly from the - * {@link Proof}: each branch is rendered as a flat list of rule applications (like the Swing - * model's linear walk), and at a branch point one sub-branch item per child is created, labeled - * with the branch label of the child (falling back to {@code Case }, mirroring + * Milestone M2. The layout is a {@link BorderPane} like the Swing view's panel: the tree + * in the center, the search bar at the bottom (hidden until requested, Swing + * {@code ProofTreeSearchBar}). The tree structure is built directly from the {@link Proof}: each + * branch is rendered as a flat list of rule applications (like the Swing model's linear walk), + * and at a branch point one sub-branch item per child is created, labeled with the branch label + * of the child (falling back to {@code Case }, mirroring * {@code GUIAbstractTreeNode.ensureBranchLabelIsSet}). Leaves are styled by their goal state * (open/closed/interactive). Clicking a node selects it in the {@link KeYSelectionModel}; * selection changes (and proof changes) drive the view. *

- * Deliberately deferred to later M2/M3 chunks: search, filters (ProofTreeViewFilter), heatmap, - * notes/tooltips of the rich Swing renderer, one-step-simplifier protocol children, lazy - * population for very large proofs (currently built eagerly), and incremental per-event tree - * model updates (live updates currently coalesce into a full rebuild). + * Search (Swing {@code ProofTreeSearchBar}/{@code ProofTreeViewFilter.TreeSearchFilter}): + * the query (lowercased) is matched against the displayed text {@code serialNr:name} resp. the + * branch label. With the Collapse toggle active (the default) the tree shows only the + * subtrees containing a match and, within a branch, hides the non-matching intermediate rule + * applications; otherwise the full tree is shown and the matching rows are highlighted. Prev/Next + * cycle through the matches and select them in the selection model; the field gets an alert + * styling while there is no match. Open with {@code Ctrl+Shift+F}, close with {@code Escape}. + *

+ * Deliberately deferred to later M2/M3 chunks: the further filters ({@code + * HIDE_CLOSED_SUBTREES}, {@code HIDE_INTERACTIVE_GOALS}), heatmap, notes/tooltips of the rich + * Swing renderer, one-step-simplifier protocol children, lazy population for very large proofs + * (currently built eagerly), and incremental per-event tree model updates (live updates currently + * coalesce into a full rebuild). */ -public class ProofTreeViewF extends TreeView { +public class ProofTreeViewF extends BorderPane { private static final Logger LOGGER = LoggerFactory.getLogger(ProofTreeViewF.class); + private static final KeyCombination OPEN_SEARCH = + new KeyCodeCombination(KeyCode.F, KeyCombination.CONTROL_DOWN, KeyCombination.SHIFT_DOWN); + /** * One tree entry: either a proof node entry (a rule application) or a branch entry (labeled * sub-tree; {@code node} is the root of the branch). @@ -93,6 +124,12 @@ public String displayText() { } } + private final TreeView tree = new TreeView<>(); + + private final HBox searchBar = new HBox(4); + private final TextField searchField = new TextField(); + private final ToggleButton collapseToggle = new ToggleButton("Collapse"); + private KeYSelectionModel selectionModel; private Proof proof; private boolean updatingSelection; @@ -109,6 +146,21 @@ public String displayText() { /** Number of coalesced refreshes executed for live events (verification). */ private final AtomicInteger liveRefreshCount = new AtomicInteger(); + /** the current search query (lowercase), empty if the search is inactive. */ + private String query = ""; + + /** the visible items matching {@link #query}, in row order (recomputed after each rebuild). */ + private final List> matches = new ArrayList<>(); + + /** index into {@link #matches} of the current match, {@code -1} initially. */ + private int matchIndex = -1; + + /** + * Cache for the "the subtree of this node contains a match" information (Swing + * {@code TreeSearchFilter.containsMatchCache}); cleared on proof and query changes. + */ + private final Map containsMatchCache = new HashMap<>(); + /** * Listens to structural changes of the displayed proof and schedules a coalesced rebuild on * the FX thread — the analogue of the Swing view's {@code GUIProofTreeModel} update calls. @@ -171,10 +223,82 @@ public void selectedProofChanged(KeYSelectionEvent event) { * Creates an empty proof tree view. */ public ProofTreeViewF() { - getStyleClass().add("proof-tree"); - setCellFactory(view -> new ProofTreeCell()); - getSelectionModel().selectedItemProperty() + tree.getStyleClass().add("proof-tree"); + tree.setCellFactory(view -> new ProofTreeCell()); + tree.getSelectionModel().selectedItemProperty() .addListener((obs, oldItem, newItem) -> handleTreeSelection(newItem)); + setCenter(tree); + + createSearchBar(); + setBottom(searchBar); + searchBar.setVisible(false); + searchBar.setManaged(false); + // Swing ProofTreeView registers Ctrl+Shift+F with WHEN_ANCESTOR_OF_FOCUSED_COMPONENT: + // the shortcut fires whenever the keyboard focus is anywhere inside this view (the key + // events bubble from the focused control up to this pane). + setOnKeyPressed(this::handleTreeKeyPressed); + } + + /** + * Builds the search bar (Swing {@code SearchBar}): a labeled text field, prev/next/close + * buttons and the Collapse toggle. Live search on every keystroke, {@code Enter} selects the + * next match, {@code Escape} closes the bar. + */ + private void createSearchBar() { + searchBar.getStyleClass().add("proof-tree-search-bar"); + searchBar.setPadding(new Insets(4)); + + Button prevButton = new Button(); + prevButton.setGraphic(IconFactoryF.createIcon(IconFactoryF.Key.PREVIOUS)); + prevButton.getStyleClass().add("proof-tree-search-button"); + prevButton.setTooltip(new Tooltip("Previous match")); + prevButton.setOnAction(e -> searchPrevious()); + + Button nextButton = new Button(); + nextButton.setGraphic(IconFactoryF.createIcon(IconFactoryF.Key.NEXT)); + nextButton.getStyleClass().add("proof-tree-search-button"); + nextButton.setTooltip(new Tooltip("Next match")); + nextButton.setOnAction(e -> searchNext()); + + Button closeButton = new Button(); + closeButton.setGraphic(IconFactoryF.createIcon(IconFactoryF.Key.CLOSE)); + closeButton.getStyleClass().add("proof-tree-search-button"); + closeButton.setTooltip(new Tooltip("Close search bar")); + closeButton.setOnAction(e -> hideSearchBar()); + + searchField.getStyleClass().add("proof-tree-search-field"); + searchField.setPromptText("Search proof tree"); + HBox.setHgrow(searchField, Priority.ALWAYS); + searchField.textProperty().addListener((obs, oldText, newText) -> search()); + searchField.setOnAction(e -> searchNext()); + searchField.setOnKeyPressed(e -> { + if (KeyCode.ESCAPE.equals(e.getCode())) { + hideSearchBar(); + } + }); + + collapseToggle.getStyleClass().add("proof-tree-search-collapse"); + collapseToggle.setSelected(true); + // never truncate the label in a narrow dock (the search bar shares one row) + collapseToggle.setMinWidth(Region.USE_PREF_SIZE); + collapseToggle.setTooltip(new Tooltip( + "Collapse the proof tree to the search matches (otherwise only highlight them)")); + collapseToggle.setOnAction(e -> search(true)); + + searchBar.getChildren().addAll(searchField, prevButton, nextButton, closeButton, + collapseToggle); + searchBar.setOnKeyPressed(e -> { + if (KeyCode.ESCAPE.equals(e.getCode())) { + hideSearchBar(); + } + }); + } + + private void handleTreeKeyPressed(KeyEvent event) { + if (OPEN_SEARCH.match(event)) { + event.consume(); + showSearchBar(); + } } /** @@ -218,25 +342,27 @@ public void setProof(Proof newProof) { } liveEventCount.set(0); liveRefreshCount.set(0); + containsMatchCache.clear(); } updatingSelection = true; try { if (proof == null) { - setRoot(null); + tree.setRoot(null); } else { TreeItem root = buildBranch(proof.root(), "Proof Tree"); - setRoot(root); + tree.setRoot(root); root.setExpanded(true); } } finally { updatingSelection = false; } + updateMatches(); } /** * Rebuilds the tree from the current proof, preserving the expansion state of the branches - * and the selection. Used when rule applications changed the proof (live updates) and by the - * self tests. + * and the selection. Used when rule applications changed the proof (live updates), when the + * search query changed, and by the self tests. */ public void refresh() { if (proof == null) { @@ -244,10 +370,10 @@ public void refresh() { return; } Set expandedBranches = Collections.newSetFromMap(new IdentityHashMap<>()); - collectExpandedBranches(getRoot(), expandedBranches); + collectExpandedBranches(tree.getRoot(), expandedBranches); setProof(proof); - if (getRoot() != null) { - applyExpandedBranches(getRoot(), expandedBranches); + if (tree.getRoot() != null) { + applyExpandedBranches(tree.getRoot(), expandedBranches); revealSelectedNode(); } } @@ -318,12 +444,12 @@ private static void applyExpandedBranches(TreeItem item, Set expand * @return a one-line report, {@code "... PASS"} if the structure is consistent */ public String verifyTreeStructure() { - if (proof == null || getRoot() == null) { + if (proof == null || tree.getRoot() == null) { return "no proof"; } Set seen = Collections.newSetFromMap(new IdentityHashMap<>()); int[] counters = { 0, 0 }; // node entries, branch entries - collect(getRoot(), seen, counters); + collect(tree.getRoot(), seen, counters); int proofNodeCount = 0; for (Iterator it = proof.root().subtreeIterator(); it.hasNext();) { it.next(); @@ -334,6 +460,55 @@ public String verifyTreeStructure() { + seen.size() + " branches=" + counters[1] + " " + (pass ? "PASS" : "FAIL"); } + /** + * Development self-test (M2): exercises the search bar like the Swing one — searches a term + * that matches some rule applications, checks that the collapsed tree only shows matching + * entries plus branch folders, then restores the full tree. + * + * @param queryString the query to search for + * @return a one-line report, {@code "... PASS"} if the search behaves as expected + */ + public String verifySearch(String queryString) { + if (proof == null || tree.getRoot() == null) { + return "no proof"; + } + showSearchBar(); + searchField.setText(queryString); + int matchCount = matches.size(); + int[] counters = { 0, 0, 0 }; // matching node entries, non-matching node entries, branches + checkCollapsed(tree.getRoot(), counters); + // within a collapsed branch, the visible node entries must be all matching or all + // non-matching — a mixture means the chain filter is inconsistent + boolean collapsedConsistent = counters[0] == 0 || counters[1] == 0; + // restore the full tree + hideSearchBar(); + String restored = verifyTreeStructure(); + boolean pass = matchCount > 0 && collapsedConsistent && restored.endsWith("PASS"); + return "query=" + queryString + " matches=" + matchCount + " collapsedNodes=" + counters[0] + + " nonMatchingNodes=" + counters[1] + " collapsedBranches=" + counters[2] + + " restored=[" + restored + "] " + (pass ? "PASS" : "FAIL"); + } + + /** + * Collects the entries of the collapsed tree: matching node entries, non-matching node + * entries and branch folders are counted separately. + */ + private void checkCollapsed(TreeItem item, int[] counters) { + Entry entry = item.getValue(); + if (entry != null && entry.node != null) { + if (entry.isBranch()) { + counters[2]++; + } else if (matches(entry)) { + counters[0]++; + } else { + counters[1]++; + } + } + for (TreeItem child : item.getChildren()) { + checkCollapsed(child, counters); + } + } + private static void collect(TreeItem item, Set seen, int[] counters) { Entry entry = item.getValue(); if (entry != null && entry.node != null) { @@ -355,8 +530,9 @@ private TreeItem buildBranch(Node branchRoot, String label) { Node current = branchRoot; // linear walk along single-child nodes, mirroring GUIBranchNode.fillChildrenCache while (true) { - TreeItem item = new TreeItem<>(Entry.node(current)); - branchItem.getChildren().add(item); + if (!filterActive() || matches(current)) { + branchItem.getChildren().add(new TreeItem<>(Entry.node(current))); + } if (current.childrenCount() == 1) { current = current.child(0); continue; @@ -369,6 +545,10 @@ private TreeItem buildBranch(Node branchRoot, String label) { int branchCount = current.childrenCount(); for (int i = 0; i < branchCount; i++) { Node child = current.child(i); + if (filterActive() && !containsMatch(child)) { + // the subtree contains no match: hidden (Swing TreeSearchFilter.showSubtree) + continue; + } branchItem.getChildren().add(buildBranch(child, ensureBranchLabelIsSet(child))); } return branchItem; @@ -410,13 +590,20 @@ private void handleTreeSelection(TreeItem item) { */ private void revealSelectedNode() { Node selected = selectionModel != null ? selectionModel.getSelectedNode() : null; - if (selected == null || getRoot() == null) { + if (selected == null || tree.getRoot() == null) { return; } - TreeItem item = findItem(getRoot(), selected); - if (item == null || getSelectionModel().getSelectedItem() == item) { + TreeItem item = findItem(tree.getRoot(), selected); + if (item == null || tree.getSelectionModel().getSelectedItem() == item) { return; } + selectAndReveal(item); + } + + /** + * Expands the ancestors of the given item, selects it and scrolls it into view. + */ + private void selectAndReveal(TreeItem item) { updatingSelection = true; try { // expand the ancestors so the item is visible @@ -425,8 +612,8 @@ private void revealSelectedNode() { a.setExpanded(true); a = a.getParent(); } - getSelectionModel().select(item); - scrollTo(getRow(item)); + tree.getSelectionModel().select(item); + tree.scrollTo(tree.getRow(item)); } finally { updatingSelection = false; } @@ -445,25 +632,213 @@ private static TreeItem findItem(TreeItem start, Node node) { return null; } + // ----------------------------------------------------------------------- + // Search (Swing ProofTreeSearchBar + ProofTreeViewFilter.TreeSearchFilter) + // ----------------------------------------------------------------------- + + /** + * Shows the search bar (Swing: {@code Ctrl+Shift+F}) and focuses the field. A query kept + * from a previous opening is re-applied. + */ + public void showSearchBar() { + searchBar.setVisible(true); + searchBar.setManaged(true); + searchField.selectAll(); + searchField.requestFocus(); + if (!searchField.getText().isEmpty()) { + search(); + } + } + + /** + * Hides the search bar and clears the query so the full tree is shown again (Swing + * {@code setVisible(false)}). + */ + public void hideSearchBar() { + searchBar.setVisible(false); + searchBar.setManaged(false); + if (!query.isEmpty() || filterActive()) { + query = ""; + containsMatchCache.clear(); + refresh(); + } + tree.requestFocus(); + } + + /** + * @return whether the collapsing search filter is currently applied to the tree structure + */ + private boolean filterActive() { + return searchBar.isVisible() && collapseToggle.isSelected() && !query.isEmpty(); + } + + /** + * Runs the search for the current field text (Swing {@code search()}): reapplies the + * collapsing filter or the cell highlights, recomputes the matches and selects the first one. + */ + private void search() { + search(false); + } + + /** + * Runs the search, optionally forcing a structural rebuild (needed when the Collapse toggle + * flips: the filter must be applied to or removed from the tree structure). + * + * @param forceRebuild whether a structural rebuild is required regardless of the query change + */ + private void search(boolean forceRebuild) { + String newQuery = searchField.getText().toLowerCase(); + boolean queryChanged = !newQuery.equals(query); + query = newQuery; + if (queryChanged) { + containsMatchCache.clear(); + } + if (forceRebuild || (queryChanged && collapseToggle.isSelected())) { + // structural rebuild: apply the filter, or restore the full tree when inactive + refresh(); + } else if (queryChanged) { + // highlight-only mode: the tree structure is unchanged, re-render the cells + tree.refresh(); + } + updateMatches(); + if (!matches.isEmpty()) { + selectMatch(0); + } + setAlert(!query.isEmpty() && matches.isEmpty()); + } + + /** Switches to the next match (Swing {@code searchNext}), wrapping around. */ + public void searchNext() { + if (matches.isEmpty()) { + return; + } + selectMatch(matchIndex + 1 < matches.size() ? matchIndex + 1 : 0); + } + + /** Switches to the previous match (Swing {@code searchPrevious}), wrapping around. */ + public void searchPrevious() { + if (matches.isEmpty()) { + return; + } + selectMatch(matchIndex > 0 ? matchIndex - 1 : matches.size() - 1); + } + + private void selectMatch(int index) { + matchIndex = index; + TreeItem item = matches.get(matchIndex); + // update the selection model like a user click would (Swing setSelectionPath drives the + // mediator, so the sequent view follows the search) + Node node = item.getValue().node(); + if (node != null && selectionModel != null && node != selectionModel.getSelectedNode()) { + selectionModel.setSelectedNode(node); + } + selectAndReveal(item); + } + + /** + * Recomputes {@link #matches} over the currently visible items (row order) and updates the + * alert styling of the field. + */ + private void updateMatches() { + matches.clear(); + matchIndex = -1; + if (!query.isEmpty() && tree.getRoot() != null) { + collectMatches(tree.getRoot()); + } + setAlert(!query.isEmpty() && matches.isEmpty()); + } + + private void collectMatches(TreeItem item) { + Entry entry = item.getValue(); + if (entry != null && matches(entry)) { + matches.add(item); + } + for (TreeItem child : item.getChildren()) { + collectMatches(child); + } + } + + /** + * @param entry the entry to test + * @return whether the entry's displayed text contains the query (case-insensitive) + */ + private boolean matches(Entry entry) { + return entry.displayText().toLowerCase().contains(query); + } + + /** + * @param node the node to test + * @return whether the node's search text (Swing {@code TreeSearchFilter.matches}) contains + * the query (case-insensitive) + */ + private boolean matches(Node node) { + return matches(new Entry(node, null)); + } + + /** + * @param node the root of the subtree to test + * @return whether the subtree contains a match for the query (memoized per node, Swing + * {@code TreeSearchFilter.containsMatch}) + */ + private boolean containsMatch(Node node) { + Boolean cached = containsMatchCache.get(node); + if (cached != null) { + return cached; + } + boolean result = matches(new Entry(node, null)); + for (int i = 0; !result && i < node.childrenCount(); i++) { + result = containsMatch(node.child(i)); + } + containsMatchCache.put(node, result); + return result; + } + + private void setAlert(boolean alert) { + if (alert) { + searchField.getStyleClass().add("search-alert"); + } else { + searchField.getStyleClass().remove("search-alert"); + } + } + + // ----------------------------------------------------------------------- + /** * The tree cell rendering: text plus the goal-state style class (Swing colors map onto the - * theme's CSS properties). + * theme's CSS properties). Matching rows are highlighted while a search is active. The + * default {@code tree-cell} class is kept so the Modena selection styling applies; the state + * and highlight classes are swapped individually on re-render. */ private final class ProofTreeCell extends TreeCell { + private String stateClass; + private boolean matched; + @Override protected void updateItem(Entry item, boolean empty) { super.updateItem(item, empty); + if (!getStyleClass().contains("proof-tree-cell")) { + getStyleClass().add("proof-tree-cell"); + } + if (stateClass != null) { + getStyleClass().remove(stateClass); + stateClass = null; + } + if (matched) { + getStyleClass().remove("proof-tree-match"); + matched = false; + } if (empty || item == null) { setText(null); setTooltip(null); - getStyleClass().clear(); - getStyleClass().add("proof-tree-cell"); return; } setText(item.displayText()); - getStyleClass().clear(); - getStyleClass().add("proof-tree-cell"); - getStyleClass().add(styleClassOf(item)); + stateClass = styleClassOf(item); + getStyleClass().add(stateClass); + matched = !query.isEmpty() && matches(item); + if (matched) { + getStyleClass().add("proof-tree-match"); + } setTooltip(new Tooltip(tooltipText(item))); } diff --git a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css index 82f09a81311..7468d414304 100644 --- a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css +++ b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css @@ -135,6 +135,54 @@ -fx-text-fill: -key-interactive; } +/* proof tree search bar (M2, Swing ProofTreeSearchBar) */ +.proof-tree-search-bar { + -fx-background-color: -key-surface; + -fx-border-color: -key-border transparent transparent transparent; + -fx-border-width: 1 0 0 0; + -fx-padding: 4; +} + +.proof-tree-search-field { + -fx-background-color: -key-background; + -fx-text-fill: -key-text; + -fx-prompt-text-fill: -key-text-secondary; + -fx-highlight-fill: derive(-key-accent, -40%); + -fx-border-color: -key-border; + -fx-border-radius: 3; + -fx-background-radius: 3; + -fx-padding: 2 6 2 6; +} + +.proof-tree-search-field.search-alert { + -fx-background-color: #552828; +} + +.proof-tree-search-button { + -fx-background-color: transparent; + -fx-padding: 3 5 3 5; +} + +.proof-tree-search-button:hover { + -fx-background-color: derive(-key-accent, -65%); +} + +.proof-tree-match { + -fx-background-color: derive(-key-warning, -70%); + -fx-text-fill: -key-text; +} + +.proof-tree-cell:selected { + -fx-background-color: derive(-key-accent, -60%); + -fx-text-fill: -key-text; +} + +/* a row that is both selected and a search match shows the selection (Swing parity) */ +.proof-tree-cell.proof-tree-match:selected { + -fx-background-color: derive(-key-accent, -60%); + -fx-text-fill: -key-text; +} + /* info view (M2 first version) */ .info-view { -fx-background: -key-surface; diff --git a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css index 12c7ec3c03d..edcbd4088a4 100644 --- a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css +++ b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css @@ -131,6 +131,54 @@ -fx-text-fill: -key-interactive; } +/* proof tree search bar (M2, Swing ProofTreeSearchBar) */ +.proof-tree-search-bar { + -fx-background-color: -key-surface; + -fx-border-color: -key-border transparent transparent transparent; + -fx-border-width: 1 0 0 0; + -fx-padding: 4; +} + +.proof-tree-search-field { + -fx-background-color: -key-background; + -fx-text-fill: -key-text; + -fx-prompt-text-fill: -key-text-secondary; + -fx-highlight-fill: derive(-key-accent, 40%); + -fx-border-color: -key-border; + -fx-border-radius: 3; + -fx-background-radius: 3; + -fx-padding: 2 6 2 6; +} + +.proof-tree-search-field.search-alert { + -fx-background-color: #ffb2b2; +} + +.proof-tree-search-button { + -fx-background-color: transparent; + -fx-padding: 3 5 3 5; +} + +.proof-tree-search-button:hover { + -fx-background-color: derive(-key-accent, 80%); +} + +.proof-tree-match { + -fx-background-color: derive(-key-warning, 80%); + -fx-text-fill: -key-text; +} + +.proof-tree-cell:selected { + -fx-background-color: derive(-key-accent, 85%); + -fx-text-fill: -key-text; +} + +/* a row that is both selected and a search match shows the selection (Swing parity) */ +.proof-tree-cell.proof-tree-match:selected { + -fx-background-color: derive(-key-accent, 85%); + -fx-text-fill: -key-text; +} + /* info view (M2 first version) */ .info-view { -fx-background: -key-surface; From d42ed3b5fedaca7fc9fab6d54619aacd3478aaee Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Wed, 7 Oct 2026 16:07:01 +0200 Subject: [PATCH 019/281] key.ui.fx: sequent view search bar (Swing SequentViewSearchBar parity) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Restructures SequentViewF into a BorderPane (the scroll pane in the center, the search bar hidden at the bottom like the Swing view's SearchBar) and ports the Swing sequent search: the query is matched with the same pattern semantics as SearchSequentPrintFilter.createPattern (reused from key.core — an all-lowercase query matches case- insensitively, whitespace runs match line breaks, the RegExp toggle switches to raw regular expressions); all matches are highlighted in the rendered text, Prev/Next cycle the current match (stronger styling) and scroll it into view, Ctrl+Shift+F opens (Swing WHEN_ANCESTOR_OF_FOCUSED_COMPONENT — the key events bubble from the focused control up to the pane), Escape closes and restores the plain sequent, an alert field styling marks an empty result or an invalid regex. The highlight is a per-segment run split in the TextFlow (the clicked term highlight and the search matches coexist as intervals); the Swing modes that change the printed text (Hide, Regroup — sequent print filters) and translucent rectangle highlights behind the matches are deferred (inline background spans cannot split across wrapped lines). The search self test runs with key.fx.verify.sequentsearch= (property forwarded in build.gradle). Verified on :99: query agatha finds 10 matches with valid ranges, next/prev cycles 0,1,0,9 with the sequent view restored afterwards (149/149 PASS), position mapping 513/513 exact PASS, highlights and alert styling checked visually in both themes. --- key.ui.fx/build.gradle | 3 +- .../de/uka/ilkd/key/gui/fx/MainWindowF.java | 24 ++ .../key/gui/fx/nodeviews/SequentViewF.java | 380 +++++++++++++++++- .../de/uka/ilkd/key/gui/fx/theme/key-dark.css | 42 ++ .../uka/ilkd/key/gui/fx/theme/key-light.css | 42 ++ 5 files changed, 470 insertions(+), 21 deletions(-) diff --git a/key.ui.fx/build.gradle b/key.ui.fx/build.gradle index c1ea67fe443..3f28eeb2161 100644 --- a/key.ui.fx/build.gradle +++ b/key.ui.fx/build.gradle @@ -59,10 +59,11 @@ run { // the live-update refresh for the non-closed case) // * key.fx.verify.goallist = any (M2: run the goal list self test after the demo load) // * key.fx.verify.search = any (M2: run the proof tree search self test after the demo load) + // * key.fx.verify.sequentsearch = q (M2: run the sequent search self test with query q) ["key.fx.driver", "key.fx.theme", "key.fx.demo.sequent", "key.fx.verify.sequent", "key.fx.show", "key.fx.demo.autoprove", "key.fx.verify.tree", "key.fx.demo.autoprove.live", "key.fx.demo.maxsteps", "key.fx.verify.goallist", - "key.fx.verify.search"].each { name -> + "key.fx.verify.search", "key.fx.verify.sequentsearch"].each { name -> def value = System.getProperty(name) if (value != null) { systemProperties[name] = value 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 index 0d8ad8f023e..dc7a8c93667 100644 --- 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 @@ -307,6 +307,12 @@ protected KeYEnvironment call() throws Exception { 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.tree") != null) { String report = proofTreeView.verifyTreeStructure(); LOGGER.info("Proof tree structure verification: {}", report); @@ -371,6 +377,9 @@ public void autoModeStopped(ProofEvent e) { if (System.getProperty("key.fx.verify.search") != null) { runSearchVerification(); } + if (System.getProperty("key.fx.verify.sequentsearch") != null) { + runSequentSearchVerification(); + } String report = proofTreeView.verifyTreeStructure() + " " + proofTreeView.getLiveUpdateReport(); LOGGER.info("Proof tree live update verification: {}", report); @@ -389,6 +398,21 @@ public void autoModeStopped(ProofEvent e) { 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); + } + /** * 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 diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentViewF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentViewF.java index 4f900e75360..34cb47a763d 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentViewF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentViewF.java @@ -4,12 +4,29 @@ * SPDX-License-Identifier: GPL-2.0-or-later */ package de.uka.ilkd.key.gui.fx.nodeviews; +import java.util.ArrayList; +import java.util.List; import java.util.Objects; +import java.util.TreeSet; import java.util.function.Consumer; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import javafx.geometry.Insets; import javafx.geometry.Point2D; +import javafx.scene.control.Button; import javafx.scene.control.ScrollPane; +import javafx.scene.control.TextField; +import javafx.scene.control.ToggleButton; +import javafx.scene.control.Tooltip; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyCodeCombination; +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.Priority; +import javafx.scene.layout.Region; import javafx.scene.shape.HLineTo; import javafx.scene.shape.LineTo; import javafx.scene.shape.MoveTo; @@ -24,13 +41,16 @@ import de.uka.ilkd.key.core.fx.KeYSelectionListener; import de.uka.ilkd.key.core.fx.KeYSelectionModel; import de.uka.ilkd.key.gui.fx.configuration.ConfigF; +import de.uka.ilkd.key.gui.fx.fonticons.IconFactoryF; import de.uka.ilkd.key.logic.label.TermLabel; import de.uka.ilkd.key.pp.IdentitySequentPrintFilter; +import de.uka.ilkd.key.pp.IllegalRegexException; import de.uka.ilkd.key.pp.InitialPositionTable; import de.uka.ilkd.key.pp.NotationInfo; import de.uka.ilkd.key.pp.PosInSequent; import de.uka.ilkd.key.pp.PosTableLayouter; import de.uka.ilkd.key.pp.Range; +import de.uka.ilkd.key.pp.SearchSequentPrintFilter; import de.uka.ilkd.key.pp.SequentViewLogicPrinter; import de.uka.ilkd.key.pp.VisibleTermLabels; import de.uka.ilkd.key.proof.Node; @@ -39,23 +59,40 @@ import org.key_project.logic.Name; import org.key_project.util.javafx.FxUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + /** - * First JavaFX version of the sequent view, the counter-part of + * JavaFX version of the sequent view, the counter-part of * {@code de.uka.ilkd.key.gui.nodeviews.SequentView} (a Swing {@code JEditorPane} with HTML * content) in the module {@code key.ui}. *

- * Milestone M2a spike. The rendering pipeline reuses the UI-agnostic pretty-printer of + * Milestone M2. The layout is a {@link BorderPane} like the Swing view's panel: the + * sequent in the center, the search bar at the bottom (hidden until requested, Swing + * {@code SequentViewSearchBar}). The rendering pipeline reuses the UI-agnostic pretty-printer of * {@code key.core}: {@link SequentViewLogicPrinter} produces the printed sequent string together - * with an {@link InitialPositionTable} that maps character indexes to {@link PosInSequent}s. - * The string is rendered as {@link Text} runs inside a {@link TextFlow}; mouse clicks are mapped - * with {@link TextFlow#getHitInfo} back to a character index and hence to a {@link PosInSequent} - * -- no HTML, no AWT. + * with an {@link InitialPositionTable} that maps character indexes to {@link PosInSequent}s. The + * string is rendered as {@link Text} runs inside a {@link TextFlow}; mouse clicks are mapped with + * {@link TextFlow#getHitInfo} back to a character index and hence to a {@link PosInSequent} -- no + * HTML, no AWT. + *

+ * Search (Swing {@code SequentViewSearchBar}): the query (with the RegExp toggle switching + * between literal and regular-expression matching, same pattern semantics as + * {@code SearchSequentPrintFilter.createPattern}: an all-lowercase query matches + * case-insensitively, whitespace runs match line breaks) highlights all matches in the rendered + * text; Prev/Next cycle the current match (stronger styling) and scroll it into view. Open with + * {@code Ctrl+Shift+F}, close with {@code Escape}. The Swing modes that change the printed text + * (Hide, Regroup — implemented as sequent print filters) are deferred to the print-filter chunk. *

- * The spike intentionally supports only a plain character-grid rendering (monospaced font like - * the Swing view) and click highlighting; syntax highlighting, hover tooltips, update - * highlighting, sequent hiding and mediator wiring arrive with the milestone M2 implementation. + * Deliberately deferred to later chunks: syntax highlighting of the printed terms, hover + * tooltips, update highlighting (highlighting the formulas changed by a rule application), + * sequent hiding, mediator wiring (shared {@code NotationInfo} and term label visibility), and + * translucent rectangle highlights behind the matches (the search currently colors the matched + * text; inline background spans of a {@link TextFlow} cannot split across wrapped lines). */ -public class SequentViewF extends ScrollPane { +public class SequentViewF extends BorderPane { + + private static final Logger LOGGER = LoggerFactory.getLogger(SequentViewF.class); /** * Visible term labels of the spike: none. The real view feeds this from the mediator's term @@ -73,8 +110,16 @@ public boolean contains(Name name) { } }; + private static final KeyCombination OPEN_SEARCH = + new KeyCodeCombination(KeyCode.F, KeyCombination.CONTROL_DOWN, KeyCombination.SHIFT_DOWN); + + private final ScrollPane scrollPane = new ScrollPane(); private final TextFlow textFlow = new TextFlow(); + private final HBox searchBar = new HBox(4); + private final TextField searchField = new TextField(); + private final ToggleButton regexToggle = new ToggleButton("RegExp"); + private final IdentitySequentPrintFilter filter = new IdentitySequentPrintFilter(); private SequentViewLogicPrinter printer; @@ -83,6 +128,12 @@ public boolean contains(Name name) { private String printed; private Range highlightedRange; + // search state (Swing SequentViewSearchBar) + /** the matches of the current query as {@code [start, end)} ranges into {@link #printed}. */ + private final List searchMatches = new ArrayList<>(); + /** the index of the current match in {@link #searchMatches}, {@code -1} if none. */ + private int searchResultPos = -1; + private KeYSelectionModel selectionModel; private final KeYSelectionListener selectionListener = new KeYSelectionListener() { @Override @@ -106,13 +157,87 @@ public void selectedProofChanged(KeYSelectionEvent event) { */ public SequentViewF() { getStyleClass().add("sequent-view"); - setFitToWidth(true); - setFitToHeight(true); - setContent(textFlow); + scrollPane.getStyleClass().add("sequent-view"); + scrollPane.setFitToWidth(true); + scrollPane.setFitToHeight(true); + scrollPane.setContent(textFlow); + setCenter(scrollPane); textFlow.getStyleClass().add("sequent-view-flow"); textFlow.setPadding(new Insets(6)); textFlow.setOnMouseClicked(this::handleMouseClick); printPlaceholder(); + + createSearchBar(); + setBottom(searchBar); + searchBar.setVisible(false); + searchBar.setManaged(false); + // Swing SequentView registers Ctrl+Shift+F with WHEN_ANCESTOR_OF_FOCUSED_COMPONENT: the + // shortcut fires whenever the keyboard focus is anywhere inside this view (the key + // events bubble from the focused control up to this pane). + setOnKeyPressed(this::handlePaneKeyPressed); + } + + /** + * Builds the search bar (Swing {@code SearchBar}): a labeled text field, the RegExp toggle, + * prev/next/close buttons. Live search on every keystroke, {@code Enter} selects the next + * match, {@code Escape} closes the bar. + */ + private void createSearchBar() { + searchBar.getStyleClass().add("sequent-search-bar"); + searchBar.setPadding(new Insets(4)); + + Button prevButton = new Button(); + prevButton.setGraphic(IconFactoryF.createIcon(IconFactoryF.Key.PREVIOUS)); + prevButton.getStyleClass().add("sequent-search-button"); + prevButton.setTooltip(new Tooltip("Previous match")); + prevButton.setOnAction(e -> searchPrevious()); + + Button nextButton = new Button(); + nextButton.setGraphic(IconFactoryF.createIcon(IconFactoryF.Key.NEXT)); + nextButton.getStyleClass().add("sequent-search-button"); + nextButton.setTooltip(new Tooltip("Next match")); + nextButton.setOnAction(e -> searchNext()); + + Button closeButton = new Button(); + closeButton.setGraphic(IconFactoryF.createIcon(IconFactoryF.Key.CLOSE)); + closeButton.getStyleClass().add("sequent-search-button"); + closeButton.setTooltip(new Tooltip("Close search bar")); + closeButton.setOnAction(e -> hideSearchBar()); + + searchField.getStyleClass().add("sequent-search-field"); + searchField.setPromptText("Search sequent"); + HBox.setHgrow(searchField, Priority.ALWAYS); + searchField.textProperty().addListener((obs, oldText, newText) -> runSearch()); + searchField.setOnAction(e -> searchNext()); + searchField.setOnKeyPressed(e -> { + if (KeyCode.ESCAPE.equals(e.getCode())) { + hideSearchBar(); + } + }); + + regexToggle.getStyleClass().add("sequent-search-regex"); + regexToggle.setSelected(false); + regexToggle.setMinWidth(Region.USE_PREF_SIZE); + regexToggle.setTooltip(new Tooltip("Evaluate as regular expression")); + regexToggle.setOnAction(e -> { + searchField.requestFocus(); + runSearch(); + }); + + searchBar.getChildren().addAll(searchField, prevButton, nextButton, closeButton, + regexToggle); + searchBar.setOnKeyPressed(e -> { + if (KeyCode.ESCAPE.equals(e.getCode())) { + hideSearchBar(); + } + }); + } + + private void handlePaneKeyPressed(KeyEvent event) { + if (OPEN_SEARCH.match(event)) { + event.consume(); + showSearchBar(); + } } /** @@ -244,21 +369,201 @@ private void handleMouseClick(MouseEvent event) { onPosSelected.accept(pos); } + // ----------------------------------------------------------------------- + // Search (Swing SequentViewSearchBar) + // ----------------------------------------------------------------------- + + /** + * Shows the search bar (Swing: {@code Ctrl+Shift+F}) and focuses the field. A query kept + * from a previous opening is re-applied. + */ + public void showSearchBar() { + searchBar.setVisible(true); + searchBar.setManaged(true); + searchField.selectAll(); + searchField.requestFocus(); + if (!searchField.getText().isEmpty()) { + runSearch(); + } + } + + /** + * Hides the search bar and clears the highlights so the plain sequent is shown again (Swing + * {@code setVisible(false)}). The field text is kept like in the Swing view and re-applied on + * the next opening. + */ + public void hideSearchBar() { + searchBar.setVisible(false); + searchBar.setManaged(false); + if (!searchMatches.isEmpty() || searchResultPos >= 0) { + searchMatches.clear(); + searchResultPos = -1; + rebuildRuns(); + } + setAlert(false); + scrollPane.requestFocus(); + } + + /** + * Runs the search for the current field text (Swing {@code search()}): highlights all + * matches of the query in the rendered text. An invalid regular expression and a query + * without matches switch the field to the alert styling. + */ + private void runSearch() { + String text = searchField.getText(); + searchMatches.clear(); + searchResultPos = -1; + if (text.isEmpty()) { + setAlert(false); + rebuildRuns(); + return; + } + Pattern pattern; + try { + pattern = SearchSequentPrintFilter.createPattern(text, regexToggle.isSelected()); + } catch (IllegalRegexException e) { + LOGGER.debug("runSearch: text={} invalid regex", text); + setAlert(true); + rebuildRuns(); + return; + } + if (pattern == null || printed == null) { + setAlert(true); + rebuildRuns(); + return; + } + // the Swing search matches against the text with non-breaking spaces normalized + Matcher matcher = pattern.matcher(printed.replace('\u00A0', ' ')); + while (matcher.find()) { + searchMatches.add(new int[] { matcher.start(), matcher.end() }); + } + LOGGER.debug("runSearch: text={} matches={}", text, searchMatches.size()); + rebuildRuns(); + setAlert(searchMatches.isEmpty()); + } + + /** Switches to the next match (Swing {@code searchNext}), wrapping around. */ + public void searchNext() { + if (searchMatches.isEmpty()) { + return; + } + searchResultPos = (searchResultPos + 1) % searchMatches.size(); + rebuildRuns(); + scrollToMatch(searchResultPos); + } + + /** Switches to the previous match (Swing {@code searchPrevious}), wrapping around. */ + public void searchPrevious() { + if (searchMatches.isEmpty()) { + return; + } + searchResultPos = + (searchResultPos + searchMatches.size() - 1) % searchMatches.size(); + rebuildRuns(); + scrollToMatch(searchResultPos); + } + + /** + * Scrolls the current match into view (Swing {@code setCaretPosition(foundAt)}): the vertical + * position of the match's shape is centered in the viewport. + */ + private void scrollToMatch(int index) { + int[] match = searchMatches.get(index); + if (printed == null || match[0] >= printed.length()) { + return; + } + textFlow.applyCss(); + textFlow.layout(); + double minY = Double.POSITIVE_INFINITY; + for (PathElement element : textFlow.getRangeShape(match[0], + Math.min(match[1], printed.length()), true)) { + if (element instanceof MoveTo m) { + minY = Math.min(minY, m.getY()); + } else if (element instanceof LineTo l) { + minY = Math.min(minY, l.getY()); + } else if (element instanceof VLineTo v) { + minY = Math.min(minY, v.getY()); + } + } + if (!Double.isFinite(minY)) { + return; + } + double contentHeight = textFlow.getBoundsInLocal().getHeight(); + double viewHeight = scrollPane.getViewportBounds().getHeight(); + if (contentHeight <= viewHeight) { + return; + } + double vvalue = (minY - viewHeight / 2.0) / (contentHeight - viewHeight); + scrollPane.setVvalue(Math.clamp(vvalue, 0.0, 1.0)); + } + + private void setAlert(boolean alert) { + if (alert) { + searchField.getStyleClass().add("search-alert"); + } else { + searchField.getStyleClass().remove("search-alert"); + } + } + + // ----------------------------------------------------------------------- + private void rebuildRuns() { textFlow.getChildren().clear(); - if (printed == null) { + if (printed == null || printed.isEmpty()) { return; } Font font = ConfigF.DEFAULT.monoFont(); - if (highlightedRange == null) { + + // highlight intervals: the clicked term plus the search matches; the current search + // match is styled stronger (Swing highlight_1 vs highlight_2) + List intervals = new ArrayList<>(); + List intervalStyles = new ArrayList<>(); + if (highlightedRange != null) { + int start = Math.clamp(highlightedRange.start(), 0, printed.length()); + int end = Math.clamp(highlightedRange.start() + highlightedRange.length(), start, + printed.length()); + if (end > start) { + intervals.add(new int[] { start, end }); + intervalStyles.add("sequent-term-highlight"); + } + } + for (int i = 0; i < searchMatches.size(); i++) { + intervals.add(searchMatches.get(i)); + intervalStyles.add(i == searchResultPos ? "sequent-search-match-current" + : "sequent-search-match"); + } + if (intervals.isEmpty()) { addRun(printed, font, "sequent-text"); return; } - int start = Math.clamp(highlightedRange.start(), 0, printed.length()); - int end = Math.clamp(highlightedRange.end(), start, printed.length()); - addRun(printed.substring(0, start), font, "sequent-text"); - addRun(printed.substring(start, end), font, "sequent-text", "sequent-term-highlight"); - addRun(printed.substring(end), font, "sequent-text"); + + // split the text at every interval boundary and style each segment with the classes of + // all intervals covering it + TreeSet points = new TreeSet<>(); + points.add(0); + points.add(printed.length()); + for (int[] interval : intervals) { + if (interval[0] > 0 && interval[0] < printed.length()) { + points.add(interval[0]); + } + if (interval[1] > 0 && interval[1] < printed.length()) { + points.add(interval[1]); + } + } + Integer[] sorted = points.toArray(new Integer[0]); + for (int p = 0; p < sorted.length - 1; p++) { + int start = sorted[p]; + int end = sorted[p + 1]; + List styles = new ArrayList<>(); + styles.add("sequent-text"); + for (int i = 0; i < intervals.size(); i++) { + int[] interval = intervals.get(i); + if (interval[0] <= start && end <= interval[1]) { + styles.add(intervalStyles.get(i)); + } + } + addRun(printed.substring(start, end), font, styles.toArray(new String[0])); + } } private void addRun(String content, Font font, String... styleClasses) { @@ -294,6 +599,41 @@ String printed() { return printed; } + /** + * Development self-test (M2): exercises the sequent search bar like the Swing one — searches + * the given term, checks that the matches are valid ranges into the printed text, navigates + * with next and restores the plain view. + * + * @param query the query to search for + * @return a one-line report, {@code "... PASS"} if the search behaves as expected + */ + public String verifySequentSearch(String query) { + if (printed == null) { + return "no printed sequent"; + } + showSearchBar(); + searchField.setText(query); + int count = searchMatches.size(); + boolean rangesOk = true; + for (int[] match : searchMatches) { + rangesOk &= match[0] >= 0 && match[1] > match[0] && match[1] <= printed.length(); + } + // the current match starts at -1 (none highlighted); next/prev cycle through the matches + searchNext(); + int first = searchResultPos; + searchNext(); + int second = searchResultPos; + searchPrevious(); + int back = searchResultPos; + searchPrevious(); + int wrapped = searchResultPos; + hideSearchBar(); + boolean pass = count > 0 && rangesOk && first == 0 && second == 1 && back == 0 + && wrapped == count - 1 && searchMatches.isEmpty(); + return "query=" + query + " matches=" + count + " next=" + first + "," + second + "," + + back + "," + wrapped + " rangesOk=" + rangesOk + " " + (pass ? "PASS" : "FAIL"); + } + /** * Development self-test (M2a): verifies that the character model of the {@link TextFlow} * matches the {@link InitialPositionTable}. Every non-whitespace character of the printed diff --git a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css index 7468d414304..dcb97391f24 100644 --- a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css +++ b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css @@ -106,6 +106,48 @@ -fx-fill: -key-text-secondary; } +/* sequent search bar (M2, Swing SequentViewSearchBar) */ +.sequent-search-bar { + -fx-background-color: -key-surface; + -fx-border-color: -key-border transparent transparent transparent; + -fx-border-width: 1 0 0 0; + -fx-padding: 4; +} + +.sequent-search-field { + -fx-background-color: -key-background; + -fx-text-fill: -key-text; + -fx-prompt-text-fill: -key-text-secondary; + -fx-highlight-fill: derive(-key-accent, -40%); + -fx-border-color: -key-border; + -fx-border-radius: 3; + -fx-background-radius: 3; + -fx-padding: 2 6 2 6; +} + +.sequent-search-field.search-alert { + -fx-background-color: #552828; +} + +.sequent-search-button { + -fx-background-color: transparent; + -fx-padding: 3 5 3 5; +} + +.sequent-search-button:hover { + -fx-background-color: derive(-key-accent, -65%); +} + +/* the search colors the matched text (Swing paints translucent blue rectangles behind it) */ +.sequent-search-match { + -fx-fill: derive(-key-accent, 30%); +} + +.sequent-search-match-current { + -fx-fill: derive(-key-accent, 60%); + -fx-font-weight: bold; +} + /* proof tree (M2 first version) */ .proof-tree { -fx-background-color: -key-surface; diff --git a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css index edcbd4088a4..b6759707d0b 100644 --- a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css +++ b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css @@ -102,6 +102,48 @@ -fx-fill: -key-text-secondary; } +/* sequent search bar (M2, Swing SequentViewSearchBar) */ +.sequent-search-bar { + -fx-background-color: -key-surface; + -fx-border-color: -key-border transparent transparent transparent; + -fx-border-width: 1 0 0 0; + -fx-padding: 4; +} + +.sequent-search-field { + -fx-background-color: -key-background; + -fx-text-fill: -key-text; + -fx-prompt-text-fill: -key-text-secondary; + -fx-highlight-fill: derive(-key-accent, 40%); + -fx-border-color: -key-border; + -fx-border-radius: 3; + -fx-background-radius: 3; + -fx-padding: 2 6 2 6; +} + +.sequent-search-field.search-alert { + -fx-background-color: #ffb2b2; +} + +.sequent-search-button { + -fx-background-color: transparent; + -fx-padding: 3 5 3 5; +} + +.sequent-search-button:hover { + -fx-background-color: derive(-key-accent, 80%); +} + +/* the search colors the matched text (Swing paints translucent blue rectangles behind it) */ +.sequent-search-match { + -fx-fill: derive(-key-accent, 20%); +} + +.sequent-search-match-current { + -fx-fill: -key-accent; + -fx-font-weight: bold; +} + /* proof tree (M2 first version) */ .proof-tree { -fx-background-color: -key-surface; From 40a69dc2f0f7b049ae165e21df10e5a951b1e178 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Wed, 7 Oct 2026 16:17:21 +0200 Subject: [PATCH 020/281] key.ui.fx: sequent view syntax highlighting (Swing HTMLSyntaxHighlighter parity) Adds SequentSyntaxHighlighterF, the plain-text counterpart of the Swing HTMLSyntaxHighlighter: the same pattern categories produce highlight intervals over the printed string, which the sequent view renders as styled Text runs (the run builder now sweeps a segment list where one syntax category wins per segment, mirroring the innermost-wins nesting of the Swing HTML spans: progvars > JML > comments > Java > arrow > dynamic logic > propositional logic). Categories and colors follow the Swing definitions: bold propositional operators, bold dark-blue dynamic-logic keywords, bold Java keywords and comments/JML restricted to modality blocks (\[..\] and \<..\>), program variables and the sequent arrow in the accent color, with the Swing performance guards (progvar highlighting is skipped for large sequents). Deviations noted in the javadoc: keywords are Pattern.quote-wrapped because the Swing regexes join raw strings (\forall compiled as a form-feed escape, $inv as an anchor and never matched anything), and the 1.7em arrow is only bolded+colored since a size change would break the position table's character grid. The View menu 'Syntax Highlighting' checkbox now toggles the real view state, and the sequent self test (key.fx.verify.sequent) additionally reports the highlighting intervals. Verified on :99: the Agatha sequent highlights 42 intervals (bold connectives, blue quantifiers) with the position mapping still 513/513 exact PASS; the ReverseArray example exercises the modality path (try/catch colored inside \<{...}\>) in both themes. --- .../de/uka/ilkd/key/gui/fx/MainWindowF.java | 8 + .../nodeviews/SequentSyntaxHighlighterF.java | 223 ++++++++++++++++++ .../key/gui/fx/nodeviews/SequentViewF.java | 131 ++++++++-- .../de/uka/ilkd/key/gui/fx/theme/key-dark.css | 34 +++ .../uka/ilkd/key/gui/fx/theme/key-light.css | 34 +++ 5 files changed, 411 insertions(+), 19 deletions(-) create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentSyntaxHighlighterF.java 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 index dc7a8c93667..afb6d5500bf 100644 --- 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 @@ -306,6 +306,11 @@ protected KeYEnvironment call() throws Exception { .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); } if (System.getProperty("key.fx.verify.sequentsearch") != null && System.getProperty("key.fx.demo.autoprove.live") == null) { @@ -557,6 +562,9 @@ private Menu buildViewMenu() { CheckMenuItem prettyPrint = new CheckMenuItem("Pretty Print"); CheckMenuItem unicode = new CheckMenuItem("Unicode Symbols"); CheckMenuItem syntaxHighlighting = new CheckMenuItem("Syntax Highlighting"); + syntaxHighlighting.setSelected(sequentView.isSyntaxHighlightingEnabled()); + syntaxHighlighting.setOnAction( + e -> sequentView.setSyntaxHighlightingEnabled(syntaxHighlighting.isSelected())); ToggleGroup themeGroup = new ToggleGroup(); RadioMenuItem lightTheme = new RadioMenuItem("Light Theme"); diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentSyntaxHighlighterF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentSyntaxHighlighterF.java new file mode 100644 index 00000000000..be534b1fb52 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentSyntaxHighlighterF.java @@ -0,0 +1,223 @@ +/* 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.nodeviews; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import de.uka.ilkd.key.logic.op.IProgramVariable; +import de.uka.ilkd.key.proof.Node; +import de.uka.ilkd.key.proof.init.InitConfig; +import de.uka.ilkd.key.util.UnicodeHelper; +import de.uka.ilkd.key.util.mergerule.MergeRuleUtils; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Pattern-based syntax highlighting for printed sequents; the JavaFX counterpart of + * {@code de.uka.ilkd.key.gui.nodeviews.HTMLSyntaxHighlighter}. The Swing version inserts styled + * HTML spans into the printed text; here the same regular expressions produce + * {@link Highlight} intervals over the plain printed string, which + * {@link SequentViewF#rebuildRuns()} renders as styled {@link javafx.scene.text.Text} runs (no + * HTML, no AWT). + *

+ * The categories and colors follow the Swing highlighter: bold propositional operators, bold + * dark-blue dynamic-logic keywords, bold Java keywords and comments/JML inside modality blocks, + * program variables and the sequent arrow in the accent color. Where the Swing patterns overlap, + * the pattern applied last wins (innermost HTML span), which is mirrored here by an + * explicit priority: program variables > JML > comments > Java > arrow > + * dynamic logic > propositional logic. + *

+ * Deviations from the Swing version (noted for parity review): the keywords are + * {@link Pattern#quote}-wrapped. The Swing regexes join the raw strings, so {@code \forall} + * compiled as a form-feed escape and {@code $inv} as an end-of-input anchor and never matched + * anything; the quoted variants match the intended literals. The Swing arrow highlight enlarges + * the arrow to 1.7em, which would break the character grid of the position table, so the arrow is + * only colored and bolded here. Like in Swing, all throwables are caught: highlighting must never + * break the view. + */ +final class SequentSyntaxHighlighterF { + + private static final Logger LOGGER = LoggerFactory.getLogger(SequentSyntaxHighlighterF.class); + + /** Style category of a highlight interval. */ + enum Kind { + PROP, DYN, JAVA, PROGVAR, COMMENT, JML, ARROW + } + + /** A highlighted range {@code [start, end)} of the printed sequent text. */ + record Highlight(int start, int end, Kind kind) { + } + + /** Swing thresholds: program-variable highlighting is skipped for large sequents. */ + private static final int NUM_FORMULAE_IN_SEQ_THRESHOLD = 25; + private static final int NUM_PROGVAR_THRESHOLD = 10; + + /** Delimiters around Java keywords and program variables (plain-text form of the Swing set). */ + private static final String DELIMITERS = "[{}\\[\\]=*/.!,:<>();+\\-\\s]"; + + private static final Pattern PROP_PATTERN = Pattern.compile(alternatives(List.of("<->", "->", + " & ", " | ", "!", "true", "false", "" + UnicodeHelper.EQV, "" + UnicodeHelper.IMP, + "" + UnicodeHelper.AND, "" + UnicodeHelper.OR, "" + UnicodeHelper.NEG, + "" + UnicodeHelper.TOP, "" + UnicodeHelper.BOT))); + + private static final Pattern DYN_PATTERN = Pattern + .compile(alternatives(List.of("\\forall", "\\exists", "TRUE", "FALSE", "\\if", + "\\then", "\\else", "\\sum", "bsum", "\\in", "instance", "exactInstance", + "wellFormed", "measuredByEmpty", "", "$inv", "\\cup", + "" + UnicodeHelper.FORALL, "" + UnicodeHelper.EXISTS, "" + UnicodeHelper.IN, + "" + UnicodeHelper.EMPTY))); + + private static final Pattern ARROW_PATTERN = + Pattern.compile(Pattern.quote("==>") + "|" + Pattern.quote("\u27F9")); + + private static final Pattern JAVA_PATTERN = + Pattern.compile("(?<=" + DELIMITERS + ")(" + alternatives(Arrays.asList("if", "else", + "for", "do", "while", "return", "break", "switch", "case", "continue", "try", "catch", + "finally", "assert", "null", "throw", "this", "true", "false", "int", "char", "long", + "short", "byte", "method-frame", "loop-scope", "boolean", "exec", "ccatch", + "\\Return", "\\Break", "\\Continue", "final", "volatile", "default")) + ")(?=" + + DELIMITERS + ")"); + + /** Modality blocks {@code \[...\]} and {@code \<...\>}, across line breaks like in Swing. */ + private static final Pattern MODALITY_PATTERN = + Pattern.compile("\\\\[\\[<].*?\\\\[\\]>]", Pattern.DOTALL); + + private static final Pattern COMMENT_PATTERN = Pattern.compile("//[^@].*?(?=\\n|$)"); + + private static final Pattern JML_PATTERN = Pattern.compile("//@.*?(?=\\n|$)"); + + private SequentSyntaxHighlighterF() { + } + + /** + * Computes the highlight intervals of the printed sequent text. + * + * @param text the printed sequent, may be {@code null} + * @param node the displayed node, source of the program variables; may be {@code null} + * @return the highlights in no particular order; empty on any error + */ + static List highlight(String text, Node node) { + List result = new ArrayList<>(); + if (text == null || text.isEmpty() || node == null) { + return result; + } + try { + findAll(result, text, PROP_PATTERN, Kind.PROP); + findAll(result, text, DYN_PATTERN, Kind.DYN); + findAll(result, text, ARROW_PATTERN, Kind.ARROW); + // Java keywords, comments and JML only inside modality blocks, like in Swing; the + // patterns run on the block substring so the block borders act as delimiters + Matcher modality = MODALITY_PATTERN.matcher(text); + while (modality.find()) { + String block = text.substring(modality.start(), modality.end()); + addMatches(result, block, JAVA_PATTERN, Kind.JAVA, modality.start()); + addMatches(result, block, COMMENT_PATTERN, Kind.COMMENT, modality.start()); + addMatches(result, block, JML_PATTERN, Kind.JML, modality.start()); + } + // program variables, with the Swing performance guards for large sequents + StringBuilder alternation = new StringBuilder(); + for (IProgramVariable progvar : collectProgramVariables(node)) { + if (!alternation.isEmpty()) { + alternation.append('|'); + } + alternation.append(Pattern.quote(progvar.name().toString())); + } + if (!alternation.isEmpty()) { + Pattern progvarPattern = Pattern.compile( + "(?<=" + DELIMITERS + ")(" + alternation + ")(?=" + DELIMITERS + ")"); + findAll(result, text, progvarPattern, Kind.PROGVAR); + } + } catch (Throwable t) { + // Syntax highlighting should never break the view; the Swing highlighter catches all + // throwables for the same reason. + LOGGER.warn("Syntax highlighting failed", t); + return List.of(); + } + return result; + } + + /** + * @return the program variables to highlight, with the Swing guards: the node's local program + * variables if few, otherwise the sequent's location variables if the sequent is + * small, otherwise none + */ + private static Iterable collectProgramVariables(Node node) { + if (node.getLocalProgVars().size() < NUM_PROGVAR_THRESHOLD) { + return node.getLocalProgVars(); + } + InitConfig initConfig = node.proof().getInitConfig(); + if (initConfig != null && node.sequent().size() < NUM_FORMULAE_IN_SEQ_THRESHOLD) { + return MergeRuleUtils.getLocationVariablesHashSet(node.sequent(), + initConfig.getServices()); + } + return List.of(); + } + + private static void findAll(List result, String text, Pattern pattern, Kind kind) { + addMatches(result, text, pattern, kind, 0); + } + + private static void addMatches(List result, String text, Pattern pattern, + Kind kind, int offset) { + Matcher matcher = pattern.matcher(text); + while (matcher.find()) { + if (matcher.end() > matcher.start()) { + result.add(new Highlight(offset + matcher.start(), offset + matcher.end(), kind)); + } else { + matcher.region(matcher.start() + 1, text.length()); + } + } + } + + /** @return the alternatives joined with {@code |}, each {@link Pattern#quote}-wrapped */ + private static String alternatives(List keywords) { + StringBuilder sb = new StringBuilder(); + for (String keyword : keywords) { + if (!sb.isEmpty()) { + sb.append('|'); + } + sb.append(Pattern.quote(keyword)); + } + return sb.toString(); + } + + /** + * @return the CSS style class of the category, defined in the key-light/key-dark themes + */ + static String styleClass(Kind kind) { + return switch (kind) { + case PROP -> "sequent-hl-prop"; + case DYN -> "sequent-hl-dyn"; + case JAVA -> "sequent-hl-java"; + case PROGVAR -> "sequent-hl-progvar"; + case COMMENT -> "sequent-hl-comment"; + case JML -> "sequent-hl-jml"; + case ARROW -> "sequent-hl-arrow"; + }; + } + + /** + * @return the rendering priority of the category; lower wins where intervals overlap. The + * order mirrors the Swing HTML nesting (the last applied span is innermost and wins): + * program variables > JML > comments > Java > arrow > dynamic logic + * > propositional logic. + */ + static int priority(Kind kind) { + return switch (kind) { + case PROP -> 7; + case DYN -> 6; + case ARROW -> 5; + case JAVA -> 4; + case COMMENT -> 3; + case JML -> 2; + case PROGVAR -> 1; + }; + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentViewF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentViewF.java index 34cb47a763d..36bd12465a3 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentViewF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentViewF.java @@ -128,6 +128,12 @@ public boolean contains(Name name) { private String printed; private Range highlightedRange; + /** Syntax highlighting on/off (Swing View menu, on by default). */ + private boolean syntaxHighlighting = true; + + /** The syntax highlight intervals of the current printing, sorted by start. */ + private List syntaxHighlights = List.of(); + // search state (Swing SequentViewSearchBar) /** the matches of the current query as {@code [start, end)} ranges into {@link #printed}. */ private final List searchMatches = new ArrayList<>(); @@ -308,9 +314,34 @@ public void printSequent() { // TODO(M2): compute the line width from the font metrics and the viewport width printer.update(filter, PosTableLayouter.DEFAULT_LINE_WIDTH); printed = printer.result(); + syntaxHighlights = syntaxHighlighting + ? SequentSyntaxHighlighterF.highlight(printed, selectedNode) + : List.of(); + syntaxHighlights.sort((a, b) -> Integer.compare(a.start(), b.start())); rebuildRuns(); } + /** + * Switches the syntax highlighting of the printed sequent (Swing View menu "Syntax + * Highlighting"). + * + * @param enabled {@code true} to color keywords, program variables and comments + */ + public void setSyntaxHighlightingEnabled(boolean enabled) { + if (syntaxHighlighting == enabled) { + return; + } + syntaxHighlighting = enabled; + printSequent(); + } + + /** + * @return whether the syntax highlighting of the printed sequent is enabled + */ + public boolean isSyntaxHighlightingEnabled() { + return syntaxHighlighting; + } + /** * @return the initial position table of the current printing, or {@code null} */ @@ -514,52 +545,85 @@ private void rebuildRuns() { } Font font = ConfigF.DEFAULT.monoFont(); - // highlight intervals: the clicked term plus the search matches; the current search + // syntax highlight intervals (one category wins per segment, see priority below) + List syntax = syntaxHighlights; + + // overlay intervals: the clicked term plus the search matches; the current search // match is styled stronger (Swing highlight_1 vs highlight_2) - List intervals = new ArrayList<>(); - List intervalStyles = new ArrayList<>(); + List overlays = new ArrayList<>(); + List overlayStyles = new ArrayList<>(); if (highlightedRange != null) { int start = Math.clamp(highlightedRange.start(), 0, printed.length()); int end = Math.clamp(highlightedRange.start() + highlightedRange.length(), start, printed.length()); if (end > start) { - intervals.add(new int[] { start, end }); - intervalStyles.add("sequent-term-highlight"); + overlays.add(new int[] { start, end }); + overlayStyles.add("sequent-term-highlight"); } } for (int i = 0; i < searchMatches.size(); i++) { - intervals.add(searchMatches.get(i)); - intervalStyles.add(i == searchResultPos ? "sequent-search-match-current" + overlays.add(searchMatches.get(i)); + overlayStyles.add(i == searchResultPos ? "sequent-search-match-current" : "sequent-search-match"); } - if (intervals.isEmpty()) { + if (syntax.isEmpty() && overlays.isEmpty()) { addRun(printed, font, "sequent-text"); return; } - // split the text at every interval boundary and style each segment with the classes of - // all intervals covering it + // split the text at every interval boundary; each segment is then uniformly covered by + // the intervals that overlap it TreeSet points = new TreeSet<>(); points.add(0); points.add(printed.length()); - for (int[] interval : intervals) { - if (interval[0] > 0 && interval[0] < printed.length()) { - points.add(interval[0]); + for (SequentSyntaxHighlighterF.Highlight highlight : syntax) { + if (highlight.start() > 0 && highlight.start() < printed.length()) { + points.add(highlight.start()); } - if (interval[1] > 0 && interval[1] < printed.length()) { - points.add(interval[1]); + if (highlight.end() > 0 && highlight.end() < printed.length()) { + points.add(highlight.end()); } } + for (int[] overlay : overlays) { + if (overlay[0] > 0 && overlay[0] < printed.length()) { + points.add(overlay[0]); + } + if (overlay[1] > 0 && overlay[1] < printed.length()) { + points.add(overlay[1]); + } + } + + // sweep over the segments: the syntax intervals starting at or before the segment become + // active, ended ones drop out; the winner is the lowest priority among the active ones + // (mirrors the innermost-wins nesting of the Swing HTML spans) + List active = new ArrayList<>(); + int pointer = 0; Integer[] sorted = points.toArray(new Integer[0]); for (int p = 0; p < sorted.length - 1; p++) { int start = sorted[p]; int end = sorted[p + 1]; + while (pointer < syntax.size() && syntax.get(pointer).start() <= start) { + active.add(syntax.get(pointer++)); + } + active.removeIf(highlight -> highlight.end() <= start); List styles = new ArrayList<>(); styles.add("sequent-text"); - for (int i = 0; i < intervals.size(); i++) { - int[] interval = intervals.get(i); - if (interval[0] <= start && end <= interval[1]) { - styles.add(intervalStyles.get(i)); + if (!active.isEmpty()) { + SequentSyntaxHighlighterF.Kind winner = active.get(0).kind(); + int winnerPriority = SequentSyntaxHighlighterF.priority(winner); + for (SequentSyntaxHighlighterF.Highlight highlight : active) { + int priority = SequentSyntaxHighlighterF.priority(highlight.kind()); + if (priority < winnerPriority) { + winnerPriority = priority; + winner = highlight.kind(); + } + } + styles.add(SequentSyntaxHighlighterF.styleClass(winner)); + } + for (int i = 0; i < overlays.size(); i++) { + int[] overlay = overlays.get(i); + if (overlay[0] <= start && end <= overlay[1]) { + styles.add(overlayStyles.get(i)); } } addRun(printed.substring(start, end), font, styles.toArray(new String[0])); @@ -634,6 +698,35 @@ public String verifySequentSearch(String query) { + back + "," + wrapped + " rangesOk=" + rangesOk + " " + (pass ? "PASS" : "FAIL"); } + /** + * Development self-test (M2): verifies the syntax highlighting of the current printing — the + * intervals must be valid ranges of the printed text, at least the sequent arrow must be + * highlighted, and the category of every interval must map to a style class. + * + * @return a one-line report, {@code "... PASS"} if the highlighting behaves as expected + */ + public String verifySyntaxHighlighting() { + if (printed == null) { + return "no printed sequent"; + } + int arrows = 0; + int progvars = 0; + boolean inBounds = true; + for (SequentSyntaxHighlighterF.Highlight highlight : syntaxHighlights) { + inBounds &= highlight.start() >= 0 && highlight.end() > highlight.start() + && highlight.end() <= printed.length(); + if (highlight.kind() == SequentSyntaxHighlighterF.Kind.ARROW) { + arrows++; + } + if (highlight.kind() == SequentSyntaxHighlighterF.Kind.PROGVAR) { + progvars++; + } + } + boolean pass = inBounds && !syntaxHighlights.isEmpty() && arrows >= 1; + return "intervals=" + syntaxHighlights.size() + " arrows=" + arrows + " progvars=" + + progvars + " inBounds=" + inBounds + " " + (pass ? "PASS" : "FAIL"); + } + /** * Development self-test (M2a): verifies that the character model of the {@link TextFlow} * matches the {@link InitialPositionTable}. Every non-whitespace character of the printed diff --git a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css index dcb97391f24..035a8d6d2ad 100644 --- a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css +++ b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css @@ -106,6 +106,40 @@ -fx-fill: -key-text-secondary; } +/* syntax highlighting of the printed sequent (Swing HTMLSyntaxHighlighter colors); + these rules must stay before the search rules so the search and click highlights + override the syntax coloring for -fx-fill */ +.sequent-hl-prop { + -fx-font-weight: bold; +} + +.sequent-hl-dyn { + -fx-fill: #9696fa; + -fx-font-weight: bold; +} + +.sequent-hl-java { + -fx-fill: #cf50a5; + -fx-font-weight: bold; +} + +.sequent-hl-progvar { + -fx-fill: #6464fa; +} + +.sequent-hl-comment { + -fx-fill: #9fbf9f; +} + +.sequent-hl-jml { + -fx-fill: #8888cf; +} + +.sequent-hl-arrow { + -fx-fill: #6464fa; + -fx-font-weight: bold; +} + /* sequent search bar (M2, Swing SequentViewSearchBar) */ .sequent-search-bar { -fx-background-color: -key-surface; diff --git a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css index b6759707d0b..bc30b607f08 100644 --- a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css +++ b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css @@ -102,6 +102,40 @@ -fx-fill: -key-text-secondary; } +/* syntax highlighting of the printed sequent (Swing HTMLSyntaxHighlighter colors); + these rules must stay before the search rules so the search and click highlights + override the syntax coloring for -fx-fill */ +.sequent-hl-prop { + -fx-font-weight: bold; +} + +.sequent-hl-dyn { + -fx-fill: #0000d0; + -fx-font-weight: bold; +} + +.sequent-hl-java { + -fx-fill: #7f0055; + -fx-font-weight: bold; +} + +.sequent-hl-progvar { + -fx-fill: #6a3e3e; +} + +.sequent-hl-comment { + -fx-fill: #3f7f5f; +} + +.sequent-hl-jml { + -fx-fill: #0000f0; +} + +.sequent-hl-arrow { + -fx-fill: #6a3e3e; + -fx-font-weight: bold; +} + /* sequent search bar (M2, Swing SequentViewSearchBar) */ .sequent-search-bar { -fx-background-color: -key-surface; From 495581415d8f8685360d3a1f484413245a64777c Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Wed, 7 Oct 2026 16:27:33 +0200 Subject: [PATCH 021/281] key.ui.fx: proof tree filters (Swing ProofTreeViewFilter parity) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ports the four Swing tree filters, with their state in ProofIndependentSettings' ViewSettings like in Swing (persisted, shared with the classic UI): - Hide Intermediate Proofsteps: a branch shows only the last element of its [chain..., branch folders...] list — leaf chains keep their goal, chains below a split are hidden entirely (the Swing NodeFilter shows only the child at the last position; branch folders always show). - Hide Non-interactive Proofsteps: interactive steps stay visible plus the final node of leaf chains; the two node filters are mutually exclusive like in Swing's GUIProofTreeModel. - Hide Closed Subtrees and Hide Subtrees Whose Goals are Interactive prune branch folders while building (all-interactive subtrees via Proof.getSubtreeGoals + Goal.isAutomatic, subtrees without goals stay visible). While the collapsing search is active the node filters are bypassed (GUIProofTreeModel.bypassNodeFilter), so the search presentation is unaffected. The toggles live in a right-click context menu — Swing keeps them in the dockable's 'Settings' menu, which the FX docking framework does not have yet (noted for M3). verifyTreeStructure accounts for active filters (a filtered tree is a subset by design). key.fx.verify.treefilters runs the self test after the auto mode: every filter application shrinks the tree and the baseline is restored (162 entries on the closed Agatha proof: hideIntermediate=8, hideClosed=111, hideAutomode=8, restored=162 PASS); the context menu and the collapsed presentation checked visually on :99. --- key.ui.fx/build.gradle | 4 +- .../de/uka/ilkd/key/gui/fx/MainWindowF.java | 7 + .../key/gui/fx/prooftree/ProofTreeViewF.java | 268 +++++++++++++++++- 3 files changed, 269 insertions(+), 10 deletions(-) diff --git a/key.ui.fx/build.gradle b/key.ui.fx/build.gradle index 3f28eeb2161..6c72d647341 100644 --- a/key.ui.fx/build.gradle +++ b/key.ui.fx/build.gradle @@ -60,10 +60,12 @@ run { // * key.fx.verify.goallist = any (M2: run the goal list self test after the demo load) // * key.fx.verify.search = any (M2: run the proof tree search self test after the demo load) // * key.fx.verify.sequentsearch = q (M2: run the sequent search self test with query q) + // * key.fx.verify.treefilters = any (M2: run the tree filter self test after auto mode) ["key.fx.driver", "key.fx.theme", "key.fx.demo.sequent", "key.fx.verify.sequent", "key.fx.show", "key.fx.demo.autoprove", "key.fx.verify.tree", "key.fx.demo.autoprove.live", "key.fx.demo.maxsteps", "key.fx.verify.goallist", - "key.fx.verify.search", "key.fx.verify.sequentsearch"].each { name -> + "key.fx.verify.search", "key.fx.verify.sequentsearch", "key.fx.verify.treefilters"] + .each { name -> def value = System.getProperty(name) if (value != null) { systemProperties[name] = value 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 index afb6d5500bf..cb54352c46f 100644 --- 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 @@ -385,6 +385,13 @@ public void autoModeStopped(ProofEvent e) { if (System.getProperty("key.fx.verify.sequentsearch") != null) { runSequentSearchVerification(); } + 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); + } String report = proofTreeView.verifyTreeStructure() + " " + proofTreeView.getLiveUpdateReport(); LOGGER.info("Proof tree live update verification: {}", report); diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/prooftree/ProofTreeViewF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/prooftree/ProofTreeViewF.java index 0c6a558f7d5..cdc5c49a1b9 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/prooftree/ProofTreeViewF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/prooftree/ProofTreeViewF.java @@ -17,6 +17,9 @@ import java.util.concurrent.atomic.AtomicInteger; import javafx.geometry.Insets; import javafx.scene.control.Button; +import javafx.scene.control.CheckMenuItem; +import javafx.scene.control.ContextMenu; +import javafx.scene.control.SeparatorMenuItem; import javafx.scene.control.TextField; import javafx.scene.control.ToggleButton; import javafx.scene.control.Tooltip; @@ -41,7 +44,9 @@ import de.uka.ilkd.key.proof.Proof; import de.uka.ilkd.key.proof.ProofTreeEvent; import de.uka.ilkd.key.proof.ProofTreeListener; +import de.uka.ilkd.key.settings.ProofIndependentSettings; +import org.key_project.util.collection.ImmutableList; import org.key_project.util.javafx.FxUtil; import org.slf4j.Logger; @@ -161,6 +166,14 @@ public String displayText() { */ private final Map containsMatchCache = new HashMap<>(); + /** + * The filter toggle menu shown on right click (Swing keeps these checkboxes in the dockable's + * "Settings" menu, which the FX docking framework does not have yet). The state itself lives + * in {@link ProofIndependentSettings} like in Swing, so it persists and is shared with the + * classic UI. + */ + private final ContextMenu filterMenu = createFilterMenu(); + /** * Listens to structural changes of the displayed proof and schedules a coalesced rebuild on * the FX thread — the analogue of the Swing view's {@code GUIProofTreeModel} update calls. @@ -237,6 +250,12 @@ public ProofTreeViewF() { // the shortcut fires whenever the keyboard focus is anywhere inside this view (the key // events bubble from the focused control up to this pane). setOnKeyPressed(this::handleTreeKeyPressed); + // the filter toggles live in a right-click menu (Swing exposes them in the dockable's + // "Settings" menu, which the FX docking framework does not have yet) + tree.setOnContextMenuRequested(e -> { + filterMenu.show(tree, e.getScreenX(), e.getScreenY()); + e.consume(); + }); } /** @@ -456,8 +475,16 @@ public String verifyTreeStructure() { proofNodeCount++; } boolean pass = seen.size() == proofNodeCount && counters[0] == proofNodeCount; + boolean filtersActive = hideIntermediateSteps() || hideAutomodeSteps() + || hideClosedSubtrees() || hideInteractiveGoals(); + if (filtersActive) { + // with an active filter the displayed tree is intentionally a subset of the proof; + // consistency means: every displayed entry is a real proof node, without duplicates + pass = seen.size() == counters[0] + counters[1] && counters[0] <= proofNodeCount; + } return "proofNodes=" + proofNodeCount + " nodeEntries=" + counters[0] + " unique=" - + seen.size() + " branches=" + counters[1] + " " + (pass ? "PASS" : "FAIL"); + + seen.size() + " branches=" + counters[1] + (filtersActive ? " filtered" : "") + + " " + (pass ? "PASS" : "FAIL"); } /** @@ -489,6 +516,63 @@ public String verifySearch(String queryString) { + " restored=[" + restored + "] " + (pass ? "PASS" : "FAIL"); } + /** + * Development self-test (M2): exercises the tree filters like the Swing "Settings" menu — + * counts the entries with every filter off, then with Hide Intermediate Proofsteps, Hide + * Closed Subtrees and Hide Non-interactive Proofsteps in turn (each must shrink the tree), + * then restores the baseline. The filter state is left exactly as it was found. + * + * @return a one-line report, {@code "... PASS"} if all filter applications shrink the tree + * and the baseline is restored + */ + public String verifyTreeFilters() { + if (proof == null || tree.getRoot() == null) { + return "no proof"; + } + boolean wasIntermediate = hideIntermediateSteps(); + boolean wasAutomode = hideAutomodeSteps(); + boolean wasClosed = hideClosedSubtrees(); + boolean wasInteractive = hideInteractiveGoals(); + setHideIntermediateSteps(false); + setHideAutomodeSteps(false); + setHideClosedSubtrees(false); + setHideInteractiveGoals(false); + refresh(); + int baseline = countEntries(); + setHideIntermediateSteps(true); + refresh(); + int intermediate = countEntries(); + setHideIntermediateSteps(false); + setHideClosedSubtrees(true); + refresh(); + int closedHidden = countEntries(); + setHideClosedSubtrees(false); + setHideAutomodeSteps(true); + refresh(); + int automode = countEntries(); + setHideAutomodeSteps(false); + refresh(); + int restored = countEntries(); + // restore the persisted state found on entry + setHideIntermediateSteps(wasIntermediate); + setHideAutomodeSteps(wasAutomode); + setHideClosedSubtrees(wasClosed); + setHideInteractiveGoals(wasInteractive); + refresh(); + boolean pass = baseline > 0 && intermediate < baseline && closedHidden < baseline + && automode < baseline && restored == baseline; + return "baseline=" + baseline + " hideIntermediate=" + intermediate + " hideClosed=" + + closedHidden + " hideAutomode=" + automode + " restored=" + restored + " " + + (pass ? "PASS" : "FAIL"); + } + + /** @return the number of entries (node + branch) currently displayed */ + private int countEntries() { + int[] counters = { 0, 0 }; + collect(tree.getRoot(), Collections.newSetFromMap(new IdentityHashMap<>()), counters); + return counters[0] + counters[1]; + } + /** * Collects the entries of the collapsed tree: matching node entries, non-matching node * entries and branch folders are counted separately. @@ -527,33 +611,199 @@ private static void collect(TreeItem item, Set seen, int[] counters private TreeItem buildBranch(Node branchRoot, String label) { TreeItem branchItem = new TreeItem<>(Entry.branch(branchRoot, label)); + // collect the branch's linear chain first (mirrors GUIBranchNode.fillChildrenCache) + List chain = new ArrayList<>(); Node current = branchRoot; - // linear walk along single-child nodes, mirroring GUIBranchNode.fillChildrenCache while (true) { - if (!filterActive() || matches(current)) { - branchItem.getChildren().add(new TreeItem<>(Entry.node(current))); - } + chain.add(current); if (current.childrenCount() == 1) { current = current.child(0); continue; } break; } - // at a branch point (or a leaf): one branch item per child. Read by index instead of - // iterating the live children list: the prover thread may add children concurrently - // (ArrayList view); a concurrent mutation is healed by the next structural event. + boolean searchActive = filterActive(); + // Swing: while the collapsing search is active it takes precedence over the + // intermediate-step filters (GUIProofTreeModel.bypassNodeFilter) + boolean nodeFilterActive = + !searchActive && (hideIntermediateSteps() || hideAutomodeSteps()); int branchCount = current.childrenCount(); + + if (!nodeFilterActive) { + // all chain steps, search-filtered + for (Node node : chain) { + if (!searchActive || matches(node)) { + branchItem.getChildren().add(new TreeItem<>(Entry.node(node))); + } + } + } else if (hideIntermediateSteps()) { + // Hide Intermediate Proofsteps: a branch shows only the last element of its + // [chain..., branch folders...] list (the Swing NodeFilter shows the child at the + // last position); with branch folders below, the whole chain — including the split + // step — is hidden, without them the chain's final node (the goal) remains + if (branchCount == 0) { + branchItem.getChildren() + .add(new TreeItem<>(Entry.node(chain.get(chain.size() - 1)))); + } + } else { + // Hide Non-interactive Proofsteps: interactive steps stay visible, plus the final + // node of leaf chains + for (int i = 0; i < chain.size(); i++) { + Node node = chain.get(i); + boolean last = i == chain.size() - 1; + if (node.getNodeInfo().getInteractiveRuleApplication() + || last && branchCount == 0) { + branchItem.getChildren().add(new TreeItem<>(Entry.node(node))); + } + } + } + + // at a branch point (or a leaf): one branch item per child, pruned by the global filters. + // Read by index instead of iterating the live children list: the prover thread may add + // children concurrently (ArrayList view); a concurrent mutation is healed by the next + // structural event. for (int i = 0; i < branchCount; i++) { Node child = current.child(i); - if (filterActive() && !containsMatch(child)) { + if (searchActive && !containsMatch(child)) { // the subtree contains no match: hidden (Swing TreeSearchFilter.showSubtree) continue; } + if (hiddenByGlobalFilters(child)) { + continue; + } branchItem.getChildren().add(buildBranch(child, ensureBranchLabelIsSet(child))); } return branchItem; } + /** + * @return whether the subtree starting at {@code node} is hidden by an active global filter + * (Swing {@code ProofTreeViewFilter.hiddenByGlobalFilters}): the search filter, Hide + * Closed Subtrees and Hide Subtrees Whose Goals are Interactive + */ + private boolean hiddenByGlobalFilters(Node node) { + if (filterActive() && !containsMatch(node)) { + return true; + } + if (hideClosedSubtrees() && node.isClosed()) { + return true; + } + if (hideInteractiveGoals() && subtreeGoalsAllInteractive(node)) { + return true; + } + return false; + } + + /** + * @return whether the subtree rooted at {@code node} has open goals but none of them is + * automatic (Swing {@code HideInteractiveGoalsFilter.showSubtree} hides such + * subtrees; subtrees without goals stay visible) + */ + private boolean subtreeGoalsAllInteractive(Node node) { + ImmutableList goals = proof.getSubtreeGoals(node); + if (goals.isEmpty()) { + return false; + } + for (Goal goal : goals) { + if (goal.isAutomatic()) { + return false; + } + } + return true; + } + + // ----------------------------------------------------------------------- + // Filters (Swing ProofTreeViewFilter, state in ProofIndependentSettings) + // ----------------------------------------------------------------------- + + private static boolean hideIntermediateSteps() { + return ProofIndependentSettings.DEFAULT_INSTANCE.getViewSettings() + .getHideIntermediateProofsteps(); + } + + private static boolean hideAutomodeSteps() { + return ProofIndependentSettings.DEFAULT_INSTANCE.getViewSettings() + .getHideAutomodeProofsteps(); + } + + private static boolean hideClosedSubtrees() { + return ProofIndependentSettings.DEFAULT_INSTANCE.getViewSettings().getHideClosedSubtrees(); + } + + private static boolean hideInteractiveGoals() { + return ProofIndependentSettings.DEFAULT_INSTANCE.getViewSettings() + .getHideInteractiveGoals(); + } + + private static void setHideIntermediateSteps(boolean active) { + ProofIndependentSettings.DEFAULT_INSTANCE.getViewSettings() + .setHideIntermediateProofsteps(active); + } + + private static void setHideAutomodeSteps(boolean active) { + ProofIndependentSettings.DEFAULT_INSTANCE.getViewSettings() + .setHideAutomodeProofsteps(active); + } + + private static void setHideClosedSubtrees(boolean active) { + ProofIndependentSettings.DEFAULT_INSTANCE.getViewSettings().setHideClosedSubtrees(active); + } + + private static void setHideInteractiveGoals(boolean active) { + ProofIndependentSettings.DEFAULT_INSTANCE.getViewSettings().setHideInteractiveGoals(active); + } + + /** + * Builds the right-click filter menu: the two mutually exclusive node filters and the two + * independently toggleable global filters, like the Swing dockable's "Settings" menu. + */ + private ContextMenu createFilterMenu() { + CheckMenuItem hideIntermediateItem = new CheckMenuItem("Hide Intermediate Proofsteps"); + hideIntermediateItem.setSelected(hideIntermediateSteps()); + CheckMenuItem onlyInteractiveItem = new CheckMenuItem("Hide Non-interactive Proofsteps"); + onlyInteractiveItem.setSelected(hideAutomodeSteps()); + // Swing keeps only one node filter active: activating one deactivates the other + hideIntermediateItem.setOnAction(e -> { + setHideIntermediateSteps(hideIntermediateItem.isSelected()); + if (hideIntermediateItem.isSelected() && hideAutomodeSteps()) { + setHideAutomodeSteps(false); + onlyInteractiveItem.setSelected(false); + } + refresh(); + }); + onlyInteractiveItem.setOnAction(e -> { + setHideAutomodeSteps(onlyInteractiveItem.isSelected()); + if (onlyInteractiveItem.isSelected() && hideIntermediateSteps()) { + setHideIntermediateSteps(false); + hideIntermediateItem.setSelected(false); + } + refresh(); + }); + CheckMenuItem hideClosedItem = new CheckMenuItem("Hide Closed Subtrees"); + hideClosedItem.setSelected(hideClosedSubtrees()); + hideClosedItem.setOnAction(e -> { + setHideClosedSubtrees(hideClosedItem.isSelected()); + refresh(); + }); + CheckMenuItem hideInteractiveItem = + new CheckMenuItem("Hide Subtrees Whose Goals are Interactive"); + hideInteractiveItem.setSelected(hideInteractiveGoals()); + hideInteractiveItem.setOnAction(e -> { + setHideInteractiveGoals(hideInteractiveItem.isSelected()); + refresh(); + }); + ContextMenu menu = new ContextMenu(hideIntermediateItem, onlyInteractiveItem, + new SeparatorMenuItem(), hideClosedItem, hideInteractiveItem); + menu.setOnShowing(e -> { + // pick up changes made elsewhere (e.g. by the classic UI sharing the settings) + hideIntermediateItem.setSelected(hideIntermediateSteps()); + onlyInteractiveItem.setSelected(hideAutomodeSteps()); + hideClosedItem.setSelected(hideClosedSubtrees()); + hideInteractiveItem.setSelected(hideInteractiveGoals()); + }); + return menu; + } + /** * Returns the branch label of the given node, falling back to {@code Case } and storing it * on the node like {@code GUIAbstractTreeNode.ensureBranchLabelIsSet}. From be6d12dcc1b05e6afe686d09bc331fc6c113d570 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Wed, 7 Oct 2026 16:37:46 +0200 Subject: [PATCH 022/281] key.ui.fx: proof tree popup actions (Swing ProofTreePopupFactory parity) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The right-click menu of the proof tree now offers the view actions of the Swing proof tree popup next to the filter toggles: Expand All Below, Expand Goals Only Below (collapse below, then expand the branch paths of the open goals), Collapse Below, Collapse Other Branches (collapse every expanded branch that is not an ancestor of the target) and Previous/Next Sibling (nearest branch neighbor first, then wrapping around from the far end like the Swing actions). The action context is resolved when the menu opens — the selected entry itself if it is a branch, otherwise its parent (Swing ProofTreeContext.branch). The items needing prover control (Run Strategy On Node, Prune, Notes, goal enablement, statistics, macro menu) are deferred to the M3 action framework. All six actions exercised visually on :99 against the closed Agatha proof; the sequent view follows the sibling selection through the mediator. --- .../key/gui/fx/prooftree/ProofTreeViewF.java | 198 ++++++++++++++++-- 1 file changed, 186 insertions(+), 12 deletions(-) diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/prooftree/ProofTreeViewF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/prooftree/ProofTreeViewF.java index cdc5c49a1b9..e7200b28bf7 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/prooftree/ProofTreeViewF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/prooftree/ProofTreeViewF.java @@ -19,6 +19,7 @@ import javafx.scene.control.Button; import javafx.scene.control.CheckMenuItem; import javafx.scene.control.ContextMenu; +import javafx.scene.control.MenuItem; import javafx.scene.control.SeparatorMenuItem; import javafx.scene.control.TextField; import javafx.scene.control.ToggleButton; @@ -167,12 +168,20 @@ public String displayText() { private final Map containsMatchCache = new HashMap<>(); /** - * The filter toggle menu shown on right click (Swing keeps these checkboxes in the dockable's - * "Settings" menu, which the FX docking framework does not have yet). The state itself lives - * in {@link ProofIndependentSettings} like in Swing, so it persists and is shared with the - * classic UI. + * The context menu shown on right click: the filter toggles (Swing keeps these checkboxes in + * the dockable's "Settings" menu, which the FX docking framework does not have yet) plus the + * expand/collapse and sibling actions of the Swing proof tree popup menu + * (ProofTreePopupFactory). The state lives in {@link ProofIndependentSettings} like in Swing, + * so it persists and is shared with the classic UI. */ - private final ContextMenu filterMenu = createFilterMenu(); + private final ContextMenu contextMenu = createContextMenu(); + + /** + * The branch entry the popup actions apply to, resolved when the menu opens: the selected + * entry itself if it is a branch, otherwise its parent — like the Swing popup's + * {@code context.branch}. + */ + private TreeItem popupBranchItem; /** * Listens to structural changes of the displayed proof and schedules a coalesced rebuild on @@ -250,10 +259,16 @@ public ProofTreeViewF() { // the shortcut fires whenever the keyboard focus is anywhere inside this view (the key // events bubble from the focused control up to this pane). setOnKeyPressed(this::handleTreeKeyPressed); - // the filter toggles live in a right-click menu (Swing exposes them in the dockable's - // "Settings" menu, which the FX docking framework does not have yet) + // the filter toggles and the popup actions live in a right-click menu (Swing exposes + // them in the dockable's "Settings" menu and the proof tree popup) tree.setOnContextMenuRequested(e -> { - filterMenu.show(tree, e.getScreenX(), e.getScreenY()); + TreeItem selected = tree.getSelectionModel().getSelectedItem(); + TreeItem item = selected != null ? selected : tree.getRoot(); + if (item != null && !item.getValue().isBranch()) { + item = item.getParent(); + } + popupBranchItem = item; + contextMenu.show(tree, e.getScreenX(), e.getScreenY()); e.consume(); }); } @@ -754,10 +769,13 @@ private static void setHideInteractiveGoals(boolean active) { } /** - * Builds the right-click filter menu: the two mutually exclusive node filters and the two - * independently toggleable global filters, like the Swing dockable's "Settings" menu. + * Builds the right-click context menu: the two mutually exclusive node filters and the two + * independently toggleable global filters, like the Swing dockable's "Settings" menu, plus + * the expand/collapse and sibling actions of the Swing proof tree popup menu + * ({@code ProofTreePopupFactory}). The action items that need prover control (Run Strategy + * On Node, Prune, Notes, goals enablement, statistics) arrive with the M3 action framework. */ - private ContextMenu createFilterMenu() { + private ContextMenu createContextMenu() { CheckMenuItem hideIntermediateItem = new CheckMenuItem("Hide Intermediate Proofsteps"); hideIntermediateItem.setSelected(hideIntermediateSteps()); CheckMenuItem onlyInteractiveItem = new CheckMenuItem("Hide Non-interactive Proofsteps"); @@ -793,7 +811,17 @@ private ContextMenu createFilterMenu() { refresh(); }); ContextMenu menu = new ContextMenu(hideIntermediateItem, onlyInteractiveItem, - new SeparatorMenuItem(), hideClosedItem, hideInteractiveItem); + new SeparatorMenuItem(), hideClosedItem, hideInteractiveItem, new SeparatorMenuItem(), + actionItem("Expand All Below", IconFactoryF.Key.PLUS, + () -> expandAllBelow(popupBranchItem)), + actionItem("Expand Goals Only Below", IconFactoryF.Key.EXPAND_GOALS, + () -> expandGoalsOnlyBelow(popupBranchItem)), + actionItem("Collapse Below", IconFactoryF.Key.MINUS, + () -> collapseAllBelow(popupBranchItem)), + actionItem("Collapse Other Branches", null, () -> collapseOthers(popupBranchItem)), + new SeparatorMenuItem(), + actionItem("Previous Sibling", IconFactoryF.Key.PREVIOUS, () -> gotoSibling(-1)), + actionItem("Next Sibling", IconFactoryF.Key.NEXT, () -> gotoSibling(1))); menu.setOnShowing(e -> { // pick up changes made elsewhere (e.g. by the classic UI sharing the settings) hideIntermediateItem.setSelected(hideIntermediateSteps()); @@ -804,6 +832,152 @@ private ContextMenu createFilterMenu() { return menu; } + /** @return a menu item with an optional icon and the given action */ + private static MenuItem actionItem(String label, IconFactoryF.Key icon, Runnable action) { + MenuItem item = new MenuItem(label); + if (icon != null) { + item.setGraphic(IconFactoryF.createIcon(icon)); + } + item.setOnAction(e -> action.run()); + return item; + } + + // ----------------------------------------------------------------------- + // Popup expand/collapse and sibling actions (Swing ProofTreePopupFactory) + // ----------------------------------------------------------------------- + + /** Expands every branch below the given item (Swing Expand All Below). */ + private void expandAllBelow(TreeItem item) { + if (item == null) { + return; + } + expandRec(item); + } + + private static void expandRec(TreeItem item) { + item.setExpanded(true); + for (TreeItem child : new ArrayList<>(item.getChildren())) { + expandRec(child); + } + } + + /** Collapses every branch below the given item, the item itself stays expanded. */ + private void collapseAllBelow(TreeItem item) { + if (item == null) { + return; + } + for (TreeItem child : new ArrayList<>(item.getChildren())) { + collapseRec(child); + } + } + + private static void collapseRec(TreeItem item) { + item.setExpanded(false); + for (TreeItem child : new ArrayList<>(item.getChildren())) { + collapseRec(child); + } + } + + /** + * Collapses everything below the given branch and then expands the branches along the paths + * of the open goals, so only the goals remain visible (Swing Expand Goals Only Below). + */ + private void expandGoalsOnlyBelow(TreeItem branchItem) { + if (branchItem == null) { + return; + } + collapseAllBelow(branchItem); + branchItem.setExpanded(true); + for (Goal goal : proof.openGoals()) { + TreeItem item = findItem(branchItem, goal.node()); + for (TreeItem i = item == null ? null : item.getParent(); i != null + && i != branchItem.getParent(); i = i.getParent()) { + i.setExpanded(true); + } + } + } + + /** + * Collapses every expanded branch that is neither the given branch nor one of its ancestors + * (Swing {@code collapseOthers}). + */ + private void collapseOthers(TreeItem target) { + if (target == null) { + return; + } + collapseOthersRec(tree.getRoot(), target); + } + + private static void collapseOthersRec(TreeItem item, TreeItem target) { + if (item == null || !item.isExpanded() || item == target) { + return; + } + if (isAncestorOf(item, target)) { + for (TreeItem child : new ArrayList<>(item.getChildren())) { + collapseOthersRec(child, target); + } + } else { + item.setExpanded(false); + } + } + + private static boolean isAncestorOf(TreeItem ancestor, TreeItem item) { + for (TreeItem i = item; i != null; i = i.getParent()) { + if (i == ancestor) { + return true; + } + } + return false; + } + + /** + * Selects the next ({@code direction == 1}) or previous ({@code direction == -1}) branch + * sibling of the selected entry's branch (Swing Previous/Next Sibling): the immediate + * neighbor first, then wrapping around from the far end like the Swing actions. + */ + private void gotoSibling(int direction) { + TreeItem start = tree.getSelectionModel().getSelectedItem(); + if (start == null) { + return; + } + TreeItem branchItem = start.getValue().isBranch() ? start : start.getParent(); + if (branchItem == null || !branchItem.getValue().isBranch()) { + return; + } + TreeItem parent = branchItem.getParent(); + if (parent == null) { + return; + } + List> children = new ArrayList<>(parent.getChildren()); + int index = children.indexOf(branchItem); + int count = children.size(); + if (index < 0 || count < 2) { + return; + } + List candidates = new ArrayList<>(); + candidates.add(index + direction); + if (direction < 0) { + for (int i = count - 1; i > index; i--) { + candidates.add(i); + } + } else { + for (int i = 0; i < index; i++) { + candidates.add(i); + } + } + for (int candidate : candidates) { + if (candidate < 0 || candidate >= count) { + continue; + } + TreeItem sibling = children.get(candidate); + if (sibling.getValue().isBranch()) { + tree.getSelectionModel().select(sibling); + tree.scrollTo(tree.getRow(sibling)); + return; + } + } + } + /** * Returns the branch label of the given node, falling back to {@code Case } and storing it * on the node like {@code GUIAbstractTreeNode.ensureBranchLabelIsSet}. From b42cfb533756196d44fb2528fb28d6dbac558d0f Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Wed, 7 Oct 2026 16:52:14 +0200 Subject: [PATCH 023/281] key.ui.fx: interactive start/stop of the automatic prover (M3 opener) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wires the automatic proof control that was reserved for M3, following Swing's AutoModeAction + AutoModeUserAction: the mediator gains startAutoMode/stopAutoMode (guarded by isInAutoMode and ProofControl.isAutoModeSupported, started with the enabled open goals) and an observable autoModeRunningProperty, updated on the FX thread from the auto mode events. MainWindowF binds the toolbar buttons and the Proof > Automation menu items to the property (start enabled while idle, stop while running, like the Swing glasspane behavior), registers the global action keys (Ctrl+Space starts, Escape stops — an open search bar consumes Escape first, so the stop never fires while a bar is visible) and refreshes the views from the final state after interactive runs (MainWindow.autoModeStopped). The demo's live run keeps its own listener with the verification reports; the UI listener skips the refresh there to avoid duplicate work. Verified on :99 with ReverseArray: idle buttons, mid-run flip, Escape interrupt (the tree rebuilds to the partial proof, status shows the open goals), resume with the enabled goals until the proof closes, and the full M2 regression sweep stays green. --- .../de/uka/ilkd/key/core/fx/KeYMediatorF.java | 43 ++++++++++ .../de/uka/ilkd/key/gui/fx/MainWindowF.java | 78 ++++++++++++++++--- 2 files changed, 110 insertions(+), 11 deletions(-) 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 index 86d1f0b12b9..0af6a44b61f 100644 --- 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 @@ -4,6 +4,9 @@ * SPDX-License-Identifier: GPL-2.0-or-later */ package de.uka.ilkd.key.core.fx; +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.java.Services; @@ -64,6 +67,13 @@ public final class KeYMediatorF implements KeYSelectionModel.ProofBinder { /** the proof control this mediator is attached to, may be {@code null} until M3. */ private ProofControl proofControl; + /** + * 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()}. @@ -168,6 +178,37 @@ 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(); + } + } + /** * Counts a closed goal (called when a rule application produced no new goals). */ @@ -241,11 +282,13 @@ public void ruleApplied(ProofEvent e) { public void autoModeStarted(ProofEvent e) { inAutoMode = true; resetNrGoalsClosedByHeuristics(); + FxUtil.runLater(() -> autoModeRunning.set(true)); } @Override public void autoModeStopped(ProofEvent e) { inAutoMode = false; + FxUtil.runLater(() -> autoModeRunning.set(false)); } } } 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 index cb54352c46f..06d531c30e4 100644 --- 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 @@ -27,6 +27,8 @@ import javafx.scene.control.ToolBar; import javafx.scene.control.Tooltip; import javafx.scene.image.Image; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyEvent; import javafx.scene.layout.BorderPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; @@ -196,6 +198,9 @@ public void initialize() { Scene scene = new Scene(root, 1100, 800); ThemeManager.getInstance().manage(scene); + // 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 + scene.setOnKeyPressed(this::handleMainWindowKeyPressed); stage.setScene(scene); stage.show(); @@ -287,8 +292,10 @@ protected KeYEnvironment call() throws Exception { }; loadTask.setOnSucceeded(event -> { KeYEnvironment env = loadTask.getValue(); - // the mediator observes the proof control (auto mode state, closed-goal counter) + // 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()); + env.getProofControl().addAutoModeListener(autoModeUiListener); // 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. @@ -455,6 +462,47 @@ private void refreshViewsFromFinalState() { updateProofStatus(); } + /** + * 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(); + } + } + + /** + * 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. + */ + private final AutoModeListener autoModeUiListener = new AutoModeListener() { + @Override + public void autoModeStarted(ProofEvent e) { + LOGGER.info("Auto mode started"); + } + + @Override + public void autoModeStopped(ProofEvent e) { + if (System.getProperty("key.fx.demo.autoprove.live") != null) { + return; // the demo listener handles the final state incl. the verification reports + } + FxUtil.runLater(() -> { + refreshViewsFromFinalState(); + LOGGER.info("Views refreshed after the auto mode stop"); + }); + } + }; + private void setWindowIcons() { Image icon = new Image(MainWindowF.class.getResourceAsStream(IMAGE_DIR + "key-color-icon-square.png")); @@ -601,11 +649,14 @@ private Menu buildViewMenu() { private Menu buildProofMenu() { Menu proof = new Menu("Proof"); Menu automation = new Menu("Automation"); - automation.getItems().addAll( + MenuItem startAuto = menuItem("Start Automatic Proof", "de.uka.ilkd.key.gui.actions.AutoModeAction", - IconFactoryF.Key.AUTO_MODE_START, this::notYetImplemented), - menuItem("Stop Automatic Proof", IconFactoryF.Key.AUTO_MODE_STOP, - this::notYetImplemented)); + 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().addAll(startAuto, stopAuto); proof.getItems().addAll(automation, new SeparatorMenuItem(), menuItem("Goal Back", "de.uka.ilkd.key.gui.actions.GoalBackAction", IconFactoryF.Key.GOAL_BACK, this::notYetImplemented), @@ -646,19 +697,24 @@ private ToolBar buildFileToolBar() { } private ToolBar buildProofToolBar() { + javafx.scene.control.Button startAuto = + toolbarButton("Start Automatic Proof (Ctrl+Space)", IconFactoryF.Key.AUTO_MODE_START, + mediator::startAutoMode); + startAuto.disableProperty().bind(mediator.autoModeRunningProperty()); + javafx.scene.control.Button stopAuto = + toolbarButton("Stop Automatic Proof (Escape)", IconFactoryF.Key.AUTO_MODE_STOP, + mediator::stopAutoMode); + stopAuto.disableProperty().bind(mediator.autoModeRunningProperty().not()); ToolBar bar = new ToolBar(); bar.getStyleClass().add("key-proof-tool-bar"); - bar.getItems().addAll( - toolbarButton("Start Automatic Proof", IconFactoryF.Key.AUTO_MODE_START, - this::notYetImplemented), - toolbarButton("Stop Automatic Proof", IconFactoryF.Key.AUTO_MODE_STOP, - this::notYetImplemented), + bar.getItems().addAll(startAuto, stopAuto, toolbarButton("Goal Back", IconFactoryF.Key.GOAL_BACK, this::notYetImplemented), toolbarButton("Prune Proof", IconFactoryF.Key.PRUNE, this::notYetImplemented)); return bar; } - private Node toolbarButton(String tooltip, IconFactoryF.Key icon, Runnable action) { + private javafx.scene.control.Button toolbarButton(String tooltip, IconFactoryF.Key icon, + Runnable action) { javafx.scene.control.Button button = new javafx.scene.control.Button(null, IconFactoryF.createIcon(icon)); button.setTooltip(new Tooltip(tooltip)); From 121454713a1d02262b1087f225b194e63b5b28a2 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Wed, 7 Oct 2026 17:24:29 +0200 Subject: [PATCH 024/281] key.ui.fx: goal back and prune proof (M3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ports Swing's GoalBackAction + PruneProofAction on top of the mediator: goalBack() takes the selected goal, or — when a plain node is selected — the newest goal of its subtree (the highest node serial number, as GoalBackAction.findNewestGoal), and undoes the rule application that created it; pruneProof() removes the subtree below the selected node. Both route through KeYMediator.setBack(Goal)/setBack(Node) equivalents that prune via Proof.pruneProof and reselect; the Swing task-finished notification is not yet mirrored. The selection is read from the goal AFTER the prune: the pruner re-associates the goal with the cutting point (Goal.pruneToParent) and clears the detached node's parent pointer (Node.remove), so capturing the old node reference first selects null and crashes. As in Swing, a prune of a closed cutting point is refused while GeneralSettings.noPruningClosed is set (the default), so a goal back on a closed branch is a no-op; the GoalBackAction javadoc's claim that the flag empties the closed goal list no longer holds in this core. The toolbar buttons and the Proof menu items are wired; the views refresh through the selection and proof events (the goal list rebuilds when its displayed goals differ from the proof's). Verified on :99 with ReverseArray: goal back after Escape-interrupted auto mode (the goal list swaps the pruned goal for the re-opened one, the sequent shows the pre-rule state), prune of a selected chain tail (the subtree collapses, the cutting point stays selected and becomes the open goal), and the refusal path on a closed branch. --- .../de/uka/ilkd/key/core/fx/KeYMediatorF.java | 94 +++++++++++++++++++ .../de/uka/ilkd/key/gui/fx/MainWindowF.java | 8 +- 2 files changed, 98 insertions(+), 4 deletions(-) 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 index 0af6a44b61f..58701682c05 100644 --- 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 @@ -21,6 +21,7 @@ import de.uka.ilkd.key.proof.RuleAppListener; import de.uka.ilkd.key.rule.OneStepSimplifier; +import org.key_project.util.collection.ImmutableList; import org.key_project.util.javafx.FxUtil; import org.slf4j.Logger; @@ -209,6 +210,99 @@ public void stopAutoMode() { } } + /** + * 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); + } + + /** + * 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). */ 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 index 06d531c30e4..d341848b8f4 100644 --- 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 @@ -659,9 +659,9 @@ private Menu buildProofMenu() { automation.getItems().addAll(startAuto, stopAuto); proof.getItems().addAll(automation, new SeparatorMenuItem(), menuItem("Goal Back", "de.uka.ilkd.key.gui.actions.GoalBackAction", - IconFactoryF.Key.GOAL_BACK, this::notYetImplemented), + IconFactoryF.Key.GOAL_BACK, mediator::goalBack), menuItem("Prune Proof", "de.uka.ilkd.key.gui.actions.PruneProofAction", - IconFactoryF.Key.PRUNE, this::notYetImplemented)); + IconFactoryF.Key.PRUNE, mediator::pruneProof)); return proof; } @@ -708,8 +708,8 @@ private ToolBar buildProofToolBar() { ToolBar bar = new ToolBar(); bar.getStyleClass().add("key-proof-tool-bar"); bar.getItems().addAll(startAuto, stopAuto, - toolbarButton("Goal Back", IconFactoryF.Key.GOAL_BACK, this::notYetImplemented), - toolbarButton("Prune Proof", IconFactoryF.Key.PRUNE, this::notYetImplemented)); + toolbarButton("Goal Back", IconFactoryF.Key.GOAL_BACK, mediator::goalBack), + toolbarButton("Prune Proof", IconFactoryF.Key.PRUNE, mediator::pruneProof)); return bar; } From a72438ec9ede2ed41376f2cee3dcb26a2d1e2f59 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Wed, 7 Oct 2026 17:39:11 +0200 Subject: [PATCH 025/281] =?UTF-8?q?key.ui.fx:=20goal=20list=20popup=20?= =?UTF-8?q?=E2=80=94=20enable/disable=20goals=20(M3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ports the Swing GoalList popup (DisableGoal actions): right-clicking a row selects it first (GoalList's mouse listener) and the menu is built fresh on every open, with the labels flipping by the goal's state — Interactive Goal / Automatic Goal toggles the goal itself, Set Other Goals Interactive / Set Other Goals Automatic puts all other goals into the opposite state and is only enabled when the list holds more than one goal. Toggling calls Goal.setEnabled, which is the automatic state (a disabled goal is skipped by the automatic prover). After a state change the rows are re-rendered from the proof's open goals (Swing updateUI), keeping the selection; the GoalListener-based live updates stay deferred. Verified on :99: single-goal toggle both ways with the marker restyled, the >1 enablement rule, and the functional effect — after Set Other Goals Interactive the automatic prover closed only the enabled goal and the interactive goal remained open. --- .../key/gui/fx/goallist/GoalListViewF.java | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/goallist/GoalListViewF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/goallist/GoalListViewF.java index 7f8b1075957..1147fea9c6c 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/goallist/GoalListViewF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/goallist/GoalListViewF.java @@ -10,11 +10,14 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import javafx.scene.control.ContextMenu; import javafx.scene.control.Label; import javafx.scene.control.ListCell; import javafx.scene.control.ListView; +import javafx.scene.control.MenuItem; import javafx.scene.control.OverrunStyle; import javafx.scene.control.Tooltip; +import javafx.scene.input.ContextMenuEvent; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; import javafx.scene.layout.Region; @@ -290,6 +293,72 @@ private void handleListSelection(Goal newItem) { } } + /** + * Shows the goal popup on the given goal (Swing {@code GoalList.popupMenu}, rebuilt on every + * open): toggles the automatic/interactive state of the goal itself or of all other goals. + * Swing's right-click handler selects the row under the pointer first, which the cell does + * before calling this. + */ + private void showGoalPopup(Goal goal, ContextMenuEvent e) { + if (goal == null) { + return; + } + ContextMenu menu = new ContextMenu(); + + // DisableSingleGoal: the label flips with the goal's state, the action toggles it + MenuItem single = new MenuItem(goal.isAutomatic() ? "Interactive Goal" : "Automatic Goal"); + single.setOnAction(ev -> { + goal.setEnabled(!goal.isAutomatic()); + refreshAfterGoalStateChange(); + }); + + // DisableOtherGoals: all other goals get the opposite of this goal's state; Swing + // enables the action only when the model holds more than one goal + MenuItem others = new MenuItem( + goal.isAutomatic() ? "Set Other Goals Interactive" : "Set Other Goals Automatic"); + others.setDisable(getItems().size() <= 1); + others.setOnAction(ev -> { + boolean enable = !goal.isAutomatic(); + for (Goal other : getItems()) { + if (other != goal) { + other.setEnabled(enable); + } + } + refreshAfterGoalStateChange(); + }); + + menu.getItems().addAll(single, others); + menu.show(this, e.getScreenX(), e.getScreenY()); + e.consume(); + } + + /** + * Re-renders the rows after a goal state change (Swing {@code GoalList.updateUI}): the items + * are re-set from the proof so every cell recomputes marker text and style. + */ + private void refreshAfterGoalStateChange() { + if (proof == null || proof.isDisposed()) { + return; + } + List goals = new ArrayList<>(); + for (Goal goal : proof.openGoals()) { + goals.add(goal); + } + updatingSelection = true; + try { + Goal selected = getSelectionModel().getSelectedItem(); + getItems().setAll(goals); + if (selected != null) { + int index = getItems().indexOf(selected); + if (index >= 0) { + getSelectionModel().select(index); + } + } + } finally { + updatingSelection = false; + } + } + /** * @return the one-line printed sequent text of the goal (truncated, no term labels), cached * per goal node @@ -385,6 +454,14 @@ private final class GoalListCell extends ListCell { sequentLabel.setTextOverrun(OverrunStyle.ELLIPSIS); HBox.setHgrow(sequentLabel, Priority.ALWAYS); content = new HBox(6, nameLabel, markerLabel, sequentLabel); + // Swing GoalList's mouse listener selects the row under the pointer before the popup + // opens; the popup acts on the (now selected) goal + setOnContextMenuRequested(e -> { + if (getItem() != null && getListView() != null) { + getListView().getSelectionModel().select(getIndex()); + showGoalPopup(getItem(), e); + } + }); } /** From c9af0d98309dbab12cc1b156b5fcaf15a9a53fe0 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Wed, 7 Oct 2026 18:02:00 +0200 Subject: [PATCH 026/281] key.ui.fx: goal enablement from the proof tree popup (M3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ports Swing ProofTreePopupFactory's SetGoalsBelowEnableStatus: the proof tree popup gains Set All Goals Below to Interactive/Automatic, acting on the open goals below the invoked node (Proof.getSubtreeGoals) — the invoked node is now resolved at popup-open time like the Swing ProofTreeContext (the clicked entry's node, node and branch entries alike). The tree refreshes after the change because the hide-interactive filter depends on the goal states. GoalListViewF now observes its displayed goals with a GoalListener and re-renders the rows (coalesced, selection-preserving) whenever a goal's automatic state changes — this covers changes from the proof tree popup and any other source, so the goal list popup no longer needs its own refresh pass; part of the deferred live goal updates. Verified on :99 with ReverseArray (key.fx.demo.maxsteps=300 for a deterministic two-goal state): both popup directions update the goal list markers, the goal list popup round-trips through the listener, and starting the auto mode with every goal disabled finishes immediately without touching the proof. --- .../key/gui/fx/goallist/GoalListViewF.java | 68 ++++++++++++++++--- .../key/gui/fx/prooftree/ProofTreeViewF.java | 33 ++++++++- 2 files changed, 90 insertions(+), 11 deletions(-) diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/goallist/GoalListViewF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/goallist/GoalListViewF.java index 1147fea9c6c..d4a7292f227 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/goallist/GoalListViewF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/goallist/GoalListViewF.java @@ -31,10 +31,13 @@ import de.uka.ilkd.key.pp.SequentViewLogicPrinter; import de.uka.ilkd.key.pp.VisibleTermLabels; import de.uka.ilkd.key.proof.Goal; +import de.uka.ilkd.key.proof.GoalListener; import de.uka.ilkd.key.proof.Node; import de.uka.ilkd.key.proof.Proof; import org.key_project.logic.Name; +import org.key_project.prover.sequent.SequentChangeInfo; +import org.key_project.util.collection.ImmutableList; import org.key_project.util.javafx.FxUtil; import org.slf4j.Logger; @@ -110,6 +113,9 @@ public boolean contains(Name name) { /** true while this view mutates the items or the ListView selection programmatically */ private boolean updatingSelection; + /** guards against queueing more than one pending goal state refresh (see the listener) */ + private boolean goalStateRefreshPending; + private final KeYSelectionListener selectionListener = new KeYSelectionListener() { @Override public void selectedNodeChanged(KeYSelectionEvent event) { @@ -139,6 +145,38 @@ public GoalListViewF() { .addListener((obs, oldItem, newItem) -> handleListSelection(newItem)); } + /** + * Re-renders the rows when a goal's automatic state changes from anywhere (the goal list + * popup, the proof tree popup, ...). The listener is attached to the displayed goals on + * rebuild; only {@code automaticStateChanged} is handled, sequent changes are covered by the + * proof-level rebuilds. Several goals can change in one batch (the proof tree popup disables + * a whole subtree), so the refreshes are coalesced. + */ + private final GoalListener goalStateListener = new GoalListener() { + @Override + public void automaticStateChanged(Goal source, boolean oldAutomatic, + boolean newAutomatic) { + if (goalStateRefreshPending) { + return; + } + goalStateRefreshPending = true; + FxUtil.runLater(() -> { + goalStateRefreshPending = false; + refreshAfterGoalStateChange(); + }); + } + + @Override + public void sequentChanged(Goal source, SequentChangeInfo sci) { + // covered by the proof-level rebuilds + } + + @Override + public void goalReplaced(Goal source, Node parent, ImmutableList newGoals) { + // covered by the proof-level rebuilds + } + }; + /** * Registers this view as a selection listener on the given model and shows the open goals of * the currently selected proof, if any. @@ -216,6 +254,7 @@ private void rebuild(Proof newProof) { if (proof != newProof) { sequentTextCache.clear(); } + detachGoalStateListener(); proof = newProof; updatingSelection = true; try { @@ -228,6 +267,7 @@ private void rebuild(Proof newProof) { goals.add(goal); } getItems().setAll(goals); + attachGoalStateListener(goals); setPlaceholder(goals.isEmpty() ? closedPlaceholder() : emptyPlaceholder()); } highlightSelectedGoal(); @@ -236,6 +276,20 @@ private void rebuild(Proof newProof) { } } + /** Stops observing the goals that are currently displayed, before the items are replaced. */ + private void detachGoalStateListener() { + for (Goal goal : getItems()) { + goal.removeGoalListener(goalStateListener); + } + } + + /** Observes the displayed goals for automatic state changes (see {@code goalStateListener}). */ + private void attachGoalStateListener(List goals) { + for (Goal goal : goals) { + goal.addGoalListener(goalStateListener); + } + } + /** * @return whether the currently displayed goals differ from the open goals of the displayed * proof (identity and size comparison, cheap enough to run on every selection event) @@ -305,12 +359,10 @@ private void showGoalPopup(Goal goal, ContextMenuEvent e) { } ContextMenu menu = new ContextMenu(); - // DisableSingleGoal: the label flips with the goal's state, the action toggles it + // DisableSingleGoal: the label flips with the goal's state, the action toggles it; the + // row re-render happens through the goal state listener MenuItem single = new MenuItem(goal.isAutomatic() ? "Interactive Goal" : "Automatic Goal"); - single.setOnAction(ev -> { - goal.setEnabled(!goal.isAutomatic()); - refreshAfterGoalStateChange(); - }); + single.setOnAction(ev -> goal.setEnabled(!goal.isAutomatic())); // DisableOtherGoals: all other goals get the opposite of this goal's state; Swing // enables the action only when the model holds more than one goal @@ -324,7 +376,6 @@ private void showGoalPopup(Goal goal, ContextMenuEvent e) { other.setEnabled(enable); } } - refreshAfterGoalStateChange(); }); menu.getItems().addAll(single, others); @@ -333,8 +384,9 @@ private void showGoalPopup(Goal goal, ContextMenuEvent e) { } /** - * Re-renders the rows after a goal state change (Swing {@code GoalList.updateUI}): the items - * are re-set from the proof so every cell recomputes marker text and style. + * Re-renders the rows after a goal state change: the items are re-set from the proof so every + * cell recomputes marker text and style. Invoked by {@code goalStateListener}; keeping the + * selection. */ private void refreshAfterGoalStateChange() { if (proof == null || proof.isDisposed()) { diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/prooftree/ProofTreeViewF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/prooftree/ProofTreeViewF.java index e7200b28bf7..93fc765eb04 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/prooftree/ProofTreeViewF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/prooftree/ProofTreeViewF.java @@ -183,6 +183,9 @@ public String displayText() { */ private TreeItem popupBranchItem; + /** the node the popup was invoked on (Swing {@code ProofTreeContext.invokedNode}) */ + private Node popupNode; + /** * Listens to structural changes of the displayed proof and schedules a coalesced rebuild on * the FX thread — the analogue of the Swing view's {@code GUIProofTreeModel} update calls. @@ -264,8 +267,13 @@ public ProofTreeViewF() { tree.setOnContextMenuRequested(e -> { TreeItem selected = tree.getSelectionModel().getSelectedItem(); TreeItem item = selected != null ? selected : tree.getRoot(); - if (item != null && !item.getValue().isBranch()) { - item = item.getParent(); + if (item != null) { + // Swing ProofTreeContext: the invoked node is the clicked entry's node, for node + // entries and branch entries alike; the branch item is its parent for node entries + popupNode = item.getValue().node(); + if (!item.getValue().isBranch()) { + item = item.getParent(); + } } popupBranchItem = item; contextMenu.show(tree, e.getScreenX(), e.getScreenY()); @@ -821,7 +829,10 @@ private ContextMenu createContextMenu() { actionItem("Collapse Other Branches", null, () -> collapseOthers(popupBranchItem)), new SeparatorMenuItem(), actionItem("Previous Sibling", IconFactoryF.Key.PREVIOUS, () -> gotoSibling(-1)), - actionItem("Next Sibling", IconFactoryF.Key.NEXT, () -> gotoSibling(1))); + actionItem("Next Sibling", IconFactoryF.Key.NEXT, () -> gotoSibling(1)), + new SeparatorMenuItem(), + actionItem("Set All Goals Below to Interactive", null, () -> setGoalsBelow(false)), + actionItem("Set All Goals Below to Automatic", null, () -> setGoalsBelow(true))); menu.setOnShowing(e -> { // pick up changes made elsewhere (e.g. by the classic UI sharing the settings) hideIntermediateItem.setSelected(hideIntermediateSteps()); @@ -978,6 +989,22 @@ private void gotoSibling(int direction) { } } + /** + * Sets the automatic state of all open goals below the node the popup was invoked on (Swing + * {@code SetGoalsBelowEnableStatus}, {@code Proof.getSubtreeGoals}). The goal list picks the + * change up via its goal listener; the tree is refreshed because the "hide interactive + * goals" filter depends on the goal states. + */ + private void setGoalsBelow(boolean enable) { + if (proof == null || proof.isDisposed() || popupNode == null) { + return; + } + for (Goal goal : proof.getSubtreeGoals(popupNode)) { + goal.setEnabled(enable); + } + refresh(); + } + /** * Returns the branch label of the given node, falling back to {@code Case } and storing it * on the node like {@code GUIAbstractTreeNode.ensureBranchLabelIsSet}. From 1dd26ea36f5979f3bd3e1f4b3da77e718c98b27f Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Wed, 7 Oct 2026 18:02:02 +0200 Subject: [PATCH 027/281] key.ui.fx: keep the status bar single-line for long selected terms MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Clicking a term in the sequent view put the term's pretty-printed text into the status bar's right label; a Label's min size defaults to its content, so a long (multi-line) term blew the bar up to hundreds of pixels and squeezed the workspace to a sliver — the window itself kept its size, which made the damage hard to attribute. Both status labels now have min sizes of 0 and ellipsize, and the selected-position text is flattened to one line and capped at 120 characters; the full position stays in the log line used by the M2a verification. Verified on :99 by clicking the ReverseArray program-block term that triggered the squeeze: the layout is untouched and the status bar shows the flattened excerpt. --- .../de/uka/ilkd/key/gui/fx/MainWindowF.java | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) 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 index d341848b8f4..2c687e02454 100644 --- 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 @@ -21,6 +21,7 @@ import javafx.scene.control.Menu; import javafx.scene.control.MenuBar; import javafx.scene.control.MenuItem; +import javafx.scene.control.OverrunStyle; import javafx.scene.control.RadioMenuItem; import javafx.scene.control.SeparatorMenuItem; import javafx.scene.control.ToggleGroup; @@ -262,7 +263,13 @@ private void wireSequentView() { return; } String text = sequentView.getHighlightedText(pos); - statusRight.setText(text.isBlank() ? String.valueOf(pos) : text); + // 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); }); } @@ -759,12 +766,20 @@ private HBox buildStatusBar() { HBox bar = new HBox(); bar.getStyleClass().add("status-bar"); bar.setPadding(new Insets(2, 8, 2, 8)); + // 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.CENTER_LEFT); + statusLeft.setTextOverrun(OverrunStyle.ELLIPSIS); + statusLeft.setMinWidth(0); + statusLeft.setMinHeight(0); statusLeft.setMaxWidth(Double.MAX_VALUE); HBox.setHgrow(statusLeft, Priority.ALWAYS); Region spacer = new Region(); HBox.setHgrow(spacer, Priority.ALWAYS); statusRight.setAlignment(Pos.CENTER_RIGHT); + statusRight.setTextOverrun(OverrunStyle.ELLIPSIS); + statusRight.setMinWidth(0); + statusRight.setMinHeight(0); bar.getChildren().addAll(statusLeft, spacer, statusRight); return bar; } From aa29345dcb7a04fc71fc60102404922c80885881 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Wed, 7 Oct 2026 20:19:12 +0200 Subject: [PATCH 028/281] key.ui.fx: goal list key-hole icons as font glyphs The goal list marker becomes a FontAwesome glyph rendered into a Text node instead of a text label, mirroring the icon choice of the Swing IconCellRenderer: a key for automatic goals (Swing's black key-hole, ekey-mono.gif), a hand for interactive goals (Swing's yellow hand, keyinteractive.gif), a link for goals joined by a one-step simplification (Swing's linked icon, keylinked.gif). IconFactoryF gains the semantic keys KEY_HOLE, KEY_HOLE_INTERACTIVE and KEY_HOLE_LINKED plus a render() helper that swaps the glyph of an existing Text node. The state style classes now set -fx-fill (the marker is a Text node, no longer a Label with -fx-text-fill); automatic keeps the themed text color, matching Swing's black key-hole. --- .../key/gui/fx/fonticons/IconFactoryF.java | 22 ++++++++- .../key/gui/fx/goallist/GoalListViewF.java | 48 +++++++++++++------ .../de/uka/ilkd/key/gui/fx/theme/key-dark.css | 9 ++-- .../uka/ilkd/key/gui/fx/theme/key-light.css | 9 ++-- 4 files changed, 63 insertions(+), 25 deletions(-) diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/fonticons/IconFactoryF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/fonticons/IconFactoryF.java index cfb1fdc6b65..f350f405606 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/fonticons/IconFactoryF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/fonticons/IconFactoryF.java @@ -61,7 +61,10 @@ public enum Key { AUTO_MODE_STOP(FontAwesomeSolid.STOP_CIRCLE), PROOF_TREE(FontAwesomeSolid.SITEMAP), INFO_VIEW(FontAwesomeSolid.INFO_CIRCLE), - PROOF_SEARCH_STRATEGY(FontAwesomeSolid.COG); + PROOF_SEARCH_STRATEGY(FontAwesomeSolid.COG), + KEY_HOLE(FontAwesomeSolid.KEY), + KEY_HOLE_INTERACTIVE(FontAwesomeSolid.HAND_PAPER), + KEY_HOLE_LINKED(FontAwesomeSolid.LINK); private final IconFont glyph; @@ -119,8 +122,23 @@ public static Node createIcon(IconFont glyph) { * @return a new icon node */ public static Node createIcon(IconFont glyph, double size) { - Text text = new Text(String.valueOf(glyph.getUnicode())); + Text text = render(new Text(), glyph, size); text.getStyleClass().add(ICON_STYLE_CLASS); + return text; + } + + /** + * Renders the given glyph into an existing text node, for icon nodes that swap glyphs without + * being recreated (the node keeps its style classes). The caller adds the icon style class and + * the themed fill classes. + * + * @param text the text node to render into + * @param glyph the glyph + * @param size the font size of the icon + * @return the text node, rendering the glyph + */ + public static Text render(Text text, IconFont glyph, double size) { + text.setText(String.valueOf(glyph.getUnicode())); javafx.scene.text.Font base = glyph.getFont(); text.setFont(Font.font(base.getFamily(), size)); return text; diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/goallist/GoalListViewF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/goallist/GoalListViewF.java index d4a7292f227..0c5599051ec 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/goallist/GoalListViewF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/goallist/GoalListViewF.java @@ -10,6 +10,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import javafx.geometry.Pos; import javafx.scene.control.ContextMenu; import javafx.scene.control.Label; import javafx.scene.control.ListCell; @@ -21,11 +22,14 @@ import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; import javafx.scene.layout.Region; +import javafx.scene.text.Text; 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.configuration.ConfigF; +import de.uka.ilkd.key.gui.fx.fonticons.FontAwesomeSolid; +import de.uka.ilkd.key.gui.fx.fonticons.IconFactoryF; import de.uka.ilkd.key.logic.label.TermLabel; import de.uka.ilkd.key.pp.NotationInfo; import de.uka.ilkd.key.pp.SequentViewLogicPrinter; @@ -56,8 +60,9 @@ * user's cursor. Clicking a row selects the goal in the {@link KeYSelectionModel}; the ListView * selection is kept in sync with the model's selected goal. *

- * A cell shows the node serial ({@code #n}), an automatic/interactive/linked marker (text - * analogue of the key-hole icons of the Swing {@code IconCellRenderer}), and a one-line printed + * A cell shows the node serial ({@code #n}), an automatic/interactive/linked marker icon (font + * glyphs standing in for the key-hole icons of the Swing {@code IconCellRenderer}), and a + * one-line printed * sequent truncated to {@value #MAX_DISPLAYED_SEQUENT_LENGTH} characters, exactly like the Swing * renderer. The sequent text is produced by the core pretty printer * ({@link SequentViewLogicPrinter} with {@link NotationInfo} and the proof's {@code Services}, @@ -442,8 +447,8 @@ private String sequentText(Goal goal) { } /** - * @return the marker text of the goal, the text analogue of the icon choice of the Swing - * {@code IconCellRenderer} (linked, key-hole for automatic, disabled key-hole for + * @return the marker text of the goal for the cell tooltip, the text analogue of the icon + * choice of the Swing {@code IconCellRenderer} (linked, key-hole for automatic, * interactive) */ private static String markerText(Goal goal) { @@ -453,6 +458,19 @@ private static String markerText(Goal goal) { return goal.isAutomatic() ? "automatic" : "interactive"; } + /** + * @return the glyph of the goal's marker icon, the font-glyph counterpart of the icon choice + * of the Swing {@code IconCellRenderer}: a key-hole for automatic goals (Swing's black + * key-hole), a hand for interactive goals (Swing's yellow hand over the key-hole), a + * link for goals joined by a one-step simplification (Swing's linked icon) + */ + private static FontAwesomeSolid markerGlyph(Goal goal) { + if (goal.isLinked()) { + return FontAwesomeSolid.LINK; + } + return goal.isAutomatic() ? FontAwesomeSolid.KEY : FontAwesomeSolid.HAND_PAPER; + } + /** * @return the style class highlighting the marker of the goal */ @@ -482,30 +500,30 @@ private static Label closedPlaceholder() { } /** - * The cell rendering: node serial, goal marker, one-line sequent; selected state comes from - * the ListView selection (styled via {@code .goal-list-cell:selected}). + * The cell rendering: node serial, goal state icon (the Swing key-hole icons as font glyphs), + * one-line sequent; selected state comes from the ListView selection (styled via + * {@code .goal-list-cell:selected}). */ private final class GoalListCell extends ListCell { private final Label nameLabel = new Label(); - private final Label markerLabel = new Label(); + private final Text markerIcon = new Text(); private final Label sequentLabel = new Label(); private final HBox content; GoalListCell() { nameLabel.getStyleClass().add("goal-list-label"); - markerLabel.getStyleClass().add("goal-list-marker"); + markerIcon.getStyleClass().add("goal-list-marker"); sequentLabel.getStyleClass().add("goal-list-sequent"); nameLabel.setFont(ConfigF.DEFAULT.systemFont()); - markerLabel.setFont(ConfigF.DEFAULT.systemFont()); sequentLabel.setFont(ConfigF.DEFAULT.monoFont()); - // keep the node serial and the marker at their preferred size: only the sequent - // label shrinks and ellipsizes when the cell is too narrow + // keep the node serial at its preferred size: only the sequent label shrinks and + // ellipsizes when the cell is too narrow nameLabel.setMinWidth(Region.USE_PREF_SIZE); - markerLabel.setMinWidth(Region.USE_PREF_SIZE); sequentLabel.setMaxWidth(Double.MAX_VALUE); sequentLabel.setTextOverrun(OverrunStyle.ELLIPSIS); HBox.setHgrow(sequentLabel, Priority.ALWAYS); - content = new HBox(6, nameLabel, markerLabel, sequentLabel); + content = new HBox(6, nameLabel, markerIcon, sequentLabel); + content.setAlignment(Pos.CENTER_LEFT); // Swing GoalList's mouse listener selects the row under the pointer before the popup // opens; the popup acts on the (now selected) goal setOnContextMenuRequested(e -> { @@ -543,9 +561,9 @@ protected void updateItem(Goal goal, boolean empty) { } String text = sequentText(goal); String marker = markerText(goal); - markerLabel.getStyleClass().setAll("goal-list-marker", markerStyleClass(goal)); + markerIcon.getStyleClass().setAll("goal-list-marker", markerStyleClass(goal)); + IconFactoryF.render(markerIcon, markerGlyph(goal), IconFactoryF.DEFAULT_SIZE); nameLabel.setText("#" + goal.node().serialNr()); - markerLabel.setText(marker); sequentLabel.setText(text); setGraphic(content); setText(null); diff --git a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css index 035a8d6d2ad..8cc517e2b2d 100644 --- a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css +++ b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css @@ -316,20 +316,21 @@ -fx-text-fill: -key-text-secondary; } +/* the marker is a font glyph (a Text node); its size comes from IconFactoryF.DEFAULT_SIZE */ .goal-list-marker { - -fx-font-size: 0.9em; + -fx-fill: -key-text; } .goal-list-automatic { - -fx-text-fill: -key-success; + -fx-fill: -key-text; } .goal-list-interactive { - -fx-text-fill: -key-interactive; + -fx-fill: -key-interactive; } .goal-list-linked { - -fx-text-fill: -key-linked; + -fx-fill: -key-linked; } .goal-list-sequent { diff --git a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css index bc30b607f08..b3a2d0ab1ea 100644 --- a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css +++ b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css @@ -312,20 +312,21 @@ -fx-text-fill: -key-text-secondary; } +/* the marker is a font glyph (a Text node); its size comes from IconFactoryF.DEFAULT_SIZE */ .goal-list-marker { - -fx-font-size: 0.9em; + -fx-fill: -key-text; } .goal-list-automatic { - -fx-text-fill: -key-success; + -fx-fill: -key-text; } .goal-list-interactive { - -fx-text-fill: -key-interactive; + -fx-fill: -key-interactive; } .goal-list-linked { - -fx-text-fill: -key-linked; + -fx-fill: -key-linked; } .goal-list-sequent { From 61f339b53975e1e7f48104e21a458a5900f1e888 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Wed, 7 Oct 2026 20:33:16 +0200 Subject: [PATCH 029/281] key.ui.fx: source view (first version, RichTextFX) First JavaFX version of the Swing source view (de.uka.ilkd.key.gui.sourceview.SourceView, tabbed read-only JTextPanes): - SourceViewF: a read-only StyleClassedTextArea with line numbers and the configured monospaced font. The relevant source files are obtained exactly like in the Swing view: the proof's ProofJavaSourceCollection is populated from the FileOrigin labels of the root sequent and read through the proof's FileRepo. Pure .key problems without Java source (e.g. the Agatha example) fall back to the loaded problem file. Reading and highlighting run on a background thread, applied to the document on the FX thread (generation guard against stale loads); the view scrolls to the top after loading. - SourceHighlighter: minimal Java/JML highlighting as a single regex pass (subset of the Swing JavaJMLEditorLexer: keywords, comments, strings, annotations, JML keywords inside annotation regions) producing StyleSpans with the source-* style classes, themed via the stylesheets. - SourceViewDriver: standalone FxDriver for per-view verification. - MainWindowF: the Source dockable (right side in the default layout) now shows the real view with a one-line file header; the demo loader registers the problem file as fallback and the content hook logs the self test. - gradle/libs.versions.toml: richtextfx 0.11.5 -> 0.11.7; 0.11.5 is incompatible with JavaFX 25 (IncompatibleClassChangeError, FXMisc/ RichTextFX#1282, fixed in 0.11.7; public API unchanged). --- gradle/libs.versions.toml | 2 +- .../de/uka/ilkd/key/gui/fx/MainWindowF.java | 29 +- .../gui/fx/sourceview/SourceHighlighter.java | 273 +++++++++++ .../gui/fx/sourceview/SourceViewDriver.java | 95 ++++ .../key/gui/fx/sourceview/SourceViewF.java | 445 ++++++++++++++++++ .../de/uka/ilkd/key/gui/fx/theme/key-dark.css | 48 ++ .../uka/ilkd/key/gui/fx/theme/key-light.css | 48 ++ 7 files changed, 938 insertions(+), 2 deletions(-) create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/sourceview/SourceHighlighter.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/sourceview/SourceViewDriver.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/sourceview/SourceViewF.java diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e6f297adabb..7b618eefc62 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -34,7 +34,7 @@ docking-frames = "1.1.3p1" # JavaFX user interface (key.ui.fx) javafx = "25.0.4" -richtextfx = "0.11.5" +richtextfx = "0.11.7" openjfx-plugin = "0.1.0" # External tools 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 index 2c687e02454..5caf0d2f9ea 100644 --- 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 @@ -61,6 +61,7 @@ import de.uka.ilkd.key.gui.fx.prooftree.ProofTreeViewF; import de.uka.ilkd.key.gui.fx.settings.SettingsManagerF; import de.uka.ilkd.key.gui.fx.settings.ThemeSettingsProviderF; +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.theme.Theme; import de.uka.ilkd.key.gui.fx.theme.ThemeManager; @@ -151,6 +152,13 @@ public final class MainWindowF { */ 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(); + /** * 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 @@ -256,6 +264,10 @@ private void wireSequentView() { infoView.attach(selectionModel); goalListView.attach(selectionModel); strategyView.attach(selectionModel); + sourceView.attach(selectionModel); + // 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) { @@ -285,6 +297,8 @@ private void startDemoProofLoad() { return; } Path location = Path.of(file); + // pure .key problems carry no Java source; the source view then shows the problem file + sourceView.setFallbackSourceFile(location); Task> loadTask = new Task<>() { @Override protected KeYEnvironment call() throws Exception { @@ -532,7 +546,20 @@ private void buildDockables() { dockables.put(ID_STRATEGY, new SimpleDockable(ID_STRATEGY, "Strategy", strategyView)); dockables.put(ID_SEQUENT, new SimpleDockable(ID_SEQUENT, "Sequent", sequentView)); - registerDockable(ID_SOURCE_VIEW, "Source"); + dockables.put(ID_SOURCE_VIEW, + new SimpleDockable(ID_SOURCE_VIEW, "Source", buildSourceViewContent())); + } + + /** + * 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 void registerDockable(String id, String title) { diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/sourceview/SourceHighlighter.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/sourceview/SourceHighlighter.java new file mode 100644 index 00000000000..2e3dd000fb6 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/sourceview/SourceHighlighter.java @@ -0,0 +1,273 @@ +/* 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.sourceview; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import org.fxmisc.richtext.model.StyleSpans; +import org.fxmisc.richtext.model.StyleSpansBuilder; + +/** + * Minimal Java/JML syntax highlighter for the first JavaFX source view (milestone M2, read-only + * version). It is a small subset of the Swing original + * {@code de.uka.ilkd.key.gui.sourceview.JavaJMLEditorLexer}: the Swing lexer is a character-wise + * state machine with modes for normal text, block/line comments, JavaDoc, JML annotations and + * Java annotations; here the same categories are recognized by a single left-to-right regex pass + * (alternation order encodes the priority, e.g. {@code /*@} before {@code /*}) plus a second pass + * over annotation regions for a trimmed JML keyword set. + *

+ * The result is a flat {@link StyleSpans} value covering the entire text; segments carry style + * classes (see the {@code source-*} constants) which are colored by the theme stylesheets + * ({@code key-light.css} / {@code key-dark.css}), mirroring how the Swing lexer's token + * attributes map onto theme colors. + *

+ * Deliberately not ported (deferred): JML annotation markers with feature lists ({@code /*+@}), + * JavaDoc distinct from comments, number literals, and the incremental re-parsing (the Swing + * document re-parses on edits; this view is read-only). + */ +public final class SourceHighlighter { + + /** Style class of plain (unmatched) text segments; themes give it the regular text color. */ + public static final String CLASS_PLAIN = "source-text"; + + /** Style class of Java keywords. */ + public static final String CLASS_KEYWORD = "source-keyword"; + + /** Style class of block and line comments (JavaDoc is folded into this category). */ + public static final String CLASS_COMMENT = "source-comment"; + + /** Style class of string and character literals. */ + public static final String CLASS_STRING = "source-string"; + + /** + * Style class of JML annotations ({@code /*@ ... *}{@code /}, {@code //@}) and Java annotations + * ({@code @Word}). + */ + public static final String CLASS_ANNOTATION = "source-annotation"; + + /** + * Style class of JML keywords inside annotation regions (applied in addition to + * {@link #CLASS_ANNOTATION}). + */ + public static final String CLASS_JML_KEYWORD = "source-jml-keyword"; + + /** + * Style class of placeholder texts (e.g. "No source loaded") rendered by {@link SourceViewF}. + */ + public static final String CLASS_PLACEHOLDER = "source-placeholder"; + + /** + * The Java keywords of the Swing lexer, reused unchanged from + * {@code de.uka.ilkd.key.gui.sourceview.JavaJMLEditorLexer.KEYWORDS} (module {@code key.ui}). + */ + private static final String[] JAVA_KEYWORDS = { "abstract", "assert", "boolean", "break", + "byte", "case", "catch", "char", "class", "continue", "default", "do", "double", "else", + "enum", "extends", "final", "finally", "float", "for", "if", "implements", "import", + "instanceof", "int", "interface", "long", "native", "new", "package", "private", + "protected", "public", "return", "short", "static", "strictfp", "super", "switch", + "synchronized", "this", "throw", "throws", "transient", "try", "void", "volatile", + "while", "true", "false", "null" }; + + /** + * Trimmed subset of the JML keywords of {@code JavaJMLEditorLexer.JMLKEYWORDS} (the Swing list + * has more than 200 entries; this keeps the common specification clauses and expressions). + * Applied only inside annotation regions, so it cannot clash with Java identifiers. + */ + private static final String[] JML_KEYWORDS = { + // clause keywords + "requires", "ensures", "assignable", "assignable_free", "modifiable", "modifiable_free", + "modifies", "accessible", "decreases", "diverges", "loop_invariant", "measured_by", + "maintaining", "signals", "signals_only", "unreachable", "when", + // behavior/invariant-like keywords + "normal_behavior", "normal_behaviour", "exceptional_behavior", "exceptional_behaviour", + "behavior", "behaviour", "also", "axiom", "invariant", "initially", "constraint", + "represents", "for_example", "implies_that", "hence_by", + // modifiers + "ghost", "model", "pure", "helper", "nullable", "non_null", "spec_public", + "spec_protected", "instance", "peer", "code", "extract", "two_state", + // special JML expressions (backslash forms; Pattern.quote escapes them safely) + "\\old", "\\result", "\\forall", "\\exists", "\\fresh", "\\at", "\\reach", "\\seq", + "\\seq_contains", "\\bigint", "\\real", "\\locset", "\\TYPE", "\\everything", + "\\nothing", "\\not_specified", "\\same", "\\type", "\\typeof", "\\invariant_for", + "\\working_space", "\\lockset", "\\duration", "\\index", "\\min", "\\max", "\\sum", + "\\num_of", "\\only_assigned", "\\not_assigned", "\\not_modified" }; + + /** + * Alternation order matters: at each position the first matching alternative wins. Each + * alternative is wrapped in exactly one capturing group (nested groups are non-capturing), + * so the group index identifies the token category. + */ + private static final Pattern TOKEN_PATTERN = Pattern + .compile(String.join("|", + // 1: JML block annotation /*@ ... @*/ (lazy: stops at the first "*"+"/") + "(/\\*@[\\s\\S]*?\\*/)", + // 2: block comment / JavaDoc /* ... */ + "(/\\*[\\s\\S]*?\\*/)", + // 3: single-line JML annotation //@ ... (before plain //) + "(//@[^\n]*)", + // 4: line comment + "(//[^\n]*)", + // 5: string literal (no newline) + "(\"(?:\\\\.|[^\"\\\\\n])*\")", + // 6: character literal + "('(?:\\\\.|[^'\\\\\n])*')", + // 7: Java annotation @Name.dotted + "(@[A-Za-z_$][A-Za-z0-9_$.]*)", + // 8: Java keyword + "\\b((?:" + String.join("|", JAVA_KEYWORDS) + "))\\b")); + + /** Number of capturing groups in {@link #TOKEN_PATTERN}. */ + private static final int GROUP_COUNT = 8; + + /** Style class per group index of {@link #TOKEN_PATTERN} (index 0 is unused). */ + private static final String[] CLASS_BY_GROUP = { null, + CLASS_ANNOTATION, CLASS_COMMENT, CLASS_ANNOTATION, CLASS_COMMENT, CLASS_STRING, + CLASS_STRING, CLASS_ANNOTATION, CLASS_KEYWORD }; + + /** Groups that are JML regions and therefore receive the JML keyword overlay. */ + private static final boolean[] JML_REGION_BY_GROUP = { false, true, false, true, false, false, + false, false, false }; + + /** JML keyword pattern, used on annotation region texts only. */ + private static final Pattern JML_KEYWORD_PATTERN = Pattern.compile( + "(? PLAIN_STYLES = List.of(CLASS_PLAIN); + private static final List ANNOTATION_STYLES = List.of(CLASS_ANNOTATION); + private static final List ANNOTATION_JML_STYLES = + List.of(CLASS_ANNOTATION, CLASS_JML_KEYWORD); + + private SourceHighlighter() { + } + + /** + * Tokenizes the given text into style spans covering the whole text. Run this off the + * JavaFX-critical path (e.g. on a loader thread) and apply the spans on the FX thread with + * {@code setStyleSpans(0, spans)}. + * + * @param text the text to highlight, may be empty + * @return the spans together with the per-category token counts (see {@link Result}) + */ + public static Result highlight(String text) { + if (text == null || text.isEmpty()) { + return Result.EMPTY; + } + List spans = new ArrayList<>(); + int keywords = 0; + int comments = 0; + int strings = 0; + int annotations = 0; + int jmlKeywords = 0; + int pos = 0; + Matcher matcher = TOKEN_PATTERN.matcher(text); + while (matcher.find()) { + if (matcher.start() > pos) { + spans.add(new Span(pos, matcher.start(), PLAIN_STYLES)); + } + int group = matchedGroup(matcher); + String token = matcher.group(); + if (JML_REGION_BY_GROUP[group]) { + // JML region: split at JML keywords and stack the second style class on them + int from = matcher.start(); + Matcher jmlMatcher = JML_KEYWORD_PATTERN.matcher(token); + int jpos = 0; + while (jmlMatcher.find()) { + if (jmlMatcher.start() > jpos) { + spans.add(new Span(from + jpos, from + jmlMatcher.start(), + ANNOTATION_STYLES)); + } + spans.add(new Span(from + jmlMatcher.start(), from + jmlMatcher.end(), + ANNOTATION_JML_STYLES)); + jmlKeywords++; + jpos = jmlMatcher.end(); + } + if (jpos < token.length()) { + spans.add(new Span(from + jpos, matcher.end(), ANNOTATION_STYLES)); + } + } else { + spans.add(new Span(matcher.start(), matcher.end(), List.of(CLASS_BY_GROUP[group]))); + } + switch (group) { + case 1, 3, 7 -> annotations++; + case 2, 4 -> comments++; + case 5, 6 -> strings++; + case 8 -> keywords++; + default -> { + } + } + pos = matcher.end(); + } + if (pos < text.length()) { + spans.add(new Span(pos, text.length(), PLAIN_STYLES)); + } + StyleSpansBuilder> builder = new StyleSpansBuilder<>(); + for (Span span : spans) { + builder.add(span.classes(), span.to() - span.from()); + } + int total = keywords + comments + strings + annotations + jmlKeywords; + return new Result(text.length(), keywords, comments, strings, annotations, jmlKeywords, + total, builder.create()); + } + + /** + * Builds the single placeholder span for an explanatory text (e.g. "No source loaded"). + * + * @param text the placeholder text, must not be empty + * @return spans styling the whole text with {@link #CLASS_PLACEHOLDER} + */ + public static StyleSpans> placeholderSpans(String text) { + StyleSpansBuilder> builder = new StyleSpansBuilder<>(); + builder.add(List.of(CLASS_PLACEHOLDER), text.length()); + return builder.create(); + } + + /** + * @return the first capturing group of the given match (all alternatives are groups) + */ + private static int matchedGroup(Matcher matcher) { + for (int group = 1; group <= GROUP_COUNT; group++) { + if (matcher.group(group) != null) { + return group; + } + } + throw new AssertionError("Token without a matching group"); + } + + /** + * One highlighted region of the text; the class list is the style of the region. + */ + private record Span(int from, int to, List classes) { + } + + /** + * The outcome of one highlighting run: the spans for the whole text plus the token counts per + * category. Used by {@link SourceViewF#verifySourceView()} for the self-test report. + * + * @param length length of the highlighted text + * @param keywords number of Java keyword tokens + * @param comments number of comment tokens + * @param strings number of string/char literal tokens + * @param annotations number of annotation tokens (JML blocks, //@ lines, @Words) + * @param jmlKeywords number of JML keyword tokens inside annotation regions + * @param total sum of all token counts + * @param spans the style spans covering the whole text ({@code null} only for + * {@link #EMPTY}) + */ + public record Result(int length, int keywords, int comments, int strings, int annotations, + int jmlKeywords, int total, StyleSpans> spans) { + + /** The empty result (no text, no tokens, no spans). */ + public static final Result EMPTY = + new Result(0, 0, 0, 0, 0, 0, 0, null); + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/sourceview/SourceViewDriver.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/sourceview/SourceViewDriver.java new file mode 100644 index 00000000000..0eb7642f6c5 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/sourceview/SourceViewDriver.java @@ -0,0 +1,95 @@ +/* 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.sourceview; + +import java.nio.file.Path; +import javafx.geometry.Insets; +import javafx.scene.Scene; +import javafx.scene.control.Label; +import javafx.scene.layout.BorderPane; +import javafx.stage.Stage; + +import de.uka.ilkd.key.control.DefaultUserInterfaceControl; +import de.uka.ilkd.key.control.KeYEnvironment; +import de.uka.ilkd.key.core.fx.KeYSelectionModel; +import de.uka.ilkd.key.gui.fx.FxDriver; +import de.uka.ilkd.key.gui.fx.theme.ThemeManager; +import de.uka.ilkd.key.proof.Proof; + +import org.key_project.util.javafx.FxUtil; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Standalone driver for the source view (milestone M2), used with the {@code key.fx.driver} hook + * of {@code MainApplication} for per-view verification without the full main window (same + * pattern as the sequent view spike). Shows a single {@link SourceViewF} with a header label and + * a local {@link KeYSelectionModel} (the {@code ProofBinder} seam is a no-op, since no mediator + * is involved). + *

+ * If the system property {@code key.fx.demo.sequent} names a problem file, it is loaded with the + * core {@link KeYEnvironment} on a background thread; the loaded proof is routed through the + * selection model (which drives the view), and the same file is registered as the + * {@link SourceViewF#setFallbackSourceFile(Path) fallback} so that pure {@code .key} problems + * without Java source (e.g. the Agatha example) still display their content. After the first + * content load the driver logs the view's self-test via {@code Source self test: ...}. + */ +public final class SourceViewDriver implements FxDriver { + + public static final Logger LOGGER = LoggerFactory.getLogger(SourceViewDriver.class); + + @Override + public void start(Stage stage) throws Exception { + SourceViewF view = new SourceViewF(); + + String demoFile = System.getProperty("key.fx.demo.sequent"); + if (demoFile != null && !demoFile.isBlank()) { + // pure .key problems carry no Java source; show the problem file itself in that case + view.setFallbackSourceFile(Path.of(demoFile)); + } + + // own selection model for the driver; the mediator seam is a no-op here + KeYSelectionModel selectionModel = new KeYSelectionModel((newProof, previousProof) -> { + }); + view.attach(selectionModel); + view.setOnContentLoaded(() -> LOGGER.info("Source self test: {}", view.verifySourceView())); + + Label header = new Label("No source loaded"); + header.getStyleClass().add("source-view-header"); + header.textProperty().bind(view.headerTextProperty()); + BorderPane.setMargin(header, new Insets(0)); + BorderPane root = new BorderPane(view, header, null, null, null); + + Scene scene = new Scene(root, 1100, 750); + ThemeManager.getInstance().manage(scene); + stage.setTitle("KeY · Source"); + stage.setScene(scene); + stage.show(); + + if (demoFile == null || demoFile.isBlank()) { + LOGGER.info("No key.fx.demo.sequent set; the source view stays empty. " + + "Start with -Dkey.fx.demo.sequent= to try the source view."); + return; + } + + Path location = Path.of(demoFile); + Thread loader = new Thread(() -> { + try { + KeYEnvironment env = KeYEnvironment.load(location); + Proof proof = env.getLoadedProof(); + LOGGER.info("Demo proof loaded: {}", + proof != null ? proof.name() : "(no proof in " + location + ")"); + FxUtil.runLater(() -> selectionModel.setSelectedProof(proof)); + } catch (Exception e) { + LOGGER.error("Demo proof loading failed", e); + // no proof -> the view keeps/returns to the "No source loaded" placeholder + FxUtil.runLater(() -> selectionModel.setSelectedProof(null)); + } + }, "fx-source-demo-loader"); + loader.setDaemon(true); + loader.start(); + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/sourceview/SourceViewF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/sourceview/SourceViewF.java new file mode 100644 index 00000000000..09e7fab3b9c --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/sourceview/SourceViewF.java @@ -0,0 +1,445 @@ +/* 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.sourceview; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Collection; +import java.util.Objects; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; +import javafx.scene.text.Font; + +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.configuration.ConfigF; +import de.uka.ilkd.key.logic.JTerm; +import de.uka.ilkd.key.logic.label.OriginTermLabel; +import de.uka.ilkd.key.proof.Node; +import de.uka.ilkd.key.proof.Proof; +import de.uka.ilkd.key.proof.ProofJavaSourceCollection; +import de.uka.ilkd.key.proof.io.consistency.FileRepo; + +import org.key_project.util.java.IOUtil; +import org.key_project.util.javafx.FxUtil; + +import org.fxmisc.richtext.Caret; +import org.fxmisc.richtext.LineNumberFactory; +import org.fxmisc.richtext.StyleClassedTextArea; +import org.fxmisc.richtext.model.StyleSpans; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * First JavaFX version of the source view, the counter-part of + * {@code de.uka.ilkd.key.gui.sourceview.SourceView} (a tabbed pane of styled, read-only + * {@code JTextPane}s) in the module {@code key.ui}. + *

+ * Milestone M2, first read-only version. The view is a {@link StyleClassedTextArea} + * (RichTextFX) showing the source file(s) of the selected proof: + *

    + *
  • the relevant files are obtained exactly like in the Swing view: the proof's + * {@link ProofJavaSourceCollection} is populated from the {@link OriginTermLabel.FileOrigin}s of + * the root sequent (port of {@code SourceView.ensureProofJavaSourceCollectionExists}) and read + * through the proof's {@link FileRepo} ({@code proof.getInitConfig().getFileRepo()});
  • + *
  • if the proof has no Java source at all (e.g. pure {@code .key} problem files like the + * Agatha example, whose terms carry no origin labels), the view falls back to the file given via + * {@link #setFallbackSourceFile(Path)} (typically the loaded {@code .key} file) so that the user + * still sees the problem specification; if even that is unavailable, a dimmed + * {@code "No source loaded"} placeholder is shown;
  • + *
  • minimal syntax highlighting (keywords, comments, strings, annotations, JML keywords inside + * annotations) is computed by {@link SourceHighlighter} on a background thread and applied in one + * {@code setStyleSpans} call on the FX thread;
  • + *
  • line numbers come from {@link LineNumberFactory}, the view is not editable and uses the + * monospaced font from {@link ConfigF#DEFAULT}.
  • + *
+ *

+ * Deliberately deferred to later milestones (the Swing view does much more): the tab-per-file UI, + * the proof-node ↔ source-line mapping (symbolic execution highlighting derived from the + * {@code PositionInfo} of the active statements), tooltips, the click-to-jump navigation into the + * proof tree, and re-highlighting on {@code selectedNodeChanged}. + */ +public class SourceViewF extends StyleClassedTextArea { + + private static final Logger LOGGER = LoggerFactory.getLogger(SourceViewF.class); + + /** + * Placeholder for an empty view (as in the Swing source view's status bar). + */ + public static final String NO_SOURCE = "No source loaded"; + + /** + * Placeholder for a file that could not be read (as in the Swing source view's tabs). + */ + private static final String SOURCE_COULD_NOT_BE_LOADED = "[SOURCE COULD NOT BE LOADED]"; + + /** + * Indicates how many spaces are inserted instead of one tab (used in the Swing source view). + */ + private static final int TAB_SIZE = 4; + + private KeYSelectionModel selectionModel; + private Proof proof; + /** identity of the currently shown source (URI or fallback path), used by the reload guard */ + private String shownKey = ""; + /** guards against applying the result of a stale background load */ + private int generation; + /** the last highlighting result applied to the document (volatile: read by the self-test) */ + private volatile SourceHighlighter.Result applied = SourceHighlighter.Result.EMPTY; + /** + * optional fallback file (e.g. the loaded .key file) shown when the proof has no Java source + */ + private volatile Path fallbackSourceFile; + /** invoked on the FX thread after real source content (not a placeholder) was applied */ + private Runnable onContentLoaded = () -> { + }; + + private final StringProperty headerText = + new SimpleStringProperty(this, "headerText", NO_SOURCE); + + private final KeYSelectionListener selectionListener = new KeYSelectionListener() { + @Override + public void selectedNodeChanged(KeYSelectionEvent event) { + // deferred: the Swing view updates the symbolic execution highlights here + } + + @Override + public void selectedProofChanged(KeYSelectionEvent event) { + reload(event.getSource().getSelectedProof()); + } + }; + + /** + * Creates an empty source view showing the "No source loaded" placeholder. + */ + public SourceViewF() { + getStyleClass().add("source-view"); + setEditable(false); + setWrapText(false); + getCaretSelectionBind().setShowCaret(Caret.CaretVisibility.OFF); + paragraphGraphicFactoryProperty().set(LineNumberFactory.get(this)); + applyMonoFont(); + showPlaceholder(NO_SOURCE); + } + + /** + * Applies the monospaced font configured by {@link ConfigF} to the view. The font is set as + * an inline style on the area node (the only way to feed a runtime {@link Font} into + * RichTextFX's style-classed text); the segment colors themselves come from the theme + * stylesheets via the {@code source-*} style classes. + */ + private void applyMonoFont() { + Font font = ConfigF.DEFAULT.monoFont(); + setStyle("-fx-font-family: \"" + font.getFamily() + "\"; -fx-font-size: " + + font.getSize() + "px;"); + LOGGER.debug("Source view font: family={}, size={}", font.getFamily(), font.getSize()); + } + + /** + * Registers this view as a selection listener on the given model and shows the source of the + * currently selected proof, if any (same {@code attach} pattern as the other M2 views). + * + * @param model the selection model to observe + */ + public void attach(KeYSelectionModel model) { + Objects.requireNonNull(model); + if (selectionModel == model) { + return; + } + if (selectionModel != null) { + selectionModel.removeKeYSelectionListener(selectionListener); + } + selectionModel = model; + model.addKeYSelectionListenerChecked(selectionListener); + reload(model.getSelectedProof()); + } + + /** + * Sets the fallback file shown when the selected proof carries no Java source (pure + * {@code .key} problem files have no {@link OriginTermLabel.FileOrigin}s in their sequent, so + * the {@link ProofJavaSourceCollection} stays empty). The standalone drivers pass the loaded + * problem file here; {@code MainWindowF} may keep this unset and always show the placeholder. + * + * @param file the file to fall back to, or {@code null} to disable the fallback + */ + public void setFallbackSourceFile(Path file) { + fallbackSourceFile = file; + } + + /** + * Text describing the currently shown source (file name and mode); bind a header label to + * it. Updated by the view whenever new content is applied. + * + * @return the header text property + */ + public StringProperty headerTextProperty() { + return headerText; + } + + /** + * Registers a hook that is invoked on the FX thread after real source content (not the + * placeholder) has been applied. Used by the drivers to run the self-test after display. + * + * @param hook the hook to run, may be {@code null} to clear it + */ + public void setOnContentLoaded(Runnable hook) { + onContentLoaded = hook != null ? hook : () -> { + }; + } + + /** + * Reloads the view for the given proof. Skips the reload if the same proof would show the + * same file again. The source file is determined on the FX thread (this registers the + * {@link ProofJavaSourceCollection} like the Swing view does), while reading the file and + * computing the highlighting run on a background thread. + * + * @param newProof the proof whose source should be shown, may be {@code null} + */ + private void reload(Proof newProof) { + if (!FxUtil.isFxThread()) { + FxUtil.runLater(() -> reload(newProof)); + return; + } + URI uri = newProof != null ? ensureAndPickSourceFile(newProof) : null; + Path fallback = uri == null && newProof != null ? fallbackSourceFile : null; + String key = uri != null ? uri.toString() + : fallback != null ? fallback.toString() : ""; + if (newProof == proof && key.equals(shownKey)) { + return; // guard: same proof, same file + } + proof = newProof; + shownKey = key; + final int gen = ++generation; + if (newProof == null) { + showPlaceholder(NO_SOURCE); + return; + } + Thread loader = new Thread(() -> { + Loaded loaded = readAndHighlight(gen, newProof, uri, fallback); + FxUtil.runLater(() -> apply(loaded)); + }, "fx-source-view-loader"); + loader.setDaemon(true); + loader.start(); + } + + /** + * Ports {@code SourceView.ensureProofJavaSourceCollectionExists} of the Swing view: makes + * sure the proof has a {@link ProofJavaSourceCollection} and populates it from the + * {@link OriginTermLabel.FileOrigin}s found on the terms of the root sequent. Then returns + * one of the relevant files (the first; the Swing view opens all of them in tabs, which is + * deferred here). + * + * @param proof the proof to inspect, not {@code null} + * @return a relevant source file URI, or {@code null} if the proof has none + */ + private static URI ensureAndPickSourceFile(Proof proof) { + if (proof.lookup(ProofJavaSourceCollection.class) == null) { + final var sources = new ProofJavaSourceCollection(); + proof.register(sources, ProofJavaSourceCollection.class); + proof.root().sequent().forEach(formula -> { + OriginTermLabel originLabel = + (OriginTermLabel) ((JTerm) formula.formula()).getLabel(OriginTermLabel.NAME); + if (originLabel != null) { + if (originLabel.getOrigin() instanceof OriginTermLabel.FileOrigin fileOrigin) { + fileOrigin.getFileName().ifPresent(sources::addRelevantFile); + } + + originLabel.getSubtermOrigins().stream() + .filter(o -> o instanceof OriginTermLabel.FileOrigin) + .map(o -> (OriginTermLabel.FileOrigin) o) + .forEach(o -> o.getFileName().ifPresent(sources::addRelevantFile)); + } + }); + } + ProofJavaSourceCollection sources = proof.lookup(ProofJavaSourceCollection.class); + return sources != null && !sources.getRelevantFiles().isEmpty() + ? sources.getRelevantFiles().iterator().next() + : null; + } + + /** + * Reads the source text and computes the highlighting off the FX-critical path. + *

+ * Tries, in order: the relevant file of the proof's source collection (via the proof's + * {@link FileRepo}, like the Swing view), then the fallback file ({@code .key} mode), then + * the "No source loaded" placeholder. + * + * @param gen the reload generation this load belongs to + * @param proof the proof (source of the {@link FileRepo}), not {@code null} + * @param uri the relevant source file, may be {@code null} + * @param fallback the fallback file, may be {@code null} + * @return the loaded content together with the highlighting result + */ + private static Loaded readAndHighlight(int gen, Proof proof, URI uri, Path fallback) { + if (uri != null) { + Loaded loaded = readRepoFile(gen, proof, uri); + if (loaded != null) { + return loaded; + } + } + if (fallback != null) { + Loaded loaded = readLocalFile(gen, fallback); + if (loaded != null) { + return loaded; + } + } + return Loaded.placeholder(gen, NO_SOURCE); + } + + /** + * Reads a relevant file of the proof through the proof's {@link FileRepo} (the Swing view's + * {@code addFile} path: {@code repo.getInputStream(fileURI.toURL())}). + * + * @return the loaded content, or {@code null} if the file could not be read + */ + private static Loaded readRepoFile(int gen, Proof proof, URI uri) { + try { + FileRepo repo = proof.getInitConfig().getFileRepo(); + try (InputStream is = repo.getInputStream(uri.toURL())) { + if (is == null) { + LOGGER.debug("FileRepo has no stream for {}", uri); + return null; + } + return build(gen, simpleFileName(uri), IOUtil.readFrom(is)); + } + } catch (IOException e) { + LOGGER.debug("Could not read source file {}", uri, e); + return null; + } + } + + /** + * Reads the fallback file directly from disk ({@code .key} mode: the proof has no Java + * source, so the problem specification is shown instead). + * + * @return the loaded content, or {@code null} if the file could not be read + */ + private static Loaded readLocalFile(int gen, Path file) { + try (InputStream is = Files.newInputStream(file)) { + String header = file.getFileName() + + " (no Java source in proof — showing problem file)"; + return build(gen, header, IOUtil.readFrom(is)); + } catch (IOException e) { + LOGGER.debug("Could not read fallback source file {}", file, e); + return null; + } + } + + /** + * Computes the highlighting for the given text (replacing tabs like the Swing view). + * + * @return the loaded content, or the "could not be loaded" placeholder on I/O errors + */ + private static Loaded build(int gen, String header, String text) { + String cleaned = replaceTabs(text); + if (cleaned.isBlank()) { + return Loaded.placeholder(gen, SOURCE_COULD_NOT_BE_LOADED); + } + SourceHighlighter.Result result = SourceHighlighter.highlight(cleaned); + int lines = cleaned.split("\n", -1).length; + return new Loaded(gen, header + " · " + lines + " lines", cleaned, result.spans(), result, + true); + } + + /** + * Applies a finished background load to the document (FX thread only). + */ + private void apply(Loaded loaded) { + FxUtil.assertFxThread(); + if (loaded.generation() != generation) { + LOGGER.debug("Ignoring stale source load (generation {})", loaded.generation()); + return; + } + clear(); + if (!loaded.text().isEmpty()) { + appendText(loaded.text()); + if (loaded.spans() != null) { + setStyleSpans(0, loaded.spans()); + } + // appendText parks the caret (and the scroll) at the end; the Swing view shows the + // top of the file + showParagraphAtTop(0); + } + applied = loaded.result(); + headerText.set(loaded.header()); + if (loaded.success() && !loaded.text().isEmpty()) { + onContentLoaded.run(); + } + } + + /** + * Shows a dimmed placeholder text (no proof, or nothing could be loaded). + */ + private void showPlaceholder(String text) { + FxUtil.assertFxThread(); + clear(); + applied = SourceHighlighter.Result.EMPTY; + headerText.set(NO_SOURCE); + if (!text.isEmpty()) { + appendText(text); + setStyleSpans(0, SourceHighlighter.placeholderSpans(text)); + showParagraphAtTop(0); + } + } + + /** + * Development self-test (M2): verifies that the view shows non-empty source text and that + * the highlighting produced highlighted spans (counted per category while tokenizing; the + * categories themselves may legitimately be zero, e.g. no string literals in a {@code .key} + * file). Also checks that the applied text length matches the document length. + * + * @return a one-line report ending in {@code PASS} or {@code FAIL} + */ + public String verifySourceView() { + SourceHighlighter.Result result = applied; + if (result == null || result.length() <= 0 || getLength() <= 0) { + return "len=" + (result == null ? 0 : result.length()) + " textLen=" + getLength() + + " total=0 FAIL (no source loaded)"; + } + boolean pass = result.length() == getLength() && result.total() > 0; + return "len=" + result.length() + " textLen=" + getLength() + " keywords=" + + result.keywords() + " comments=" + result.comments() + " strings=" + result.strings() + + " annotations=" + result.annotations() + " jmlKeywords=" + result.jmlKeywords() + + " total=" + result.total() + " " + (pass ? "PASS" : "FAIL"); + } + + /** + * Replaces each tab by {@link #TAB_SIZE} spaces (port of the Swing view's {@code replaceTabs}). + */ + private static String replaceTabs(String s) { + char[] replacement = new char[TAB_SIZE]; + Arrays.fill(replacement, ' '); + return s.replace("\t", new String(replacement)); + } + + /** + * @return the file name of the given URI (after the last '/'), like the Swing view's tab + * titles + */ + private static String simpleFileName(URI uri) { + String s = uri.toString(); + int index = s.lastIndexOf('/'); + return index < 0 ? s : s.substring(index + 1); + } + + /** + * One finished (background) load: text, highlighting spans and the header line for it. + */ + private record Loaded(int generation, String header, String text, + StyleSpans> spans, SourceHighlighter.Result result, + boolean success) { + + static Loaded placeholder(int generation, String text) { + return new Loaded(generation, NO_SOURCE, text, + text.isEmpty() ? null : SourceHighlighter.placeholderSpans(text), + SourceHighlighter.Result.EMPTY, false); + } + } +} diff --git a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css index 8cc517e2b2d..1f0ec45d726 100644 --- a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css +++ b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css @@ -394,3 +394,51 @@ .strategy-status { -fx-text-fill: -key-text-secondary; } + +/* source view */ +/* M2, first read-only version; see SourceViewF */ +.source-view { + -fx-background-color: -key-surface; +} + +.source-view-header { + -fx-background-color: -key-background; + -fx-text-fill: -key-text-secondary; + -fx-padding: 4 8 4 8; + -fx-border-color: transparent transparent -key-border transparent; +} + +/* + * Segment classes applied to the text nodes of the area (see SourceHighlighter). + * The colors reuse the -key-* palette so the dark theme needs no extra rules. + */ +.source-text { + -fx-fill: -key-text; +} + +.source-placeholder { + -fx-fill: -key-text-secondary; + -fx-font-style: italic; +} + +.source-keyword { + -fx-fill: -key-accent; + -fx-font-weight: bold; +} + +.source-comment { + -fx-fill: -key-text-secondary; + -fx-font-style: italic; +} + +.source-string { + -fx-fill: -key-success; +} + +.source-annotation { + -fx-fill: -key-warning; +} + +.source-jml-keyword { + -fx-font-weight: bold; +} diff --git a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css index b3a2d0ab1ea..6a890f7128b 100644 --- a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css +++ b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css @@ -390,3 +390,51 @@ .strategy-status { -fx-text-fill: -key-text-secondary; } + +/* source view */ +/* M2, first read-only version; see SourceViewF */ +.source-view { + -fx-background-color: -key-surface; +} + +.source-view-header { + -fx-background-color: -key-background; + -fx-text-fill: -key-text-secondary; + -fx-padding: 4 8 4 8; + -fx-border-color: transparent transparent -key-border transparent; +} + +/* + * Segment classes applied to the text nodes of the area (see SourceHighlighter). + * The colors reuse the -key-* palette so the dark theme needs no extra rules. + */ +.source-text { + -fx-fill: -key-text; +} + +.source-placeholder { + -fx-fill: -key-text-secondary; + -fx-font-style: italic; +} + +.source-keyword { + -fx-fill: -key-accent; + -fx-font-weight: bold; +} + +.source-comment { + -fx-fill: -key-text-secondary; + -fx-font-style: italic; +} + +.source-string { + -fx-fill: -key-success; +} + +.source-annotation { + -fx-fill: -key-warning; +} + +.source-jml-keyword { + -fx-font-weight: bold; +} From 1c9282bc1167d69c164d18fb4371f913031343da Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Wed, 7 Oct 2026 21:48:38 +0200 Subject: [PATCH 030/281] key.ui.fx: M3g File menu open/reload/save with recent files Ports the Swing file actions of key.ui to the JavaFX main window: - Open File (OpenFileAction): FileChooser for problem/proof/Java files with the Swing default extension filter; remembers the last directory. - Reload (OpenMostRecentFileAction) and Save File (SaveFileAction + WindowUserInterfaceControl.saveProof): enablement bound to autoModeRunning or !proofLoaded like the Swing originals; saving runs ProofSaver on the FX thread (EDT parity, interaction lock prevents concurrent auto mode), reports errors via notifications and stores the target with Proof.setProofFile; the dialog is pre-filled with the proof's file name or the sanitized proof name (fileName() port). - RecentFilesF: recent-files store persisting to the SAME recentFiles_v2.json as the Swing UI (same JSON keys, max 8 entries, move-to-front, vanished temp-folder files dropped on load), plus the ShortUniqueFileNames suffix-growth port for unique display names. - Recent Files submenu rebuilt on every store change, profile suffix, disabled while empty; entries open the file. - The demo property load and the menu actions share startProofLoad, which also sets the source view fallback; only user-initiated loads register recents. Deferred: Open Example, Save Bundle/Quick Save/Quick Load (stubs), the zproof bundle flow, the load-warning dialog and the gzip save option. --- .../de/uka/ilkd/key/gui/fx/MainWindowF.java | 273 +++++++++++++++++- .../key/gui/fx/recentfiles/RecentFilesF.java | 264 +++++++++++++++++ 2 files changed, 521 insertions(+), 16 deletions(-) create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/recentfiles/RecentFilesF.java 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 index 5caf0d2f9ea..d5e6f93214a 100644 --- 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 @@ -4,13 +4,16 @@ * 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.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Optional; import javafx.application.Platform; +import javafx.beans.property.ReadOnlyBooleanWrapper; import javafx.concurrent.Task; import javafx.geometry.Insets; import javafx.geometry.Pos; @@ -36,6 +39,7 @@ import javafx.scene.layout.Region; import javafx.scene.layout.StackPane; import javafx.scene.layout.VBox; +import javafx.stage.FileChooser; import javafx.stage.Stage; import de.uka.ilkd.key.control.AutoModeListener; @@ -59,6 +63,7 @@ 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.prooftree.ProofTreeViewF; +import de.uka.ilkd.key.gui.fx.recentfiles.RecentFilesF; import de.uka.ilkd.key.gui.fx.settings.SettingsManagerF; import de.uka.ilkd.key.gui.fx.settings.ThemeSettingsProviderF; import de.uka.ilkd.key.gui.fx.sourceview.SourceViewF; @@ -67,11 +72,13 @@ import de.uka.ilkd.key.gui.fx.theme.ThemeManager; import de.uka.ilkd.key.proof.Proof; import de.uka.ilkd.key.proof.ProofEvent; +import de.uka.ilkd.key.proof.io.ProofSaver; import de.uka.ilkd.key.settings.PathConfig; import de.uka.ilkd.key.settings.ProofIndependentSettings; import de.uka.ilkd.key.settings.ViewSettings; 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.util.javafx.FxUtil; @@ -159,6 +166,31 @@ public final class MainWindowF { */ 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(); + + /** + * The "Recent Files" submenu of the File menu; rebuilt by {@link #updateRecentFilesMenu()} + * whenever the store changes. + */ + private final Menu recentFilesMenu = new Menu("Recent Files"); + + /** + * Last directory of the open/save dialogs (Swing {@code OpenFileAction.lastSelectedPath}). + */ + private Path lastSelectedDir = Path.of(System.getProperty("user.dir")); + + /** + * 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 @@ -219,6 +251,8 @@ public void initialize() { updateStatus(); wireSequentView(); + recentFiles.setOnChange(this::updateRecentFilesMenu); + recentFiles.load(); startDemoProofLoad(); NotificationManagerF.getInstance() @@ -296,7 +330,24 @@ private void startDemoProofLoad() { if (file == null || file.isBlank()) { return; } - Path location = Path.of(file); + 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). 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) { // pure .key problems carry no Java source; the source view then shows the problem file sourceView.setFallbackSourceFile(location); Task> loadTask = new Task<>() { @@ -323,10 +374,11 @@ protected KeYEnvironment call() throws Exception { selectionModel.setSelectedProof(env.getLoadedProof()); String show = System.getProperty("key.fx.show", ID_SEQUENT); String target = ID_PROOF_TREE.equalsIgnoreCase(show) ? ID_PROOF_TREE : ID_SEQUENT; - LOGGER.info("Demo: selecting dockable '{}' (key.fx.show={})", target, show); + LOGGER.info("Selecting dockable '{}' (key.fx.show={})", target, show); workspace.select(dockables.get(target)); NotificationManagerF.getInstance() - .notify("Demo proof loaded: " + location, Kind.INFO); + .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); @@ -368,15 +420,179 @@ protected KeYEnvironment call() throws Exception { }); loadTask.setOnFailed(event -> { Throwable error = loadTask.getException(); - LOGGER.error("Demo proof loading failed", error); + LOGGER.error((demo ? "Demo proof" : "Proof") + " loading failed", error); NotificationManagerF.getInstance() - .notify("Demo proof loading failed: " + error.getMessage(), Kind.ERROR); + .notify((demo ? "Demo proof" : "Proof") + " loading failed: " + + error.getMessage(), Kind.ERROR); }); Thread loader = new Thread(loadTask, "fx-demo-proof-loader"); loader.setDaemon(true); loader.start(); } + // ------------------------------------------------------------------ + // file menu actions: open / reload / recent files / save (M3) + // ------------------------------------------------------------------ + + /** + * 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(); + } + 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 require the Swing {@code ProofSelectionDialog} flow and are deferred. + * + * @param file the file to load + */ + private void openProofFile(Path file) { + if (file.toString().endsWith(".zproof")) { + LOGGER.info("Proof bundle requested: {} (deferred)", file); + NotificationManagerF.getInstance() + .notify("Proof bundles (.zproof) are not supported yet.", Kind.WARNING); + return; + } + recentFiles.add(file.toAbsolutePath().toString(), null, false, null); + startProofLoad(file, false); + } + + /** + * 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)); + } + + /** + * 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")); + File file = chooser.showSaveDialog(stage); + if (file == null) { + return; + } + if (file.getParentFile() != null) { + lastSelectedDir = file.getParentFile().toPath(); + } + Path target = file.toPath().toAbsolutePath(); + // the Swing chooser offers a "compressed" checkbox (GZipProofSaver); deferred, the plain + // saver is the default + ProofSaver 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; + } + + /** + * 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; + recentFilesMenu.getItems() + .add(menuItem(text, () -> openProofFile(Path.of(entry.path())))); + } + // an empty submenu would render as a dark clickable nothing (Swing leaves it enabled) + recentFilesMenu.setDisable(entries.isEmpty()); + } + /** * 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 @@ -624,15 +840,28 @@ private MenuBar buildMenuBar() { private Menu buildFileMenu() { 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())); + 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())); file.getItems().addAll( - menuItem("Open Example…", IconFactoryF.Key.OPEN_KEY_FILE, this::notYetImplemented), - menuItem("Open File…", "de.uka.ilkd.key.gui.actions.OpenFileAction", + menuItem("Open Example…", "de.uka.ilkd.key.gui.actions.OpenExampleAction", IconFactoryF.Key.OPEN_KEY_FILE, this::notYetImplemented), - menuItem("Open Most Recent…", "de.uka.ilkd.key.gui.actions.OpenMostRecentFileAction", - this::notYetImplemented), + openFile, + reload, new SeparatorMenuItem(), - menuItem("Save File…", "de.uka.ilkd.key.gui.actions.SaveFileAction", - IconFactoryF.Key.SAVE_FILE, this::notYetImplemented), + saveFile, menuItem("Save Bundle…", "de.uka.ilkd.key.gui.actions.SaveBundleAction", this::notYetImplemented), menuItem("Quick Save", "de.uka.ilkd.key.gui.actions.QuickSaveAction", @@ -640,6 +869,8 @@ private Menu buildFileMenu() { menuItem("Quick Load", "de.uka.ilkd.key.gui.actions.QuickLoadAction", this::notYetImplemented), new SeparatorMenuItem(), + recentFilesMenu, + new SeparatorMenuItem(), menuItem("Exit", "de.uka.ilkd.key.gui.actions.ExitMainAction", IconFactoryF.Key.QUIT, Platform::exit)); return file; @@ -722,11 +953,20 @@ private Menu buildAboutMenu() { private ToolBar buildFileToolBar() { ToolBar bar = new ToolBar(); bar.getStyleClass().add("key-file-tool-bar"); - bar.getItems().addAll( - toolbarButton("Open File", IconFactoryF.Key.OPEN_KEY_FILE, this::notYetImplemented), - toolbarButton("Open Most Recent", IconFactoryF.Key.OPEN_MOST_RECENT, - this::notYetImplemented), - toolbarButton("Save File", IconFactoryF.Key.SAVE_FILE, this::notYetImplemented)); + javafx.scene.control.Button openFile = + toolbarButton("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 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 current proof", IconFactoryF.Key.SAVE_FILE, this::saveProofFile); + saveFile.disableProperty() + .bind(mediator.autoModeRunningProperty().or(proofLoaded.not())); + bar.getItems().addAll(openFile, reload, saveFile); return bar; } @@ -829,6 +1069,7 @@ private void updateProofStatus() { return; } Proof proof = selectionModel.getSelectedProof(); + proofLoaded.set(proof != null); if (proof == null) { statusLeft.setText(KeYConstants.COPYRIGHT); return; diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/recentfiles/RecentFilesF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/recentfiles/RecentFilesF.java new file mode 100644 index 00000000000..2a64384356e --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/recentfiles/RecentFilesF.java @@ -0,0 +1,264 @@ +/* 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.recentfiles; + +import java.io.BufferedWriter; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +import de.uka.ilkd.key.nparser.ParsingFacade; +import de.uka.ilkd.key.settings.Configuration; +import de.uka.ilkd.key.settings.PathConfig; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The recent files store of the JavaFX UI, the counter-part of + * {@code de.uka.ilkd.key.gui.RecentFileMenu} in the Swing module {@code key.ui} (the menu UI + * itself lives in {@code MainWindowF}). + *

+ * The entries are persisted in the same file as the Swing UI + * ({@code ~/.key//recentFiles_v2.json}, see {@link PathConfig.KeyPaths#recentFileStorage}) + * with the same JSON keys, so both UIs share one recent-files list. Like the Swing original, at + * most {@value #MAX_RECENT_FILES} entries are kept, an already present path is moved to the front, + * and only existing files are added. + */ +public final class RecentFilesF { + + private static final Logger LOGGER = LoggerFactory.getLogger(RecentFilesF.class); + + /** + * The maximum number of recent files kept (Swing {@code RecentFileMenu.MAX_RECENT_FILES}). + */ + public static final int MAX_RECENT_FILES = 8; + + private static final String KEY_PATH = "path"; + private static final String KEY_PROFILE = "profile"; + private static final String KEY_OPTIONS = "options"; + private static final String KEY_LOAD_SINGLE_JAVA = "singleJava"; + + /** + * One recent file entry; the fields mirror the Swing {@code RecentFileEntry}. + * + * @param path absolute path of the file + * @param profile ident of the profile the file was loaded with, {@code null} for the default + * @param singleJava whether the file was loaded as a single Java file + * @param additionalOption additional profile options, {@code null} if none + */ + public record Entry(String path, String profile, boolean singleJava, + Configuration additionalOption) { + + static Entry of(Configuration options) { + return new Entry(Objects.requireNonNull(options.getString(KEY_PATH)), + options.getString(KEY_PROFILE), options.getBool(KEY_LOAD_SINGLE_JAVA, false), + options.getTable(KEY_OPTIONS)); + } + + Configuration asConfiguration() { + Configuration config = new Configuration(); + config.set(KEY_PATH, path); + // only store a real profile name; writing null serializes to "null", which fails to + // resolve on reload (same comment as the Swing original) + if (profile != null) { + config.set(KEY_PROFILE, profile); + } + config.set(KEY_LOAD_SINGLE_JAVA, singleJava); + config.set(KEY_OPTIONS, additionalOption); + return config; + } + } + + /** most-recent-first */ + private final List entries = new ArrayList<>(); + /** invoked after every model change, used by the main window to rebuild the menu */ + private Runnable onChange = () -> { + }; + + /** + * Loads the entries from the current default configuration folder (falls back to the previous + * version's folder like the Swing original). + */ + public void load() { + Path storage = PathConfig.currentPaths.recentFileStorage; + if (!Files.exists(storage) && PathConfig.previousPaths != null) { + storage = PathConfig.previousPaths.recentFileStorage; + } + if (!Files.exists(storage)) { + return; + } + try { + List stored = ParsingFacade.parseConfigurationFile(storage) + .asConfigurationList(); + entries.clear(); + String tmpFolder = System.getProperty("java.io.tmpdir"); + for (Configuration configuration : stored) { + Entry entry = Entry.of(configuration); + // silently drop vanished files of the temp folder (quick saves), like Swing + if (entry.path().startsWith(tmpFolder) && !Files.exists(Path.of(entry.path()))) { + continue; + } + entries.add(entry); + } + fireChange(); + } catch (IOException e) { + LOGGER.info("Could not read the recent files list {}", storage, e); + } catch (Exception e) { + LOGGER.error("Could not read the recent files list.", e); + } + } + + /** + * Adds a new file to the beginning of the recent files list; an already present path is moved + * to the front; non-existing files are silently ignored; the list is saved afterwards. + * + * @param path absolute path of the file + */ + public void add(String path) { + add(path, null, false, null); + } + + /** + * Adds a new file with the load options of the Swing original (profile, single Java file, + * additional profile options). + */ + public void add(String path, String profile, boolean singleJava, + Configuration additionalOption) { + Optional existing = entries.stream().filter(it -> path.equals(it.path())) + .findFirst(); + if (existing.isPresent()) { + Entry entry = existing.get(); + entries.remove(entry); + entries.addFirst(entry); + fireChange(); + save(); + return; + } + if (!Files.exists(Path.of(path))) { + return; + } + entries.addFirst(new Entry(path, profile, singleJava, additionalOption)); + while (entries.size() > MAX_RECENT_FILES) { + entries.removeLast(); + } + fireChange(); + save(); + } + + /** + * @return the absolute path of the most recently opened file, or {@code null} if the list is + * empty + */ + public String getMostRecent() { + return entries.isEmpty() ? null : entries.getFirst().path(); + } + + /** + * @return an immutable copy of the entries, most-recent-first + */ + public List getEntries() { + return List.copyOf(entries); + } + + /** + * Registers the hook invoked after every model change (on the FX thread; all mutators must be + * called on the FX thread). + * + * @param hook the hook, may be {@code null} to clear it + */ + public void setOnChange(Runnable hook) { + onChange = hook != null ? hook : () -> { + }; + } + + private void fireChange() { + onChange.run(); + } + + private void save() { + List stored = + entries.stream().map(Entry::asConfiguration).toList(); + try (BufferedWriter writer = + Files.newBufferedWriter(PathConfig.currentPaths.recentFileStorage)) { + new Configuration.ConfigurationWriter(writer).printValue(stored); + } catch (IOException e) { + LOGGER.info("Could not write the recent files list " + + PathConfig.currentPaths.recentFileStorage, e); + } + } + + /** + * Short unique display names for the given paths (Swing {@code ShortUniqueFileNames}): each + * name starts as the file name and is extended by parent directory segments (from the end of + * the path) until all names are distinct. + * + * @param paths the paths, in display order + * @return one display name per path + */ + public static List uniqueNames(List paths) { + int size = paths.size(); + String[][] segments = new String[size][]; + for (int i = 0; i < size; i++) { + segments[i] = paths.get(i).replace('\\', '/').split("/"); + } + int[] depth = new int[size]; + Arrays.fill(depth, 1); + boolean changed = true; + while (changed) { + changed = false; + for (int i = 0; i < size; i++) { + for (int j = i + 1; j < size; j++) { + if (!name(segments[i], depth[i]).equals(name(segments[j], depth[j]))) { + continue; + } + boolean iCanGrow = depth[i] < segments[i].length; + boolean jCanGrow = depth[j] < segments[j].length; + if (!iCanGrow && !jCanGrow) { + continue; // identical full paths cannot be disambiguated + } + // grow the one with the shorter displayed name first + if (iCanGrow && jCanGrow) { + if (name(segments[i], depth[i]).length() <= name(segments[j], + depth[j]).length()) { + depth[i]++; + } else { + depth[j]++; + } + } else if (iCanGrow) { + depth[i]++; + } else { + depth[j]++; + } + changed = true; + } + } + } + List names = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + names.add(name(segments[i], depth[i])); + } + return names; + } + + /** + * @return the last {@code depth} segments of the path joined by '/' + */ + private static String name(String[] segments, int depth) { + StringBuilder name = new StringBuilder(); + for (int i = Math.max(0, segments.length - depth); i < segments.length; i++) { + if (name.length() > 0) { + name.append('/'); + } + name.append(segments[i]); + } + return name.toString(); + } +} From 0d581f4260256cd74c6492bba146fcc9b59f9b6c Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Wed, 7 Oct 2026 23:43:42 +0200 Subject: [PATCH 031/281] key.ui.fx: M4 settings dialog framework + appearance provider Ports the settings dialog framework of the Swing module (SettingsManager, SettingsDialog, SettingsUi, SettingsPanel/SimpleSettingsPanel, SettingsProvider, Validator, InvalidSettingsInputException) into de.uka.ilkd.key.gui.fx.settings, plus the first provider StandardUISettingsF (Appearance & Behaviour), the counter-part of the Swing StandardUISettings reading/writing the shared core ViewSettings and GeneralSettings. - SettingsManagerF merges the three Swing classes; provider tree on the left (root 'KeY Settings', sorted by priority, search field highlighting matching cells), panel on the right, OK/Apply/Cancel with the Swing semantics (OK closes only without errors, Apply shows errors and stays, Cancel/Escape closes without applying); APPLICATION_MODAL 900x600 with the KeY logo. - SettingsProviderF.getPanel(MainWindowF)/apply(MainWindowF); the Swing getChildren is renamed to getChildProviders because getChildren clashes with javafx.scene.Parent; contains() defaults to the description match. - SettingsPanelF builds the form as a code-built GridPane (three columns, MigLayout wrapAfter(3) of the Swing original); input factories with live validation marking errors via the settings-input-error style class (-key-settings-error, the SETTINGS_TEXTFIELD_ERROR color). - StandardUISettingsF: theme radios (live switching replaces the Swing look-and-feel combo), font factor/size (persisted, applied to new views; live resize deferred), tooltips, load-examples dialog, confirm exit, autosave, macro notification, pretty/unicode/syntax/hide-prefix, right-click macros, whole/uninstantiated taclet, clutter rules/sets, minimize interaction, source consistency, classic taclet dialog. - ThemeSettingsProviderF (M1 shell) is absorbed and removed. - The View menu theme radios follow ThemeManager.themeProperty; the Options menu opens the dialog via its first item 'Settings' (Swing action name). - ThemeManager exposes the managed scenes as an observable list. - IconFactoryF.Key gains WARNING_INCOMPLETE/WARNING_UNSOUND (orange/red exclamation-triangle glyphs of the Swing IconFactory). - Both theme stylesheets define the colors.json-backed -key-* variables (Swing light/dark defaults), convert the sequent/source highlighting rules to them and add the .settings-* styles. --- .../de/uka/ilkd/key/gui/fx/MainWindowF.java | 30 +- .../key/gui/fx/fonticons/IconFactoryF.java | 18 +- .../InvalidSettingsInputExceptionF.java | 63 +++ .../key/gui/fx/settings/SettingsManagerF.java | 418 +++++++++++---- .../key/gui/fx/settings/SettingsPanelF.java | 505 ++++++++++++++++++ .../gui/fx/settings/SettingsProviderF.java | 86 ++- .../gui/fx/settings/StandardUISettingsF.java | 240 +++++++++ .../fx/settings/ThemeSettingsProviderF.java | 101 ---- .../ilkd/key/gui/fx/settings/Validator.java | 24 + .../ilkd/key/gui/fx/theme/ThemeManager.java | 13 +- .../de/uka/ilkd/key/gui/fx/theme/key-dark.css | 120 ++++- .../uka/ilkd/key/gui/fx/theme/key-light.css | 120 ++++- 12 files changed, 1491 insertions(+), 247 deletions(-) create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/InvalidSettingsInputExceptionF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/SettingsPanelF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/StandardUISettingsF.java delete mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/ThemeSettingsProviderF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/Validator.java 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 index d5e6f93214a..4f8a4923620 100644 --- 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 @@ -65,7 +65,6 @@ 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.SettingsManagerF; -import de.uka.ilkd.key.gui.fx.settings.ThemeSettingsProviderF; 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.theme.Theme; @@ -226,7 +225,6 @@ public void initialize() { stage.setTitle(KeYResourceManager.getManager().getUserInterfaceTitle()); setWindowIcons(); - registerSettingsProviders(); buildDockables(); workspace.setDefaultLayout(defaultLayout()); @@ -274,6 +272,20 @@ public Map getDockables() { return dockables; } + /** + * @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; + } + /** * @return the sequent view docked in the main area */ @@ -895,6 +907,9 @@ private Menu buildViewMenu() { .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); @@ -933,7 +948,7 @@ private Menu buildProofMenu() { private Menu buildOptionsMenu() { Menu options = new Menu("Options"); options.getItems().addAll( - menuItem("Preferences…", + menuItem("Settings", "de.uka.ilkd.key.gui.settings.SettingsManager$ShowSettingsAction", IconFactoryF.Key.CONFIGURE, this::openSettings), new SeparatorMenuItem(), @@ -1019,7 +1034,8 @@ private MenuItem menuItem(String text, String actionId, IconFactoryF.Key icon, item.setGraphic(IconFactoryF.createIcon(icon)); } if (actionId != null) { - KeyStrokeManagerF.getInstance().binding(actionId).ifPresent(item::setAccelerator); + KeyStrokeManagerF manager = KeyStrokeManagerF.getInstance(); + manager.binding(actionId).ifPresent(item::setAccelerator); } item.setOnAction(e -> action.run()); return item; @@ -1095,7 +1111,7 @@ private void changeFontSize(int delta) { } private void openSettings() { - SettingsManagerF.getInstance().openSettings(stage); + SettingsManagerF.getInstance().showSettingsDialog(this); } private void resetLayout() { @@ -1118,8 +1134,4 @@ private void notYetImplemented() { NotificationManagerF.getInstance().notify( "This action arrives in a later milestone of the key.ui.fx rewrite.", Kind.WARNING); } - - private void registerSettingsProviders() { - SettingsManagerF.getInstance().addProvider(new ThemeSettingsProviderF()); - } } diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/fonticons/IconFactoryF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/fonticons/IconFactoryF.java index f350f405606..6ceb08ea727 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/fonticons/IconFactoryF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/fonticons/IconFactoryF.java @@ -64,12 +64,22 @@ public enum Key { PROOF_SEARCH_STRATEGY(FontAwesomeSolid.COG), KEY_HOLE(FontAwesomeSolid.KEY), KEY_HOLE_INTERACTIVE(FontAwesomeSolid.HAND_PAPER), - KEY_HOLE_LINKED(FontAwesomeSolid.LINK); + KEY_HOLE_LINKED(FontAwesomeSolid.LINK), + WARNING_INCOMPLETE(FontAwesomeSolid.EXCLAMATION_TRIANGLE, "key-icon-warning"), + WARNING_UNSOUND(FontAwesomeSolid.EXCLAMATION_TRIANGLE, "key-icon-error"); private final IconFont glyph; + /** an optional extra style class giving the icon its themed color, may be null */ + private final String colorStyleClass; + Key(IconFont glyph) { + this(glyph, null); + } + + Key(IconFont glyph, String colorStyleClass) { this.glyph = glyph; + this.colorStyleClass = colorStyleClass; } /** @@ -101,7 +111,11 @@ public static Node createIcon(Key key) { * @return a new icon node */ public static Node createIcon(Key key, double size) { - return createIcon(key.glyph(), size); + Node node = createIcon(key.glyph(), size); + if (key.colorStyleClass != null) { + node.getStyleClass().add(key.colorStyleClass); + } + return node; } /** diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/InvalidSettingsInputExceptionF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/InvalidSettingsInputExceptionF.java new file mode 100644 index 00000000000..5ed3f86fd52 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/InvalidSettingsInputExceptionF.java @@ -0,0 +1,63 @@ +/* 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.settings; + +import javafx.scene.Node; + +/** + * Thrown by a {@link SettingsProviderF} if an input component is not properly filled; prevents + * the settings dialog from closing, counter-part of {@code InvalidSettingsInputException} of the + * Swing module {@code key.ui}. + * + * @author Alexander Weigl + */ +public class InvalidSettingsInputExceptionF extends Exception { + + /** the provider whose panel caused the error */ + private final transient SettingsProviderF panel; + + /** the input node that should receive the focus */ + private final transient Node focusable; + + public InvalidSettingsInputExceptionF(SettingsProviderF panel, Node focusable) { + this.panel = panel; + this.focusable = focusable; + } + + public InvalidSettingsInputExceptionF(String message, SettingsProviderF panel, + Node focusable) { + super(message); + this.panel = panel; + this.focusable = focusable; + } + + public InvalidSettingsInputExceptionF(String message, Throwable cause, + SettingsProviderF panel, Node focusable) { + super(message, cause); + this.panel = panel; + this.focusable = focusable; + } + + public InvalidSettingsInputExceptionF(Throwable cause, SettingsProviderF panel, + Node focusable) { + super(cause); + this.panel = panel; + this.focusable = focusable; + } + + /** + * @return the provider whose panel caused the error + */ + public SettingsProviderF getPanel() { + return panel; + } + + /** + * @return the input node that should receive the focus + */ + public Node getFocusable() { + return focusable; + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/SettingsManagerF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/SettingsManagerF.java index 4077926648a..9bec269d3a6 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/SettingsManagerF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/SettingsManagerF.java @@ -7,169 +7,397 @@ import java.util.ArrayList; import java.util.Comparator; import java.util.IdentityHashMap; +import java.util.LinkedList; import java.util.List; import java.util.Map; -import java.util.TreeMap; - +import java.util.stream.Collectors; import javafx.geometry.Insets; -import javafx.geometry.Pos; +import javafx.scene.Scene; +import javafx.scene.control.Alert; import javafx.scene.control.Button; import javafx.scene.control.ButtonBar; +import javafx.scene.control.Label; import javafx.scene.control.ScrollPane; +import javafx.scene.control.TextField; +import javafx.scene.control.TreeCell; import javafx.scene.control.TreeItem; import javafx.scene.control.TreeView; +import javafx.scene.image.Image; import javafx.scene.layout.BorderPane; +import javafx.scene.layout.HBox; import javafx.scene.layout.StackPane; +import javafx.scene.layout.VBox; import javafx.stage.Modality; import javafx.stage.Stage; -import javafx.stage.Window; + +import de.uka.ilkd.key.gui.fx.MainWindowF; +import de.uka.ilkd.key.proof.Proof; +import de.uka.ilkd.key.settings.ChoiceSettings; +import de.uka.ilkd.key.settings.ProofSettings; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** - * Aggregates the registered {@link SettingsProviderF}s and opens the settings dialog, counter-part - * of {@code SettingsManager}/{@code SettingsDialog} of the Swing module {@code key.ui}. + * Aggregates the registered {@link SettingsProviderF}s and opens the settings dialog. + * Counter-part of the Swing classes {@code SettingsManager}, {@code SettingsDialog} and + * {@code SettingsUi} of the module {@code key.ui} (the Swing originals are three classes; they + * are merged here like the other close Swing pairs in this module). *

- * The dialog is built in code (no FXML): a tree of categories on the left (implemented via - * {@link TreeView}), the selected provider's panel on the right, and OK / Apply / Cancel controls. + * The dialog is built in code (no FXML): a tree of providers on the left (root entry "KeY + * Settings", providers and their {@link SettingsProviderF#getChildProviders() children}), the + * selected + * provider's panel on the right, and OK / Apply / Cancel controls with the Swing semantics: + *

    + *
  • OK applies every provider; on errors the errors are shown and the dialog stays + * open, otherwise it closes,
  • + *
  • Apply applies every provider and shows occurring errors, staying open,
  • + *
  • Cancel closes the dialog without applying; {@code Escape} is the cancel key.
  • + *
+ * Providers are sorted by {@link SettingsProviderF#getPriorityOfSettings()} (higher values last) + * and initialized ({@code getPanel}) before the dialog is shown, like the Swing + * {@code initializeProviders}. */ public final class SettingsManagerF { - private static final SettingsManagerF INSTANCE = new SettingsManagerF(); + private static final Logger LOGGER = LoggerFactory.getLogger(SettingsManagerF.class); + + /** the first provider of the tree (Swing {@code SettingsManager.STANDARD_UI_SETTINGS}) */ + public static final StandardUISettingsF STANDARD_UI_SETTINGS = new StandardUISettingsF(); + + // Deliberately deferred to later milestones (the provider seam stays open): the taclet + // options provider follows in this milestone, SMTSettingsProvider, JavacSettingsProvider, + // ParallelProverSettingsProvider, FeatureSettingsPanel and the ShowActiveSettings dump later. - private final List providers = new ArrayList<>(); - private final TreeMap> providersByCategory = new TreeMap<>(); + private static SettingsManagerF INSTANCE; + + private final List settingsProviders = new ArrayList<>(); private SettingsManagerF() { } /** - * @return the global settings manager instance + * @return the global settings manager instance with the built-in providers registered */ public static SettingsManagerF getInstance() { + if (INSTANCE == null) { + INSTANCE = new SettingsManagerF(); + INSTANCE.add(STANDARD_UI_SETTINGS); + } return INSTANCE; } /** - * Registers a settings provider. + * The choice settings of the given window: a detached copy initialized from the selected + * proof (its default choices and categories) if a proof is loaded, otherwise the global + * default settings (Swing {@code SettingsManager.getChoiceSettings}). * - * @param provider the provider to register + * @param window the main window + * @return the choice settings to edit in the taclet options panel */ - public void addProvider(SettingsProviderF provider) { - providers.add(provider); - providersByCategory - .computeIfAbsent(provider.getCategory() == null ? "" : provider.getCategory(), - key -> new ArrayList<>()) - .add(provider); + public static ChoiceSettings getChoiceSettings(MainWindowF window) { + Proof selectedProof = window.getMediator().getSelectedProof(); + if (selectedProof != null) { + ChoiceSettings settings = new ChoiceSettings(); + settings.setDefaultChoices( + selectedProof.getSettings().getChoiceSettings().getDefaultChoices()); + var cat = selectedProof.getSettings().getChoiceSettings().getCategory2Choices(); + if (cat.isEmpty()) { + cat = ProofSettings.DEFAULT_SETTINGS.getChoiceSettings().getCategory2Choices(); + } + settings.setChoiceCategories(new java.util.TreeMap<>(cat)); + return settings; + } + return ProofSettings.DEFAULT_SETTINGS.getChoiceSettings(); } /** - * Registers multiple settings providers. + * Registers a settings provider. * - * @param providers the providers to register + * @param settingsProvider the provider to register + * @return whether the provider was added */ - public void addProviders(List providers) { - providers.forEach(this::addProvider); + public boolean add(SettingsProviderF settingsProvider) { + return settingsProviders.add(settingsProvider); } /** - * @return the currently registered providers, in registration order + * Removes the given settings provider. + * + * @param settingsProvider the provider to remove + * @return whether the provider was removed */ - public List getProviders() { - return List.copyOf(providers); + public boolean remove(SettingsProviderF settingsProvider) { + return settingsProviders.remove(settingsProvider); } /** - * Applies all registered providers; used by auto mode and profile loading. + * Opens the settings dialog, modal to the main window (Swing + * {@code SettingsManager.showSettingsDialog}). + * + * @param mainWindow the main window */ - public void applyAll() { - providers.forEach(SettingsProviderF::apply); + public void showSettingsDialog(MainWindowF mainWindow) { + Stage dialog = createDialog(mainWindow); + dialog.show(); } /** - * Opens the settings dialog, modal to the given owner window. + * Opens the settings dialog with the given provider selected in the tree (Swing + * {@code showSettingsDialog(MainWindow, SettingsProvider)}). * - * @param owner the owner window, or {@code null} for a standalone dialog + * @param mainWindow the main window + * @param selectedPanel the provider to select */ - public void openSettings(Window owner) { + public void showSettingsDialog(MainWindowF mainWindow, SettingsProviderF selectedPanel) { + Stage dialog = createDialog(mainWindow); + ((SettingsUi) dialog.getUserData()).selectPanel(selectedPanel); + dialog.show(); + } + + private Stage createDialog(MainWindowF mainWindow) { + List providers = new ArrayList<>(settingsProviders); + providers.sort(Comparator.comparingInt(SettingsProviderF::getPriorityOfSettings)); + initializeProviders(providers, mainWindow); + Stage stage = new Stage(); - stage.initOwner(owner); - stage.initModality(Modality.WINDOW_MODAL); - stage.setTitle("KeY - Settings"); - - BorderPane content = new BorderPane(); - content.setPadding(new Insets(10)); - - Map, SettingsProviderF> providerByItem = new IdentityHashMap<>(); - TreeView tree = buildTree(providerByItem); - content.setLeft(tree); - BorderPane.setMargin(tree, new Insets(0, 10, 0, 0)); - - ScrollPane scrollPane = new ScrollPane(); - scrollPane.setFitToWidth(true); - StackPane panelArea = new StackPane(); - scrollPane.setContent(panelArea); - content.setCenter(scrollPane); - BorderPane.setAlignment(scrollPane, Pos.CENTER); - - tree.getSelectionModel().selectedItemProperty().addListener((obs, old, selected) -> { - SettingsProviderF provider = selected == null ? null : providerByItem.get(selected); - if (provider != null) { - panelArea.getChildren().setAll(provider.getPanel()); + stage.initOwner(mainWindow.getStage()); + stage.initModality(Modality.APPLICATION_MODAL); + stage.setTitle("Settings"); + Image logo = new Image(getClass() + .getResourceAsStream("/de/uka/ilkd/key/gui/images/key-color-icon-square.png")); + if (!logo.isError()) { + stage.getIcons().add(logo); + } + + SettingsUi ui = new SettingsUi(mainWindow, providers); + BorderPane root = new BorderPane(ui); + root.setPadding(new Insets(8)); + root.setBottom(createButtonBar(mainWindow, providers, stage)); + stage.setScene(new Scene(root, 900, 600)); + stage.setUserData(ui); + + stage.getScene().setOnKeyPressed(e -> { + // Swing registers ESCAPE on the root pane (WHEN_IN_FOCUSED_WINDOW) + if (ui.isEscapeConsumed(e)) { + stage.close(); + e.consume(); } }); + return stage; + } - ButtonBar buttons = new ButtonBar(); - Button okButton = new Button("OK"); + private javafx.scene.Node createButtonBar(MainWindowF mainWindow, + List providers, Stage stage) { + ButtonBar bar = new ButtonBar(); + Button acceptButton = new Button("OK"); Button applyButton = new Button("Apply"); Button cancelButton = new Button("Cancel"); - ButtonBar.setButtonData(okButton, ButtonBar.ButtonData.OK_DONE); + ButtonBar.setButtonData(acceptButton, ButtonBar.ButtonData.OK_DONE); ButtonBar.setButtonData(applyButton, ButtonBar.ButtonData.APPLY); ButtonBar.setButtonData(cancelButton, ButtonBar.ButtonData.CANCEL_CLOSE); - buttons.getButtons().addAll(okButton, applyButton, cancelButton); - - applyButton.setOnAction(ignored -> providers.forEach(SettingsProviderF::apply)); - okButton.setOnAction(ignored -> { - providers.forEach(SettingsProviderF::apply); - stage.close(); + cancelButton.setOnAction(e -> stage.close()); // Swing CancelAction: setVisible(false) + acceptButton.setOnAction(e -> { // Swing AcceptAction: setVisible(!showErrors(apply())) + if (showErrors(apply(providers, mainWindow), stage)) { + stage.close(); + } }); - cancelButton.setOnAction(ignored -> stage.close()); + applyButton.setOnAction(e -> showErrors(apply(providers, mainWindow), stage)); + bar.getButtons().addAll(acceptButton, applyButton, cancelButton); + return bar; + } - content.setBottom(buttons); + /** + * Applies all registered providers recursively, collecting the thrown exceptions (Swing + * {@code SettingsDialog.apply}). + * + * @param providers the providers to apply + * @param mainWindow the main window + * @return the collected errors, empty on success + */ + private List apply(List providers, MainWindowF mainWindow) { + List exceptions = new LinkedList<>(); + apply(providers, mainWindow, exceptions); + return exceptions; + } - stage.setScene(new javafx.scene.Scene(content, 900, 600)); - selectFirstNonEmpty(tree); - stage.show(); + private void apply(List providers, MainWindowF mainWindow, + List exceptions) { + for (SettingsProviderF it : providers) { + try { + it.apply(mainWindow); + apply(it.getChildProviders(), mainWindow, exceptions); + } catch (Exception e) { + exceptions.add(e); + } + } } - private TreeView buildTree(Map, SettingsProviderF> providerByItem) { - TreeItem root = new TreeItem<>("Settings"); - root.setExpanded(true); - - for (var entry : providersByCategory.entrySet()) { - TreeItem categoryItem = new TreeItem<>(entry.getKey()); - categoryItem.setExpanded(true); - entry.getValue().stream() - .sorted(Comparator.comparing(SettingsProviderF::getDescription)) - .forEach(provider -> { - TreeItem item = new TreeItem<>(provider.getDescription()); - providerByItem.put(item, provider); - categoryItem.getChildren().add(item); - }); - root.getChildren().add(categoryItem); + /** + * Shows the collected errors in a dialog and logs them (Swing + * {@code SettingsDialog.showErrors}). + * + * @param exceptions the errors + * @param owner the owner window + * @return true iff there were no errors + */ + private boolean showErrors(List exceptions, javafx.stage.Window owner) { + if (exceptions.isEmpty()) { + return true; } + for (Exception e : exceptions) { + LOGGER.error("", e); + } + String msg = exceptions.stream().map(Throwable::getMessage).collect(Collectors + .joining("\n")); + Alert alert = new Alert(Alert.AlertType.ERROR, msg); + alert.setTitle("Error in Settings"); + alert.setHeaderText("Error in Settings"); + if (owner != null) { + alert.initOwner(owner); + } + alert.showAndWait(); + return false; + } - TreeView tree = new TreeView<>(root); - tree.setShowRoot(false); - tree.setPrefWidth(260); - return tree; + /** + * Ensures that every given setting provider can update its model based on the current main + * window, including the children (Swing {@code SettingsManager.initializeProviders}). + */ + private void initializeProviders(List providers, MainWindowF mainWindow) { + providers.forEach(it -> it.getPanel(mainWindow)); + providers.forEach(it -> initializeProviders(it.getChildProviders(), mainWindow)); } - private void selectFirstNonEmpty(TreeView tree) { - TreeItem root = tree.getRoot(); - if (root != null && !root.getChildren().isEmpty()) { - TreeItem firstCategory = root.getChildren().get(0); - if (!firstCategory.getChildren().isEmpty()) { - tree.getSelectionModel().select(firstCategory.getChildren().get(0)); + /** + * The content of the settings dialog: the provider tree with a search field on the left and + * the selected panel on the right (Swing {@code SettingsUi}). + */ + private static class SettingsUi extends BorderPane { + + private final MainWindowF mainWindow; + private final TreeView treeSettingsPanels = new TreeView<>(); + private final TextField txtSearch = new TextField(); + private final Map, SettingsProviderF> providerByItem = + new IdentityHashMap<>(); + private final ScrollPane panelArea = new ScrollPane(); + private final StackPane panelHolder = new StackPane(); + + SettingsUi(MainWindowF mainWindow, List providers) { + this.mainWindow = mainWindow; + + txtSearch.setPromptText("Search"); + txtSearch.textProperty() + .addListener((obs, old, value) -> treeSettingsPanels.refresh()); + + treeSettingsPanels.setShowRoot(true); + treeSettingsPanels.setCellFactory(this::createCell); + buildTree(providers); + + HBox searchRow = new HBox(6, new Label("Search: "), txtSearch); + searchRow.getStyleClass().add("settings-search-row"); + VBox westPanel = new VBox(6, searchRow, treeSettingsPanels); + westPanel.getStyleClass().add("settings-tree-panel"); + VBox.setVgrow(treeSettingsPanels, javafx.scene.layout.Priority.ALWAYS); + setLeft(westPanel); + + panelArea.setContent(panelHolder); + panelArea.setFitToWidth(true); + panelArea.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); + panelHolder.getStyleClass().add("settings-panel-holder"); + panelHolder.getChildren().add(new Label("empty")); + setCenter(panelArea); + + treeSettingsPanels.getSelectionModel().selectedItemProperty() + .addListener((obs, old, item) -> { + if (item == null) { + return; + } + SettingsProviderF provider = item.getValue(); + if (provider != null) { + setSettingsPanel(provider.getPanel(mainWindow)); + } + }); + } + + private TreeCell createCell( + javafx.scene.control.TreeView view) { + return new TreeCell<>() { + @Override + protected void updateItem(SettingsProviderF item, boolean empty) { + super.updateItem(item, empty); + getStyleClass().remove("settings-tree-match"); + if (empty || item == null) { + setText(null); + return; + } + setText(item.getDescription()); + String search = txtSearch.getText(); + if (!search.isEmpty() && item.contains(search)) { + getStyleClass().add("settings-tree-match"); + } + } + }; + } + + private void buildTree(List providers) { + TreeItem root = new TreeItem<>(null); + root.setExpanded(true); + for (SettingsProviderF provider : providers) { + root.getChildren().add(createItem(provider)); + } + treeSettingsPanels.setRoot(root); + if (!providers.isEmpty()) { + // Swing shows the first provider's panel initially + setSettingsPanel(providers.get(0).getPanel(mainWindow)); + treeSettingsPanels.getSelectionModel().select(1); + } + } + + private TreeItem createItem(SettingsProviderF provider) { + TreeItem item = new TreeItem<>(provider); + item.setExpanded(true); + providerByItem.put(item, provider); + provider.getChildProviders().forEach(child -> item.getChildren() + .add(createItem(child))); + return item; + } + + private void setSettingsPanel(javafx.scene.Node comp) { + panelHolder.getChildren().setAll(comp); + panelArea.setVvalue(0); + } + + /** + * Selects the given provider in the tree (Swing {@code SettingsUi.selectPanel}). + * + * @param provider the provider to select + */ + void selectPanel(SettingsProviderF provider) { + findItem(treeSettingsPanels.getRoot(), provider) + .ifPresent(item -> treeSettingsPanels.getSelectionModel().select(item)); + } + + private java.util.Optional> findItem( + TreeItem node, SettingsProviderF provider) { + if (node.getValue() == provider) { + return java.util.Optional.of(node); + } + for (TreeItem child : node.getChildren()) { + var found = findItem(child, provider); + if (found.isPresent()) { + return found; + } } + return java.util.Optional.empty(); + } + + /** + * @return whether the given key event is the dialog's escape key (consumed by the tree + * and the search field otherwise) + */ + boolean isEscapeConsumed(javafx.scene.input.KeyEvent event) { + return !event.isConsumed() && event.getCode() == javafx.scene.input.KeyCode.ESCAPE; } } } diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/SettingsPanelF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/SettingsPanelF.java new file mode 100644 index 00000000000..3b129e940ea --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/SettingsPanelF.java @@ -0,0 +1,505 @@ +/* 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.settings; + +import java.io.File; +import java.util.List; +import javafx.beans.value.ObservableValue; +import javafx.geometry.HPos; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Node; +import javafx.scene.control.CheckBox; +import javafx.scene.control.ComboBox; +import javafx.scene.control.Control; +import javafx.scene.control.Label; +import javafx.scene.control.ScrollPane; +import javafx.scene.control.Separator; +import javafx.scene.control.Slider; +import javafx.scene.control.Spinner; +import javafx.scene.control.SpinnerValueFactory; +import javafx.scene.control.TextArea; +import javafx.scene.control.TextField; +import javafx.scene.control.Tooltip; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.ColumnConstraints; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.Region; +import javafx.scene.layout.VBox; +import javafx.stage.FileChooser; + +import de.uka.ilkd.key.gui.fx.fonticons.IconFactoryF; +import de.uka.ilkd.key.gui.fx.fonticons.IconFactoryF.Key; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A simple panel for using inside the settings dialog, counter-part of + * {@code SimpleSettingsPanel}/{@code SettingsPanel} of the Swing module {@code key.ui}. + *

+ * The panel provides a header (title and sub title) and a scrollable form area. The Swing + * originals build the form with MigLayout; here the form is a code-built {@link GridPane} with + * three columns (title, input, help icon), mirroring the MigLayout {@code wrapAfter(3)} setup. + *

+ * Holds factory methods for creating input components which validate their content while being + * edited: invalid input marks the component (style class {@code settings-input-error} backed by + * the {@code SETTINGS_TEXTFIELD_ERROR} color) and shows the error message as a tooltip. + */ +public abstract class SettingsPanelF extends BorderPane { + + private static final Logger LOGGER = LoggerFactory.getLogger(SettingsPanelF.class); + + /** the form area; subclasses append rows via the {@code add*} factory methods */ + protected final GridPane pCenter = new GridPane(); + + private final Label lblHead = new Label(); + private final Label lblSubhead = new Label(); + + protected SettingsPanelF() { + lblHead.getStyleClass().add("settings-panel-title"); + lblSubhead.getStyleClass().add("settings-panel-subtitle"); + + VBox header = new VBox(2, lblHead, lblSubhead, new Separator()); + header.getStyleClass().add("settings-panel-header"); + header.setPadding(new Insets(5, 5, 5, 5)); + setTop(header); + + // three columns like the Swing MigLayout: title (right aligned, no grow), input (grows), + // help icon (fixed, right) + ColumnConstraints titles = new ColumnConstraints(); + titles.setHalignment(HPos.RIGHT); + ColumnConstraints inputs = new ColumnConstraints(); + inputs.setHgrow(Priority.ALWAYS); + inputs.setFillWidth(true); + ColumnConstraints helps = new ColumnConstraints(); + helps.setMinWidth(24); + helps.setPrefWidth(24); + helps.setHalignment(HPos.RIGHT); + pCenter.getColumnConstraints().setAll(titles, inputs, helps); + pCenter.getStyleClass().add("settings-form"); + pCenter.setHgap(8); + pCenter.setVgap(6); + pCenter.setPadding(new Insets(8, 8, 8, 8)); + + ScrollPane scrollPane = new ScrollPane(pCenter); + scrollPane.setFitToWidth(true); + scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); + setCenter(scrollPane); + } + + /** + * Sets the header text (the large bold title). + * + * @param text the title + */ + public void setHeaderText(String text) { + lblHead.setText(text); + } + + /** + * Sets the sub header text (the small secondary line below the title). + * + * @param text the sub title + */ + public void setSubHeaderText(String text) { + lblSubhead.setText(text); + lblSubhead.setVisible(text != null && !text.isEmpty()); + } + + // ------------------------------------------------------------------ + // error marking (Swing markComponentAsErrornous / demarkComponentAsErrornous) + // ------------------------------------------------------------------ + + /** + * Marks the given component as erroneous: the error style class is applied and the error + * message shown as a tooltip (Swing sets the background color from + * {@code SETTINGS_TEXTFIELD_ERROR}). + * + * @param component the input component + * @param error the error message + */ + protected void markComponentAsErrornous(Control component, String error) { + if (!component.getStyleClass().contains("settings-input-error")) { + component.getStyleClass().add("settings-input-error"); + } + component.setTooltip(new Tooltip(error)); + } + + /** + * Removes the error marking of the given component. + * + * @param component the input component + */ + protected void demarkComponentAsErrornous(Control component) { + component.getStyleClass().remove("settings-input-error"); + component.setTooltip(null); + } + + /** + * Validates the value behind the given observable on every change and marks the component + * accordingly (the port of the Swing {@code DocumentListener}/{@code ChangeListener} + * adapters). + * + * @param component the input component to mark + * @param observable the value observable + * @param validator the validator, may be null + * @param the value type + */ + protected void wireValidator(Control component, ObservableValue observable, + Validator validator) { + if (validator == null) { + return; + } + observable.addListener((obs, old, value) -> { + try { + validator.validate(value); + demarkComponentAsErrornous(component); + } catch (Exception ex) { + LOGGER.debug("Rejected settings input: {}", ex.getMessage()); + markComponentAsErrornous(component, ex.getMessage()); + } + }); + } + + // ------------------------------------------------------------------ + // component factories (ports of the Swing SettingsPanel factory methods) + // ------------------------------------------------------------------ + + /** + * Creates a labeled separator line (Swing {@code addSeparator}). + * + * @param titleText the title shown left of the line + */ + protected void addSeparator(String titleText) { + Label label = new Label(titleText); + label.getStyleClass().add("settings-separator-title"); + Separator separator = new Separator(); + separator.setMinWidth(0); + HBox.setHgrow(separator, Priority.ALWAYS); + separator.setMaxWidth(Double.MAX_VALUE); + HBox box = new HBox(8, label, separator); + box.setAlignment(Pos.CENTER_LEFT); + pCenter.add(box, 0, pCenter.getRowCount(), 3, 1); + GridPane.setHgrow(box, Priority.ALWAYS); + } + + /** + * Creates a checkbox row (Swing {@code addCheckBox}); the Swing originals pass an empty + * validator here, so no validation is wired. + * + * @param title the label of the checkbox + * @param info the help text, may be empty + * @param value the initial state + * @return the created checkbox + */ + protected CheckBox addCheckBox(String title, String info, boolean value) { + CheckBox checkBox = new CheckBox(title); + checkBox.setSelected(value); + addRowWithHelp(info, new Label(), checkBox); + return checkBox; + } + + /** + * Adds a text field row with live validation (Swing {@code addTextField}). + * + * @param title the label of the field + * @param text the initial content + * @param info the help text, may be empty + * @param validator the validator, may be null + * @return the created text field + */ + protected TextField addTextField(String title, String text, String info, + Validator validator) { + TextField field = new TextField(text); + wireValidator(field, field.textProperty(), validator); + addTitledComponent(title, field, info); + return field; + } + + /** + * Adds a text area row with live validation (Swing {@code addTextArea}, without the extra + * scroll pane: the whole panel scrolls here). + * + * @param title the label of the area + * @param text the initial content + * @param info the help text, may be empty + * @param validator the validator, may be null + * @param rows the preferred number of visible rows + * @return the created text area + */ + protected TextArea addTextArea(String title, String text, String info, + Validator validator, int rows) { + TextArea field = new TextArea(text); + field.setPrefRowCount(rows); + field.setWrapText(true); + wireValidator(field, field.textProperty(), validator); + addTitledComponent(title, field, info); + return field; + } + + /** + * Adds an integer spinner row for numbers in {@code [min, max]} with live validation (Swing + * {@code addNumberField}). + * + * @param title the label of the field + * @param min the minimum value + * @param max the maximum value + * @param step the step size of the arrow buttons + * @param value the initial value + * @param info the help text, may be empty + * @param validator the validator, may be null + * @return the created spinner + */ + protected Spinner addIntNumberField(String title, int min, int max, int step, + int value, String info, Validator validator) { + Spinner field = + new Spinner<>( + new SpinnerValueFactory.IntegerSpinnerValueFactory(min, max, value, step)); + field.setEditable(true); + wireSpinner(field, validator); + addTitledComponent(title, field, info); + return field; + } + + /** + * Adds a double spinner row for numbers in {@code [min, max]} with live validation (Swing + * {@code addNumberField} with a fractional model). + * + * @param title the label of the field + * @param min the minimum value + * @param max the maximum value + * @param step the step size of the arrow buttons + * @param value the initial value + * @param info the help text, may be empty + * @param validator the validator, may be null + * @return the created spinner + */ + protected Spinner addDoubleNumberField(String title, double min, double max, + double step, double value, String info, Validator validator) { + Spinner field = + new Spinner<>( + new SpinnerValueFactory.DoubleSpinnerValueFactory(min, max, value, step)); + field.setEditable(true); + wireSpinner(field, validator); + addTitledComponent(title, field, info); + return field; + } + + private void wireSpinner(Spinner field, Validator validator) { + if (validator == null) { + return; + } + field.valueProperty().addListener((obs, old, value) -> { + try { + validator.validate(value); + demarkComponentAsErrornous(field); + } catch (Exception ex) { + LOGGER.debug("Rejected settings input: {}", ex.getMessage()); + markComponentAsErrornous(field, ex.getMessage()); + } + }); + } + + /** + * Adds a combo box row (Swing {@code addComboBox}). + * + * @param the item type + * @param title the label of the combo box, may be null to skip the title + * @param info the help text, may be empty + * @param items the items + * @return the created combo box + */ + @SafeVarargs + protected final ComboBox addComboBox(String title, String info, T... items) { + ComboBox comboBox = new ComboBox<>(); + comboBox.getItems().setAll(items); + addTitledComponent(title, comboBox, info); + return comboBox; + } + + /** + * Creates a combo box with live validation without adding a row (Swing + * {@code createSelection}). + * + * @param the item type + * @param items the items + * @param validator the validator, may be null + * @return the created combo box + */ + @SafeVarargs + protected final ComboBox createSelection(Validator validator, T... items) { + ComboBox comboBox = new ComboBox<>(); + comboBox.getItems().setAll(items); + wireValidator(comboBox, comboBox.getSelectionModel().selectedItemProperty(), validator); + return comboBox; + } + + /** + * Adds a titled component row: a right-aligned title, the component and the help icon (Swing + * {@code addTitledComponent}). + * + * @param title the label of the component, may be null to skip the title + * @param component the input component + * @param helpText the help text, may be empty + */ + protected void addTitledComponent(String title, Node component, String helpText) { + Node titleNode = title == null ? new Label() : new Label(title); + addRowWithHelp(helpText, titleNode, component); + } + + /** + * Adds a row of components followed by the help icon column (Swing + * {@code addRowWithHelp}): the first component goes into the title column, the remaining + * components share the input column. + * + * @param info the help text; empty text leaves an empty help cell + * @param components the components of the row + */ + protected void addRowWithHelp(String info, Node... components) { + int row = pCenter.getRowCount(); + Node title = components.length > 0 ? components[0] : new Label(); + pCenter.add(title, 0, row); + Node input; + if (components.length == 2) { + input = components[1]; + } else if (components.length > 2) { + HBox box = new HBox(8, java.util.Arrays.copyOfRange(components, 1, components.length)); + box.setAlignment(Pos.CENTER_LEFT); + input = box; + } else { + input = new Label(); + } + pCenter.add(input, 1, row); + if (info != null && !info.isEmpty()) { + pCenter.add(createHelpLabel(info), 2, row); + } else { + pCenter.add(new Label(), 2, row); + } + } + + /** + * Adds a file chooser row: a text field with a browse button (Swing + * {@code addFileChooserPanel}; the Swing original uses {@code KeYFileChooser}, here a plain + * JavaFX {@link FileChooser}). + * + * @param title the label of the field + * @param file the initial file name + * @param info the help text, may be empty + * @param isSave whether the chooser opens in save mode + * @param validator the validator, may be null + * @return the created text field + */ + protected TextField addFileChooserPanel(String title, String file, String info, + boolean isSave, Validator validator) { + TextField textField = new TextField(file); + wireValidator(textField, textField.textProperty(), validator); + javafx.scene.control.Button btnFileChooser = + new javafx.scene.control.Button(null, IconFactoryF.createIcon(Key.SEARCH, 12)); + btnFileChooser.setOnAction(e -> { + FileChooser chooser = + new FileChooser(); + chooser.setTitle(isSave ? "Save file" : "Open file"); + chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("All Files", "*.*")); + File initial = new File(textField.getText()); + if (isSave) { + if (!initial.isDirectory() && initial.getParentFile() != null) { + chooser.setInitialDirectory(initial.getParentFile()); + } + chooser.setInitialFileName(initial.getName()); + } else if (initial.isDirectory()) { + chooser.setInitialDirectory(initial); + } + File selected = + isSave ? chooser.showSaveDialog(btnFileChooser.getScene().getWindow()) + : chooser.showOpenDialog(btnFileChooser.getScene().getWindow()); + if (selected != null) { + textField.setText(selected.getAbsolutePath()); + } + }); + HBox box = new HBox(4, textField, btnFileChooser); + HBox.setHgrow(textField, Priority.ALWAYS); + addTitledComponent(title, box, info); + return textField; + } + + /** + * Adds a group of radio buttons as one row (Swing {@code addRadioButtons}). + * + * @param title the label of the group + * @param alternatives the alternatives, rendered via {@code toString} + * @param description the help text, may be empty + * @return the toggle group selecting the alternatives + */ + protected javafx.scene.control.ToggleGroup addRadioButtons(String title, + List alternatives, String description) { + VBox items = new VBox(2); + javafx.scene.control.ToggleGroup group = new javafx.scene.control.ToggleGroup(); + for (Object alt : alternatives) { + javafx.scene.control.RadioButton button = + new javafx.scene.control.RadioButton(alt.toString()); + button.setToggleGroup(group); + button.setUserData(alt); + items.getChildren().add(button); + } + addTitledComponent(title, items, description); + return group; + } + + /** + * Creates a help label showing the given information as a tooltip (Swing + * {@code createHelpLabel}). + * + * @param s the information + * @return a label with a question mark icon and the tooltip + */ + public static Label createHelpLabel(String s) { + Label infoLabel = new Label(null, IconFactoryF.createIcon(Key.HELP)); + if (s != null && !s.isEmpty()) { + infoLabel.setTooltip(new Tooltip(s)); + } + return infoLabel; + } + + /** + * Creates an empty validator instance (Swing {@code emptyValidator}). + * + * @param arbitrary + * @return a validator accepting everything + */ + protected static Validator emptyValidator() { + return value -> { + }; + } + + /** + * Adds a slider row (not present in the Swing original; helper for range inputs that would + * otherwise need a spinner). + * + * @param title the label of the slider + * @param min the minimum value + * @param max the maximum value + * @param value the initial value + * @param info the help text, may be empty + * @return the created slider + */ + protected Slider addSlider(String title, double min, double max, double value, String info) { + Slider slider = new Slider(min, max, value); + slider.setShowTickLabels(true); + addTitledComponent(title, slider, info); + return slider; + } + + /** + * A filler region for grid rows without content in the first column. + * + * @return an empty region + */ + protected static Region spacer() { + return new Region(); + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/SettingsProviderF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/SettingsProviderF.java index 53de9176a26..9f0274baed9 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/SettingsProviderF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/SettingsProviderF.java @@ -5,54 +5,96 @@ package de.uka.ilkd.key.gui.fx.settings; import java.util.List; - import javafx.scene.Node; +import de.uka.ilkd.key.gui.fx.MainWindowF; + /** - * A single entry of the KeY settings dialog, counter-part of {@code SettingsProvider} of the - * Swing module {@code key.ui}. + * A settings provider is an entry in the settings dialog, counter-part of + * {@code de.uka.ilkd.key.gui.settings.SettingsProvider} of the Swing module {@code key.ui}. *

- * The provider supplies a human readable description (shown in the settings tree), a category - * (grouping in the tree), and a JavaFX panel building the input controls. When the user accepts - * the dialog, {@link #apply()} is invoked for every provider; implementors must transfer the - * values of their input components into the respective settings. + * It is displayed within the settings tree by {@link #getDescription()}. Tree children are + * determined by {@link #getChildProviders()} (named differently than in the Swing original + * because {@code getChildren} clashes with {@link javafx.scene.Parent#getChildren()}). The most + * important functions are {@link #apply(MainWindowF)} + * and {@link #getPanel(MainWindowF)}. + * + * @author Alexander Weigl */ public interface SettingsProviderF { /** - * @return a human readable description shown in the settings tree + * A textual human readable description of the settings panel. Used at the overview tree at + * the left. + * + * @return non-null non-empty string */ String getDescription(); /** - * @return the name of the category group in the settings tree + * Provides the visual component for the right side. + *

+ * This panel will be wrapped inside a {@link javafx.scene.control.ScrollPane}. + *

+ * You are allowed to reuse the returned component, in which case you should update its input + * components on every call (the Swing originals re-read the settings in + * {@code getPanel(MainWindow)} as well). + * + * @param window the main window, non-null + * @return the panel node */ - String getCategory(); + Node getPanel(MainWindowF window); /** - * Provides the panel shown on the right side of the settings dialog when this provider is - * selected. + * Tree children of your settings provider. + *

+ * Use this method to split your settings into multiple panels; they are displayed as children + * within the tree. (Swing {@code SettingsProvider.getChildren}; the FX port renames the + * accessor because {@code getChildren} clashes with + * {@link javafx.scene.Parent#getChildren()}.) * - * @return a fresh panel; may also be reused, in which case it must be updated on selection + * @return non-null list, default returns the empty list */ - Node getPanel(); + default List getChildProviders() { + return List.of(); + } /** - * Transfers the values of the panel's input components into the corresponding settings. - * Called for every provider when the user accepts or applies the settings. + * The method is called if the settings should be applied to the main window. + *

+ * If the user clicks on Apply or OK, the settings dialog calls this method for every + * registered provider (including the children). Read your values from the input components + * and update the settings of the main window. If a field is not in the appropriate format, + * throw an {@link InvalidSettingsInputExceptionF} with a reference to the panel and the + * component; this prevents the settings dialog from closing (Swing parity). + * + * @param window the main window, non-null + * @throws InvalidSettingsInputExceptionF if an input component is not properly filled */ - void apply(); + void apply(MainWindowF window) throws InvalidSettingsInputExceptionF; /** - * Restores the values shown in the panel to the values currently stored in the settings. + * Determines whether the given search string matches this settings provider; matching + * providers are highlighted in the tree of the settings dialog. + *

+ * The default matches the description (in the Swing module the default is {@code false}, + * which leaves the search without effect for the built-in providers; the description match + * is the obvious fallback). + * + * @param substring a possibly empty, non-null string + * @return true iff the search should highlight this settings provider */ - default void reset() { + default boolean contains(String substring) { + return substring != null && !substring.isEmpty() + && getDescription().toLowerCase().contains(substring.toLowerCase()); } /** - * @return provider-specific sub categories/panels, empty if none + * Determines the order in the tree of the settings. Higher values are shown last. + * + * @return the priority */ - default List getChildren() { - return List.of(); + default int getPriorityOfSettings() { + return 0; } } diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/StandardUISettingsF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/StandardUISettingsF.java new file mode 100644 index 00000000000..715e401bb7a --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/StandardUISettingsF.java @@ -0,0 +1,240 @@ +/* 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.settings; + +import javafx.scene.Node; +import javafx.scene.control.CheckBox; +import javafx.scene.control.ComboBox; +import javafx.scene.control.Label; +import javafx.scene.control.RadioButton; +import javafx.scene.control.Spinner; +import javafx.scene.control.TextArea; +import javafx.scene.control.Toggle; +import javafx.scene.control.ToggleGroup; + +import de.uka.ilkd.key.gui.fx.MainWindowF; +import de.uka.ilkd.key.gui.fx.configuration.ConfigF; +import de.uka.ilkd.key.gui.fx.theme.Theme; +import de.uka.ilkd.key.gui.fx.theme.ThemeManager; +import de.uka.ilkd.key.settings.GeneralSettings; +import de.uka.ilkd.key.settings.ProofIndependentSettings; +import de.uka.ilkd.key.settings.ViewSettings; + +/** + * Settings provider for the appearance and behaviour of the UI, counter-part of + * {@code de.uka.ilkd.key.gui.settings.StandardUISettings} of the Swing module {@code key.ui}. + *

+ * It reads and writes the core {@link ViewSettings} and {@link GeneralSettings} (untouched code + * in {@code key.core}, shared with the Swing UI). The Swing look-and-feel selector does not + * carry over: the JavaFX UI has the two themes of the {@link ThemeManager} instead, selectable + * here (applied immediately, like the Swing theme concept). The font size and font factor + * settings are persisted like in Swing but apply to newly created views (the FX views pick the + * fonts up at construction; a live resize facade is deferred — see the M4 report). + *

+ * The children are the colors and the keyboard shortcuts panels, mirroring the Swing + * {@code getChildren()} (renamed to {@link #getChildProviders()} here, see + * {@link SettingsProviderF}). + */ +public class StandardUISettingsF extends SettingsPanelF implements SettingsProviderF { + + private static final String INFO_CLUTTER_RULESET = + "Comma separated list of rule set names, containing clutter rules."; + private static final String INFO_CLUTTER_RULE = "Comma separated listof clutter rules, \n" + + "which are rules with less priority in the taclet menu"; + private static final String INFO_THEME = + "Light and dark theme of the JavaFX UI. The change is applied immediately to all open " + + "windows."; + private static final String INFO_FONT_SIZE = + "Font size of the tree and sequent views. Applies to newly created views."; + private static final String INFO_FONT_FACTOR = + "Global scaling factor of the UI fonts. Applies to newly created views."; + private static final String INFO_CLASSIC_TACLET_DIALOG = """ + Use the classic (pre-2026) dialog when completing an interactive taclet \ + application. + Offered as a fallback for a migration period; the redesigned dialog is the \ + default."""; + private static final String MINIMIZE_INTERACTION_TIP = + "If not ticked, applying a taclet manually will require you to instantiate " + + "all schema variables."; + private static final String INFO_MAX_TOOLTIP_LINES = """ + Maximum size (line count) of the tooltips of applicable rules + with schema variable instantiations displayed. + In case of longer tooltips the instantiation will be suppressed. + """; + + private final ToggleGroup themeGroup = new ToggleGroup(); + private RadioButton lightRadio; + private RadioButton darkRadio; + private Spinner spFontSizeGlobal; + private ComboBox spFontSizeTreeSequent; + private Spinner txtMaxTooltipLines; + private CheckBox chkShowLoadExamplesDialog; + private CheckBox chkConfirmExit; + private Spinner spAutoSaveProof; + private ComboBox notificationAfterMacro; + private CheckBox chkPrettyPrint; + private CheckBox chkUseUnicode; + private CheckBox chkSyntaxHighlightning; + private CheckBox chkHidePackagePrefix; + private CheckBox chkRightClickMacros; + private CheckBox chkShowWholeTacletCB; + private CheckBox chkShowUninstantiatedTaclet; + private TextArea txtClutterRules; + private TextArea txtClutterRuleSets; + private CheckBox chkMinimizeInteraction; + private CheckBox chkEnsureSourceConsistency; + private CheckBox chkUseClassicTacletDialog; + + /** Creates the provider and builds the form once (the panel is reused). */ + public StandardUISettingsF() { + setHeaderText(getDescription()); + + addSeparator("General"); + + lightRadio = new RadioButton("Light"); + darkRadio = new RadioButton("Dark"); + lightRadio.setToggleGroup(themeGroup); + darkRadio.setToggleGroup(themeGroup); + lightRadio.setUserData(Theme.LIGHT); + darkRadio.setUserData(Theme.DARK); + addRowWithHelp(INFO_THEME, new Label("Theme:"), lightRadio, darkRadio); + spFontSizeGlobal = addDoubleNumberField("Global font factor", 0.1, 5, 0.1, 1.0, + INFO_FONT_FACTOR, emptyValidator()); + String[] sizes = java.util.Arrays.stream(ConfigF.SIZES) + .boxed().map(it -> it + " pt").toArray(String[]::new); + spFontSizeTreeSequent = addComboBox("Tree and sequent font size", INFO_FONT_SIZE, sizes); + txtMaxTooltipLines = addIntNumberField("Maximum line number for tooltips", 1, 100, 5, 40, + INFO_MAX_TOOLTIP_LINES, emptyValidator()); + chkShowLoadExamplesDialog = + addCheckBox("Show load examples dialog on startup", "", true); + chkConfirmExit = addCheckBox("Confirm program exit", "", false); + spAutoSaveProof = + addIntNumberField("Auto save proof", 0, 10000000, 1000, 0, "", emptyValidator()); + notificationAfterMacro = addComboBox("Notification after macro finished", "", + ViewSettings.NOTIFICATION_ALWAYS, ViewSettings.NOTIFICATION_UNFOCUSED, + ViewSettings.NOTIFICATION_NEVER); + + addSeparator("Sequent View"); + chkPrettyPrint = addCheckBox("Pretty print terms", "", false); + chkUseUnicode = addCheckBox("Use unicode", "", false); + chkSyntaxHighlightning = addCheckBox("Use syntax highlighting", "", false); + chkHidePackagePrefix = addCheckBox("Hide package prefix", "", false); + chkRightClickMacros = addCheckBox("Right click for proof macros", "", false); + + addSeparator("Interaction"); + chkShowWholeTacletCB = addCheckBox("Show whole taclet", + "Pretty-print whole Taclet including 'name', 'find', 'varCond' and 'heuristics'\n" + + "(applies to tooltips in context menu)", + false); + chkShowUninstantiatedTaclet = + addCheckBox("Show uninstantiated taclet", "recommended for unexperienced users", + false); + txtClutterRules = addTextArea("Clutter rules", "", INFO_CLUTTER_RULE, emptyValidator(), 4); + txtClutterRuleSets = + addTextArea("Clutter Rulesets", "", INFO_CLUTTER_RULESET, emptyValidator(), 4); + chkMinimizeInteraction = addCheckBox("Minimise interactions", MINIMIZE_INTERACTION_TIP, + false); + chkEnsureSourceConsistency = addCheckBox("Ensure source consistency", "", true); + chkUseClassicTacletDialog = addCheckBox("Use classic taclet instantiation dialog", + INFO_CLASSIC_TACLET_DIALOG, false); + + refreshFromSettings(); + } + + @Override + public String getDescription() { + return "Appearance & Behaviour"; + } + + @Override + public java.util.List getChildProviders() { + // the colors and keyboard shortcut panels are added with their commits in this milestone + return java.util.List.of(); + } + + @Override + public Node getPanel(MainWindowF window) { + refreshFromSettings(); + return this; + } + + /** + * Re-reads the current settings into the input components (Swing + * {@code StandardUISettings.getPanel}). + */ + private void refreshFromSettings() { + ViewSettings vs = ProofIndependentSettings.DEFAULT_INSTANCE.getViewSettings(); + GeneralSettings generalSettings = + ProofIndependentSettings.DEFAULT_INSTANCE.getGeneralSettings(); + + txtClutterRules.setText(vs.clutterRules().value().replace(',', '\n')); + txtClutterRuleSets.setText(vs.clutterRuleSets().value().replace(',', '\n')); + + Toggle themeToggle = ThemeManager.getInstance().getTheme() == Theme.DARK ? darkRadio + : lightRadio; + themeToggle.setSelected(true); + + spFontSizeGlobal.getValueFactory().setValue(vs.getUIFontSizeFactor()); + spFontSizeTreeSequent.getSelectionModel().select(vs.sizeIndex()); + txtMaxTooltipLines.getValueFactory().setValue(vs.getMaxTooltipLines()); + chkShowLoadExamplesDialog.setSelected(vs.getShowLoadExamplesDialog()); + chkShowWholeTacletCB.setSelected(vs.getShowWholeTaclet()); + chkShowUninstantiatedTaclet.setSelected(vs.getShowUninstantiatedTaclet()); + chkHidePackagePrefix.setSelected(vs.isHidePackagePrefix()); + chkPrettyPrint.setSelected(vs.isUsePretty()); + chkUseUnicode.setSelected(vs.isUseUnicode()); + chkSyntaxHighlightning.setSelected(vs.isUseSyntaxHighlighting()); + chkEnsureSourceConsistency.setSelected(generalSettings.isEnsureSourceConsistency()); + chkRightClickMacros.setSelected(generalSettings.isRightClickMacro()); + chkConfirmExit.setSelected(vs.confirmExit()); + spAutoSaveProof.getValueFactory().setValue(generalSettings.autoSavePeriod()); + chkMinimizeInteraction.setSelected(generalSettings.getTacletFilter()); + chkUseClassicTacletDialog.setSelected(vs.isUseClassicTacletDialog()); + notificationAfterMacro.getSelectionModel().select(vs.notificationAfterMacro()); + } + + @Override + public void apply(MainWindowF window) throws InvalidSettingsInputExceptionF { + ViewSettings vs = ProofIndependentSettings.DEFAULT_INSTANCE.getViewSettings(); + GeneralSettings gs = ProofIndependentSettings.DEFAULT_INSTANCE.getGeneralSettings(); + + vs.setUIFontSizeFactor(spFontSizeGlobal.getValue()); + vs.setMaxTooltipLines(txtMaxTooltipLines.getValue()); + vs.setShowLoadExamplesDialog(chkShowLoadExamplesDialog.isSelected()); + vs.setShowWholeTaclet(chkShowWholeTacletCB.isSelected()); + vs.setShowUninstantiatedTaclet(chkShowUninstantiatedTaclet.isSelected()); + vs.setHidePackagePrefix(chkHidePackagePrefix.isSelected()); + vs.setUsePretty(chkPrettyPrint.isSelected()); + vs.setUseUnicode(chkUseUnicode.isSelected()); + vs.setUseSyntaxHighlighting(chkSyntaxHighlightning.isSelected()); + gs.setEnsureSourceConsistency(chkEnsureSourceConsistency.isSelected()); + gs.setRightClickMacros(chkRightClickMacros.isSelected()); + vs.setConfirmExit(chkConfirmExit.isSelected()); + gs.setAutoSave(spAutoSaveProof.getValue()); + gs.setTacletFilter(chkMinimizeInteraction.isSelected()); + vs.setUseClassicTacletDialog(chkUseClassicTacletDialog.isSelected()); + vs.setFontIndex(spFontSizeTreeSequent.getSelectionModel().getSelectedIndex()); + String notification = notificationAfterMacro.getSelectionModel().getSelectedItem(); + if (notification != null) { + vs.setNotificationAfterMacro(notification); + } + vs.clutterRules().parseFrom(txtClutterRules.getText().replace('\n', ',')); + vs.clutterRuleSets().parseFrom(txtClutterRuleSets.getText().replace('\n', ',')); + + // theme switch applies immediately (the Swing look and feel needs a restart instead) + Toggle themeToggle = themeGroup.getSelectedToggle(); + if (themeToggle != null) { + ThemeManager.getInstance().setTheme((Theme) themeToggle.getUserData()); + } + // the Swing original additionally rescales the fonts (FontSizeFacade) and fires + // Config.DEFAULT.fireConfigChange(); the FX views pick font changes up when they are + // (re)created, so no equivalent call exists yet + } + + @Override + public int getPriorityOfSettings() { + return Integer.MIN_VALUE; + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/ThemeSettingsProviderF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/ThemeSettingsProviderF.java deleted file mode 100644 index c1d8c71e9d8..00000000000 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/ThemeSettingsProviderF.java +++ /dev/null @@ -1,101 +0,0 @@ -/* 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.settings; - -import javafx.collections.FXCollections; -import javafx.geometry.Insets; -import javafx.scene.Node; -import javafx.scene.control.ComboBox; -import javafx.scene.control.Label; -import javafx.scene.control.RadioButton; -import javafx.scene.control.ToggleGroup; -import javafx.scene.layout.GridPane; -import javafx.scene.layout.VBox; - -import de.uka.ilkd.key.gui.fx.configuration.ConfigF; -import de.uka.ilkd.key.gui.fx.theme.Theme; -import de.uka.ilkd.key.gui.fx.theme.ThemeManager; -import de.uka.ilkd.key.settings.ProofIndependentSettings; -import de.uka.ilkd.key.settings.ViewSettings; - -/** - * Settings provider for the look of the JavaFX UI: light/dark theme and the base font size. The - * font size is persisted within the core {@link ViewSettings} (same {@code SIZES} as the Swing - * module's {@code Config}); the theme is managed by the {@link ThemeManager}. - */ -public final class ThemeSettingsProviderF implements SettingsProviderF { - - private final ToggleGroup themeGroup = new ToggleGroup(); - private final ComboBox fontSizeComboBox = new ComboBox<>(); - - private RadioButton lightRadio; - private RadioButton darkRadio; - - @Override - public String getDescription() { - return "Look & Feel"; - } - - @Override - public String getCategory() { - return "General"; - } - - @Override - public Node getPanel() { - lightRadio = createRadio("Light", Theme.LIGHT); - darkRadio = createRadio("Dark", Theme.DARK); - Theme current = ThemeManager.getInstance().getTheme(); - (current == Theme.DARK ? darkRadio : lightRadio).setSelected(true); - - fontSizeComboBox.setItems( - FXCollections.observableArrayList(ConfigF.SIZES[0], ConfigF.SIZES[1], ConfigF.SIZES[2], - ConfigF.SIZES[3], ConfigF.SIZES[4], ConfigF.SIZES[5])); - fontSizeComboBox.getSelectionModel().select(ConfigF.DEFAULT.sizeIndex()); - - VBox packet = new VBox(16); - packet.setPadding(new Insets(12)); - - GridPane themeRow = new GridPane(); - themeRow.setHgap(8); - themeRow.add(new Label("Theme:"), 0, 0); - themeRow.add(lightRadio, 1, 0); - themeRow.add(darkRadio, 2, 0); - - GridPane fontRow = new GridPane(); - fontRow.setHgap(8); - fontRow.add(new Label("Base font size:"), 0, 0); - fontRow.add(fontSizeComboBox, 1, 0); - - packet.getChildren().addAll(themeRow, fontRow); - return packet; - } - - @Override - public void apply() { - Theme selected = darkRadio.isSelected() ? Theme.DARK : Theme.LIGHT; - ThemeManager.getInstance().setTheme(selected); - int index = fontSizeComboBox.getSelectionModel().getSelectedIndex(); - if (index >= 0) { - ViewSettings viewSettings = - ProofIndependentSettings.DEFAULT_INSTANCE.getViewSettings(); - viewSettings.setFontIndex(index); - } - } - - @Override - public void reset() { - Theme current = ThemeManager.getInstance().getTheme(); - (current == Theme.DARK ? darkRadio : lightRadio).setSelected(true); - fontSizeComboBox.getSelectionModel().select(ConfigF.DEFAULT.sizeIndex()); - } - - private RadioButton createRadio(String label, Theme theme) { - RadioButton radio = new RadioButton(label); - radio.setToggleGroup(themeGroup); - radio.setUserData(theme); - return radio; - } -} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/Validator.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/Validator.java new file mode 100644 index 00000000000..1f6cd56abd5 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/Validator.java @@ -0,0 +1,24 @@ +/* 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.settings; + +/** + * A validator for input components of a settings panel, counter-part of {@code Validator} of the + * Swing module {@code key.ui}. + * + * @author Alexander Weigl + */ +@FunctionalInterface +public interface Validator { + + /** + * Validates the given value; the empty implementation accepts everything. + * + * @param obj the value to validate + * @throws Exception if the value is rejected; the message is shown as a tooltip on the + * rejected input component + */ + void validate(T obj) throws Exception; +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/theme/ThemeManager.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/theme/ThemeManager.java index 8b1b4928524..5130ff4ce2b 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/theme/ThemeManager.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/theme/ThemeManager.java @@ -4,11 +4,11 @@ * SPDX-License-Identifier: GPL-2.0-or-later */ package de.uka.ilkd.key.gui.fx.theme; -import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; import javafx.beans.property.ReadOnlyObjectProperty; import javafx.beans.property.ReadOnlyObjectWrapper; +import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.scene.Scene; @@ -33,7 +33,8 @@ public final class ThemeManager { */ public static final String THEME_PROPERTY = "key.fx.theme"; - private final List scenes = new CopyOnWriteArrayList<>(); + private final ObservableList scenes = + FXCollections.observableList(new CopyOnWriteArrayList<>()); private final ReadOnlyObjectWrapper currentTheme = new ReadOnlyObjectWrapper<>(this, "theme", initialTheme()); @@ -71,6 +72,14 @@ public void manage(Scene scene) { updateScene(scene); } + /** + * @return the managed scenes (observable, so listeners can follow scene registration, e.g. + * the {@code ColorSettingsF} applying its CSS overrides to new scenes) + */ + public ObservableList getScenes() { + return scenes; + } + /** * Applies the current theme to the given scene without registering it for later updates. * Handy for one-off windows (e.g. dialogs) that should follow the next programmatic theme diff --git a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css index 1f0ec45d726..9837bea94b6 100644 --- a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css +++ b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css @@ -21,6 +21,22 @@ -key-interactive: #e3a94f; -key-border: #555555; + /* + * Configurable colors shared with the Swing UI (colors.json); the file keys are mapped to + * these properties by ColorSettingsF.CSS_VARIABLES and the defaults are the dark defaults + * of the Swing ColorSettings. The sequent prop-logic default is the plain text color + * (white in the Swing UI). + */ + -key-settings-error: #c86464; + -key-hl-java: #cf50a5; + -key-hl-comment: #9fbf9f; + -key-hl-jml: #8888cf; + -key-hl-jml-keyword: #8888cf; + -key-sequent-hl-prop: -key-text; + -key-sequent-hl-dyn: #9696fa; + -key-sequent-hl-progvar: #6464fa; + -key-sequent-hl-arrow: #6464fa; + /* restyle the built-in Modena controls for the dark theme */ -fx-base: #3c3f41; -fx-background: #2b2b2b; @@ -110,33 +126,34 @@ these rules must stay before the search rules so the search and click highlights override the syntax coloring for -fx-fill */ .sequent-hl-prop { + -fx-fill: -key-sequent-hl-prop; -fx-font-weight: bold; } .sequent-hl-dyn { - -fx-fill: #9696fa; + -fx-fill: -key-sequent-hl-dyn; -fx-font-weight: bold; } .sequent-hl-java { - -fx-fill: #cf50a5; + -fx-fill: -key-hl-java; -fx-font-weight: bold; } .sequent-hl-progvar { - -fx-fill: #6464fa; + -fx-fill: -key-sequent-hl-progvar; } .sequent-hl-comment { - -fx-fill: #9fbf9f; + -fx-fill: -key-hl-comment; } .sequent-hl-jml { - -fx-fill: #8888cf; + -fx-fill: -key-hl-jml; } .sequent-hl-arrow { - -fx-fill: #6464fa; + -fx-fill: -key-sequent-hl-arrow; -fx-font-weight: bold; } @@ -422,12 +439,12 @@ } .source-keyword { - -fx-fill: -key-accent; + -fx-fill: -key-hl-java; -fx-font-weight: bold; } .source-comment { - -fx-fill: -key-text-secondary; + -fx-fill: -key-hl-comment; -fx-font-style: italic; } @@ -440,5 +457,92 @@ } .source-jml-keyword { + -fx-fill: -key-hl-jml-keyword; + -fx-font-weight: bold; +} + +/* + * Settings dialog (M4, Swing SettingsDialog/SettingsUi/SimpleSettingsPanel). + */ +.settings-panel-header { + -fx-background-color: -key-surface; +} + +.settings-panel-title { + -fx-font-size: 1.3em; -fx-font-weight: bold; + -fx-text-fill: -key-text; +} + +.settings-panel-subtitle { + -fx-text-fill: -key-text-secondary; +} + +.settings-form { + -fx-background-color: -key-surface; +} + +.settings-separator-title { + -fx-font-weight: bold; + -fx-text-fill: -key-text; +} + +.settings-warning-header { + -fx-font-size: 1.1em; + -fx-text-fill: -key-text; +} + +.settings-taclet-option-title { + -fx-font-size: 1.1em; + -fx-text-fill: -key-text; +} + +.settings-explanation { + -fx-text-fill: -key-text-secondary; +} + +/* the provider tree with the search field on the left */ +.settings-tree-panel { + -fx-background-color: -key-surface; + -fx-border-color: -key-border; + -fx-border-radius: 3; + -fx-background-radius: 3; + -fx-padding: 4; + -fx-min-width: 200; +} + +.settings-search-row .label { + -fx-text-fill: -key-text-secondary; +} + +.settings-tree-match { + -fx-text-fill: -key-accent; + -fx-font-weight: bold; +} + +/* the selected provider's panel on the right */ +.settings-panel-holder { + -fx-background-color: -key-surface; + -fx-padding: 4; +} + +/* erroneous input components (SettingsPanelF.markComponentAsErrornous); -key-settings-error is + configurable as SETTINGS_TEXTFIELD_ERROR in colors.json */ +.settings-input-error { + -fx-background-color: -key-settings-error; +} + +.color-swatch { + -fx-border-color: -key-border; + -fx-background-radius: 2; + -fx-border-radius: 2; +} + +/* warning icons (IconFactoryF.Key.WARNING_*; the plain .key-icon fill is the text color) */ +.key-icon.key-icon-warning { + -fx-fill: -key-warning; +} + +.key-icon.key-icon-error { + -fx-fill: -key-error; } diff --git a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css index 6a890f7128b..d002d80a39c 100644 --- a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css +++ b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css @@ -26,6 +26,22 @@ -key-linked: #1a73e8; -key-interactive: #b06000; -key-border: #d0d0d0; + + /* + * Configurable colors shared with the Swing UI (colors.json); the file keys are mapped to + * these properties by ColorSettingsF.CSS_VARIABLES and the defaults are the light defaults + * of the Swing ColorSettings. The sequent prop-logic default is the plain text color + * (black in the Swing UI). + */ + -key-settings-error: #c86464; + -key-hl-java: #7f0055; + -key-hl-comment: #3f7f5f; + -key-hl-jml: #0000c0; + -key-hl-jml-keyword: #0000f0; + -key-sequent-hl-prop: -key-text; + -key-sequent-hl-dyn: #0000d0; + -key-sequent-hl-progvar: #6a3e3e; + -key-sequent-hl-arrow: #6a3e3e; } /* status bar */ @@ -106,33 +122,34 @@ these rules must stay before the search rules so the search and click highlights override the syntax coloring for -fx-fill */ .sequent-hl-prop { + -fx-fill: -key-sequent-hl-prop; -fx-font-weight: bold; } .sequent-hl-dyn { - -fx-fill: #0000d0; + -fx-fill: -key-sequent-hl-dyn; -fx-font-weight: bold; } .sequent-hl-java { - -fx-fill: #7f0055; + -fx-fill: -key-hl-java; -fx-font-weight: bold; } .sequent-hl-progvar { - -fx-fill: #6a3e3e; + -fx-fill: -key-sequent-hl-progvar; } .sequent-hl-comment { - -fx-fill: #3f7f5f; + -fx-fill: -key-hl-comment; } .sequent-hl-jml { - -fx-fill: #0000f0; + -fx-fill: -key-hl-jml; } .sequent-hl-arrow { - -fx-fill: #6a3e3e; + -fx-fill: -key-sequent-hl-arrow; -fx-font-weight: bold; } @@ -418,12 +435,12 @@ } .source-keyword { - -fx-fill: -key-accent; + -fx-fill: -key-hl-java; -fx-font-weight: bold; } .source-comment { - -fx-fill: -key-text-secondary; + -fx-fill: -key-hl-comment; -fx-font-style: italic; } @@ -436,5 +453,92 @@ } .source-jml-keyword { + -fx-fill: -key-hl-jml-keyword; + -fx-font-weight: bold; +} + +/* + * Settings dialog (M4, Swing SettingsDialog/SettingsUi/SimpleSettingsPanel). + */ +.settings-panel-header { + -fx-background-color: -key-surface; +} + +.settings-panel-title { + -fx-font-size: 1.3em; + -fx-font-weight: bold; + -fx-text-fill: -key-text; +} + +.settings-panel-subtitle { + -fx-text-fill: -key-text-secondary; +} + +.settings-form { + -fx-background-color: -key-surface; +} + +.settings-separator-title { -fx-font-weight: bold; + -fx-text-fill: -key-text; +} + +.settings-warning-header { + -fx-font-size: 1.1em; + -fx-text-fill: -key-text; +} + +.settings-taclet-option-title { + -fx-font-size: 1.1em; + -fx-text-fill: -key-text; +} + +.settings-explanation { + -fx-text-fill: -key-text-secondary; +} + +/* the provider tree with the search field on the left */ +.settings-tree-panel { + -fx-background-color: -key-surface; + -fx-border-color: -key-border; + -fx-border-radius: 3; + -fx-background-radius: 3; + -fx-padding: 4; + -fx-min-width: 200; +} + +.settings-search-row .label { + -fx-text-fill: -key-text-secondary; +} + +.settings-tree-match { + -fx-text-fill: -key-accent; + -fx-font-weight: bold; +} + +/* the selected provider's panel on the right */ +.settings-panel-holder { + -fx-background-color: -key-surface; + -fx-padding: 4; +} + +/* erroneous input components (SettingsPanelF.markComponentAsErrornous); -key-settings-error is + configurable as SETTINGS_TEXTFIELD_ERROR in colors.json */ +.settings-input-error { + -fx-background-color: -key-settings-error; +} + +.color-swatch { + -fx-border-color: -key-border; + -fx-background-radius: 2; + -fx-border-radius: 2; +} + +/* warning icons (IconFactoryF.Key.WARNING_*; the plain .key-icon fill is the text color) */ +.key-icon.key-icon-warning { + -fx-fill: -key-warning; +} + +.key-icon.key-icon-error { + -fx-fill: -key-error; } From e936f8c840597233557b67dc9354ac38fd694e19 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Wed, 7 Oct 2026 23:43:53 +0200 Subject: [PATCH 032/281] key.ui.fx: M4 settings dialog taclet options provider Ports the Swing TacletOptionsSettings into TacletOptionsSettingsF: one collapsible TitledPane section per choice category (sorted, collapsed by default like the Swing toggle panes), a radio button per available choice with the incomplete/unsound warning icons and the extra-information help tooltips, and the category explanation from the shared /de/uka/ilkd/key/gui/help/choiceExplanations.xml resource (copied from the Swing module). - Like the Swing original, the radio buttons are only shown when a proof is loaded (otherwise the 'No Proof loaded' hint stays visible and the explanations are read-only); the section title shows the current choice and warns when it differs from the loaded proof. - The panel reads a detached copy of the choice settings (proof-specific when a proof is loaded, SettingsManagerF.getChoiceSettings) and applying writes the edited choices through to ProofSettings.DEFAULT_SETTINGS, so reloading a problem picks them up (the write-through fix of the Swing original). - SettingsManagerF registers the provider and carries the static getChoiceSettings helper (Swing SettingsManager.getChoiceSettings). --- .../key/gui/fx/settings/SettingsManagerF.java | 11 +- .../fx/settings/TacletOptionsSettingsF.java | 501 ++++++++++++++++++ .../ilkd/key/gui/help/choiceExplanations.xml | 271 ++++++++++ 3 files changed, 780 insertions(+), 3 deletions(-) create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/TacletOptionsSettingsF.java create mode 100644 key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/help/choiceExplanations.xml diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/SettingsManagerF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/SettingsManagerF.java index 9bec269d3a6..bbf7b276a7a 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/SettingsManagerF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/SettingsManagerF.java @@ -65,9 +65,13 @@ public final class SettingsManagerF { /** the first provider of the tree (Swing {@code SettingsManager.STANDARD_UI_SETTINGS}) */ public static final StandardUISettingsF STANDARD_UI_SETTINGS = new StandardUISettingsF(); - // Deliberately deferred to later milestones (the provider seam stays open): the taclet - // options provider follows in this milestone, SMTSettingsProvider, JavacSettingsProvider, - // ParallelProverSettingsProvider, FeatureSettingsPanel and the ShowActiveSettings dump later. + /** the taclet options provider (Swing {@code SettingsManager.TACLET_OPTIONS_SETTINGS}) */ + public static final TacletOptionsSettingsF TACLET_OPTIONS_SETTINGS = + new TacletOptionsSettingsF(); + + // Deliberately deferred to later milestones (the provider seam stays open): + // SMTSettingsProvider, JavacSettingsProvider, ParallelProverSettingsProvider, + // FeatureSettingsPanel and the ShowActiveSettings dump. private static SettingsManagerF INSTANCE; @@ -83,6 +87,7 @@ public static SettingsManagerF getInstance() { if (INSTANCE == null) { INSTANCE = new SettingsManagerF(); INSTANCE.add(STANDARD_UI_SETTINGS); + INSTANCE.add(TACLET_OPTIONS_SETTINGS); } return INSTANCE; } diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/TacletOptionsSettingsF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/TacletOptionsSettingsF.java new file mode 100644 index 00000000000..2950abc5340 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/TacletOptionsSettingsF.java @@ -0,0 +1,501 @@ +/* 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.settings; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.stream.Collectors; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Node; +import javafx.scene.control.ContentDisplay; +import javafx.scene.control.Label; +import javafx.scene.control.RadioButton; +import javafx.scene.control.TitledPane; +import javafx.scene.control.ToggleGroup; +import javafx.scene.control.Tooltip; +import javafx.scene.layout.HBox; +import javafx.scene.layout.VBox; + +import de.uka.ilkd.key.gui.fx.MainWindowF; +import de.uka.ilkd.key.gui.fx.fonticons.IconFactoryF; +import de.uka.ilkd.key.gui.fx.fonticons.IconFactoryF.Key; +import de.uka.ilkd.key.proof.Proof; +import de.uka.ilkd.key.settings.ChoiceSettings; +import de.uka.ilkd.key.settings.ProofSettings; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Settings provider for the taclet options, counter-part of + * {@code de.uka.ilkd.key.gui.settings.TacletOptionsSettings} of the Swing module {@code key.ui}. + *

+ * The panel shows one collapsible section ({@link TitledPane}) per choice category with a radio + * button per available choice, marked with an incomplete ({@link Key#WARNING_INCOMPLETE}) or + * unsound ({@link Key#WARNING_UNSOUND}) icon if applicable, followed by the explanation of the + * category (from the shared {@code choiceExplanations.xml} resource, see + * {@link #getExplanation(String)}). + *

+ * Like the Swing original, the radio buttons are only shown when a proof is loaded (Swing hides + * them together with the "No Proof loaded" hint staying visible); the warning that the options + * take effect only on new proofs is always shown, and the section title warns if the current + * choice differs from the loaded proof. + *

+ * Applying writes the edited choices through to the global default choice settings, + * {@link ProofSettings#DEFAULT_SETTINGS}; that is what loading a problem reads to build a new + * proof (Swing {@code TacletOptionsSettings.applySettings}). + */ +public class TacletOptionsSettingsF extends SettingsPanelF implements SettingsProviderF { + + private static final Logger LOGGER = LoggerFactory.getLogger(TacletOptionsSettingsF.class); + + /** + * the explanations resource, shared with the Swing UI (Swing + * {@code TacletOptionsSettings.EXPLANATIONS_RESOURCE}) + */ + private static final String EXPLANATIONS_RESOURCE = + "/de/uka/ilkd/key/gui/help/choiceExplanations.xml"; + + private static Properties explanationMap; + + private final Map category2Choice = new HashMap<>(); + private final Map> category2Choices = new HashMap<>(); + + private boolean warnNoProof = true; + + private Proof loadedProof = null; + + public TacletOptionsSettingsF() { + setHeaderText(getDescription()); + setChoiceSettings(ProofSettings.DEFAULT_SETTINGS.getChoiceSettings()); + } + + /** + *

+ * Returns the explanation for the given category. + *

+ *

+ * This method is public and static because it is independent from the dialog and it is also + * used outside (Swing {@code TacletOptionsSettings.getExplanation}). + *

+ * + * @param category the category for which the explanation is requested + * @return the explanation for the given category + */ + public static String getExplanation(String category) { + synchronized (TacletOptionsSettingsF.class) { + if (explanationMap == null) { + explanationMap = new Properties(); + InputStream is = + TacletOptionsSettingsF.class.getResourceAsStream(EXPLANATIONS_RESOURCE); + try { + if (is == null) { + throw new FileNotFoundException(EXPLANATIONS_RESOURCE + " not found"); + } + explanationMap.loadFromXML(is); + } catch (IllegalArgumentException e) { + LOGGER.error("Cannot load help message in rule view (malformed XML).", e); + } catch (IOException e) { + LOGGER.error("Cannot load help messages in rule view.", e); + } + } + } + String result = explanationMap.getProperty(category); + if (result == null) { + result = "No explanation for " + category + " available."; + } + return result; + } + + /** + * Checks if the given choice makes a proof unsound (Swing + * {@code TacletOptionsSettings.isUnsound}). + * + * @param choice the choice to check + * @return {@code true} proof will be unsound, {@code false} proof will be sound as long as + * all other choices are sound + */ + public static boolean isUnsound(String choice) { + return "runtimeExceptions:ignore".equals(choice) + || "initialisation:disableStaticInitialisation".equals(choice) + || "intRules:arithmeticSemanticsIgnoringOF".equals(choice); + } + + /** + * Checks if the given choice makes a proof incomplete (Swing + * {@code TacletOptionsSettings.isIncomplete}). + * + * @param choice the choice to check + * @return {@code true} proof will be incomplete, {@code false} proof will be complete as long + * as all other choices are complete + */ + public static boolean isIncomplete(String choice) { + return "runtimeExceptions:ban".equals(choice) || "Strings:off".equals(choice) + || "intRules:arithmeticSemanticsCheckingOF".equals(choice) + || "integerSimplificationRules:minimal".equals(choice) + || "programRules:None".equals(choice); + } + + /** + * Checks if additional information for the choice are available (Swing + * {@code TacletOptionsSettings.getInformation}). + * + * @param choice the choice to check + * @return the additional information or {@code null} if no information are available + */ + public static String getInformation(String choice) { + if ("JavaCard:on".equals(choice)) { + return "Sound if a JavaCard program is proven."; + } else if ("JavaCard:off".equals(choice)) { + return "Sound if a Java program is proven."; + } else if ("assertions:on".equals(choice)) { + return "Sound if JVM is started with enabled assertions for the whole system."; + } else if ("assertions:off".equals(choice)) { + return "Sound if JVM is started with disabled assertions for the whole system."; + } else { + return null; + } + } + + /** + * Searches the choice in the given {@link ChoiceEntry}s (Swing + * {@code TacletOptionsSettings.findChoice}). + * + * @param choices the {@link ChoiceEntry}s to search in + * @param choice the choice to search + * @return the found {@link ChoiceEntry} for the given choice or {@code null} otherwise + */ + public static ChoiceEntry findChoice(List choices, String choice) { + return choices.stream().filter(it -> it.getChoice().equals(choice)).findAny().orElse(null); + } + + /** + * Creates {@link ChoiceEntry}s for all given choices (Swing + * {@code TacletOptionsSettings.createChoiceEntries}). + * + * @param choices the choices + * @return the created {@link ChoiceEntry}s + */ + public static List createChoiceEntries(Collection choices) { + if (choices == null) { + return Collections.emptyList(); + } + return choices.stream().map(TacletOptionsSettingsF::createChoiceEntry) + .collect(Collectors.toList()); + } + + /** + * Creates a {@link ChoiceEntry} for the given choice (Swing + * {@code TacletOptionsSettings.createChoiceEntry}). + * + * @param choice the choice + * @return the created {@link ChoiceEntry} + */ + public static ChoiceEntry createChoiceEntry(String choice) { + return new ChoiceEntry(choice, isUnsound(choice), isIncomplete(choice), + getInformation(choice)); + } + + @Override + public String getDescription() { + return "Taclet Options"; + } + + @Override + public Node getPanel(MainWindowF window) { + loadedProof = window.getMediator().getSelectedProof(); + warnNoProof = loadedProof == null; + setChoiceSettings(SettingsManagerF.getChoiceSettings(window)); + return this; + } + + private void setChoiceSettings(ChoiceSettings choiceSettings) { + category2Choice.clear(); + category2Choice.putAll(choiceSettings.getDefaultChoices()); + category2Choices.clear(); + category2Choices.putAll(choiceSettings.getChoices()); + rebuild(); + } + + /** + * Rebuilds the warning headers and the collapsible category sections (Swing + * {@code layoutChoiceSelector}/ {@code layoutHead}; the Swing original rebuilds on every + * {@code getPanel} as well). + */ + private void rebuild() { + pCenter.getChildren().clear(); + addHeaderRows(); + category2Choice.keySet().stream().sorted(String::compareToIgnoreCase) + .forEach(this::addCategory); + } + + private void addHeaderRows() { + Label noProofLoadedHeader = + new Label("No Proof loaded. Taclet options may not be parsed."); + noProofLoadedHeader.setGraphic(IconFactoryF.createIcon(Key.WARNING_INCOMPLETE)); + noProofLoadedHeader.getStyleClass().add("settings-warning-header"); + // Swing makes the header invisible when a proof is loaded + noProofLoadedHeader.setVisible(warnNoProof); + noProofLoadedHeader.setManaged(warnNoProof); + addFullWidthRow(noProofLoadedHeader); + + Label lblHead2 = new Label("Taclet options will take effect only on new proofs."); + lblHead2.setGraphic(IconFactoryF.createIcon(Key.WARNING_INCOMPLETE)); + lblHead2.getStyleClass().add("settings-warning-header"); + addFullWidthRow(lblHead2); + } + + private void addFullWidthRow(Node node) { + pCenter.add(node, 0, pCenter.getRowCount(), 3, 1); + } + + private void addCategory(String cat) { + List choices = createChoiceEntries(category2Choices.get(cat)); + ChoiceEntry selectedChoice = findChoice(choices, category2Choice.get(cat)); + String explanation = getExplanation(cat); + + Label title = createTitleRow(cat, selectedChoice); + + VBox selectPanel = new VBox(4); + if (!warnNoProof) { + // the Swing original shows the radio buttons only if a proof is loaded + ToggleGroup btnGroup = new ToggleGroup(); + for (ChoiceEntry c : choices) { + HBox row = mkRadioButton(c, btnGroup, title, cat); + if (c.equals(selectedChoice)) { + ((RadioButton) row.getChildren().get(0)).setSelected(true); + } + selectPanel.getChildren().add(row); + } + } + Label explanationArea = mkExplanation(explanation); + VBox.setMargin(explanationArea, new Insets(0, 0, 0, 20)); + selectPanel.getChildren().add(explanationArea); + + // Swing collapses the sections by default (createCollapsableTitlePane) + TitledPane catEntry = new TitledPane(null, selectPanel); + catEntry.setGraphic(title); + catEntry.setExpanded(false); + addFullWidthRow(catEntry); + } + + private Label mkExplanation(String explanation) { + Label explanationArea = new Label(explanation.trim()); + explanationArea.setWrapText(true); + explanationArea.getStyleClass().add("settings-explanation"); + return explanationArea; + } + + /** + * Creates the row of one radio button with its meta information icons (Swing + * {@code mkRadioButton}): the choice marked with the incomplete/unsound icons and the extra + * information as a help tooltip. + */ + private HBox mkRadioButton(ChoiceEntry c, ToggleGroup btnGroup, Label title, String cat) { + RadioButton button = new RadioButton(c.getChoice()); + button.setToggleGroup(btnGroup); + HBox box = new HBox(6, button); + box.setAlignment(Pos.CENTER_LEFT); + + if (c.isIncomplete()) { + Label lbl = new Label(null, IconFactoryF.createIcon(Key.WARNING_INCOMPLETE)); + lbl.setTooltip(new Tooltip("Incomplete")); + box.getChildren().add(lbl); + } + if (c.isUnsound()) { + Label lbl = new Label(null, IconFactoryF.createIcon(Key.WARNING_UNSOUND)); + lbl.setTooltip(new Tooltip("Unsound")); + box.getChildren().add(lbl); + } + if (c.getInformation() != null) { + box.getChildren().add(SettingsPanelF.createHelpLabel(c.getInformation())); + } + + // Swing ChoiceSettingsSetter: record the selection and update the section title + button.setOnAction(e -> { + category2Choice.put(cat, c.getChoice()); + title.setText(createCatTitleText(cat, c)); + checkForDifferingOptions(title, cat, c); + }); + return box; + } + + private Label createTitleRow(String cat, ChoiceEntry entry) { + Label lbl = new Label(createCatTitleText(cat, entry)); + lbl.getStyleClass().add("settings-taclet-option-title"); + + // we want to display a warning if the current choice differs from the loaded proof + checkForDifferingOptions(lbl, cat, entry); + + return lbl; + } + + private String createCatTitleText(String cat, ChoiceEntry entry) { + // if no proof is loaded, we do not want to display current settings + if (warnNoProof) { + return cat; + } + + // strip the leading "cat:" from "cat:value" + return cat + (entry == null ? "" + : " (set to '" + entry.getChoice().substring(cat.length() + 1) + "')"); + } + + /** + * Checks if the current choice {@code entry} differs from the loaded proof and sets the + * warning icon if necessary (Swing {@code checkForDifferingOptions}). + * + * @param lbl the label to set the icon on + * @param cat the category of the choice + * @param entry the current choice + */ + private void checkForDifferingOptions(Label lbl, String cat, ChoiceEntry entry) { + if (loadedProof != null) { + String choiceOfLoadedProof = + loadedProof.getSettings().getChoiceSettings().getDefaultChoices().get(cat); + boolean choiceDiffers = + entry != null && !entry.getChoice().equals(choiceOfLoadedProof); + if (choiceDiffers) { + lbl.setGraphic(IconFactoryF.createIcon(Key.WARNING_INCOMPLETE)); + // the Swing original places the icon behind the text (iconTextPosition LEFT) + lbl.setContentDisplay(ContentDisplay.RIGHT); + lbl.setTooltip(new Tooltip("The current choice of this option differs from the " + + "loaded proof.\nThe loaded proof uses: " + choiceOfLoadedProof)); + } else { + lbl.setGraphic(null); + lbl.setTooltip(null); + } + } + } + + @Override + public void apply(MainWindowF window) { + // Apply to the global default choice settings - that is what (re)loading a problem reads + // to build a new proof. When a proof is loaded, getChoiceSettings() hands this panel a + // detached copy initialised from that proof (so it can show the proof's active options), + // and writing the edited choices only into that copy silently dropped them: changing e.g. + // the integer semantics and reloading kept the old taclet option. Write through to the + // global settings. (Comment ported from the Swing original.) + ProofSettings.DEFAULT_SETTINGS.getChoiceSettings().setDefaultChoices(category2Choice); + } + + /** + * Represents a choice with all its meta information (Swing + * {@code TacletOptionsSettings.ChoiceEntry}). + */ + public static class ChoiceEntry { + + /** text shown to the user in case of incompleteness */ + public static final String INCOMPLETE_TEXT = "incomplete"; + + /** text shown to the user in case of unsoundness */ + public static final String UNSOUND_TEXT = "Java modeling unsound"; + + private final String choice; + private final boolean unsound; + private final boolean incomplete; + private final String information; + + /** + * Constructor. + * + * @param choice the choice + * @param unsound is unsound? + * @param incomplete is incomplete? + * @param information an optional information + */ + public ChoiceEntry(String choice, boolean unsound, boolean incomplete, + String information) { + this.choice = choice; + this.unsound = unsound; + this.incomplete = incomplete; + this.information = information; + } + + /** @return the choice */ + public String getChoice() { + return choice; + } + + /** + * Checks for soundness. + * + * @return {@code true} unsound, {@code false} sound + */ + public boolean isUnsound() { + return unsound; + } + + /** + * Checks for completeness. + * + * @return {@code true} incomplete, {@code false} complete + */ + public boolean isIncomplete() { + return incomplete; + } + + /** @return the optionally available information */ + public String getInformation() { + return information; + } + + @Override + public int hashCode() { + int hashcode = 5; + hashcode = hashcode * 17 + choice.hashCode(); + hashcode = hashcode * 17 + (incomplete ? 5 : 3); + hashcode = hashcode * 17 + (unsound ? 5 : 3); + if (information != null) { + hashcode = hashcode * 17 + information.hashCode(); + } + return hashcode; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof ChoiceEntry other) { + return choice.equals(other.getChoice()) && incomplete == other.isIncomplete() + && unsound == other.isUnsound() + && java.util.Objects.equals(information, other.getInformation()); + } + return false; + } + + @Override + public String toString() { + if (unsound && incomplete) { + if (information != null) { + return String.format("%s (%s and %s, %s)", choice, UNSOUND_TEXT, + INCOMPLETE_TEXT, information); + } + return String.format("%s (%s and %s)", choice, UNSOUND_TEXT, INCOMPLETE_TEXT); + } else if (unsound) { + if (information != null) { + return String.format("%s (%s, %s)", choice, UNSOUND_TEXT, information); + } + return String.format("%s (%s)", choice, UNSOUND_TEXT); + } else if (incomplete) { + if (information != null) { + return String.format("%s (%s, %s)", choice, INCOMPLETE_TEXT, information); + } + return String.format("%s (%s)", choice, INCOMPLETE_TEXT); + } + if (information != null) { + return String.format("%s (%s)", choice, information); + } + return choice; + } + } +} diff --git a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/help/choiceExplanations.xml b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/help/choiceExplanations.xml new file mode 100644 index 00000000000..5a75e2e3407 --- /dev/null +++ b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/help/choiceExplanations.xml @@ -0,0 +1,271 @@ + + + + + These properties define the help comments which are to be displayed + in the taclet option dialog + +Treatment of Java assertion statements + +Java assertions can be handled in three different ways in KeY: + +* If switched 'off', assert statements are skipped. + +* If switched 'on', assertions are treated like Java would treat them: + Asserted Statements are evaluated and an AssertionException is + raised if they turn out to be false. + +* If set to 'safe', the following proof obligations have to be shown: + - The program with assertions removed must fulfill the specification + - The asserted conditions must hold (without throwing an exception, + and with termination) + - The assertions do not have side effects influencing the post + conditions. + This is the default option. + Proofs with this option are typically harder. + + + +JavaCard is a dialect of Java designed for the use in SmartCards. It +lacks floating point operations and concurrency, but provides stronger +object persistence guarantees. + +There are two values for this option 'on' and 'off'. Switching +on or off all taclets axiomatising JavaCard specific features like transac- +tion. + + + +Java loop invariant rules. + +* 'efficient': Uses the original loop scope method with two branches. + + Recommended for long and complicated loops, especially for real-world + problems. + +* 'teaching': Loop Invariant treatment producing the well-known three + branch structure (base case ('Invariant Initially Valid'), step ('Body + Preserves Invariant'), Use Case). + + Recommended for teaching. + +Background: + +The loop scope method is an efficient way to apply loop invariants to +loops in Java programs. It does not transform the body of a loop and +handles irregular termination (exceptions, breaks, returns, continues) +gracefully. In its pure variant, the preserved (step) and use case parts +are combined to one single proof branch, which spares duplicate code +execution, but is less easy to comprehend since the traditional three-step +structure frequently used in teaching is not easy to see. Therefore, +there is a loop-scope based loop invariant rule with three branches, +which is less efficient (and thus not recommended for complex and long +real-world programs), but more transparent: If a proof case in the +preserved branch cannot be closed, one can be sure that it is related to +an invariant that cannot be proven (and not to the post condition), and +vice versa for the use case. + + + +Specifies whether a special goal "Joined node is weakening" should be +generated as a child of the partner node of a join operation. + +For join procedures formally proven correct, this should not be necessary. +Enable this option when you are not sure whether a newly implemented join +procedure is sound. In this case, the generated "is weakening" goals should +only be closable if the concrete join instance is correct. + + + +Rules for how to deal with floating point operations. A JVM +implementation may choose to use a datatype with a higher precision +than float or double. + +All KeY rules concerning floating point numbers, as well as their +translation to SMT-LIB, only make sense for strictfp floats where the +semantics is fixed. + +* assumeStrictfp: treat all floats as if they came from a class class +annotated strictfp. This is convenient, but not always sound, as the +compiler might make optimizations which break some assumptions which +the KeY rules and SMT-LIB translation depend on. + +* strictfpOnly: floats are usable, but they are only translated to the +respective IEEE typed if they come from a class or method annotated +strictfp, and remain uninterpreted functions otherwise. + + + +Specifies whether static (class loading) initialization should be +taken in consideration. + +Both specifications and proof obligations become significantly more +difficult since class loading may take place at many places. + + + +This option controls how integer numbers are modeled. + +* 'Java semantics' treat integers the same way Java would treat them. + The different integer types operate within their respective value + ranges. The bitvector arithmetic is modeled in KeY using modulo + expressions. This is sound and complete. Proof obligations tend + to get more complex with this setting. + +* 'Arithmetic without overflow checking' treats integers as pure + mathematical objects. The proof obligations are often easier to + discharge. However, the model does not allow the verification of all + properties which hold with Java semantics. Moreover, it allows the + verification of properties which do not hold on Java's actual + semantics. This is the default option. + +* 'Arithmetic with overflow checking' also treats integers as + mathematical objects, but ensures that no overflow occurs. While + this model also has a completeness gap, it prevents proving + incorrect properties. + + + +Rules dealing Java language constructs can be turned off by setting +this to 'None'. + + +Treatment of implicit runtime exceptions + +* If set to 'allow', implicit runtime exceptions are raised as specified + in the Java language specification. + +* If set to 'ban', any occurrence of an implicit runtime exception is + considered an unrecoverable program failure. For programs which do not + raise implicit runtime exceptions, this is usually easier to prove than + 'allow'. This is the default option. + +* If set to 'ignore', it is assumed that no implicit runtime exception occurs. + Warning: Reasoning with these rules is unsound. + + +Rules for model field representation clauses + +JML model fields are given a semantics by represents clauses. This +switch sets how the rules handle these clauses. + +* If set to 'treatAsAxiom', the representation is seen as an axiom, + satisfiability will not be checked. This may introduce inconsistent + specifications, for example the following contradictory JML clause + will not be rejected: + //@ represents modelField == modelField + 1; + This is the default option since KeY 2.4. + +* If set to 'showSatisfiability', for every expansion of the represents + clause, it must be shown that the definition is _locally_ + satisfiable. Cross-definition inconsistencies can still be + formulated, however: + //@ represents modelField1 == modelField2; + //@ represents modelField2 == modelField1 + 1; + This had been the default option previously, until KeY 2.2. + + + +Loading rules dealing with Strings (charLists) can be disabled. + + + +Loading program rules dealing with JML's \bigint datatype can be disabled. + + + +Loading rules dealing with sequences can be disabled. + + + +KeY adds a default contract to any loaded method +that has no explicit contract (whether that method is in the loaded directory or the classpath). + +If this option is turned on, the following sound default is used (note that the method contract rule for such a contract +can only be applied when proving partial correctness): + + public behavior + ensures true; + diverges true; + signals_only Throwable; + assignable \everything; + +Otherwise, the following unsound default is used, which may be useful when developing proofs: + + public normal_behavior + ensures true; + assignable \strictly_nothing; + + + +This option allows more fine-grained control over rules dealing with sequences. By default, it is disabled because the additional rules are known to have a negative impact on overall performance. Activate this option if your problem is concerned with permutations or information flow. + + + +Loading rules dealing with reachability can be disabled. + + +Loading less commonly used rules for (mathematical) integers, such as rules for bounded sums and products, modulo, or polynomials, can be disabled. If they are loaded, their application in the strategy can still be controlled as usual. + +This option is experimental. Depending of your understanding of 'less common', you may experience incompleteness. Doing proofs with Java int semantics will not work, definitely. + +Not to be confused with intRules, which controls the semantics of the Java type int. + + + +The optimized select rules can be turned off by setting this option to off. + +The unoptimized rules use inplace rewriting for the evaluation of select terms. This may lead to heap terms growing exponentially in the number of anons/stores to be evaluated. However, in some rare cases the usage of the unoptimized rules may lead to more human readable sequents. + +The optimized rules pull out select terms before they evaluate them. Because other occurences of the same select term are replaced by the Skolem constant introduced by the pullout rule (application of equalities), each select term has to be evaluated only once. Intelligent hiding of no longer needed auxiliary equations holds the sequent human readable and increases performance. The result of the evaluation of a select term is stored such that it can be reapplied immediately if the same select term occurs again. + + + +Treatment of formulas and terms for welldefinedness checks: + +* L-Operator: + More intuitive for software developers and along the lines of + runtime assertion semantics. Well-Definedness checks will be + stricter using this operator, since the order of terms/formulas + matters. It is based on McCarthy logic. + Cf. "Are the Logical Foundations of Verifying Compiler + Prototypes Matching User Expectations?" by Patrice Chalin. + This is the default option. + +* Y-Operator: + Complete and along the lines of classical logic, where the + order of terms/formulas is irrelevant. This operator is + equivalent to the D-operator, but more efficient. + Cf. "Efficient Well-Definedness Checking" by Ádám Darvas, + Farhad Mehta, and Arsenii Rudich. + +* D-Operator: + Complete and along the lines of classical logic, where the + order of terms/formulas is irrelevant. This operator is not as + strict as the L-operator, based on strong Kleene logic. To be + used with care, since formulas may blow up exponentially. + Cf. "Well Defined B" by Patrick Behm, Lilian Burdy, and + Jean-Marc Meynadier + + + +Welldefinedness checks of JML specifications can be turned on/off. This includes class invariants, operation contracts, model fields as well as JML (annotation) statements as loop invariants and block contracts. The former ones are checked "on-the-fly", i.e., directly when they are applied in the code while proving an operation contract, since the context is needed. + + + +Final fields can only be written to from within the constructor. This is a +Java language feature. KeY can exploit this restriction by treating final fields +as immutable. This can simplify the reasoning about the program significantly. +But it is still possible to tread final fields as normal mutable fields to +ensure backward compatibility. If constructor code is to be inlined, there +can be cases where the immutable treatment is incomplete. + +immutable: + Final fields are treated as immutable entities. + This is the default option. + +onHeap: + Final fields are treated like all Java fields. + This is the original behaviour of KeY. + + From 4357eb51cb035c16c659246336d3a3d26053bcea Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Wed, 7 Oct 2026 23:44:03 +0200 Subject: [PATCH 033/281] key.ui.fx: M4 settings dialog colors adapter + colors panel Ports the Swing ColorSettings/ColorSettingsProvider into the JavaFX module: ColorSettingsF keeps the shared colors.json (same keys, same #AARRGGBB file format, single value for both themes or a [light,dark] list) and applies the user values as CSS custom property overrides on the root nodes of all managed scenes, so colors.json edits restyle the UI immediately and follow the theme switches. - CSS_VARIABLES documents the mapping of the Swing keys to the -key-* properties (error marking, source/sequent highlighting, proof tree state colors); keys without an FX counterpart are kept in the file and listed as unmapped in the Javadoc. - toHex keeps the Swing file format (#AARRGGBB); the CSS formatter toCssHex emits the JavaFX web format (#RRGGBBAA). - The colors panel (ColorSettingsProviderF) lists all defined properties with the light and dark colors each, editable as hex text with a swatch preview; applying persists the overrides and restyles the scenes. Unlike the Swing original, edits made in the panel are persisted (Swing loses them). - SettingsPanelF carries the SETTINGS_TEXTFIELD_ERROR property (the Swing SimpleSettingsPanel.COLOR_ERROR), making the error color configurable in the panel; StandardUISettingsF gains the colors panel as tree child. --- .../key/gui/fx/colors/ColorSettingsF.java | 435 ++++++++++++++++++ .../fx/settings/ColorSettingsProviderF.java | 214 +++++++++ .../key/gui/fx/settings/SettingsPanelF.java | 10 + .../gui/fx/settings/StandardUISettingsF.java | 4 +- 4 files changed, 661 insertions(+), 2 deletions(-) create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/colors/ColorSettingsF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/ColorSettingsProviderF.java 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..69a31b8f597 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/colors/ColorSettingsF.java @@ -0,0 +1,435 @@ +/* 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.Writer; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.TreeMap; +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() { + 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(); + } + + /** + * @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/settings/ColorSettingsProviderF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/ColorSettingsProviderF.java new file mode 100644 index 00000000000..bf7b0e3d56a --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/ColorSettingsProviderF.java @@ -0,0 +1,214 @@ +/* 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.settings; + +import java.util.stream.Collectors; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.geometry.Insets; +import javafx.scene.Node; +import javafx.scene.control.ContentDisplay; +import javafx.scene.control.Label; +import javafx.scene.control.TableCell; +import javafx.scene.control.TableColumn; +import javafx.scene.control.TableView; +import javafx.scene.control.TextField; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.Region; +import javafx.scene.paint.Color; + +import de.uka.ilkd.key.gui.fx.MainWindowF; +import de.uka.ilkd.key.gui.fx.colors.ColorSettingsF; + +/** + * Settings provider for the configurable colors, counter-part of + * {@code de.uka.ilkd.key.gui.colors.ColorSettingsProvider} of the Swing module {@code key.ui}. + *

+ * The panel lists the color properties of {@link ColorSettingsF} in a table with the light and + * the dark color each; both are editable as {@code #AARRGGBB} hex strings (the editor preview + * colors the text field like the Swing {@code HexColorCellEditor}). Applying transfers the table + * values into the properties, which persist the overrides into the shared {@code colors.json} + * and restyle the UI immediately via the CSS property overrides of {@link ColorSettingsF}. + */ +public class ColorSettingsProviderF extends SettingsPanelF implements SettingsProviderF { + + private final TableView tblColors = new TableView<>(); + private boolean initialized; + + public ColorSettingsProviderF() { + setHeaderText(getDescription()); + setSubHeaderText( + "Color settings are stored in: " + ColorSettingsF.SETTINGS_FILE.toAbsolutePath()); + + TableColumn keyColumn = + new TableColumn<>("Key"); + keyColumn.setCellValueFactory( + data -> new javafx.beans.property.SimpleStringProperty(data.getValue().getKey())); + TableColumn descColumn = + new TableColumn<>("Description"); + descColumn.setCellValueFactory(data -> new javafx.beans.property.SimpleStringProperty( + data.getValue().getDescription())); + TableColumn lightColumn = + new TableColumn<>("Light Color"); + lightColumn.setCellValueFactory(data -> data.getValue().lightValueProperty()); + lightColumn.setCellFactory(col -> new ColorCell()); + lightColumn.setOnEditCommit(event -> event.getRowValue() + .setLightValue(event.getNewValue())); + TableColumn darkColumn = + new TableColumn<>("Dark Color"); + darkColumn.setCellValueFactory(data -> data.getValue().darkValueProperty()); + darkColumn.setCellFactory(col -> new ColorCell()); + darkColumn.setOnEditCommit(event -> event.getRowValue().setDarkValue(event.getNewValue())); + keyColumn.setPrefWidth(220); + descColumn.setPrefWidth(200); + lightColumn.setPrefWidth(130); + darkColumn.setPrefWidth(130); + + tblColors.getColumns().setAll(keyColumn, descColumn, lightColumn, darkColumn); + tblColors.setEditable(true); + // sorted by key ascending, like the Swing TableRowSorter + tblColors.getSortOrder().add(keyColumn); + tblColors.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY_FLEX_LAST_COLUMN); + setCenter(tblColors); + } + + @Override + public String getDescription() { + return "Colors"; + } + + @Override + public Node getPanel(MainWindowF window) { + if (!initialized) { + // Swing rebuilds the model on every getPanel; the properties are fixed once defined, + // so filling the items once is enough + ObservableList properties = + FXCollections.observableList( + ColorSettingsF.getInstance().getProperties() + .collect(Collectors.toList())); + tblColors.setItems(properties); + initialized = true; + } + tblColors.sort(); + return this; + } + + @Override + public void apply(MainWindowF window) { + // the property setters already persisted the override and applied the CSS overrides + ColorSettingsF.getInstance().applyToScenes(); + } + + /** + * A color cell showing the hex string with a swatch, editable as hex text (Swing's + * {@code HexColorCellEditor} renders the editor field with the color as background and the + * inverted color as foreground). + */ + private static class ColorCell extends TableCell { + + private final Region swatch = new Region(); + private final TextField editor = new TextField(); + private final HBox box = new HBox(6, swatch, editor); + private final Region swatchView = new Region(); + private final Label view = new Label(); + private final HBox display = new HBox(6, swatchView, view); + + ColorCell() { + swatch.setPrefSize(14, 14); + swatch.getStyleClass().add("color-swatch"); + swatchView.setPrefSize(14, 14); + swatchView.getStyleClass().add("color-swatch"); + editor.setPrefColumnCount(9); + HBox.setHgrow(editor, Priority.ALWAYS); + box.getStyleClass().add("color-editor"); + box.setPadding(new Insets(1)); + editor.setOnAction(e -> commit()); + editor.focusedProperty().addListener((obs, old, focused) -> { + if (!focused) { + cancelEdit(); + } + }); + setGraphic(null); + setContentDisplay(ContentDisplay.LEFT); + } + + @Override + protected void updateItem(Color item, boolean empty) { + super.updateItem(item, empty); + if (empty || item == null) { + setText(null); + setGraphic(null); + return; + } + if (isEditing()) { + editor.setText(ColorSettingsF.toHex(item)); + return; + } + show(item); + } + + @Override + public void startEdit() { + super.startEdit(); + Color color = getItem(); + if (color == null) { + return; + } + editor.setText(ColorSettingsF.toHex(color)); + setGraphic(box); + setText(null); + styleSwatch(swatch, color); + editor.requestFocus(); + editor.selectAll(); + } + + @Override + public void cancelEdit() { + Color color = getItem(); + super.cancelEdit(); + if (color == null) { + setGraphic(null); + return; + } + show(color); + } + + private void show(Color color) { + view.setText(ColorSettingsF.toHex(color)); + view.setTextFill(readable(color)); + styleSwatch(swatchView, color); + setGraphic(display); + setText(null); + } + + /** Commits the editor content; an unchanged or unparseable value cancels the edit. */ + private void commit() { + Color color; + try { + color = ColorSettingsF.fromHex(editor.getText().trim()); + editor.setStyle(""); + } catch (NumberFormatException e) { + editor.setStyle("-fx-text-fill: -key-error;"); + return; + } + if (color.equals(getItem())) { + cancelEdit(); + return; + } + commitEdit(color); + } + + private static void styleSwatch(Region region, Color color) { + region.setStyle("-fx-background-color: " + ColorSettingsF.toCssHex(color) + + "; -fx-border-color: -key-border; -fx-background-radius: 2;"); + } + + private static javafx.scene.paint.Color readable(Color c) { + double luminance = 0.299 * c.getRed() + 0.587 * c.getGreen() + 0.114 * c.getBlue(); + return luminance > 0.5 ? Color.BLACK : Color.WHITE; + } + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/SettingsPanelF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/SettingsPanelF.java index 3b129e940ea..9cd9b4f9231 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/SettingsPanelF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/SettingsPanelF.java @@ -32,6 +32,7 @@ import javafx.scene.layout.VBox; import javafx.stage.FileChooser; +import de.uka.ilkd.key.gui.fx.colors.ColorSettingsF; import de.uka.ilkd.key.gui.fx.fonticons.IconFactoryF; import de.uka.ilkd.key.gui.fx.fonticons.IconFactoryF.Key; @@ -54,6 +55,15 @@ public abstract class SettingsPanelF extends BorderPane { private static final Logger LOGGER = LoggerFactory.getLogger(SettingsPanelF.class); + /** + * The color property marking erroneous input fields, ported from + * {@code SimpleSettingsPanel.COLOR_ERROR} (Swing {@code SimpleSettingsPanel}); the value is + * applied via the {@code settings-input-error} style class using {@code -key-settings-error}. + */ + public static final ColorSettingsF.ColorPropertyF COLOR_ERROR = ColorSettingsF + .define("SETTINGS_TEXTFIELD_ERROR", "Color for marking errornous textfields in " + + "settings dialog", ColorSettingsF.color(200, 100, 100)); + /** the form area; subclasses append rows via the {@code add*} factory methods */ protected final GridPane pCenter = new GridPane(); diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/StandardUISettingsF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/StandardUISettingsF.java index 715e401bb7a..450e3826ba6 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/StandardUISettingsF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/StandardUISettingsF.java @@ -150,8 +150,8 @@ public String getDescription() { @Override public java.util.List getChildProviders() { - // the colors and keyboard shortcut panels are added with their commits in this milestone - return java.util.List.of(); + // the keyboard shortcut panel is added with its commit in this milestone + return java.util.List.of(new ColorSettingsProviderF()); } @Override From 7b3f2e928be3f8c34f71569eb8c79f38e750f0b4 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Wed, 7 Oct 2026 23:44:13 +0200 Subject: [PATCH 034/281] key.ui.fx: M4 settings dialog keystrokes round-trip + shortcut editor Reworks KeyStrokeManagerF into the full counter-part of the Swing KeyStrokeManager/KeyStrokeSettings: the shared keystrokes.json is loaded at startup (user overrides win, empty entries fall back to the default like the Swing constructor; unknown entries are preserved verbatim on save) and saved in the same Swing KeyStroke.toString() format on every rebind and on exit. fromSwingSpec/toSwingSpec convert between the Swing spec ('shift ctrl pressed P') and JavaFX KeyCombinations (SHORTCUT resolves to ctrl on non-Mac). - Menu items register with the manager (MainWindowF.menuItem), so rebinding updates their accelerators live (Swing registerAction/weak registry). - The shortcut panel ShortcutSettingsF (Swing ShortcutSettings) lists the known actions plus the unknown file entries in a Name/Description/Shortcut table (description from the registered menu item text, the FX equivalent of Action.SHORT_DESCRIPTION); the shortcut column captures key combinations from the KeyEvent directly and stores them in the shared Swing format. - Duplicate shortcuts are marked with the error style and a tooltip instead of blocking Apply, mirroring the disabled duplicate check of the Swing original; cleared entries remove the binding (the default returns on the next start). - StandardUISettingsF gains the shortcut panel as second tree child. --- .../de/uka/ilkd/key/gui/fx/MainWindowF.java | 4 +- .../fx/keyshortcuts/KeyStrokeManagerF.java | 380 +++++++++++++++--- .../fx/keyshortcuts/ShortcutSettingsF.java | 281 +++++++++++++ .../gui/fx/settings/StandardUISettingsF.java | 4 +- 4 files changed, 614 insertions(+), 55 deletions(-) create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/keyshortcuts/ShortcutSettingsF.java 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 index 4f8a4923620..bda092e18c8 100644 --- 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 @@ -1025,7 +1025,8 @@ private MenuItem menuItem(String text, String actionId, Runnable action) { /** * Creates a menu item with an optional icon and accelerator (bound via the - * {@link KeyStrokeManagerF}). + * {@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) { @@ -1036,6 +1037,7 @@ private MenuItem menuItem(String text, String actionId, IconFactoryF.Key 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; diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/keyshortcuts/KeyStrokeManagerF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/keyshortcuts/KeyStrokeManagerF.java index 22499ca45c8..525a47306a9 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/keyshortcuts/KeyStrokeManagerF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/keyshortcuts/KeyStrokeManagerF.java @@ -4,84 +4,79 @@ * SPDX-License-Identifier: GPL-2.0-or-later */ package de.uka.ilkd.key.gui.fx.keyshortcuts; +import java.io.IOException; +import java.io.Writer; +import java.lang.ref.WeakReference; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Optional; import java.util.TreeMap; - +import javafx.scene.control.MenuItem; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyCodeCombination; import javafx.scene.input.KeyCombination; +import javafx.scene.input.KeyCombination.ModifierValue; + +import de.uka.ilkd.key.settings.Configuration; +import de.uka.ilkd.key.settings.PathConfig; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Central registry of keyboard shortcuts of the JavaFX UI, counter-part of * {@code KeyStrokeManager}/{@code KeyStrokeSettings} of the Swing module {@code key.ui}. *

- * Actions are keyed by the fully qualified class name of their Swing counterpart, so that the - * persisted {@code keystrokes.json} of the Swing UI can be adopted by a later milestone. The - * defaults mirror {@link javax.swing.KeyStroke KeyStrokeSettings}. Format parity (reading/writing - * {@code keystrokes.json}) is deferred; the registry itself is the single source of truth for - * accelerators. + * Actions are keyed by the fully qualified class name of their Swing counterpart, so the + * persisted {@code keystrokes.json} is shared with the Swing UI: the file is read at startup + * (user overrides of the registered actions win, exactly like the Swing + * {@code KeyStrokeSettings} constructor merges the file over the defaults; an empty file value + * falls back to the default) and written back in the same format on every rebind and on exit. + *

+ * The persisted format is the Swing {@code KeyStroke.toString()} format, e.g. + * {@code "shift ctrl pressed P"}; {@link #fromSwingSpec(String)} and {@link #toSwingSpec( + * KeyCombination)} convert to/from JavaFX {@link KeyCombination}s. Entries of actions unknown to + * this UI are preserved verbatim on save. */ public final class KeyStrokeManagerF { - private final Map bindings = new TreeMap<>(); - - private KeyStrokeManagerF() { - registerDefaults(); - } + private static final Logger LOGGER = LoggerFactory.getLogger(KeyStrokeManagerF.class); - private static final KeyStrokeManagerF INSTANCE = new KeyStrokeManagerF(); + /** the shared shortcut file (the same as the Swing module's) */ + public static final Path SETTINGS_FILE = PathConfig.getSettingsFile("keystrokes.json"); - /** - * @return the global shortcut manager instance - */ - public static KeyStrokeManagerF getInstance() { - return INSTANCE; - } + private static final boolean MAC = + System.getProperty("os.name", "").toLowerCase(Locale.ROOT).contains("mac"); - private static KeyCombination combo(String spec) { - return KeyCombination.keyCombination(spec); - } + private static final KeyStrokeManagerF INSTANCE = new KeyStrokeManagerF(); - private void defineDefault(String actionId, String spec) { - bindings.put(actionId, combo(spec)); - } + private final Map bindings = new TreeMap<>(); - /** - * @return the registered shortcut for the given action, if any - */ - public Optional binding(String actionId) { - return Optional.ofNullable(bindings.get(actionId)); - } + /** file entries of actions this UI does not know; preserved on save */ + private final Map persistedEntries = new TreeMap<>(); /** - * Binds a shortcut to an action, overriding a possible default. - * - * @param actionId the action id - * @param combination the shortcut, or {@code null} to remove the binding + * Menu items registered per action id (weakly, like the Swing + * {@code KeyStrokeManager.actions} registry); their accelerators are updated on rebinding. */ - public void bind(String actionId, KeyCombination combination) { - if (combination == null) { - bindings.remove(actionId); - } else { - bindings.put(actionId, combination); - } - } + private final Map>> registeredItems = new HashMap<>(); - /** - * Clears all overrides and re-establishes the default shortcuts. - */ - public void resetToDefaults() { - bindings.clear(); + private KeyStrokeManagerF() { registerDefaults(); + load(); + Runtime.getRuntime().addShutdownHook(new Thread(this::save)); } /** - * @return an immutable snapshot of the current bindings (action id → shortcut spec), ready for - * persistence + * @return the global shortcut manager instance */ - public Map snapshot() { - TreeMap copy = new TreeMap<>(); - bindings.forEach((id, combo) -> copy.put(id, combo.getDisplayText())); - return copy; + public static KeyStrokeManagerF getInstance() { + return INSTANCE; } private void registerDefaults() { @@ -131,4 +126,285 @@ private void registerDefaults() { private static String modifier() { return "SHORTCUT+"; } + + private static KeyCombination combo(String spec) { + return KeyCombination.keyCombination(spec); + } + + private void defineDefault(String actionId, String spec) { + bindings.put(actionId, combo(spec)); + } + + /** + * @return the registered shortcut for the given action, if any + */ + public Optional binding(String actionId) { + return Optional.ofNullable(bindings.get(actionId)); + } + + /** + * @return all known action ids with their current bindings + */ + public Map getBindings() { + return Map.copyOf(bindings); + } + + /** + * @return the file entries of actions unknown to this UI (they are shown in the shortcut + * table without a description and preserved on save) + */ + public Map getPersistedEntries() { + return Map.copyOf(persistedEntries); + } + + /** + * @return a short description of the action with the given id, taken from the text of a + * registered menu item (the FX equivalent of the Swing + * {@code Action.SHORT_DESCRIPTION}) + */ + public Optional descriptionOf(String actionId) { + List> items = registeredItems.get(actionId); + if (items == null) { + return Optional.empty(); + } + items.removeIf(ref -> ref.get() == null); + return items.stream().map(WeakReference::get).filter(java.util.Objects::nonNull) + .map(MenuItem::getText).filter(java.util.Objects::nonNull).findFirst(); + } + + /** + * Binds a shortcut to an action, overriding a possible default, updates the accelerators of + * the registered menu items and persists the change. + * + * @param actionId the action id + * @param combination the shortcut, or {@code null} to remove the binding (the default + * returns on the next start, mirroring the Swing handling of empty file entries) + */ + public void bind(String actionId, KeyCombination combination) { + if (combination == null) { + bindings.remove(actionId); + } else { + bindings.put(actionId, combination); + } + rebind(actionId); + save(); + } + + /** + * Clears all overrides and re-establishes the default shortcuts (persisted). + */ + public void resetToDefaults() { + bindings.clear(); + registerDefaults(); + rebindAll(); + save(); + } + + /** + * Registers a menu item for the given action id so that its accelerator follows later + * rebindings (Swing {@code KeyStrokeManager.registerAction}). + * + * @param item the menu item carrying the accelerator + * @param actionId the action id + */ + public void register(MenuItem item, String actionId) { + registeredItems.computeIfAbsent(actionId, key -> new ArrayList<>()) + .add(new WeakReference<>(item)); + } + + /** + * Updates the accelerators of all registered menu items (Swing sets the accelerator on the + * registered actions). + */ + public void rebindAll() { + bindings.keySet().forEach(this::rebind); + } + + private void rebind(String actionId) { + KeyCombination combination = bindings.get(actionId); + List> items = registeredItems.get(actionId); + if (items == null) { + return; + } + items.removeIf(ref -> ref.get() == null); + for (WeakReference ref : items) { + MenuItem item = ref.get(); + if (item != null) { + item.setAccelerator(combination); + } + } + } + + /** + * @return an immutable snapshot of the current bindings (action id → shortcut spec in the + * shared Swing file format), ready for persistence + */ + public Map snapshot() { + TreeMap copy = new TreeMap<>(); + bindings.forEach((id, keyCombination) -> copy.put(id, toSwingSpec(keyCombination))); + return copy; + } + + // ------------------------------------------------------------------ + // persistence (keystrokes.json, shared with the Swing UI) + // ------------------------------------------------------------------ + + /** + * Reads {@link #SETTINGS_FILE}: entries of the registered actions override the defaults + * (empty entries fall back to the default, Swing parity), unknown entries are preserved for + * the write-back. + */ + private void load() { + if (!Files.exists(SETTINGS_FILE)) { + return; + } + try { + LOGGER.info("Load keyboard shortcuts from {}", SETTINGS_FILE); + Configuration.load(SETTINGS_FILE).getEntries().forEach(entry -> { + String value = entry.getValue() == null ? "" : entry.getValue().toString(); + Optional combination = fromSwingSpec(value); + if (bindings.containsKey(entry.getKey())) { + // an empty entry falls back to the default, like the Swing constructor + combination.ifPresent(keyCombination -> bindings.put(entry.getKey(), + keyCombination)); + } else { + persistedEntries.put(entry.getKey(), value); + } + }); + } catch (IOException e) { + LOGGER.error("Could not read {}", SETTINGS_FILE, e); + } + } + + /** + * Writes the bindings and the preserved unknown entries back to {@link #SETTINGS_FILE} in + * the shared Swing format (Swing {@code KeyStrokeSettings.save}). + */ + public void save() { + try { + Files.createDirectories(SETTINGS_FILE.getParent()); + LOGGER.info("Save keyboard shortcuts to: {}", SETTINGS_FILE.toAbsolutePath()); + TreeMap merged = new TreeMap<>(persistedEntries); + merged.putAll(snapshot()); + try (Writer writer = Files.newBufferedWriter(SETTINGS_FILE)) { + var config = new Configuration(merged); + config.save(writer, "KeY's KeyStrokes"); + writer.flush(); + } + } catch (IOException ex) { + LOGGER.warn("Failed to save keyboard shortcuts", ex); + } + } + + // ------------------------------------------------------------------ + // conversion of the shared Swing KeyStroke format + // ------------------------------------------------------------------ + + /** + * Parses a Swing {@code KeyStroke.toString()} spec into a {@link KeyCombination}. The + * supported forms are {@code [modifier]* [pressed|released|typed] KEY} with the modifiers + * {@code shift ctrl meta alt altGraph} and mouse {@code button*} modifiers (ignored, mouse + * chords are not representable). {@code typed} keystrokes become the plain key without + * modifiers (JavaFX has no typed-key combinations) — not produced by the default sets. + * + * @param spec the spec, may be null or empty + * @return the combination, or empty for null/empty/unparseable specs + */ + public static Optional fromSwingSpec(String spec) { + if (spec == null || spec.isBlank()) { + return Optional.empty(); + } + ModifierValue shift = ModifierValue.ANY; + ModifierValue ctrl = ModifierValue.ANY; + ModifierValue meta = ModifierValue.ANY; + ModifierValue alt = ModifierValue.ANY; + String key = null; + for (String token : spec.trim().split("\\s+")) { + switch (token) { + case "shift" -> shift = ModifierValue.DOWN; + case "ctrl" -> ctrl = ModifierValue.DOWN; + case "meta" -> meta = ModifierValue.DOWN; + case "alt" -> alt = ModifierValue.DOWN; + case "altGraph", "altGraphKey" -> { + // not representable + } + case "pressed", "released" -> { + // the next token carries the key + } + case "typed" -> { + shift = ModifierValue.UP; + ctrl = ModifierValue.UP; + meta = ModifierValue.UP; + alt = ModifierValue.UP; + } + default -> { + if (token.startsWith("button")) { + // mouse chords are ignored + } else if (key == null) { + key = token; + } else { + LOGGER.warn("Unrecognized keystroke spec '{}' (extra token {})", spec, + token); + return Optional.empty(); + } + } + } + } + if (key == null) { + return Optional.empty(); + } + KeyCode code = KeyCode.valueOf(key); + if (isModifier(code)) { + return Optional.empty(); + } + return Optional.of(new KeyCodeCombination(code, shift, ctrl, alt, meta, + ModifierValue.ANY)); + } + + /** + * Formats a {@link KeyCombination} as a Swing {@code KeyStroke.toString()} spec, e.g. + * {@code "shift ctrl pressed P"}. The {@code SHORTCUT} modifier resolves to {@code ctrl} on + * non-Mac platforms and {@code meta} on Mac. Key names are the JavaFX {@link KeyCode} names, + * which match the Swing key names for all default shortcuts; exotic codes may produce names + * the Swing parser rejects (logged as a warning on the next load). + * + * @param combination the combination + * @return the Swing spec + */ + public static String toSwingSpec(KeyCombination combination) { + if (!(combination instanceof KeyCodeCombination codeCombination)) { + LOGGER.warn("Cannot convert {} into the Swing keystroke format", combination); + return combination.getName(); + } + StringBuilder builder = new StringBuilder(); + if (codeCombination.getShift() == ModifierValue.DOWN) { + builder.append("shift "); + } + if (codeCombination.getControl() == ModifierValue.DOWN + || (codeCombination.getShortcut() == ModifierValue.DOWN && !MAC)) { + builder.append("ctrl "); + } + if (codeCombination.getMeta() == ModifierValue.DOWN + || (codeCombination.getShortcut() == ModifierValue.DOWN && MAC)) { + builder.append("meta "); + } + if (codeCombination.getAlt() == ModifierValue.DOWN) { + builder.append("alt "); + } + builder.append("pressed ").append(codeCombination.getCode().getName()); + return builder.toString(); + } + + /** + * @param code a key code + * @return whether the code is a modifier key (those do not form complete shortcuts) + */ + public static boolean isModifier(KeyCode code) { + return switch (code) { + case CONTROL, SHIFT, ALT, META, ALT_GRAPH, SHORTCUT, COMMAND, WINDOWS, CONTEXT_MENU, + CAPS, NUM_LOCK, SCROLL_LOCK -> + true; + default -> false; + }; + } } diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/keyshortcuts/ShortcutSettingsF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/keyshortcuts/ShortcutSettingsF.java new file mode 100644 index 00000000000..17aa3a0faf7 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/keyshortcuts/ShortcutSettingsF.java @@ -0,0 +1,281 @@ +/* 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.keyshortcuts; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.TreeMap; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; +import javafx.collections.FXCollections; +import javafx.scene.Node; +import javafx.scene.control.TableCell; +import javafx.scene.control.TableColumn; +import javafx.scene.control.TableView; +import javafx.scene.control.TextField; +import javafx.scene.control.Tooltip; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyCodeCombination; +import javafx.scene.input.KeyCombination; +import javafx.scene.input.KeyCombination.ModifierValue; +import javafx.scene.input.KeyEvent; + +import de.uka.ilkd.key.gui.fx.MainWindowF; +import de.uka.ilkd.key.gui.fx.settings.SettingsPanelF; +import de.uka.ilkd.key.gui.fx.settings.SettingsProviderF; + +/** + * UI for configuring the keyboard shortcuts of the JavaFX UI, counter-part of + * {@code de.uka.ilkd.key.gui.keyshortcuts.ShortcutSettings} of the Swing module {@code key.ui}. + *

+ * The table lists the known actions of {@link KeyStrokeManagerF} (plus the unknown entries of the + * shared {@code keystrokes.json}, which are shown without a description like in Swing) with the + * shortcut in the last column. Editing the column opens a text field that captures the pressed + * key combination directly from the {@link KeyEvent} (Swing's capture cell editor); the value is + * stored and applied in the shared Swing {@code KeyStroke.toString()} format, e.g. + * {@code "shift ctrl pressed P"} (displayed without the {@code "pressed"} token). + *

+ * Duplicate shortcuts are highlighted with the error style and a tooltip instead of being + * rejected: the duplicate check of the Swing original is commented out, so applying stays + * possible (documented deviation). + */ +public class ShortcutSettingsF extends SettingsPanelF implements SettingsProviderF { + + private final TableView tblShortcuts = new TableView<>(); + private final TableColumn colName = new TableColumn<>("Name"); + private final TableColumn colDescription = + new TableColumn<>("Description"); + private final TableColumn colShortcut = new TableColumn<>("Shortcut"); + + public ShortcutSettingsF() { + setHeaderText(getDescription()); + setSubHeaderText( + "These settings are stored in " + KeyStrokeManagerF.SETTINGS_FILE.toAbsolutePath()); + + colName.setCellValueFactory( + data -> new SimpleStringProperty(shortName(data.getValue().actionId))); + colDescription.setCellValueFactory( + data -> new SimpleStringProperty(KeyStrokeManagerF.getInstance() + .descriptionOf(data.getValue().actionId).orElse(""))); + colShortcut.setCellValueFactory(data -> data.getValue().specProperty()); + colShortcut.setCellFactory(col -> new ShortcutCell()); + colName.setPrefWidth(260); + colDescription.setPrefWidth(180); + colShortcut.setPrefWidth(220); + + tblShortcuts.getColumns().setAll(colName, colDescription, colShortcut); + tblShortcuts.setEditable(true); + // sorted by name ascending, like the Swing TableRowSorter with the SortKey(0, ASCENDING) + tblShortcuts.getSortOrder().add(colName); + tblShortcuts.setColumnResizePolicy( + TableView.CONSTRAINED_RESIZE_POLICY_FLEX_LAST_COLUMN); + setCenter(tblShortcuts); + } + + @Override + public String getDescription() { + return "Keyboard Shortcuts"; + } + + @Override + public Node getPanel(MainWindowF window) { + refresh(); + return this; + } + + /** + * Rebuilds the table model from the current {@link KeyStrokeManagerF} state (Swing + * {@code ShortcutSettings.getPanel}); known bindings and the preserved unknown file entries + * are merged and sorted by name. + */ + private void refresh() { + KeyStrokeManagerF manager = KeyStrokeManagerF.getInstance(); + TreeMap entries = new TreeMap<>(); + for (Map.Entry entry : manager.getBindings().entrySet()) { + entries.put(entry.getKey(), KeyStrokeManagerF.toSwingSpec(entry.getValue())); + } + entries.putAll(manager.getPersistedEntries()); + + List rows = + entries.entrySet().stream().map(e -> new ShortcutRow(e.getKey(), e.getValue())) + .toList(); + tblShortcuts.setItems(FXCollections.observableList(rows)); + tblShortcuts.sort(); + } + + @Override + public void apply(MainWindowF window) { + KeyStrokeManagerF manager = KeyStrokeManagerF.getInstance(); + for (ShortcutRow row : tblShortcuts.getItems()) { + String spec = row.spec.get() == null ? "" : row.spec.get(); + if (spec.equals(row.initialSpec)) { + continue; + } + Optional combination = KeyStrokeManagerF.fromSwingSpec(spec); + // an empty or unparseable entry removes the binding, so the default returns on the + // next start (the Swing original writes an empty entry into the file instead) + manager.bind(row.actionId, combination.orElse(null)); + } + // the accelerators of the registered menu items are updated via KeyStrokeManagerF.bind + } + + /** + * @param actionId an action id (fully qualified class name of the Swing counterpart) + * @return the action id without the common package prefixes (Swing + * {@code ShortcutsTableModel.getValueAt}) + */ + private static String shortName(String actionId) { + return actionId.replaceAll("([a-z]\\w*\\.)*", ""); + } + + /** + * The model row of one action. + */ + private static class ShortcutRow { + + final String actionId; + + /** the spec at panel load time, for the change detection in {@link #apply} */ + final String initialSpec; + + /** the shortcut spec in the shared Swing format */ + final StringProperty spec = new SimpleStringProperty(); + + ShortcutRow(String actionId, String spec) { + this.actionId = actionId; + this.initialSpec = spec; + this.spec.set(spec); + } + + StringProperty specProperty() { + return spec; + } + } + + /** + * The shortcut cell: shows the spec without the {@code "pressed"} token (for readability, + * like the Swing model) and edits it in a text field that captures key combinations (Swing's + * capture {@code DefaultCellEditor}). Duplicate specs are marked with the error style and a + * tooltip. + */ + private class ShortcutCell extends TableCell { + + private final TextField editor = new TextField(); + + ShortcutCell() { + // the capture filter runs before the text field handles the key and consumes every + // key press, so the pressed combination is captured like in Swing (the characters + // the text field inserts on KEY_TYPED are cosmetic only, as in the Swing original) + editor.addEventFilter(KeyEvent.KEY_PRESSED, this::capture); + editor.focusedProperty().addListener((obs, was, focused) -> { + if (!focused && isEditing()) { + // an emptied editor clears the binding, an untouched one reverts + if (editor.getText().isEmpty()) { + ShortcutRow row = getTableRow().getItem(); + if (row != null) { + row.spec.set(""); + } + } + cancelEdit(); + } + }); + } + + @Override + protected void updateItem(String item, boolean empty) { + super.updateItem(item, empty); + getStyleClass().remove("settings-input-error"); + setTooltip(null); + if (empty || item == null) { + setText(null); + setGraphic(null); + return; + } + ShortcutRow row = getTableRow().getItem(); + String clash = row == null ? null : duplicateOf(row); + if (clash != null) { + getStyleClass().add("settings-input-error"); + setTooltip(new Tooltip( + "Clash of key bindings: this shortcut is also used by " + clash)); + } + setText(displayText(item)); + } + + @Override + public void startEdit() { + super.startEdit(); + ShortcutRow row = getTableRow().getItem(); + if (row == null) { + return; + } + editor.setText(displayText(row.spec.get())); + setText(null); + setGraphic(editor); + editor.requestFocus(); + editor.selectAll(); + } + + @Override + public void cancelEdit() { + ShortcutRow row = getTableRow().getItem(); + super.cancelEdit(); + setGraphic(null); + setText(row == null ? null : displayText(row.spec.get())); + } + + /** + * Captures the pressed key combination into the row model (Swing + * {@code ShortcutSettings.getPanel} capture KeyAdapter): modifier-only presses only + * update the editor text, every non-modifier key completes the shortcut and commits it. + * {@code Escape} cancels the edit. + */ + private void capture(KeyEvent event) { + if (event.getCode() == KeyCode.ESCAPE) { + event.consume(); + cancelEdit(); + return; + } + KeyCode code = event.getCode(); + if (code == KeyCode.UNDEFINED || KeyStrokeManagerF.isModifier(code)) { + event.consume(); + return; + } + KeyCombination combination = new KeyCodeCombination(code, + event.isShiftDown() ? ModifierValue.DOWN : ModifierValue.UP, + event.isControlDown() ? ModifierValue.DOWN : ModifierValue.UP, + event.isAltDown() ? ModifierValue.DOWN : ModifierValue.UP, + event.isMetaDown() ? ModifierValue.DOWN : ModifierValue.UP, + ModifierValue.ANY); + ShortcutRow row = getTableRow().getItem(); + if (row != null) { + row.spec.set(KeyStrokeManagerF.toSwingSpec(combination)); + } + event.consume(); + // the Swing original keeps the editor open for modifier-less keys until it loses + // the focus; here the value is already committed, so the edit closes right away + cancelEdit(); + } + + /** + * @param row the row to check + * @return the short name of another row with the same non-empty spec, or {@code null} + */ + private String duplicateOf(ShortcutRow row) { + String spec = row.spec.get(); + if (spec == null || spec.isEmpty()) { + return null; + } + return tblShortcuts.getItems().stream() + .filter(it -> it != row && spec.equals(it.spec.get())) + .map(it -> shortName(it.actionId)).findFirst().orElse(null); + } + + /** @return the spec without the {@code "pressed"} token (Swing display parity) */ + private static String displayText(String spec) { + return spec == null ? "" : spec.replace("pressed ", ""); + } + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/StandardUISettingsF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/StandardUISettingsF.java index 450e3826ba6..45b085baab7 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/StandardUISettingsF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/StandardUISettingsF.java @@ -16,6 +16,7 @@ import de.uka.ilkd.key.gui.fx.MainWindowF; import de.uka.ilkd.key.gui.fx.configuration.ConfigF; +import de.uka.ilkd.key.gui.fx.keyshortcuts.ShortcutSettingsF; import de.uka.ilkd.key.gui.fx.theme.Theme; import de.uka.ilkd.key.gui.fx.theme.ThemeManager; import de.uka.ilkd.key.settings.GeneralSettings; @@ -150,8 +151,7 @@ public String getDescription() { @Override public java.util.List getChildProviders() { - // the keyboard shortcut panel is added with its commit in this milestone - return java.util.List.of(new ColorSettingsProviderF()); + return java.util.Arrays.asList(new ColorSettingsProviderF(), new ShortcutSettingsF()); } @Override From a6df7da46cdde5d6403d865fba4e02891ebe3136 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Wed, 7 Oct 2026 23:56:09 +0200 Subject: [PATCH 035/281] key.ui.fx: M4 settings dialog: fix placeholder overlay and Swing key-name round-trip --- .../gui/fx/keyshortcuts/KeyStrokeManagerF.java | 16 ++++++++++++---- .../key/gui/fx/settings/SettingsManagerF.java | 15 ++++++++------- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/keyshortcuts/KeyStrokeManagerF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/keyshortcuts/KeyStrokeManagerF.java index 525a47306a9..960bc459ff9 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/keyshortcuts/KeyStrokeManagerF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/keyshortcuts/KeyStrokeManagerF.java @@ -353,7 +353,13 @@ public static Optional fromSwingSpec(String spec) { if (key == null) { return Optional.empty(); } - KeyCode code = KeyCode.valueOf(key); + KeyCode code; + try { + code = KeyCode.valueOf(key); + } catch (IllegalArgumentException e) { + LOGGER.warn("Unknown key name '{}' in the keystroke spec '{}'", key, spec); + return Optional.empty(); + } if (isModifier(code)) { return Optional.empty(); } @@ -365,8 +371,10 @@ public static Optional fromSwingSpec(String spec) { * Formats a {@link KeyCombination} as a Swing {@code KeyStroke.toString()} spec, e.g. * {@code "shift ctrl pressed P"}. The {@code SHORTCUT} modifier resolves to {@code ctrl} on * non-Mac platforms and {@code meta} on Mac. Key names are the JavaFX {@link KeyCode} names, - * which match the Swing key names for all default shortcuts; exotic codes may produce names - * the Swing parser rejects (logged as a warning on the next load). + * which are the Swing {@code VK_*} constant names for all default shortcuts (the human + * readable {@link KeyCode#getName()} would emit e.g. {@code "Space"}, which neither Swing nor + * this parser accept); exotic codes may produce names the Swing parser rejects (logged as a + * warning on the next load). * * @param combination the combination * @return the Swing spec @@ -391,7 +399,7 @@ public static String toSwingSpec(KeyCombination combination) { if (codeCombination.getAlt() == ModifierValue.DOWN) { builder.append("alt "); } - builder.append("pressed ").append(codeCombination.getCode().getName()); + builder.append("pressed ").append(codeCombination.getCode().name()); return builder.toString(); } diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/SettingsManagerF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/SettingsManagerF.java index bbf7b276a7a..a45e6a1e3e9 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/SettingsManagerF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/SettingsManagerF.java @@ -295,6 +295,14 @@ private static class SettingsUi extends BorderPane { txtSearch.textProperty() .addListener((obs, old, value) -> treeSettingsPanels.refresh()); + // the placeholder must be in place before buildTree shows the first panel + panelArea.setContent(panelHolder); + panelArea.setFitToWidth(true); + panelArea.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); + panelHolder.getStyleClass().add("settings-panel-holder"); + panelHolder.getChildren().add(new Label("empty")); + setCenter(panelArea); + treeSettingsPanels.setShowRoot(true); treeSettingsPanels.setCellFactory(this::createCell); buildTree(providers); @@ -306,13 +314,6 @@ private static class SettingsUi extends BorderPane { VBox.setVgrow(treeSettingsPanels, javafx.scene.layout.Priority.ALWAYS); setLeft(westPanel); - panelArea.setContent(panelHolder); - panelArea.setFitToWidth(true); - panelArea.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); - panelHolder.getStyleClass().add("settings-panel-holder"); - panelHolder.getChildren().add(new Label("empty")); - setCenter(panelArea); - treeSettingsPanels.getSelectionModel().selectedItemProperty() .addListener((obs, old, item) -> { if (item == null) { From 84d45f9535766d32ab711285ffbdcb5faab53994 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Thu, 8 Oct 2026 00:07:16 +0200 Subject: [PATCH 036/281] key.ui.fx: M5 proof diff (diff_match_patch + ProofDifference copies, ProofDiffFrameF port, View menu wiring) --- .../de/uka/ilkd/key/gui/fx/MainWindowF.java | 35 +- .../key/gui/fx/proofdiff/ProofDiffFrameF.java | 580 ++++ .../key/gui/fx/proofdiff/ProofDifference.java | 200 ++ .../gui/fx/proofdiff/diff_match_patch.java | 2419 +++++++++++++++++ .../de/uka/ilkd/key/gui/fx/theme/key-dark.css | 25 + .../uka/ilkd/key/gui/fx/theme/key-light.css | 25 + 6 files changed, 3283 insertions(+), 1 deletion(-) create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/proofdiff/ProofDiffFrameF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/proofdiff/ProofDifference.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/proofdiff/diff_match_patch.java 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 index d5e6f93214a..5d4a371a2df 100644 --- 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 @@ -62,6 +62,7 @@ import de.uka.ilkd.key.gui.fx.nodeviews.SequentViewF; 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.proofdiff.ProofDiffFrameF; 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.SettingsManagerF; @@ -288,6 +289,21 @@ public KeYSelectionModel getSelectionModel() { return selectionModel; } + /** + * @return the mediator of the window (Swing {@code MainWindow.getMediator()}); used by + * secondary windows like the proof diff frame + */ + public KeYMediatorF getMediator() { + return mediator; + } + + /** + * @return the stage of this window (the owner of secondary windows) + */ + public Stage getStage() { + return stage; + } + // ------------------------------------------------------------------ // sequent view (M2a spike) // ------------------------------------------------------------------ @@ -414,6 +430,13 @@ protected KeYEnvironment call() throws Exception { if (System.getProperty("key.fx.verify.goallist") != null) { LOGGER.info("Goal list verification: {}", goalListView.verifyGoalList()); } + 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.demo.autoprove.live") != null) { startLiveAutoMode(env); } @@ -907,7 +930,9 @@ private Menu buildViewMenu() { IconFactoryF.Key.MINUS, () -> changeFontSize(-1))); view.getItems().addAll(prettyPrint, unicode, syntaxHighlighting, new SeparatorMenuItem(), - themeMenu, fontSize); + themeMenu, fontSize, new SeparatorMenuItem(), + menuItem("Visual Node Diff", "de.uka.ilkd.key.gui.proofdiff.ProofDiffFrame$Action", + this::showProofDiffFrame)); return view; } @@ -1098,6 +1123,14 @@ private void openSettings() { SettingsManagerF.getInstance().openSettings(stage); } + /** + * 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(); + } + private void resetLayout() { workspace.restoreFactoryDefault(); } diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/proofdiff/ProofDiffFrameF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/proofdiff/ProofDiffFrameF.java new file mode 100644 index 00000000000..2b3f7eddd38 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/proofdiff/ProofDiffFrameF.java @@ -0,0 +1,580 @@ +/* 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.proofdiff; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import javafx.scene.Scene; +import javafx.scene.control.Alert; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.TextField; +import javafx.scene.control.Tooltip; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.HBox; +import javafx.scene.text.Font; +import javafx.stage.Stage; + +import de.uka.ilkd.key.gui.fx.MainWindowF; +import de.uka.ilkd.key.gui.fx.configuration.ConfigF; +import de.uka.ilkd.key.gui.fx.proofdiff.diff_match_patch.Diff; +import de.uka.ilkd.key.gui.fx.theme.ThemeManager; +import de.uka.ilkd.key.pp.LogicPrinter; +import de.uka.ilkd.key.proof.Node; +import de.uka.ilkd.key.proof.Proof; +import de.uka.ilkd.key.util.Levensthein; + +import org.key_project.util.javafx.FxUtil; + +import org.fxmisc.richtext.Caret; +import org.fxmisc.richtext.StyleClassedTextArea; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * JavaFX version of the proof node diff window, counter-part of + * {@code de.uka.ilkd.key.gui.proofdiff.ProofDiffFrame} in the module {@code key.ui}. + *

+ * Proof-of-concept implementation of a textual sequent comparison (as the Swing javadoc puts + * it): the window lets the user display an in-place comparison of two sequents of the currently + * selected proof. The comparison happens on the pretty-printed text only, using the bundled + * {@link diff_match_patch} library. Port notes: + *

    + *
  • the Swing {@code JEditorPane} with HTML content ({@code
    } plus
    + * {@code } markup) becomes a read-only
    + * {@link StyleClassedTextArea} (RichTextFX, the presentation of the source view): the diff is
    + * rendered as plain text and the deleted/inserted parts are highlighted via the style classes
    + * {@link #CLASS_DELETE} (red background) and {@link #CLASS_INSERT} (green background), computed
    + * by {@link #render(LinkedList)} — the JavaFX counterpart of the Swing HTML loop;
  • + *
  • the node fields, buttons, tooltips and the help text follow the Swing original one to one + * (including the default button "Show Diff" and the always-enabled menu action); error popups + * ({@code JOptionPane}) become {@link Alert}s;
  • + *
  • like the Swing frame, the diff is computed synchronously on the UI thread (the Swing + * original runs on the EDT) — sequent texts are small and {@code Diff_Timeout} is set to 0 as in + * the original.
  • + *
+ */ +public class ProofDiffFrameF extends Stage { + + private static final Logger LOGGER = LoggerFactory.getLogger(ProofDiffFrameF.class); + + /** Window size of the Swing original ({@code setSize(700, 600)}). */ + private static final double WIDTH = 700; + private static final double HEIGHT = 600; + + /** Style class of highlighted text present only in the parent sequent (red background). */ + public static final String CLASS_DELETE = "diff-delete"; + + /** Style class of highlighted text added in the child sequent (green background). */ + public static final String CLASS_INSERT = "diff-insert"; + + /** Style class of the bold headings in the help text. */ + public static final String CLASS_HELP_HEADING = "diff-help-heading"; + + /** + * The main window, the source of the selected proof (Swing {@code ProofDiffFrame.mainWindow}). + */ + private final MainWindowF mainWindow; + + /** + * The text area displaying the diff'ed text (Swing {@code textArea}: a read-only HTML + * {@code JEditorPane} with the sequent font). + */ + private final StyleClassedTextArea textArea = new StyleClassedTextArea(); + + /** The text field holding the lower comparison number (Swing {@code from}). */ + private final TextField from = new TextField(); + + /** The text field holding the upper comparison number (Swing {@code to}). */ + private final TextField to = new TextField(); + + /** + * Instantiates a new proof-diff window (Swing ctor: {@code ProofDiffFrame(MainWindow)}). + * + * @param mainWindow the main window of the system + */ + public ProofDiffFrameF(MainWindowF mainWindow) { + this.mainWindow = mainWindow; + setTitle("Visual difference between two sequents"); + if (mainWindow.getStage() != null) { + initOwner(mainWindow.getStage()); + } + Scene scene = new Scene(guiInit(), WIDTH, HEIGHT); + ThemeManager.getInstance().manage(scene); + setScene(scene); + } + + /** + * Opens the window centered on the main window (Swing + * {@code pdf.setLocationRelativeTo(mainWindow)}). + */ + public void showCenteredOnOwner() { + Stage owner = mainWindow.getStage(); + if (owner != null && owner.isShowing()) { + setX(owner.getX() + (owner.getWidth() - WIDTH) / 2); + setY(owner.getY() + (owner.getHeight() - HEIGHT) / 2); + } + show(); + textArea.requestFocus(); + } + + /** + * Initializes the user interface (Swing {@code guiInit}: the diff area in the center, the + * node fields and buttons at the bottom). + * + * @return the root node of the window + */ + private BorderPane guiInit() { + BorderPane root = new BorderPane(); + textArea.getStyleClass().add("proof-diff-area"); + textArea.setEditable(false); + textArea.setWrapText(false); + textArea.getCaretSelectionBind().setShowCaret(Caret.CaretVisibility.OFF); + applyMonoFont(); + showHelp(); + root.setCenter(textArea); + + HBox bottom = new HBox(8); + bottom.getStyleClass().add("proof-diff-controls"); + bottom.setAlignment(javafx.geometry.Pos.CENTER_RIGHT); + + from.setPrefColumnCount(5); + from.setTooltip( + new Tooltip("Set the parent node to compare. May be empty for the direct predecessor")); + to.setPrefColumnCount(5); + to.setTooltip(new Tooltip("Set the child node to compare. Must not be empty")); + + Button go = new Button("Show Diff"); + go.setTooltip(new Tooltip("Show difference between the two nodes specified here.")); + go.setDefaultButton(true); + go.setOnAction(e -> showDiff()); + + Button last = new Button("Show Selected Node"); + last.setTooltip(new Tooltip( + "Show difference introduced by the rule application leading to the selected node")); + last.setOnAction(e -> { + setSelectedNode(); + showDiff(); + }); + + Button close = new Button("Close"); + close.setOnAction(e -> hide()); + + bottom.getChildren() + .addAll(new Label("Parent node:"), from, new Label("Proof node:"), to, go, last, + close); + root.setBottom(bottom); + return root; + } + + /** + * Applies the monospaced sequent font configured by {@link ConfigF} to the view (the Swing + * original sets {@code UIManager.getFont(Config.KEY_FONT_SEQUENT_VIEW)}); the font is set as + * an inline style on the area node, the same way the source view does it. + */ + private void applyMonoFont() { + Font font = ConfigF.DEFAULT.monoFont(); + textArea.setStyle("-fx-font-family: \"" + font.getFamily() + "\"; -fx-font-size: " + + font.getSize() + "px;"); + } + + /** + * Sets the to field to the selected node. Clears the from field (Swing + * {@code setSelectedNode}). + */ + private void setSelectedNode() { + try { + Node node = mainWindow.getMediator().getSelectedNode(); + if (node == null) { + throw new IllegalArgumentException("There is no selected proof node or no proof!"); + } + + from.setText(""); + to.setText(Integer.toString(node.serialNr())); + } catch (IllegalArgumentException e) { + showError(e.getMessage()); + } + } + + /** + * Initiate a diff calculation and set the content of the text area (Swing {@code showDiff}). + *

+ * It uses the result of {@link diff_match_patch#diff_main(String, String, boolean)} and the + * styled-range rendering of {@link #render(LinkedList)} to show the text. + */ + private void showDiff() { + String sFrom; + String sTo; + + try { + int toNo; + String toText = to.getText(); + if (toText.isEmpty()) { + throw new IllegalArgumentException( + "At least the second proof node must be specified"); + } else { + toNo = Integer.parseInt(to.getText()); + sTo = getProofNodeText(toNo); + } + + String fromText = from.getText(); + if (fromText.isEmpty()) { + sFrom = getProofNodeText(getParent(toNo)); + } else { + int fromNo = Integer.parseInt(fromText); + sFrom = getProofNodeText(fromNo); + } + } catch (NumberFormatException e) { + showError("This is not a number: " + e.getMessage()); + return; + } catch (IllegalArgumentException e) { + showError(e.getMessage()); + return; + } + + diff_match_patch differ = new diff_match_patch(); + differ.Diff_Timeout = 0.0f; + LinkedList diffs = differ.diff_main(sFrom, sTo, false); + + Rendered rendered = render(diffs); + display(rendered.text(), rendered.ranges()); + LOGGER.info( + "Proof diff of node '{}' vs '{}': {} chars, {} highlighted range(s)", + from.getText(), to.getText(), rendered.text().length(), rendered.ranges().size()); + } + + /** + * One highlighted range of the displayed text — the JavaFX counterpart of the Swing + * {@code } markup. + */ + record StyledRange(int start, int end, String styleClass) { + } + + /** + * A rendered diff: the concatenated text (all diff chunks, like the Swing {@code

}
+     * content) plus the ranges to highlight.
+     */
+    record Rendered(String text, List ranges) {
+    }
+
+    /**
+     * Ports the HTML markup loop of the Swing frame: {@code EQUAL} stays plain, {@code DELETE}
+     * gets the red background and {@code INSERT} the green one — whitespace-only edits stay
+     * unmarked, exactly as in the Swing original.
+     *
+     * @param diffs the diff chunks produced by {@code diff_main}
+     * @return the rendered text and the highlighted ranges into it
+     */
+    static Rendered render(LinkedList diffs) {
+        StringBuilder sb = new StringBuilder();
+        List ranges = new ArrayList<>();
+        int pos = 0;
+        for (Diff diff : diffs) {
+            int start = pos;
+            sb.append(diff.text);
+            pos = sb.length();
+            switch (diff.operation) {
+                case EQUAL -> {
+                }
+                case DELETE -> {
+                    if (!onlySpaces(diff.text)) {
+                        ranges.add(new StyledRange(start, pos, CLASS_DELETE));
+                    }
+                }
+                case INSERT -> {
+                    if (!onlySpaces(diff.text)) {
+                        ranges.add(new StyledRange(start, pos, CLASS_INSERT));
+                    }
+                }
+            }
+        }
+        return new Rendered(sb.toString(), ranges);
+    }
+
+    private static boolean onlySpaces(CharSequence text) {
+        for (int i = 0; i < text.length(); i++) {
+            if (!Character.isWhitespace(text.charAt(i))) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Replaces the content of the diff area (FX thread only): the plain text plus the
+     * highlighted ranges.
+     */
+    private void display(String text, List ranges) {
+        FxUtil.assertFxThread();
+        textArea.clear();
+        if (!text.isEmpty()) {
+            textArea.appendText(text);
+            for (StyledRange range : ranges) {
+                textArea.setStyleClass(range.start(), range.end(), range.styleClass());
+            }
+            // the Swing view shows the beginning of the diff after a update
+            textArea.moveTo(0);
+            textArea.showParagraphAtTop(0);
+        }
+    }
+
+    private int getParent(int no) {
+        Proof proof = mainWindow.getMediator().getSelectedProof();
+        if (proof == null) {
+            throw new IllegalArgumentException("There is no open proof!");
+        }
+
+        Node node = findNode(proof.root(), no);
+        if (node == null) {
+            throw new IllegalArgumentException(no + " is not a node in the proof");
+        }
+
+        Node parent = node.parent();
+        if (parent == null) {
+            throw new IllegalArgumentException(no + " has no parent node");
+        }
+
+        return parent.serialNr();
+    }
+
+    /**
+     * Gets the pretty printed node text for a node (Swing {@code getProofNodeText}: the printed
+     * sequent of the node with the given serial number).
+     *
+     * @param nodeNumber the number of the node to search
+     * @return the proof node text
+     * @throws IllegalArgumentException if the number is bad or there is no proof.
+     */
+    private String getProofNodeText(int nodeNumber) {
+        Proof proof = mainWindow.getMediator().getSelectedProof();
+
+        if (proof == null) {
+            throw new IllegalArgumentException("There is no open proof!");
+        }
+
+        Node node = findNode(proof.root(), nodeNumber);
+
+        if (node == null) {
+            throw new IllegalArgumentException(nodeNumber + " does not denote a valid node");
+        }
+
+        return LogicPrinter.quickPrintSequent(node.sequent(), proof.getServices());
+    }
+
+    // This must have been implemented already, somewhere (Swing comment kept).
+    private Node findNode(Node node, int number) {
+        if (node.serialNr() == number) {
+            return node;
+        }
+
+        while ((node.serialNr() != number) && (node.childrenCount() == 1)) {
+            node = node.child(0);
+        }
+
+        if (node.serialNr() == number) {
+            return node;
+        }
+
+        Iterator it = node.childrenIterator();
+        while (it.hasNext()) {
+            Node n = it.next();
+            if (n.serialNr() <= number) {
+                Node result = findNode(n, number);
+                if (result != null) {
+                    return result;
+                }
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * The help text of the Swing frame ({@code getHelpText()}, its HTML converted to the styled
+     * text presentation; the red/green example words keep their background colors).
+     */
+    private void showHelp() {
+        List segments = List.of(
+            new HelpSegment("Visual diff between sequents of Proof Nodes\n\n", CLASS_HELP_HEADING),
+            new HelpSegment(
+                "This window can be used to select one or two sequents of an "
+                    + "ongoing or closed proof. All actions refer to the currently selected proof.\n\n",
+                null),
+            new HelpSegment(
+                "The text area shows the in-place diff between two pretty printed "
+                    + "sequents. Parts in ",
+                null),
+            new HelpSegment("red", CLASS_DELETE),
+            new HelpSegment(" are only present in the parent sequent and parts in ", null),
+            new HelpSegment("green", CLASS_INSERT),
+            new HelpSegment(" are added in the second proof node.\n\n", null),
+            new HelpSegment("One node mode\n", CLASS_HELP_HEADING),
+            new HelpSegment(
+                "If you keep the left field (parent node) empty, the difference between the "
+                    + "proof node and its direct predecessor is displayed in the text area.\n\n",
+                null),
+            new HelpSegment("Two node mode\n", CLASS_HELP_HEADING),
+            new HelpSegment(
+                "If you specify two nodes, the difference between the declared sequents "
+                    + "are displayed.\n\n",
+                null),
+            new HelpSegment("'Show selected node'\n", CLASS_HELP_HEADING),
+            new HelpSegment(
+                "Use this button to use the currently selected proof node of the proof "
+                    + "component as displayed proof node.",
+                null));
+        StringBuilder sb = new StringBuilder();
+        List ranges = new ArrayList<>();
+        for (HelpSegment segment : segments) {
+            int start = sb.length();
+            sb.append(segment.text());
+            if (segment.styleClass() != null) {
+                ranges.add(new StyledRange(start, sb.length(), segment.styleClass()));
+            }
+        }
+        display(sb.toString(), ranges);
+    }
+
+    /**
+     * One styled run of the help text ({@code null} style class means plain text).
+     */
+    private record HelpSegment(String text, String styleClass) {
+    }
+
+    /**
+     * Shows the message of a rejected diff in an error dialog (the Swing original opens a
+     * {@code JOptionPane} error message dialog).
+     *
+     * @param message the error message, not {@code null}
+     */
+    private void showError(String message) {
+        LOGGER.info("Proof diff rejected: {}", message);
+        Alert alert = new Alert(Alert.AlertType.ERROR);
+        alert.initOwner(this);
+        alert.setTitle("Error");
+        alert.setHeaderText("Error");
+        alert.setContentText(message);
+        alert.showAndWait();
+    }
+
+    /**
+     * Development self-test ({@code key.fx.verify.proofdiff}): ports the assertions of the Swing
+     * unit test {@code de.uka.ilkd.key.gui.proofdiff.ProofDifferenceTest} (edit distance and
+     * sequent formula pairing) and adds sanity checks on the bundled {@link diff_match_patch} and
+     * the styled-range rendering used by the window (identical texts produce no highlight,
+     * whitespace-only edits are not highlighted). Pure logic, no UI — runs on any thread.
+     *
+     * @return a one-line report ending in {@code PASS} or {@code FAIL}
+     */
+    public static String verifyDiffLogic() {
+        List failures = new ArrayList<>();
+
+        // edit distance (Swing ProofDifferenceTest.testLevensthein)
+        if (Levensthein.calculate("abc", "abc") != 0) {
+            failures.add("levensthein(abc,abc)!=0");
+        }
+        if (Levensthein.calculate("!p", "p") != 1) {
+            failures.add("levensthein(!p,p)!=1");
+        }
+        if (Levensthein.calculate("f(x)", "f(g(x))") != 3) {
+            failures.add("levensthein(f(x),f(g(x)))!=3");
+        }
+        if (Levensthein.calculate("f(x)", "") != 4) {
+            failures.add("levensthein(f(x),\")!=4");
+        }
+
+        // formula pairing (Swing ProofDifferenceTest.testPairs1)
+        expectPairs(failures, List.of("a", "b", "c"), List.of("a", "b", "c"),
+            "[(a, a), (b, b), (c, c)]");
+        expectPairs(failures, List.of("d", "b", "c"), List.of("a", "b", "c"),
+            "[(b, b), (c, c), (d, a)]");
+        expectPairs(failures, List.of("p->q", "!q", "p"), List.of("p", "p->!q", "!p"),
+            "[(p, p), (p->q, p->!q), (!q, !p)]");
+
+        // diff_match_patch: identical texts produce a single plain chunk (the "empty diff" case)
+        Rendered same = dmp("Hello world", "Hello world");
+        if (!same.text().equals("Hello world") || !same.ranges().isEmpty()) {
+            failures.add("identical texts: text=" + same.text() + " ranges=" + same.ranges());
+        }
+
+        // a modification: the highlighted ranges must carry exactly the DELETE/INSERT chunks the
+        // library produced (dmp may split words around common characters, e.g. "world" ->
+        // "there" is DEL"wo" INS"the" EQ"r" DEL"ld" INS"e"); the library's own reconstruction
+        // (diff_text1/diff_text2) proves the diff itself is intact
+        diff_match_patch differ = new diff_match_patch();
+        differ.Diff_Timeout = 0.0f;
+        LinkedList diffs = differ.diff_main("Hello world", "Hello there", false);
+        Rendered mod = render(diffs);
+        if (!differ.diff_text1(diffs).equals("Hello world")
+                || !differ.diff_text2(diffs).equals("Hello there")) {
+            failures.add("dmp reconstruction: t1=" + differ.diff_text1(diffs) + " t2="
+                + differ.diff_text2(diffs));
+        }
+        if (!String.join("", highlighted(mod, CLASS_DELETE))
+                .equals(chunks(diffs, diff_match_patch.Operation.DELETE))
+                || !String.join("", highlighted(mod, CLASS_INSERT))
+                        .equals(chunks(diffs, diff_match_patch.Operation.INSERT))) {
+            failures.add("modification: delete=" + highlighted(mod, CLASS_DELETE) + " insert="
+                + highlighted(mod, CLASS_INSERT));
+        }
+
+        // whitespace-only edits are not highlighted (Swing onlySpaces rule)
+        Rendered ws = dmp("a b", "ab");
+        if (!ws.ranges().isEmpty()) {
+            failures.add("whitespace-only edit highlighted: " + ws.ranges());
+        }
+
+        return "levensthein=4 pairs=3 patch=3 " + (failures.isEmpty() ? "PASS"
+                : "FAIL: "
+                    + String.join("; ", failures));
+    }
+
+    /**
+     * Runs the diff through the window's own pipeline ({@link diff_match_patch} with
+     * {@code Diff_Timeout = 0} + {@link #render}).
+     */
+    private static Rendered dmp(String left, String right) {
+        diff_match_patch differ = new diff_match_patch();
+        differ.Diff_Timeout = 0.0f;
+        return render(differ.diff_main(left, right, false));
+    }
+
+    /**
+     * @return the substrings of the rendered text highlighted with the given style class
+     */
+    private static List highlighted(Rendered rendered, String styleClass) {
+        return rendered.ranges().stream()
+                .filter(range -> range.styleClass().equals(styleClass))
+                .map(range -> rendered.text().substring(range.start(), range.end()))
+                .toList();
+    }
+
+    /**
+     * @return the concatenation of the texts of all chunks with the given operation
+     */
+    private static String chunks(List diffs, diff_match_patch.Operation operation) {
+        StringBuilder sb = new StringBuilder();
+        for (Diff diff : diffs) {
+            if (diff.operation == operation) {
+                sb.append(diff.text);
+            }
+        }
+        return sb.toString();
+    }
+
+    /**
+     * Compares the pairing result against the expectation of the Swing test.
+     */
+    private static void expectPairs(List failures, List left, List right,
+            String expected) {
+        List pairs =
+            ProofDifference.findPairs(new ArrayList<>(left), new ArrayList<>(right));
+        if (!expected.equals(pairs.toString())) {
+            failures.add("findPairs(" + left + ", " + right + ") = " + pairs);
+        }
+    }
+}
diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/proofdiff/ProofDifference.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/proofdiff/ProofDifference.java
new file mode 100644
index 00000000000..c5586eaaf1a
--- /dev/null
+++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/proofdiff/ProofDifference.java
@@ -0,0 +1,200 @@
+/* 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.proofdiff;
+
+import java.util.*;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import de.uka.ilkd.key.java.Services;
+import de.uka.ilkd.key.logic.JTerm;
+import de.uka.ilkd.key.pp.LogicPrinter;
+import de.uka.ilkd.key.proof.Node;
+import de.uka.ilkd.key.util.Levensthein;
+
+import org.key_project.logic.Term;
+import org.key_project.prover.sequent.Semisequent;
+
+/**
+ * Verbatim copy of {@code de.uka.ilkd.key.gui.proofdiff.ProofDifference} from the module
+ * {@code key.ui}; the class is UI-agnostic (no Swing references), so only the package
+ * declaration and this note differ from the original.
+ *
+ * @author Alexander Weigl
+ * @version 1 (24.05.19)
+ */
+public class ProofDifference {
+    private static final Integer THRESHOLD = 25;
+    private List leftAntec = new LinkedList<>(), rightAntec = new LinkedList<>(),
+            rightSucc = new LinkedList<>(), leftSucc = new LinkedList<>();
+
+    private final Set exclusiveAntec = new HashSet<>();
+    private final Set commonSucc = new HashSet<>();
+    private final Set exclusiveSucc = new HashSet<>();
+    private final Set commonAntec = new HashSet<>();
+
+    public static ProofDifference create(Services services, Node left, Node right) {
+        return create(left, right,
+            (Term t) -> LogicPrinter.quickPrintTerm((JTerm) t, services));
+    }
+
+    public static ProofDifference create(Node left, Node right,
+            Function printer) {
+        ProofDifference pd = new ProofDifference();
+        assert left != null && right != null;
+        pd.leftAntec = initialise(printer, left.sequent().antecedent());
+        pd.leftSucc = initialise(printer, left.sequent().succedent());
+        pd.rightAntec = initialise(printer, right.sequent().antecedent());
+        pd.rightSucc = initialise(printer, right.sequent().succedent());
+        pd.computeDiff();
+        return pd;
+    }
+
+    private static List initialise(Function printer,
+            Semisequent semisequent) {
+        return semisequent.asList().stream().map(it -> printer.apply(it.formula()))
+                .collect(Collectors.toList());
+    }
+
+    private static Collection intersect(Set left, Set right) {
+        Set intersection = new TreeSet<>(left);
+        intersection.retainAll(right);
+        return intersection;
+    }
+
+    private static void computeDiff(List left, List right, Set common,
+            Set exclusive) {
+        computeDiff(new HashSet<>(left), new HashSet<>(right), common, exclusive);
+    }
+
+    private static void computeDiff(Set left, Set right, Set common,
+            Set exclusive) {
+        common.addAll(intersect(left, right));
+        exclusive.addAll(left);
+        exclusive.addAll(right);
+        exclusive.removeAll(common);
+    }
+
+    static String findAndPopNearestMatch(String l, List right) {
+        String current = null;
+        // Ignore whitespace:
+        l = l.replaceAll("\\s", "");
+        int min = Integer.MAX_VALUE;
+        for (String r : right) {
+            int d = Levensthein.calculate(l, r.replaceAll("\\s", ""));
+            if (d <= min) {
+                current = r;
+                min = d;
+            }
+        }
+        right.remove(current);
+        return current;
+    }
+
+    /**
+     * Entry in the search queue.
+     *
+     * @param idxLeft index of the left candidate
+     * @param idxRight index of the right candidate
+     * @param distance measure of difference between candidates
+     */
+    private record QueueEntry(int idxLeft, int idxRight, int distance) {
+    }
+
+    static List findPairs(List left, List right) {
+        List pairs = new ArrayList<>(left.size() + right.size());
+        int initCap =
+            Math.max(8, Math.max(left.size() * right.size(), Math.max(left.size(), right.size())));
+        PriorityQueue queue =
+            new PriorityQueue<>(initCap, Comparator.comparingInt(QueueEntry::distance));
+        for (int i = 0; i < left.size(); i++) {
+            for (int j = 0; j < right.size(); j++) {
+                queue.add(new QueueEntry(i, j, Levensthein.calculate(left.get(i), right.get(j))));
+            }
+        }
+
+        boolean[] matchedLeft = new boolean[left.size()];
+        boolean[] matchedRight = new boolean[right.size()];
+        while (!queue.isEmpty()) {
+            QueueEntry t = queue.poll();
+            /*
+             * if(t.elseTerm>=THRESHOLD) { break; }
+             */
+            if (!matchedLeft[t.idxLeft] && !matchedRight[t.idxRight]) {
+                String l = left.get(t.idxLeft);
+                String r = right.get(t.idxRight);
+                pairs.add(new Matching(l, r, t.distance));
+                matchedLeft[t.idxLeft] = true;
+                matchedRight[t.idxRight] = true;
+            }
+        }
+
+        for (int i = 0; i < matchedLeft.length; i++) {
+            if (!matchedLeft[i]) {
+                pairs.add(new Matching(left.get(i), null, left.get(i).length()));
+            }
+        }
+        for (int i = 0; i < matchedRight.length; i++) {
+            if (!matchedRight[i]) {
+                pairs.add(new Matching(null, right.get(i), right.get(i).length()));
+            }
+        }
+
+        return pairs;
+    }
+
+    public void computeDiff() {
+        computeDiff(leftAntec, rightAntec, commonAntec, exclusiveAntec);
+        computeDiff(leftSucc, rightSucc, commonSucc, exclusiveSucc);
+    }
+
+    public List getLeftAntec() {
+        return leftAntec;
+    }
+
+    public List getRightAntec() {
+        return rightAntec;
+    }
+
+    public List getRightSucc() {
+        return rightSucc;
+    }
+
+    public List getLeftSucc() {
+        return leftSucc;
+    }
+
+    public Set getExclusiveAntec() {
+        return exclusiveAntec;
+    }
+
+    public Set getCommonSucc() {
+        return commonSucc;
+    }
+
+    public Set getExclusiveSucc() {
+        return exclusiveSucc;
+    }
+
+    public Set getCommonAntec() {
+        return commonAntec;
+    }
+
+    public List getSuccPairs() {
+        return findPairs(getLeftSucc(), getRightSucc());
+    }
+
+    public List getAntecPairs() {
+        return findPairs(getLeftAntec(), getRightAntec());
+    }
+
+    record Matching(String left, String right, int distance) {
+
+        @Override
+        public String toString() {
+            return String.format("(%s, %s)", left, right);
+        }
+    }
+}
diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/proofdiff/diff_match_patch.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/proofdiff/diff_match_patch.java
new file mode 100644
index 00000000000..3b350fed2f0
--- /dev/null
+++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/proofdiff/diff_match_patch.java
@@ -0,0 +1,2419 @@
+/* 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.proofdiff;
+
+import java.net.URLDecoder;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.util.*;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/*
+ * Functions for diff, match and patch. Computes the difference between two texts to create a patch.
+ * Applies the patch onto another text, allowing for errors.
+ *
+ * @author fraser@google.com (Neil Fraser)
+ */
+
+/**
+ * Class containing the diff, match and patch methods. Also contains the behaviour settings.
+ */
+public class diff_match_patch {
+
+    // Defaults.
+    // Set these on your diff_match_patch instance to override the defaults.
+
+    /**
+     * Number of seconds to map a diff before giving up (0 for infinity).
+     */
+    public float Diff_Timeout = 1.0f;
+    /**
+     * Cost of an empty edit operation in terms of edit characters.
+     */
+    public final short Diff_EditCost = 4;
+    /**
+     * At what point is no match declared (0.0 = perfection, 1.0 = very loose).
+     */
+    public final float Match_Threshold = 0.5f;
+    /**
+     * How far to search for a match (0 = exact location, 1000+ = broad match). A match this many
+     * characters away from the expected location will add 1.0 to the score (0.0 is a perfect
+     * match).
+     */
+    public final int Match_Distance = 1000;
+    /**
+     * When deleting a large block of text (over ~64 characters), how close do the contents have to
+     * be to match the expected contents. (0.0 = perfection, 1.0 = very loose). Note that
+     * Match_Threshold controls how closely the end points of a delete need to match.
+     */
+    public final float Patch_DeleteThreshold = 0.5f;
+    /**
+     * Chunk size for context length.
+     */
+    public final short Patch_Margin = 4;
+
+    /**
+     * The number of bits in an int.
+     */
+    private final short Match_MaxBits = 32;
+
+    /**
+     * Internal class for returning results from diff_linesToChars(). Other less paranoid languages
+     * just use a three-element array.
+     */
+    protected static class LinesToCharsResult {
+        protected final String chars1;
+        protected final String chars2;
+        protected final List lineArray;
+
+        protected LinesToCharsResult(String chars1, String chars2, List lineArray) {
+            this.chars1 = chars1;
+            this.chars2 = chars2;
+            this.lineArray = lineArray;
+        }
+    }
+
+
+    // DIFF FUNCTIONS
+
+
+    /**
+     * The data structure representing a diff is a Linked list of Diff objects:
+     * {Diff(Operation.DELETE, "Hello"), Diff(Operation.INSERT, "Goodbye"), Diff(Operation.EQUAL, "
+     * world.")} which means: delete "Hello", add "Goodbye" and keep " world."
+     */
+    public enum Operation {
+        DELETE, INSERT, EQUAL
+    }
+
+    /**
+     * Find the differences between two texts. Run a faster, slightly less optimal diff. This method
+     * allows the 'checklines' of diff_main() to be optional. Most of the time checklines is wanted,
+     * so default to true.
+     *
+     * @param text1 Old string to be diffed.
+     * @param text2 New string to be diffed.
+     * @return Linked List of Diff objects.
+     */
+    public LinkedList diff_main(String text1, String text2) {
+        return diff_main(text1, text2, true);
+    }
+
+    /**
+     * Find the differences between two texts.
+     *
+     * @param text1 Old string to be diffed.
+     * @param text2 New string to be diffed.
+     * @param checklines Speedup flag. If false, then don't run a line-level diff first to identify
+     *        the changed areas. If true, then run a faster slightly less optimal diff.
+     * @return Linked List of Diff objects.
+     */
+    public LinkedList diff_main(String text1, String text2, boolean checklines) {
+        // Set a deadline by which time the diff must be complete.
+        long deadline;
+        if (Diff_Timeout <= 0) {
+            deadline = Long.MAX_VALUE;
+        } else {
+            deadline = System.currentTimeMillis() + (long) (Diff_Timeout * 1000);
+        }
+        return diff_main(text1, text2, checklines, deadline);
+    }
+
+    /**
+     * Find the differences between two texts. Simplifies the problem by stripping any common prefix
+     * or suffix off the texts before diffing.
+     *
+     * @param text1 Old string to be diffed.
+     * @param text2 New string to be diffed.
+     * @param checklines Speedup flag. If false, then don't run a line-level diff first to identify
+     *        the changed areas. If true, then run a faster slightly less optimal diff.
+     * @param deadline Time when the diff should be complete by. Used internally for recursive
+     *        calls. Users should set DiffTimeout instead.
+     * @return Linked List of Diff objects.
+     */
+    private LinkedList diff_main(String text1, String text2, boolean checklines,
+            long deadline) {
+        // Check for null inputs.
+        if (text1 == null || text2 == null) {
+            throw new IllegalArgumentException("Null inputs. (diff_main)");
+        }
+
+        // Check for equality (speedup).
+        LinkedList diffs;
+        if (text1.equals(text2)) {
+            diffs = new LinkedList<>();
+            if (text1.length() != 0) {
+                diffs.add(new Diff(Operation.EQUAL, text1));
+            }
+            return diffs;
+        }
+
+        // Trim off common prefix (speedup).
+        int commonlength = diff_commonPrefix(text1, text2);
+        String commonprefix = text1.substring(0, commonlength);
+        text1 = text1.substring(commonlength);
+        text2 = text2.substring(commonlength);
+
+        // Trim off common suffix (speedup).
+        commonlength = diff_commonSuffix(text1, text2);
+        String commonsuffix = text1.substring(text1.length() - commonlength);
+        text1 = text1.substring(0, text1.length() - commonlength);
+        text2 = text2.substring(0, text2.length() - commonlength);
+
+        // Compute the diff on the middle block.
+        diffs = diff_compute(text1, text2, checklines, deadline);
+
+        // Restore the prefix and suffix.
+        if (commonprefix.length() != 0) {
+            diffs.addFirst(new Diff(Operation.EQUAL, commonprefix));
+        }
+        if (commonsuffix.length() != 0) {
+            diffs.addLast(new Diff(Operation.EQUAL, commonsuffix));
+        }
+
+        diff_cleanupMerge(diffs);
+        return diffs;
+    }
+
+    /**
+     * Find the differences between two texts. Assumes that the texts do not have any common prefix
+     * or suffix.
+     *
+     * @param text1 Old string to be diffed.
+     * @param text2 New string to be diffed.
+     * @param checklines Speedup flag. If false, then don't run a line-level diff first to identify
+     *        the changed areas. If true, then run a faster slightly less optimal diff.
+     * @param deadline Time when the diff should be complete by.
+     * @return Linked List of Diff objects.
+     */
+    private LinkedList diff_compute(String text1, String text2, boolean checklines,
+            long deadline) {
+        LinkedList diffs = new LinkedList<>();
+
+        if (text1.length() == 0) {
+            // Just add some text (speedup).
+            diffs.add(new Diff(Operation.INSERT, text2));
+            return diffs;
+        }
+
+        if (text2.length() == 0) {
+            // Just delete some text (speedup).
+            diffs.add(new Diff(Operation.DELETE, text1));
+            return diffs;
+        }
+
+        {
+            // New scope so as to garbage collect longtext and shorttext.
+            String longtext = text1.length() > text2.length() ? text1 : text2;
+            String shorttext = text1.length() > text2.length() ? text2 : text1;
+            int i = longtext.indexOf(shorttext);
+            if (i != -1) {
+                // Shorter text is inside the longer text (speedup).
+                Operation op =
+                    (text1.length() > text2.length()) ? Operation.DELETE : Operation.INSERT;
+                diffs.add(new Diff(op, longtext.substring(0, i)));
+                diffs.add(new Diff(Operation.EQUAL, shorttext));
+                diffs.add(new Diff(op, longtext.substring(i + shorttext.length())));
+                return diffs;
+            }
+
+            if (shorttext.length() == 1) {
+                // Single character string.
+                // After the previous speedup, the character can't be an equality.
+                diffs.add(new Diff(Operation.DELETE, text1));
+                diffs.add(new Diff(Operation.INSERT, text2));
+                return diffs;
+            }
+        }
+
+        // Check to see if the problem can be split in two.
+        String[] hm = diff_halfMatch(text1, text2);
+        if (hm != null) {
+            // A half-match was found, sort out the return data.
+            String text1_a = hm[0];
+            String text1_b = hm[1];
+            String text2_a = hm[2];
+            String text2_b = hm[3];
+            String mid_common = hm[4];
+            // Send both pairs off for separate processing.
+            LinkedList diffs_a = diff_main(text1_a, text2_a, checklines, deadline);
+            LinkedList diffs_b = diff_main(text1_b, text2_b, checklines, deadline);
+            // Merge the results.
+            diffs = diffs_a;
+            diffs.add(new Diff(Operation.EQUAL, mid_common));
+            diffs.addAll(diffs_b);
+            return diffs;
+        }
+
+        if (checklines && text1.length() > 100 && text2.length() > 100) {
+            return diff_lineMode(text1, text2, deadline);
+        }
+
+        return diff_bisect(text1, text2, deadline);
+    }
+
+    /**
+     * Do a quick line-level diff on both strings, then rediff the parts for greater accuracy. This
+     * speedup can produce non-minimal diffs.
+     *
+     * @param text1 Old string to be diffed.
+     * @param text2 New string to be diffed.
+     * @param deadline Time when the diff should be complete by.
+     * @return Linked List of Diff objects.
+     */
+    private LinkedList diff_lineMode(String text1, String text2, long deadline) {
+        // Scan the text on a line-by-line basis first.
+        LinesToCharsResult b = diff_linesToChars(text1, text2);
+        text1 = b.chars1;
+        text2 = b.chars2;
+        List linearray = b.lineArray;
+
+        LinkedList diffs = diff_main(text1, text2, false, deadline);
+
+        // Convert the diff back to original text.
+        diff_charsToLines(diffs, linearray);
+        // Eliminate freak matches (e.g. blank lines)
+        diff_cleanupSemantic(diffs);
+
+        // Rediff any replacement blocks, this time character-by-character.
+        // Add a dummy entry at the end.
+        diffs.add(new Diff(Operation.EQUAL, ""));
+        int count_delete = 0;
+        int count_insert = 0;
+        StringBuilder text_delete = new StringBuilder();
+        StringBuilder text_insert = new StringBuilder();
+        ListIterator pointer = diffs.listIterator();
+        Diff thisDiff = pointer.next();
+        while (thisDiff != null) {
+            switch (thisDiff.operation) {
+                case INSERT:
+                    count_insert++;
+                    text_insert.append(thisDiff.text);
+                    break;
+                case DELETE:
+                    count_delete++;
+                    text_delete.append(thisDiff.text);
+                    break;
+                case EQUAL:
+                    // Upon reaching an equality, check for prior redundancies.
+                    if (count_delete >= 1 && count_insert >= 1) {
+                        // Delete the offending records and add the merged ones.
+                        pointer.previous();
+                        for (int j = 0; j < count_delete + count_insert; j++) {
+                            pointer.previous();
+                            pointer.remove();
+                        }
+                        for (Diff newDiff : diff_main(text_delete.toString(),
+                            text_insert.toString(),
+                            false, deadline)) {
+                            pointer.add(newDiff);
+                        }
+                    }
+                    count_insert = 0;
+                    count_delete = 0;
+                    text_delete = new StringBuilder();
+                    text_insert = new StringBuilder();
+                    break;
+            }
+            thisDiff = pointer.hasNext() ? pointer.next() : null;
+        }
+        diffs.removeLast(); // Remove the dummy entry at the end.
+
+        return diffs;
+    }
+
+    /**
+     * Find the 'middle snake' of a diff, split the problem in two and return the recursively
+     * constructed diff. See Myers 1986 paper: An O(ND) Difference Algorithm and Its Variations.
+     *
+     * @param text1 Old string to be diffed.
+     * @param text2 New string to be diffed.
+     * @param deadline Time at which to bail if not yet complete.
+     * @return LinkedList of Diff objects.
+     */
+    protected LinkedList diff_bisect(String text1, String text2, long deadline) {
+        // Cache the text lengths to prevent multiple calls.
+        int text1_length = text1.length();
+        int text2_length = text2.length();
+        int max_d = (text1_length + text2_length + 1) / 2;
+        int v_offset = max_d;
+        int v_length = 2 * max_d;
+        int[] v1 = new int[v_length];
+        int[] v2 = new int[v_length];
+        for (int x = 0; x < v_length; x++) {
+            v1[x] = -1;
+            v2[x] = -1;
+        }
+        v1[v_offset + 1] = 0;
+        v2[v_offset + 1] = 0;
+        int delta = text1_length - text2_length;
+        // If the total number of characters is odd, then the front path will
+        // collide with the reverse path.
+        boolean front = (delta % 2 != 0);
+        // Offsets for start and end of k loop.
+        // Prevents mapping of space beyond the grid.
+        int k1start = 0;
+        int k1end = 0;
+        int k2start = 0;
+        int k2end = 0;
+        for (int d = 0; d < max_d; d++) {
+            // Bail out if deadline is reached.
+            if (System.currentTimeMillis() > deadline) {
+                break;
+            }
+
+            // Walk the front path one step.
+            for (int k1 = -d + k1start; k1 <= d - k1end; k1 += 2) {
+                int k1_offset = v_offset + k1;
+                int x1;
+                if (k1 == -d || (k1 != d && v1[k1_offset - 1] < v1[k1_offset + 1])) {
+                    x1 = v1[k1_offset + 1];
+                } else {
+                    x1 = v1[k1_offset - 1] + 1;
+                }
+                int y1 = x1 - k1;
+                while (x1 < text1_length && y1 < text2_length
+                        && text1.charAt(x1) == text2.charAt(y1)) {
+                    x1++;
+                    y1++;
+                }
+                v1[k1_offset] = x1;
+                if (x1 > text1_length) {
+                    // Ran off the right of the graph.
+                    k1end += 2;
+                } else if (y1 > text2_length) {
+                    // Ran off the bottom of the graph.
+                    k1start += 2;
+                } else if (front) {
+                    int k2_offset = v_offset + delta - k1;
+                    if (k2_offset >= 0 && k2_offset < v_length && v2[k2_offset] != -1) {
+                        // Mirror x2 onto top-left coordinate system.
+                        int x2 = text1_length - v2[k2_offset];
+                        if (x1 >= x2) {
+                            // Overlap detected.
+                            return diff_bisectSplit(text1, text2, x1, y1, deadline);
+                        }
+                    }
+                }
+            }
+
+            // Walk the reverse path one step.
+            for (int k2 = -d + k2start; k2 <= d - k2end; k2 += 2) {
+                int k2_offset = v_offset + k2;
+                int x2;
+                if (k2 == -d || (k2 != d && v2[k2_offset - 1] < v2[k2_offset + 1])) {
+                    x2 = v2[k2_offset + 1];
+                } else {
+                    x2 = v2[k2_offset - 1] + 1;
+                }
+                int y2 = x2 - k2;
+                while (x2 < text1_length && y2 < text2_length && text1
+                        .charAt(text1_length - x2 - 1) == text2.charAt(text2_length - y2 - 1)) {
+                    x2++;
+                    y2++;
+                }
+                v2[k2_offset] = x2;
+                if (x2 > text1_length) {
+                    // Ran off the left of the graph.
+                    k2end += 2;
+                } else if (y2 > text2_length) {
+                    // Ran off the top of the graph.
+                    k2start += 2;
+                } else if (!front) {
+                    int k1_offset = v_offset + delta - k2;
+                    if (k1_offset >= 0 && k1_offset < v_length && v1[k1_offset] != -1) {
+                        int x1 = v1[k1_offset];
+                        int y1 = v_offset + x1 - k1_offset;
+                        // Mirror x2 onto top-left coordinate system.
+                        x2 = text1_length - x2;
+                        if (x1 >= x2) {
+                            // Overlap detected.
+                            return diff_bisectSplit(text1, text2, x1, y1, deadline);
+                        }
+                    }
+                }
+            }
+        }
+        // Diff took too long and hit the deadline or
+        // number of diffs equals number of characters, no commonality at all.
+        LinkedList diffs = new LinkedList<>();
+        diffs.add(new Diff(Operation.DELETE, text1));
+        diffs.add(new Diff(Operation.INSERT, text2));
+        return diffs;
+    }
+
+    /**
+     * Given the location of the 'middle snake', split the diff in two parts and recurse.
+     *
+     * @param text1 Old string to be diffed.
+     * @param text2 New string to be diffed.
+     * @param x Index of split point in text1.
+     * @param y Index of split point in text2.
+     * @param deadline Time at which to bail if not yet complete.
+     * @return LinkedList of Diff objects.
+     */
+    private LinkedList diff_bisectSplit(String text1, String text2, int x, int y,
+            long deadline) {
+        String text1a = text1.substring(0, x);
+        String text2a = text2.substring(0, y);
+        String text1b = text1.substring(x);
+        String text2b = text2.substring(y);
+
+        // Compute both diffs serially.
+        LinkedList diffs = diff_main(text1a, text2a, false, deadline);
+        LinkedList diffsb = diff_main(text1b, text2b, false, deadline);
+
+        diffs.addAll(diffsb);
+        return diffs;
+    }
+
+    /**
+     * Split two texts into a list of strings. Reduce the texts to a string of hashes where each
+     * Unicode character represents one line.
+     *
+     * @param text1 First string.
+     * @param text2 Second string.
+     * @return An object containing the encoded text1, the encoded text2 and the List of unique
+     *         strings. The zeroth element of the List of unique strings is intentionally blank.
+     */
+    protected LinesToCharsResult diff_linesToChars(String text1, String text2) {
+        List lineArray = new ArrayList<>();
+        Map lineHash = new LinkedHashMap<>();
+        // e.g. linearray[4] == "Hello\n"
+        // e.g. linehash.get("Hello\n") == 4
+
+        // "\x00" is a valid character, but various debuggers don't like it.
+        // So we'll insert a junk entry to avoid generating a null character.
+        lineArray.add("");
+
+        String chars1 = diff_linesToCharsMunge(text1, lineArray, lineHash);
+        String chars2 = diff_linesToCharsMunge(text2, lineArray, lineHash);
+        return new LinesToCharsResult(chars1, chars2, lineArray);
+    }
+
+    /**
+     * Split a text into a list of strings. Reduce the texts to a string of hashes where each
+     * Unicode character represents one line.
+     *
+     * @param text String to encode.
+     * @param lineArray List of unique strings.
+     * @param lineHash Map of strings to indices.
+     * @return Encoded string.
+     */
+    private String diff_linesToCharsMunge(String text, List lineArray,
+            Map lineHash) {
+        int lineStart = 0;
+        int lineEnd = -1;
+        String line;
+        StringBuilder chars = new StringBuilder();
+        // Walk the text, pulling out a substring for each line.
+        // text.split('\n') would would temporarily double our memory footprint.
+        // Modifying text would create many large strings to garbage collect.
+        while (lineEnd < text.length() - 1) {
+            lineEnd = text.indexOf('\n', lineStart);
+            if (lineEnd == -1) {
+                lineEnd = text.length() - 1;
+            }
+            line = text.substring(lineStart, lineEnd + 1);
+            lineStart = lineEnd + 1;
+
+            if (lineHash.containsKey(line)) {
+                chars.append((char) (int) lineHash.get(line));
+            } else {
+                lineArray.add(line);
+                lineHash.put(line, lineArray.size() - 1);
+                chars.append((char) (lineArray.size() - 1));
+            }
+        }
+        return chars.toString();
+    }
+
+    /**
+     * Rehydrate the text in a diff from a string of line hashes to real lines of text.
+     *
+     * @param diffs LinkedList of Diff objects.
+     * @param lineArray List of unique strings.
+     */
+    protected void diff_charsToLines(LinkedList diffs, List lineArray) {
+        StringBuilder text;
+        for (Diff diff : diffs) {
+            text = new StringBuilder();
+            for (int y = 0; y < diff.text.length(); y++) {
+                text.append(lineArray.get(diff.text.charAt(y)));
+            }
+            diff.text = text.toString();
+        }
+    }
+
+    /**
+     * Determine the common prefix of two strings
+     *
+     * @param text1 First string.
+     * @param text2 Second string.
+     * @return The number of characters common to the start of each string.
+     */
+    public int diff_commonPrefix(String text1, String text2) {
+        // Performance analysis: http://neil.fraser.name/news/2007/10/09/
+        int n = Math.min(text1.length(), text2.length());
+        for (int i = 0; i < n; i++) {
+            if (text1.charAt(i) != text2.charAt(i)) {
+                return i;
+            }
+        }
+        return n;
+    }
+
+    /**
+     * Determine the common suffix of two strings
+     *
+     * @param text1 First string.
+     * @param text2 Second string.
+     * @return The number of characters common to the end of each string.
+     */
+    public int diff_commonSuffix(String text1, String text2) {
+        // Performance analysis: http://neil.fraser.name/news/2007/10/09/
+        int text1_length = text1.length();
+        int text2_length = text2.length();
+        int n = Math.min(text1_length, text2_length);
+        for (int i = 1; i <= n; i++) {
+            if (text1.charAt(text1_length - i) != text2.charAt(text2_length - i)) {
+                return i - 1;
+            }
+        }
+        return n;
+    }
+
+    /**
+     * Determine if the suffix of one string is the prefix of another.
+     *
+     * @param text1 First string.
+     * @param text2 Second string.
+     * @return The number of characters common to the end of the first string and the start of the
+     *         second string.
+     */
+    protected int diff_commonOverlap(String text1, String text2) {
+        // Cache the text lengths to prevent multiple calls.
+        int text1_length = text1.length();
+        int text2_length = text2.length();
+        // Eliminate the null case.
+        if (text1_length == 0 || text2_length == 0) {
+            return 0;
+        }
+        // Truncate the longer string.
+        if (text1_length > text2_length) {
+            text1 = text1.substring(text1_length - text2_length);
+        } else if (text1_length < text2_length) {
+            text2 = text2.substring(0, text1_length);
+        }
+        int text_length = Math.min(text1_length, text2_length);
+        // Quick check for the worst case.
+        if (text1.equals(text2)) {
+            return text_length;
+        }
+
+        // Start by looking for a single character match
+        // and increase length until no match is found.
+        // Performance analysis: http://neil.fraser.name/news/2010/11/04/
+        int best = 0;
+        int length = 1;
+        while (true) {
+            String pattern = text1.substring(text_length - length);
+            int found = text2.indexOf(pattern);
+            if (found == -1) {
+                return best;
+            }
+            length += found;
+            if (found == 0
+                    || text1.substring(text_length - length).equals(text2.substring(0, length))) {
+                best = length;
+                length++;
+            }
+        }
+    }
+
+    /**
+     * Do the two texts share a substring which is at least half the length of the longer text? This
+     * speedup can produce non-minimal diffs.
+     *
+     * @param text1 First string.
+     * @param text2 Second string.
+     * @return Five element String array, containing the prefix of text1, the suffix of text1, the
+     *         prefix of text2, the suffix of text2 and the common middle. Or null if there was no
+     *         match.
+     */
+    protected String[] diff_halfMatch(String text1, String text2) {
+        if (Diff_Timeout <= 0) {
+            // Don't risk returning a non-optimal diff if we have unlimited time.
+            return null;
+        }
+        String longtext = text1.length() > text2.length() ? text1 : text2;
+        String shorttext = text1.length() > text2.length() ? text2 : text1;
+        if (longtext.length() < 4 || shorttext.length() * 2 < longtext.length()) {
+            return null; // Pointless.
+        }
+
+        // First check if the second quarter is the seed for a half-match.
+        String[] hm1 = diff_halfMatchI(longtext, shorttext, (longtext.length() + 3) / 4);
+        // Check again based on the third quarter.
+        String[] hm2 = diff_halfMatchI(longtext, shorttext, (longtext.length() + 1) / 2);
+        String[] hm;
+        if (hm1 == null && hm2 == null) {
+            return null;
+        } else if (hm2 == null) {
+            hm = hm1;
+        } else if (hm1 == null) {
+            hm = hm2;
+        } else {
+            // Both matched. Select the longest.
+            hm = hm1[4].length() > hm2[4].length() ? hm1 : hm2;
+        }
+
+        // A half-match was found, sort out the return data.
+        if (text1.length() > text2.length()) {
+            return hm;
+            // return new String[]{hm[0], hm[1], hm[2], hm[3], hm[4]};
+        } else {
+            return new String[] { hm[2], hm[3], hm[0], hm[1], hm[4] };
+        }
+    }
+
+    /**
+     * Does a substring of shorttext exist within longtext such that the substring is at least half
+     * the length of longtext?
+     *
+     * @param longtext Longer string.
+     * @param shorttext Shorter string.
+     * @param i Start index of quarter length substring within longtext.
+     * @return Five element String array, containing the prefix of longtext, the suffix of longtext,
+     *         the prefix of shorttext, the suffix of shorttext and the common middle. Or null if
+     *         there was no match.
+     */
+    private String[] diff_halfMatchI(String longtext, String shorttext, int i) {
+        // Start with a 1/4 length substring at position i as a seed.
+        String seed = longtext.substring(i, i + longtext.length() / 4);
+        int j = -1;
+        String best_common = "";
+        String best_longtext_a = "", best_longtext_b = "";
+        String best_shorttext_a = "", best_shorttext_b = "";
+        while ((j = shorttext.indexOf(seed, j + 1)) != -1) {
+            int prefixLength = diff_commonPrefix(longtext.substring(i), shorttext.substring(j));
+            int suffixLength =
+                diff_commonSuffix(longtext.substring(0, i), shorttext.substring(0, j));
+            if (best_common.length() < suffixLength + prefixLength) {
+                best_common = shorttext.substring(j - suffixLength, j)
+                        + shorttext.substring(j, j + prefixLength);
+                best_longtext_a = longtext.substring(0, i - suffixLength);
+                best_longtext_b = longtext.substring(i + prefixLength);
+                best_shorttext_a = shorttext.substring(0, j - suffixLength);
+                best_shorttext_b = shorttext.substring(j + prefixLength);
+            }
+        }
+        if (best_common.length() * 2 >= longtext.length()) {
+            return new String[] { best_longtext_a, best_longtext_b, best_shorttext_a,
+                best_shorttext_b, best_common };
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Reduce the number of edits by eliminating semantically trivial equalities.
+     *
+     * @param diffs LinkedList of Diff objects.
+     */
+    public void diff_cleanupSemantic(LinkedList diffs) {
+        if (diffs.isEmpty()) {
+            return;
+        }
+        boolean changes = false;
+        Stack equalities = new Stack<>(); // Stack of qualities.
+        String lastequality = null; // Always equal to equalities.lastElement().text
+        ListIterator pointer = diffs.listIterator();
+        // Number of characters that changed prior to the equality.
+        int length_insertions1 = 0;
+        int length_deletions1 = 0;
+        // Number of characters that changed after the equality.
+        int length_insertions2 = 0;
+        int length_deletions2 = 0;
+        Diff thisDiff = pointer.next();
+        while (thisDiff != null) {
+            if (thisDiff.operation == Operation.EQUAL) {
+                // Equality found.
+                equalities.push(thisDiff);
+                length_insertions1 = length_insertions2;
+                length_deletions1 = length_deletions2;
+                length_insertions2 = 0;
+                length_deletions2 = 0;
+                lastequality = thisDiff.text;
+            } else {
+                // An insertion or deletion.
+                if (thisDiff.operation == Operation.INSERT) {
+                    length_insertions2 += thisDiff.text.length();
+                } else {
+                    length_deletions2 += thisDiff.text.length();
+                }
+                // Eliminate an equality that is smaller or equal to the edits on both
+                // sides of it.
+                if (lastequality != null
+                        && (lastequality.length() <= Math.max(length_insertions1,
+                            length_deletions1))
+                        && (lastequality.length() <= Math.max(length_insertions2,
+                            length_deletions2))) {
+                    // System.out.println("Splitting: '" + lastequality + "'");
+                    // Walk back to offending equality.
+                    while (thisDiff != equalities.lastElement()) {
+                        thisDiff = pointer.previous();
+                    }
+                    pointer.next();
+
+                    // Replace equality with a delete.
+                    pointer.set(new Diff(Operation.DELETE, lastequality));
+                    // Insert a corresponding an insert.
+                    pointer.add(new Diff(Operation.INSERT, lastequality));
+
+                    equalities.pop(); // Throw away the equality we just deleted.
+                    if (!equalities.empty()) {
+                        // Throw away the previous equality (it needs to be reevaluated).
+                        equalities.pop();
+                    }
+                    if (equalities.empty()) {
+                        // There are no previous equalities, walk back to the start.
+                        while (pointer.hasPrevious()) {
+                            pointer.previous();
+                        }
+                    } else {
+                        // There is a safe equality we can fall back to.
+                        thisDiff = equalities.lastElement();
+                        while (thisDiff != pointer.previous()) {
+                            // Intentionally empty loop.
+                        }
+                    }
+
+                    length_insertions1 = 0; // Reset the counters.
+                    length_insertions2 = 0;
+                    length_deletions1 = 0;
+                    length_deletions2 = 0;
+                    lastequality = null;
+                    changes = true;
+                }
+            }
+            thisDiff = pointer.hasNext() ? pointer.next() : null;
+        }
+
+        // Normalize the diff.
+        if (changes) {
+            diff_cleanupMerge(diffs);
+        }
+        diff_cleanupSemanticLossless(diffs);
+
+        // Find any overlaps between deletions and insertions.
+        // e.g: abcxxxxxxdef
+        // -> abcxxxdef
+        // e.g: xxxabcdefxxx
+        // -> defxxxabc
+        // Only extract an overlap if it is as big as the edit ahead or behind it.
+        pointer = diffs.listIterator();
+        Diff prevDiff = null;
+        thisDiff = null;
+        if (pointer.hasNext()) {
+            prevDiff = pointer.next();
+            if (pointer.hasNext()) {
+                thisDiff = pointer.next();
+            }
+        }
+        while (thisDiff != null) {
+            if (prevDiff.operation == Operation.DELETE && thisDiff.operation == Operation.INSERT) {
+                String deletion = prevDiff.text;
+                String insertion = thisDiff.text;
+                int overlap_length1 = this.diff_commonOverlap(deletion, insertion);
+                int overlap_length2 = this.diff_commonOverlap(insertion, deletion);
+                if (overlap_length1 >= overlap_length2) {
+                    if (overlap_length1 >= deletion.length() / 2.0
+                            || overlap_length1 >= insertion.length() / 2.0) {
+                        // Overlap found. Insert an equality and trim the surrounding edits.
+                        pointer.previous();
+                        pointer.add(
+                            new Diff(Operation.EQUAL, insertion.substring(0, overlap_length1)));
+                        prevDiff.text = deletion.substring(0, deletion.length() - overlap_length1);
+                        thisDiff.text = insertion.substring(overlap_length1);
+                        // pointer.add inserts the element before the cursor, so there is
+                        // no need to step past the new element.
+                    }
+                } else {
+                    if (overlap_length2 >= deletion.length() / 2.0
+                            || overlap_length2 >= insertion.length() / 2.0) {
+                        // Reverse overlap found.
+                        // Insert an equality and swap and trim the surrounding edits.
+                        pointer.previous();
+                        pointer.add(
+                            new Diff(Operation.EQUAL, deletion.substring(0, overlap_length2)));
+                        prevDiff.operation = Operation.INSERT;
+                        prevDiff.text =
+                            insertion.substring(0, insertion.length() - overlap_length2);
+                        thisDiff.operation = Operation.DELETE;
+                        thisDiff.text = deletion.substring(overlap_length2);
+                        // pointer.add inserts the element before the cursor, so there is
+                        // no need to step past the new element.
+                    }
+                }
+                thisDiff = pointer.hasNext() ? pointer.next() : null;
+            }
+            prevDiff = thisDiff;
+            thisDiff = pointer.hasNext() ? pointer.next() : null;
+        }
+    }
+
+    /**
+     * Look for single edits surrounded on both sides by equalities which can be shifted sideways to
+     * align the edit to a word boundary. e.g: The cat came. -> The cat came.
+     *
+     * @param diffs LinkedList of Diff objects.
+     */
+    public void diff_cleanupSemanticLossless(LinkedList diffs) {
+        String equality1, edit, equality2;
+        String commonString;
+        int commonOffset;
+        int score, bestScore;
+        String bestEquality1, bestEdit, bestEquality2;
+        // Create a new iterator at the start.
+        ListIterator pointer = diffs.listIterator();
+        Diff prevDiff = pointer.hasNext() ? pointer.next() : null;
+        Diff thisDiff = pointer.hasNext() ? pointer.next() : null;
+        Diff nextDiff = pointer.hasNext() ? pointer.next() : null;
+        // Intentionally ignore the first and last element (don't need checking).
+        while (nextDiff != null) {
+            if (prevDiff.operation == Operation.EQUAL && nextDiff.operation == Operation.EQUAL) {
+                // This is a single edit surrounded by equalities.
+                equality1 = prevDiff.text;
+                edit = thisDiff.text;
+                equality2 = nextDiff.text;
+
+                // First, shift the edit as far left as possible.
+                commonOffset = diff_commonSuffix(equality1, edit);
+                if (commonOffset != 0) {
+                    commonString = edit.substring(edit.length() - commonOffset);
+                    equality1 = equality1.substring(0, equality1.length() - commonOffset);
+                    edit = commonString + edit.substring(0, edit.length() - commonOffset);
+                    equality2 = commonString + equality2;
+                }
+
+                // Second, step character by character right, looking for the best fit.
+                bestEquality1 = equality1;
+                bestEdit = edit;
+                bestEquality2 = equality2;
+                bestScore = diff_cleanupSemanticScore(equality1, edit)
+                        + diff_cleanupSemanticScore(edit, equality2);
+                while (edit.length() != 0 && equality2.length() != 0
+                        && edit.charAt(0) == equality2.charAt(0)) {
+                    equality1 += edit.charAt(0);
+                    edit = edit.substring(1) + equality2.charAt(0);
+                    equality2 = equality2.substring(1);
+                    score = diff_cleanupSemanticScore(equality1, edit)
+                            + diff_cleanupSemanticScore(edit, equality2);
+                    // The >= encourages trailing rather than leading whitespace on edits.
+                    if (score >= bestScore) {
+                        bestScore = score;
+                        bestEquality1 = equality1;
+                        bestEdit = edit;
+                        bestEquality2 = equality2;
+                    }
+                }
+
+                if (!prevDiff.text.equals(bestEquality1)) {
+                    // We have an improvement, save it back to the diff.
+                    if (bestEquality1.length() != 0) {
+                        prevDiff.text = bestEquality1;
+                    } else {
+                        pointer.previous(); // Walk past nextDiff.
+                        pointer.previous(); // Walk past thisDiff.
+                        pointer.previous(); // Walk past prevDiff.
+                        pointer.remove(); // Delete prevDiff.
+                        pointer.next(); // Walk past thisDiff.
+                        pointer.next(); // Walk past nextDiff.
+                    }
+                    thisDiff.text = bestEdit;
+                    if (bestEquality2.length() != 0) {
+                        nextDiff.text = bestEquality2;
+                    } else {
+                        pointer.remove(); // Delete nextDiff.
+                        nextDiff = thisDiff;
+                        thisDiff = prevDiff;
+                    }
+                }
+            }
+            prevDiff = thisDiff;
+            thisDiff = nextDiff;
+            nextDiff = pointer.hasNext() ? pointer.next() : null;
+        }
+    }
+
+    /**
+     * Given two strings, compute a score representing whether the internal boundary falls on
+     * logical boundaries. Scores range from 6 (best) to 0 (worst).
+     *
+     * @param one First string.
+     * @param two Second string.
+     * @return The score.
+     */
+    private int diff_cleanupSemanticScore(String one, String two) {
+        if (one.length() == 0 || two.length() == 0) {
+            // Edges are the best.
+            return 6;
+        }
+
+        // Each port of this function behaves slightly differently due to
+        // subtle differences in each language's definition of things like
+        // 'whitespace'. Since this function's purpose is largely cosmetic,
+        // the choice has been made to use each language's native features
+        // rather than force total conformity.
+        char char1 = one.charAt(one.length() - 1);
+        char char2 = two.charAt(0);
+        boolean nonAlphaNumeric1 = !Character.isLetterOrDigit(char1);
+        boolean nonAlphaNumeric2 = !Character.isLetterOrDigit(char2);
+        boolean whitespace1 = nonAlphaNumeric1 && Character.isWhitespace(char1);
+        boolean whitespace2 = nonAlphaNumeric2 && Character.isWhitespace(char2);
+        boolean lineBreak1 = whitespace1 && Character.getType(char1) == Character.CONTROL;
+        boolean lineBreak2 = whitespace2 && Character.getType(char2) == Character.CONTROL;
+        boolean blankLine1 = lineBreak1 && BLANKLINEEND.matcher(one).find();
+        boolean blankLine2 = lineBreak2 && BLANKLINESTART.matcher(two).find();
+
+        if (blankLine1 || blankLine2) {
+            // Five points for blank lines.
+            return 5;
+        } else if (lineBreak1 || lineBreak2) {
+            // Four points for line breaks.
+            return 4;
+        } else if (nonAlphaNumeric1 && !whitespace1 && whitespace2) {
+            // Three points for end of sentences.
+            return 3;
+        } else if (whitespace1 || whitespace2) {
+            // Two points for whitespace.
+            return 2;
+        } else if (nonAlphaNumeric1 || nonAlphaNumeric2) {
+            // One point for non-alphanumeric.
+            return 1;
+        }
+        return 0;
+    }
+
+    // Define some regex patterns for matching boundaries.
+    private final Pattern BLANKLINEEND = Pattern.compile("\\n\\r?\\n\\Z", Pattern.DOTALL);
+    private final Pattern BLANKLINESTART = Pattern.compile("\\A\\r?\\n\\r?\\n", Pattern.DOTALL);
+
+    /**
+     * Reduce the number of edits by eliminating operationally trivial equalities.
+     *
+     * @param diffs LinkedList of Diff objects.
+     */
+    public void diff_cleanupEfficiency(LinkedList diffs) {
+        if (diffs.isEmpty()) {
+            return;
+        }
+        boolean changes = false;
+        Stack equalities = new Stack<>(); // Stack of equalities.
+        String lastequality = null; // Always equal to equalities.lastElement().text
+        ListIterator pointer = diffs.listIterator();
+        // Is there an insertion operation before the last equality.
+        boolean pre_ins = false;
+        // Is there a deletion operation before the last equality.
+        boolean pre_del = false;
+        // Is there an insertion operation after the last equality.
+        boolean post_ins = false;
+        // Is there a deletion operation after the last equality.
+        boolean post_del = false;
+        Diff thisDiff = pointer.next();
+        Diff safeDiff = thisDiff; // The last Diff that is known to be unsplitable.
+        while (thisDiff != null) {
+            if (thisDiff.operation == Operation.EQUAL) {
+                // Equality found.
+                if (thisDiff.text.length() < Diff_EditCost && (post_ins || post_del)) {
+                    // Candidate found.
+                    equalities.push(thisDiff);
+                    pre_ins = post_ins;
+                    pre_del = post_del;
+                    lastequality = thisDiff.text;
+                } else {
+                    // Not a candidate, and can never become one.
+                    equalities.clear();
+                    lastequality = null;
+                    safeDiff = thisDiff;
+                }
+                post_ins = post_del = false;
+            } else {
+                // An insertion or deletion.
+                if (thisDiff.operation == Operation.DELETE) {
+                    post_del = true;
+                } else {
+                    post_ins = true;
+                }
+                /*
+                 * Five types to be split: ABXYCD
+                 * AXCD ABXC
+                 * AXCD ABXC
+                 */
+                if (lastequality != null && ((pre_ins && pre_del && post_ins && post_del)
+                        || ((lastequality.length() < Diff_EditCost / 2)
+                                && ((pre_ins ? 1 : 0) + (pre_del ? 1 : 0) + (post_ins ? 1 : 0)
+                                        + (post_del ? 1 : 0)) == 3))) {
+                    // System.out.println("Splitting: '" + lastequality + "'");
+                    // Walk back to offending equality.
+                    while (thisDiff != equalities.lastElement()) {
+                        thisDiff = pointer.previous();
+                    }
+                    pointer.next();
+
+                    // Replace equality with a delete.
+                    pointer.set(new Diff(Operation.DELETE, lastequality));
+                    // Insert a corresponding an insert.
+                    pointer.add(thisDiff = new Diff(Operation.INSERT, lastequality));
+
+                    equalities.pop(); // Throw away the equality we just deleted.
+                    lastequality = null;
+                    if (pre_ins && pre_del) {
+                        // No changes made which could affect previous entry, keep going.
+                        post_ins = post_del = true;
+                        equalities.clear();
+                        safeDiff = thisDiff;
+                    } else {
+                        if (!equalities.empty()) {
+                            // Throw away the previous equality (it needs to be reevaluated).
+                            equalities.pop();
+                        }
+                        if (equalities.empty()) {
+                            // There are no previous questionable equalities,
+                            // walk back to the last known safe diff.
+                            thisDiff = safeDiff;
+                        } else {
+                            // There is an equality we can fall back to.
+                            thisDiff = equalities.lastElement();
+                        }
+                        while (thisDiff != pointer.previous()) {
+                            // Intentionally empty loop.
+                        }
+                        post_ins = post_del = false;
+                    }
+
+                    changes = true;
+                }
+            }
+            thisDiff = pointer.hasNext() ? pointer.next() : null;
+        }
+
+        if (changes) {
+            diff_cleanupMerge(diffs);
+        }
+    }
+
+    /**
+     * Reorder and merge like edit sections. Merge equalities. Any edit section can move as long as
+     * it doesn't cross an equality.
+     *
+     * @param diffs LinkedList of Diff objects.
+     */
+    public void diff_cleanupMerge(LinkedList diffs) {
+        diffs.add(new Diff(Operation.EQUAL, "")); // Add a dummy entry at the end.
+        ListIterator pointer = diffs.listIterator();
+        int count_delete = 0;
+        int count_insert = 0;
+        String text_delete = "";
+        String text_insert = "";
+        Diff thisDiff = pointer.next();
+        Diff prevEqual = null;
+        int commonlength;
+        while (thisDiff != null) {
+            switch (thisDiff.operation) {
+                case INSERT:
+                    count_insert++;
+                    text_insert += thisDiff.text;
+                    prevEqual = null;
+                    break;
+                case DELETE:
+                    count_delete++;
+                    text_delete += thisDiff.text;
+                    prevEqual = null;
+                    break;
+                case EQUAL:
+                    if (count_delete + count_insert > 1) {
+                        boolean both_types = count_delete != 0 && count_insert != 0;
+                        // Delete the offending records.
+                        pointer.previous(); // Reverse direction.
+                        while (count_delete-- > 0) {
+                            pointer.previous();
+                            pointer.remove();
+                        }
+                        while (count_insert-- > 0) {
+                            pointer.previous();
+                            pointer.remove();
+                        }
+                        if (both_types) {
+                            // Factor out any common prefixies.
+                            commonlength = diff_commonPrefix(text_insert, text_delete);
+                            if (commonlength != 0) {
+                                if (pointer.hasPrevious()) {
+                                    thisDiff = pointer.previous();
+                                    assert thisDiff.operation == Operation.EQUAL
+                                            : "Previous diff should have been an equality.";
+                                    thisDiff.text += text_insert.substring(0, commonlength);
+                                    pointer.next();
+                                } else {
+                                    pointer.add(new Diff(Operation.EQUAL,
+                                        text_insert.substring(0, commonlength)));
+                                }
+                                text_insert = text_insert.substring(commonlength);
+                                text_delete = text_delete.substring(commonlength);
+                            }
+                            // Factor out any common suffixies.
+                            commonlength = diff_commonSuffix(text_insert, text_delete);
+                            if (commonlength != 0) {
+                                thisDiff = pointer.next();
+                                thisDiff.text =
+                                    text_insert.substring(text_insert.length() - commonlength)
+                                            + thisDiff.text;
+                                text_insert =
+                                    text_insert.substring(0, text_insert.length() - commonlength);
+                                text_delete =
+                                    text_delete.substring(0, text_delete.length() - commonlength);
+                                pointer.previous();
+                            }
+                        }
+                        // Insert the merged records.
+                        if (text_delete.length() != 0) {
+                            pointer.add(new Diff(Operation.DELETE, text_delete));
+                        }
+                        if (text_insert.length() != 0) {
+                            pointer.add(new Diff(Operation.INSERT, text_insert));
+                        }
+                        // Step forward to the equality.
+                        thisDiff = pointer.hasNext() ? pointer.next() : null;
+                    } else if (prevEqual != null) {
+                        // Merge this equality with the previous one.
+                        prevEqual.text += thisDiff.text;
+                        pointer.remove();
+                        thisDiff = pointer.previous();
+                        pointer.next(); // Forward direction
+                    }
+                    count_insert = 0;
+                    count_delete = 0;
+                    text_delete = "";
+                    text_insert = "";
+                    prevEqual = thisDiff;
+                    break;
+            }
+            thisDiff = pointer.hasNext() ? pointer.next() : null;
+        }
+        if (diffs.getLast().text.length() == 0) {
+            diffs.removeLast(); // Remove the dummy entry at the end.
+        }
+
+        /*
+         * Second pass: look for single edits surrounded on both sides by equalities which can be
+         * shifted sideways to eliminate an equality. e.g: ABAC -> ABAC
+         */
+        boolean changes = false;
+        // Create a new iterator at the start.
+        // (As opposed to walking the current one back.)
+        pointer = diffs.listIterator();
+        Diff prevDiff = pointer.hasNext() ? pointer.next() : null;
+        thisDiff = pointer.hasNext() ? pointer.next() : null;
+        Diff nextDiff = pointer.hasNext() ? pointer.next() : null;
+        // Intentionally ignore the first and last element (don't need checking).
+        while (nextDiff != null) {
+            if (prevDiff.operation == Operation.EQUAL && nextDiff.operation == Operation.EQUAL) {
+                // This is a single edit surrounded by equalities.
+                if (thisDiff.text.endsWith(prevDiff.text)) {
+                    // Shift the edit over the previous equality.
+                    thisDiff.text = prevDiff.text + thisDiff.text.substring(0,
+                        thisDiff.text.length() - prevDiff.text.length());
+                    nextDiff.text = prevDiff.text + nextDiff.text;
+                    pointer.previous(); // Walk past nextDiff.
+                    pointer.previous(); // Walk past thisDiff.
+                    pointer.previous(); // Walk past prevDiff.
+                    pointer.remove(); // Delete prevDiff.
+                    pointer.next(); // Walk past thisDiff.
+                    thisDiff = pointer.next(); // Walk past nextDiff.
+                    nextDiff = pointer.hasNext() ? pointer.next() : null;
+                    changes = true;
+                } else if (thisDiff.text.startsWith(nextDiff.text)) {
+                    // Shift the edit over the next equality.
+                    prevDiff.text += nextDiff.text;
+                    thisDiff.text = thisDiff.text.substring(nextDiff.text.length()) + nextDiff.text;
+                    pointer.remove(); // Delete nextDiff.
+                    nextDiff = pointer.hasNext() ? pointer.next() : null;
+                    changes = true;
+                }
+            }
+            prevDiff = thisDiff;
+            thisDiff = nextDiff;
+            nextDiff = pointer.hasNext() ? pointer.next() : null;
+        }
+        // If shifts were made, the diff needs reordering and another shift sweep.
+        if (changes) {
+            diff_cleanupMerge(diffs);
+        }
+    }
+
+    /**
+     * loc is a location in text1, compute and return the equivalent location in text2. e.g. "The
+     * cat" vs "The big cat", 1->1, 5->8
+     *
+     * @param diffs LinkedList of Diff objects.
+     * @param loc Location within text1.
+     * @return Location within text2.
+     */
+    public int diff_xIndex(LinkedList diffs, int loc) {
+        int chars1 = 0;
+        int chars2 = 0;
+        int last_chars1 = 0;
+        int last_chars2 = 0;
+        Diff lastDiff = null;
+        for (Diff aDiff : diffs) {
+            if (aDiff.operation != Operation.INSERT) {
+                // Equality or deletion.
+                chars1 += aDiff.text.length();
+            }
+            if (aDiff.operation != Operation.DELETE) {
+                // Equality or insertion.
+                chars2 += aDiff.text.length();
+            }
+            if (chars1 > loc) {
+                // Overshot the location.
+                lastDiff = aDiff;
+                break;
+            }
+            last_chars1 = chars1;
+            last_chars2 = chars2;
+        }
+        if (lastDiff != null && lastDiff.operation == Operation.DELETE) {
+            // The location was deleted.
+            return last_chars2;
+        }
+        // Add the remaining character length.
+        return last_chars2 + (loc - last_chars1);
+    }
+
+    /**
+     * Convert a Diff list into a pretty HTML report.
+     *
+     * @param diffs LinkedList of Diff objects.
+     * @return HTML representation.
+     */
+    public String diff_prettyHtml(LinkedList diffs) {
+        StringBuilder html = new StringBuilder();
+        for (Diff aDiff : diffs) {
+            String text = aDiff.text.replace("&", "&").replace("<", "<").replace(">", ">")
+                    .replace("\n", "¶
"); + switch (aDiff.operation) { + case INSERT: + html.append("").append(text) + .append(""); + break; + case DELETE: + html.append("").append(text) + .append(""); + break; + case EQUAL: + html.append("").append(text).append(""); + break; + } + } + return html.toString(); + } + + /** + * Compute and return the source text (all equalities and deletions). + * + * @param diffs LinkedList of Diff objects. + * @return Source text. + */ + public String diff_text1(LinkedList diffs) { + StringBuilder text = new StringBuilder(); + for (Diff aDiff : diffs) { + if (aDiff.operation != Operation.INSERT) { + text.append(aDiff.text); + } + } + return text.toString(); + } + + /** + * Compute and return the destination text (all equalities and insertions). + * + * @param diffs LinkedList of Diff objects. + * @return Destination text. + */ + public String diff_text2(LinkedList diffs) { + StringBuilder text = new StringBuilder(); + for (Diff aDiff : diffs) { + if (aDiff.operation != Operation.DELETE) { + text.append(aDiff.text); + } + } + return text.toString(); + } + + /** + * Compute the Levenshtein distance; the number of inserted, deleted or substituted characters. + * + * @param diffs LinkedList of Diff objects. + * @return Number of changes. + */ + public int diff_levenshtein(LinkedList diffs) { + int levenshtein = 0; + int insertions = 0; + int deletions = 0; + for (Diff aDiff : diffs) { + switch (aDiff.operation) { + case INSERT: + insertions += aDiff.text.length(); + break; + case DELETE: + deletions += aDiff.text.length(); + break; + case EQUAL: + // A deletion and an insertion is one substitution. + levenshtein += Math.max(insertions, deletions); + insertions = 0; + deletions = 0; + break; + } + } + levenshtein += Math.max(insertions, deletions); + return levenshtein; + } + + /** + * Crush the diff into an encoded string which describes the operations required to transform + * text1 into text2. E.g. =3\t-2\t+ing -> Keep 3 chars, delete 2 chars, insert 'ing'. Operations + * are tab-separated. Inserted text is escaped using %xx notation. + * + * @param diffs Array of Diff objects. + * @return Delta text. + */ + public String diff_toDelta(LinkedList diffs) { + StringBuilder text = new StringBuilder(); + for (Diff aDiff : diffs) { + switch (aDiff.operation) { + case INSERT: + text.append("+") + .append( + URLEncoder.encode(aDiff.text, StandardCharsets.UTF_8).replace('+', + ' ')) + .append("\t"); + break; + case DELETE: + text.append("-").append(aDiff.text.length()).append("\t"); + break; + case EQUAL: + text.append("=").append(aDiff.text.length()).append("\t"); + break; + } + } + String delta = text.toString(); + if (delta.length() != 0) { + // Strip off trailing tab character. + delta = delta.substring(0, delta.length() - 1); + delta = unescapeForEncodeUriCompatability(delta); + } + return delta; + } + + /** + * Given the original text1, and an encoded string which describes the operations required to + * transform text1 into text2, compute the full diff. + * + * @param text1 Source string for the diff. + * @param delta Delta text. + * @return Array of Diff objects or null if invalid. + * @throws IllegalArgumentException If invalid input. + */ + public LinkedList diff_fromDelta(String text1, String delta) + throws IllegalArgumentException { + LinkedList diffs = new LinkedList<>(); + int pointer = 0; // Cursor in text1 + String[] tokens = delta.split("\t"); + for (String token : tokens) { + if (token.length() == 0) { + // Blank tokens are ok (from a trailing \t). + continue; + } + // Each token begins with a one character parameter which specifies the + // operation of this token (delete, insert, equality). + String param = token.substring(1); + switch (token.charAt(0)) { + case '+': + // decode would change all "+" to " " + param = param.replace("+", "%2B"); + try { + param = URLDecoder.decode(param, StandardCharsets.UTF_8); + } catch (IllegalArgumentException e) { + // Malformed URI sequence. + throw new IllegalArgumentException( + "Illegal escape in diff_fromDelta: " + param, + e); + } + diffs.add(new Diff(Operation.INSERT, param)); + break; + case '-': + // Fall through. + case '=': + int n; + try { + n = Integer.parseInt(param); + } catch (NumberFormatException e) { + throw new IllegalArgumentException( + "Invalid number in diff_fromDelta: " + param, + e); + } + if (n < 0) { + throw new IllegalArgumentException( + "Negative number in diff_fromDelta: " + param); + } + String text; + try { + text = text1.substring(pointer, pointer += n); + } catch (StringIndexOutOfBoundsException e) { + throw new IllegalArgumentException("Delta length (" + pointer + + ") larger than source text length (" + text1.length() + ").", e); + } + if (token.charAt(0) == '=') { + diffs.add(new Diff(Operation.EQUAL, text)); + } else { + diffs.add(new Diff(Operation.DELETE, text)); + } + break; + default: + // Anything else is an error. + throw new IllegalArgumentException( + "Invalid diff operation in diff_fromDelta: " + token.charAt(0)); + } + } + if (pointer != text1.length()) { + throw new IllegalArgumentException("Delta length (" + pointer + + ") smaller than source text length (" + text1.length() + ")."); + } + return diffs; + } + + + // MATCH FUNCTIONS + + + /** + * Locate the best instance of 'pattern' in 'text' near 'loc'. Returns -1 if no match found. + * + * @param text The text to search. + * @param pattern The pattern to search for. + * @param loc The location to search around. + * @return Best match index or -1. + */ + public int match_main(String text, String pattern, int loc) { + // Check for null inputs. + if (text == null || pattern == null) { + throw new IllegalArgumentException("Null inputs. (match_main)"); + } + + loc = Math.max(0, Math.min(loc, text.length())); + if (text.equals(pattern)) { + // Shortcut (potentially not guaranteed by the algorithm) + return 0; + } else if (text.length() == 0) { + // Nothing to match. + return -1; + } else if (loc + pattern.length() <= text.length() + && text.startsWith(pattern, loc)) { + // Perfect match at the perfect spot! (Includes case of null pattern) + return loc; + } else { + // Do a fuzzy compare. + return match_bitap(text, pattern, loc); + } + } + + /** + * Locate the best instance of 'pattern' in 'text' near 'loc' using the Bitap algorithm. Returns + * -1 if no match found. + * + * @param text The text to search. + * @param pattern The pattern to search for. + * @param loc The location to search around. + * @return Best match index or -1. + */ + protected int match_bitap(String text, String pattern, int loc) { + assert (Match_MaxBits == 0 || pattern.length() <= Match_MaxBits) + : "Pattern too long for this application."; + + // Initialise the alphabet. + Map s = match_alphabet(pattern); + + // Highest score beyond which we give up. + double score_threshold = Match_Threshold; + // Is there a nearby exact match? (speedup) + int best_loc = text.indexOf(pattern, loc); + if (best_loc != -1) { + score_threshold = + Math.min(match_bitapScore(0, best_loc, loc, pattern), score_threshold); + // What about in the other direction? (speedup) + best_loc = text.lastIndexOf(pattern, loc + pattern.length()); + if (best_loc != -1) { + score_threshold = + Math.min(match_bitapScore(0, best_loc, loc, pattern), score_threshold); + } + } + + // Initialise the bit arrays. + int matchmask = 1 << (pattern.length() - 1); + best_loc = -1; + + int bin_min, bin_mid; + int bin_max = pattern.length() + text.length(); + // Empty initialization added to appease Java compiler. + int[] last_rd = new int[0]; + for (int d = 0; d < pattern.length(); d++) { + // Scan for the best match; each iteration allows for one more error. + // Run a binary search to determine how far from 'loc' we can stray at + // this error level. + bin_min = 0; + bin_mid = bin_max; + while (bin_min < bin_mid) { + if (match_bitapScore(d, loc + bin_mid, loc, pattern) <= score_threshold) { + bin_min = bin_mid; + } else { + bin_max = bin_mid; + } + bin_mid = (bin_max - bin_min) / 2 + bin_min; + } + // Use the result from this iteration as the maximum for the next. + bin_max = bin_mid; + int start = Math.max(1, loc - bin_mid + 1); + int finish = Math.min(loc + bin_mid, text.length()) + pattern.length(); + + int[] rd = new int[finish + 2]; + rd[finish + 1] = (1 << d) - 1; + for (int j = finish; j >= start; j--) { + int charMatch; + if (text.length() <= j - 1 || !s.containsKey(text.charAt(j - 1))) { + // Out of range. + charMatch = 0; + } else { + charMatch = s.get(text.charAt(j - 1)); + } + if (d == 0) { + // First pass: exact match. + rd[j] = ((rd[j + 1] << 1) | 1) & charMatch; + } else { + // Subsequent passes: fuzzy match. + rd[j] = (((rd[j + 1] << 1) | 1) & charMatch) + | (((last_rd[j + 1] | last_rd[j]) << 1) | 1) | last_rd[j + 1]; + } + if ((rd[j] & matchmask) != 0) { + double score = match_bitapScore(d, j - 1, loc, pattern); + // This match will almost certainly be better than any existing + // match. But check anyway. + if (score <= score_threshold) { + // Told you so. + score_threshold = score; + best_loc = j - 1; + if (best_loc > loc) { + // When passing loc, don't exceed our current distance from loc. + start = Math.max(1, 2 * loc - best_loc); + } else { + // Already passed loc, downhill from here on in. + break; + } + } + } + } + if (match_bitapScore(d + 1, loc, loc, pattern) > score_threshold) { + // No hope for a (better) match at greater error levels. + break; + } + last_rd = rd; + } + return best_loc; + } + + /** + * Compute and return the score for a match with e errors and x location. + * + * @param e Number of errors in match. + * @param x Location of match. + * @param loc Expected location of match. + * @param pattern Pattern being sought. + * @return Overall score for match (0.0 = good, 1.0 = bad). + */ + private double match_bitapScore(int e, int x, int loc, String pattern) { + float accuracy = (float) e / pattern.length(); + int proximity = Math.abs(loc - x); + if (Match_Distance == 0) { + // Dodge divide by zero error. + return proximity == 0 ? accuracy : 1.0; + } + return accuracy + (proximity / (float) Match_Distance); + } + + /** + * Initialise the alphabet for the Bitap algorithm. + * + * @param pattern The text to encode. + * @return Hash of character locations. + */ + protected Map match_alphabet(String pattern) { + Map s = new LinkedHashMap<>(); + char[] char_pattern = pattern.toCharArray(); + for (char c : char_pattern) { + s.put(c, 0); + } + int i = 0; + for (char c : char_pattern) { + s.put(c, s.get(c) | (1 << (pattern.length() - i - 1))); + i++; + } + return s; + } + + + // PATCH FUNCTIONS + + + /** + * Increase the context until it is unique, but don't let the pattern expand beyond + * Match_MaxBits. + * + * @param patch The patch to grow. + * @param text Source text. + */ + protected void patch_addContext(Patch patch, String text) { + if (text.length() == 0) { + return; + } + String pattern = text.substring(patch.start2, patch.start2 + patch.length1); + int padding = 0; + + // Look for the first and last matches of pattern in text. If two different + // matches are found, increase the pattern length. + while (text.indexOf(pattern) != text.lastIndexOf(pattern) + && pattern.length() < Match_MaxBits - Patch_Margin - Patch_Margin) { + padding += Patch_Margin; + pattern = text.substring(Math.max(0, patch.start2 - padding), + Math.min(text.length(), patch.start2 + patch.length1 + padding)); + } + // Add one chunk for good luck. + padding += Patch_Margin; + + // Add the prefix. + String prefix = text.substring(Math.max(0, patch.start2 - padding), patch.start2); + if (prefix.length() != 0) { + patch.diffs.addFirst(new Diff(Operation.EQUAL, prefix)); + } + // Add the suffix. + String suffix = text.substring(patch.start2 + patch.length1, + Math.min(text.length(), patch.start2 + patch.length1 + padding)); + if (suffix.length() != 0) { + patch.diffs.addLast(new Diff(Operation.EQUAL, suffix)); + } + + // Roll back the start points. + patch.start1 -= prefix.length(); + patch.start2 -= prefix.length(); + // Extend the lengths. + patch.length1 += prefix.length() + suffix.length(); + patch.length2 += prefix.length() + suffix.length(); + } + + /** + * Compute a list of patches to turn text1 into text2. A set of diffs will be computed. + * + * @param text1 Old text. + * @param text2 New text. + * @return LinkedList of Patch objects. + */ + public LinkedList patch_make(String text1, String text2) { + if (text1 == null || text2 == null) { + throw new IllegalArgumentException("Null inputs. (patch_make)"); + } + // No diffs provided, compute our own. + LinkedList diffs = diff_main(text1, text2, true); + if (diffs.size() > 2) { + diff_cleanupSemantic(diffs); + diff_cleanupEfficiency(diffs); + } + return patch_make(text1, diffs); + } + + /** + * Compute a list of patches to turn text1 into text2. text1 will be derived from the provided + * diffs. + * + * @param diffs Array of Diff objects for text1 to text2. + * @return LinkedList of Patch objects. + */ + public LinkedList patch_make(LinkedList diffs) { + if (diffs == null) { + throw new IllegalArgumentException("Null inputs. (patch_make)"); + } + // No origin string provided, compute our own. + String text1 = diff_text1(diffs); + return patch_make(text1, diffs); + } + + /** + * Compute a list of patches to turn text1 into text2. text2 is ignored, diffs are the delta + * between text1 and text2. + * + * @param text1 Old text + * @param text2 Ignored. + * @param diffs Array of Diff objects for text1 to text2. + * @return LinkedList of Patch objects. + * @deprecated Prefer patch_make(String text1, LinkedList diffs). + */ + @Deprecated + public LinkedList patch_make(String text1, String text2, LinkedList diffs) { + return patch_make(text1, diffs); + } + + /** + * Compute a list of patches to turn text1 into text2. text2 is not provided, diffs are the + * delta between text1 and text2. + * + * @param text1 Old text. + * @param diffs Array of Diff objects for text1 to text2. + * @return LinkedList of Patch objects. + */ + public LinkedList patch_make(String text1, LinkedList diffs) { + if (text1 == null || diffs == null) { + throw new IllegalArgumentException("Null inputs. (patch_make)"); + } + + LinkedList patches = new LinkedList<>(); + if (diffs.isEmpty()) { + return patches; // Get rid of the null case. + } + Patch patch = new Patch(); + int char_count1 = 0; // Number of characters into the text1 string. + int char_count2 = 0; // Number of characters into the text2 string. + // Start with text1 (prepatch_text) and apply the diffs until we arrive at + // text2 (postpatch_text). We recreate the patches one by one to determine + // context info. + String prepatch_text = text1; + String postpatch_text = text1; + for (Diff aDiff : diffs) { + if (patch.diffs.isEmpty() && aDiff.operation != Operation.EQUAL) { + // A new patch starts here. + patch.start1 = char_count1; + patch.start2 = char_count2; + } + + switch (aDiff.operation) { + case INSERT: + patch.diffs.add(aDiff); + patch.length2 += aDiff.text.length(); + postpatch_text = postpatch_text.substring(0, char_count2) + aDiff.text + + postpatch_text.substring(char_count2); + break; + case DELETE: + patch.length1 += aDiff.text.length(); + patch.diffs.add(aDiff); + postpatch_text = postpatch_text.substring(0, char_count2) + + postpatch_text.substring(char_count2 + aDiff.text.length()); + break; + case EQUAL: + if (aDiff.text.length() <= 2 * Patch_Margin && !patch.diffs.isEmpty() + && aDiff != diffs.getLast()) { + // Small equality inside a patch. + patch.diffs.add(aDiff); + patch.length1 += aDiff.text.length(); + patch.length2 += aDiff.text.length(); + } + + if (aDiff.text.length() >= 2 * Patch_Margin) { + // Time for a new patch. + if (!patch.diffs.isEmpty()) { + patch_addContext(patch, prepatch_text); + patches.add(patch); + patch = new Patch(); + // Unlike Unidiff, our patch lists have a rolling context. + // http://code.google.com/p/google-diff-match-patch/wiki/Unidiff + // Update prepatch text & pos to reflect the application of the + // just completed patch. + prepatch_text = postpatch_text; + char_count1 = char_count2; + } + } + break; + } + + // Update the current character count. + if (aDiff.operation != Operation.INSERT) { + char_count1 += aDiff.text.length(); + } + if (aDiff.operation != Operation.DELETE) { + char_count2 += aDiff.text.length(); + } + } + // Pick up the leftover patch if not empty. + if (!patch.diffs.isEmpty()) { + patch_addContext(patch, prepatch_text); + patches.add(patch); + } + + return patches; + } + + /** + * Given an array of patches, return another array that is identical. + * + * @param patches Array of Patch objects. + * @return Array of Patch objects. + */ + public LinkedList patch_deepCopy(LinkedList patches) { + LinkedList patchesCopy = new LinkedList<>(); + for (Patch aPatch : patches) { + Patch patchCopy = new Patch(); + for (Diff aDiff : aPatch.diffs) { + Diff diffCopy = new Diff(aDiff.operation, aDiff.text); + patchCopy.diffs.add(diffCopy); + } + patchCopy.start1 = aPatch.start1; + patchCopy.start2 = aPatch.start2; + patchCopy.length1 = aPatch.length1; + patchCopy.length2 = aPatch.length2; + patchesCopy.add(patchCopy); + } + return patchesCopy; + } + + /** + * Merge a set of patches onto the text. Return a patched text, as well as an array of + * true/false values indicating which patches were applied. + * + * @param patches Array of Patch objects + * @param text Old text. + * @return Two element Object array, containing the new text and an array of boolean values. + */ + public Object[] patch_apply(LinkedList patches, String text) { + if (patches.isEmpty()) { + return new Object[] { text, new boolean[0] }; + } + + // Deep copy the patches so that no changes are made to originals. + patches = patch_deepCopy(patches); + + String nullPadding = patch_addPadding(patches); + text = nullPadding + text + nullPadding; + patch_splitMax(patches); + + int x = 0; + // delta keeps track of the offset between the expected and actual location + // of the previous patch. If there are patches expected at positions 10 and + // 20, but the first patch was found at 12, delta is 2 and the second patch + // has an effective expected position of 22. + int delta = 0; + boolean[] results = new boolean[patches.size()]; + for (Patch aPatch : patches) { + int expected_loc = aPatch.start2 + delta; + String text1 = diff_text1(aPatch.diffs); + int start_loc; + int end_loc = -1; + if (text1.length() > this.Match_MaxBits) { + // patch_splitMax will only provide an oversized pattern in the case of + // a monster delete. + start_loc = match_main(text, text1.substring(0, this.Match_MaxBits), expected_loc); + if (start_loc != -1) { + end_loc = match_main(text, text1.substring(text1.length() - this.Match_MaxBits), + expected_loc + text1.length() - this.Match_MaxBits); + if (end_loc == -1 || start_loc >= end_loc) { + // Can't find valid trailing context. Drop this patch. + start_loc = -1; + } + } + } else { + start_loc = match_main(text, text1, expected_loc); + } + if (start_loc == -1) { + // No match found. :( + results[x] = false; + // Subtract the delta for this failed patch from subsequent patches. + delta -= aPatch.length2 - aPatch.length1; + } else { + // Found a match. :) + results[x] = true; + delta = start_loc - expected_loc; + String text2; + if (end_loc == -1) { + text2 = text.substring(start_loc, + Math.min(start_loc + text1.length(), text.length())); + } else { + text2 = text.substring(start_loc, + Math.min(end_loc + this.Match_MaxBits, text.length())); + } + if (text1.equals(text2)) { + // Perfect match, just shove the replacement text in. + text = text.substring(0, start_loc) + diff_text2(aPatch.diffs) + + text.substring(start_loc + text1.length()); + } else { + // Imperfect match. Run a diff to get a framework of equivalent + // indices. + LinkedList diffs = diff_main(text1, text2, false); + if (text1.length() > this.Match_MaxBits && diff_levenshtein(diffs) + / (float) text1.length() > this.Patch_DeleteThreshold) { + // The end points match, but the content is unacceptably bad. + results[x] = false; + } else { + diff_cleanupSemanticLossless(diffs); + int index1 = 0; + for (Diff aDiff : aPatch.diffs) { + if (aDiff.operation != Operation.EQUAL) { + int index2 = diff_xIndex(diffs, index1); + if (aDiff.operation == Operation.INSERT) { + // Insertion + text = text.substring(0, start_loc + index2) + aDiff.text + + text.substring(start_loc + index2); + } else if (aDiff.operation == Operation.DELETE) { + // Deletion + text = text.substring(0, start_loc + index2) + text.substring( + start_loc + + diff_xIndex(diffs, index1 + aDiff.text.length())); + } + } + if (aDiff.operation != Operation.DELETE) { + index1 += aDiff.text.length(); + } + } + } + } + } + x++; + } + // Strip the padding off. + text = text.substring(nullPadding.length(), text.length() - nullPadding.length()); + return new Object[] { text, results }; + } + + /** + * Add some padding on text start and end so that edges can match something. Intended to be + * called only from within patch_apply. + * + * @param patches Array of Patch objects. + * @return The padding string added to each side. + */ + public String patch_addPadding(LinkedList patches) { + short paddingLength = this.Patch_Margin; + StringBuilder nullPadding = new StringBuilder(); + for (short x = 1; x <= paddingLength; x++) { + nullPadding.append((char) x); + } + + // Bump all the patches forward. + for (Patch aPatch : patches) { + aPatch.start1 += paddingLength; + aPatch.start2 += paddingLength; + } + + // Add some padding on start of first diff. + Patch patch = patches.getFirst(); + LinkedList diffs = patch.diffs; + if (diffs.isEmpty() || diffs.getFirst().operation != Operation.EQUAL) { + // Add nullPadding equality. + diffs.addFirst(new Diff(Operation.EQUAL, nullPadding.toString())); + patch.start1 -= paddingLength; // Should be 0. + patch.start2 -= paddingLength; // Should be 0. + patch.length1 += paddingLength; + patch.length2 += paddingLength; + } else if (paddingLength > diffs.getFirst().text.length()) { + // Grow first equality. + Diff firstDiff = diffs.getFirst(); + int extraLength = paddingLength - firstDiff.text.length(); + firstDiff.text = nullPadding.substring(firstDiff.text.length()) + firstDiff.text; + patch.start1 -= extraLength; + patch.start2 -= extraLength; + patch.length1 += extraLength; + patch.length2 += extraLength; + } + + // Add some padding on end of last diff. + patch = patches.getLast(); + diffs = patch.diffs; + if (diffs.isEmpty() || diffs.getLast().operation != Operation.EQUAL) { + // Add nullPadding equality. + diffs.addLast(new Diff(Operation.EQUAL, nullPadding.toString())); + patch.length1 += paddingLength; + patch.length2 += paddingLength; + } else if (paddingLength > diffs.getLast().text.length()) { + // Grow last equality. + Diff lastDiff = diffs.getLast(); + int extraLength = paddingLength - lastDiff.text.length(); + lastDiff.text += nullPadding.substring(0, extraLength); + patch.length1 += extraLength; + patch.length2 += extraLength; + } + + return nullPadding.toString(); + } + + /** + * Look through the patches and break up any which are longer than the maximum limit of the + * match algorithm. Intended to be called only from within patch_apply. + * + * @param patches LinkedList of Patch objects. + */ + public void patch_splitMax(LinkedList patches) { + short patch_size = Match_MaxBits; + String precontext, postcontext; + Patch patch; + int start1, start2; + boolean empty; + Operation diff_type; + String diff_text; + ListIterator pointer = patches.listIterator(); + Patch bigpatch = pointer.hasNext() ? pointer.next() : null; + while (bigpatch != null) { + if (bigpatch.length1 <= Match_MaxBits) { + bigpatch = pointer.hasNext() ? pointer.next() : null; + continue; + } + // Remove the big old patch. + pointer.remove(); + start1 = bigpatch.start1; + start2 = bigpatch.start2; + precontext = ""; + while (!bigpatch.diffs.isEmpty()) { + // Create one of several smaller patches. + patch = new Patch(); + empty = true; + patch.start1 = start1 - precontext.length(); + patch.start2 = start2 - precontext.length(); + if (precontext.length() != 0) { + patch.length1 = patch.length2 = precontext.length(); + patch.diffs.add(new Diff(Operation.EQUAL, precontext)); + } + while (!bigpatch.diffs.isEmpty() && patch.length1 < patch_size - Patch_Margin) { + diff_type = bigpatch.diffs.getFirst().operation; + diff_text = bigpatch.diffs.getFirst().text; + if (diff_type == Operation.INSERT) { + // Insertions are harmless. + patch.length2 += diff_text.length(); + start2 += diff_text.length(); + patch.diffs.addLast(bigpatch.diffs.removeFirst()); + empty = false; + } else if (diff_type == Operation.DELETE && patch.diffs.size() == 1 + && patch.diffs.getFirst().operation == Operation.EQUAL + && diff_text.length() > 2 * patch_size) { + // This is a large deletion. Let it pass in one chunk. + patch.length1 += diff_text.length(); + start1 += diff_text.length(); + empty = false; + patch.diffs.add(new Diff(diff_type, diff_text)); + bigpatch.diffs.removeFirst(); + } else { + // Deletion or equality. Only take as much as we can stomach. + diff_text = diff_text.substring(0, Math.min(diff_text.length(), + patch_size - patch.length1 - Patch_Margin)); + patch.length1 += diff_text.length(); + start1 += diff_text.length(); + if (diff_type == Operation.EQUAL) { + patch.length2 += diff_text.length(); + start2 += diff_text.length(); + } else { + empty = false; + } + patch.diffs.add(new Diff(diff_type, diff_text)); + if (diff_text.equals(bigpatch.diffs.getFirst().text)) { + bigpatch.diffs.removeFirst(); + } else { + bigpatch.diffs.getFirst().text = + bigpatch.diffs.getFirst().text.substring(diff_text.length()); + } + } + } + // Compute the head context for the next patch. + precontext = diff_text2(patch.diffs); + precontext = precontext.substring(Math.max(0, precontext.length() - Patch_Margin)); + // Append the end context for this patch. + if (diff_text1(bigpatch.diffs).length() > Patch_Margin) { + postcontext = diff_text1(bigpatch.diffs).substring(0, Patch_Margin); + } else { + postcontext = diff_text1(bigpatch.diffs); + } + if (postcontext.length() != 0) { + patch.length1 += postcontext.length(); + patch.length2 += postcontext.length(); + if (!patch.diffs.isEmpty() + && patch.diffs.getLast().operation == Operation.EQUAL) { + patch.diffs.getLast().text += postcontext; + } else { + patch.diffs.add(new Diff(Operation.EQUAL, postcontext)); + } + } + if (!empty) { + pointer.add(patch); + } + } + bigpatch = pointer.hasNext() ? pointer.next() : null; + } + } + + /** + * Take a list of patches and return a textual representation. + * + * @param patches List of Patch objects. + * @return Text representation of patches. + */ + public String patch_toText(List patches) { + StringBuilder text = new StringBuilder(); + for (Patch aPatch : patches) { + text.append(aPatch); + } + return text.toString(); + } + + /** + * Parse a textual representation of patches and return a List of Patch objects. + * + * @param textline Text representation of patches. + * @return List of Patch objects. + * @throws IllegalArgumentException If invalid input. + */ + public List patch_fromText(String textline) throws IllegalArgumentException { + List patches = new LinkedList<>(); + if (textline.length() == 0) { + return patches; + } + List textList = Arrays.asList(textline.split("\n")); + LinkedList text = new LinkedList<>(textList); + Patch patch; + Pattern patchHeader = Pattern.compile("^@@ -(\\d+),?(\\d*) \\+(\\d+),?(\\d*) @@$"); + Matcher m; + char sign; + String line; + while (!text.isEmpty()) { + m = patchHeader.matcher(text.getFirst()); + if (!m.matches()) { + throw new IllegalArgumentException("Invalid patch string: " + text.getFirst()); + } + patch = new Patch(); + patches.add(patch); + patch.start1 = Integer.parseInt(m.group(1)); + if (m.group(2).length() == 0) { + patch.start1--; + patch.length1 = 1; + } else if (m.group(2).equals("0")) { + patch.length1 = 0; + } else { + patch.start1--; + patch.length1 = Integer.parseInt(m.group(2)); + } + + patch.start2 = Integer.parseInt(m.group(3)); + if (m.group(4).length() == 0) { + patch.start2--; + patch.length2 = 1; + } else if (m.group(4).equals("0")) { + patch.length2 = 0; + } else { + patch.start2--; + patch.length2 = Integer.parseInt(m.group(4)); + } + text.removeFirst(); + + while (!text.isEmpty()) { + try { + sign = text.getFirst().charAt(0); + } catch (IndexOutOfBoundsException e) { + // Blank line? Whatever. + text.removeFirst(); + continue; + } + line = text.getFirst().substring(1); + line = line.replace("+", "%2B"); // decode would change all "+" to " " + try { + line = URLDecoder.decode(line, StandardCharsets.UTF_8); + } catch (IllegalArgumentException e) { + // Malformed URI sequence. + throw new IllegalArgumentException("Illegal escape in patch_fromText: " + line, + e); + } + if (sign == '-') { + // Deletion. + patch.diffs.add(new Diff(Operation.DELETE, line)); + } else if (sign == '+') { + // Insertion. + patch.diffs.add(new Diff(Operation.INSERT, line)); + } else if (sign == ' ') { + // Minor equality. + patch.diffs.add(new Diff(Operation.EQUAL, line)); + } else if (sign == '@') { + // Start of next patch. + break; + } else { + // WTF? + throw new IllegalArgumentException( + "Invalid patch mode '" + sign + "' in: " + line); + } + text.removeFirst(); + } + } + return patches; + } + + + /** + * Class representing one diff operation. + */ + public static class Diff { + /** + * One of: INSERT, DELETE or EQUAL. + */ + public Operation operation; + /** + * The text associated with this diff operation. + */ + public String text; + + /** + * Constructor. Initializes the diff with the provided values. + * + * @param operation One of INSERT, DELETE or EQUAL. + * @param text The text being applied. + */ + public Diff(Operation operation, String text) { + // Construct a diff with the specified operation and text. + this.operation = operation; + this.text = text; + } + + /** + * Display a human-readable version of this Diff. + * + * @return text version. + */ + public String toString() { + String prettyText = this.text.replace('\n', '\u00b6'); + return "Diff(" + this.operation + ",\"" + prettyText + "\")"; + } + + /** + * Create a numeric hash value for a Diff. This function is not used by DMP. + * + * @return Hash value. + */ + @Override + public int hashCode() { + final int prime = 31; + int result = (operation == null) ? 0 : operation.hashCode(); + result += prime * ((text == null) ? 0 : text.hashCode()); + return result; + } + + /** + * Is this Diff equivalent to another Diff? + * + * @param obj Another Diff to compare against. + * @return true or false. + */ + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + Diff other = (Diff) obj; + if (operation != other.operation) { + return false; + } + if (text == null) { + return other.text == null; + } else { + return text.equals(other.text); + } + } + } + + + /** + * Class representing one patch operation. + */ + public static class Patch { + public final LinkedList diffs; + public int start1; + public int start2; + public int length1; + public int length2; + + /** + * Constructor. Initializes with an empty list of diffs. + */ + public Patch() { + this.diffs = new LinkedList<>(); + } + + /** + * Emmulate GNU diff's format. Header: @@ -382,8 +481,9 @@ Indicies are printed as 1-based, + * not 0-based. + * + * @return The GNU diff string. + */ + public String toString() { + String coords1, coords2; + if (this.length1 == 0) { + coords1 = this.start1 + ",0"; + } else if (this.length1 == 1) { + coords1 = Integer.toString(this.start1 + 1); + } else { + coords1 = (this.start1 + 1) + "," + this.length1; + } + if (this.length2 == 0) { + coords2 = this.start2 + ",0"; + } else if (this.length2 == 1) { + coords2 = Integer.toString(this.start2 + 1); + } else { + coords2 = (this.start2 + 1) + "," + this.length2; + } + StringBuilder text = new StringBuilder(); + text.append("@@ -").append(coords1).append(" +").append(coords2).append(" @@\n"); + // Escape the body of the patch with %xx notation. + for (Diff aDiff : this.diffs) { + switch (aDiff.operation) { + case INSERT: + text.append('+'); + break; + case DELETE: + text.append('-'); + break; + case EQUAL: + text.append(' '); + break; + } + text.append(URLEncoder.encode(aDiff.text, StandardCharsets.UTF_8).replace('+', ' ')) + .append("\n"); + } + return unescapeForEncodeUriCompatability(text.toString()); + } + } + + /** + * Unescape selected chars for compatability with JavaScript's encodeURI. In speed critical + * applications this could be dropped since the receiving application will certainly decode + * these fine. Note that this function is case-sensitive. Thus "%3f" would not be unescaped. But + * this is ok because it is only called with the output of URLEncoder.encode which returns + * uppercase hex. + * + * Example: "%3F" -> "?", "%24" -> "$", etc. + * + * @param str The string to escape. + * @return The escaped string. + */ + private static String unescapeForEncodeUriCompatability(String str) { + return str.replace("%21", "!").replace("%7E", "~").replace("%27", "'").replace("%28", "(") + .replace("%29", ")").replace("%3B", ";").replace("%2F", "/").replace("%3F", "?") + .replace("%3A", ":").replace("%40", "@").replace("%26", "&").replace("%3D", "=") + .replace("%2B", "+").replace("%24", "$").replace("%2C", ",").replace("%23", "#"); + } +} diff --git a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css index 1f0ec45d726..b423b740666 100644 --- a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css +++ b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css @@ -442,3 +442,28 @@ .source-jml-keyword { -fx-font-weight: bold; } + +/* ------------------------------------------------------------------ + * proof diff frame (M5, Swing ProofDiffFrame) + * The diff area is a StyleClassedTextArea; deleted/inserted text carries + * the Swing span background colors (#ff8080 / #80ff80) as RichTextFX + * -rtfx-background-color on the styled text segments. + * ------------------------------------------------------------------ */ +.proof-diff-area { + -fx-background-color: -key-surface; +} + +.diff-delete { + -rtfx-background-color: #ff8080; + -fx-fill: -key-text; +} + +.diff-insert { + -rtfx-background-color: #80ff80; + -fx-fill: -key-text; +} + +.diff-help-heading { + -fx-font-weight: bold; + -fx-underline: true; +} diff --git a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css index 6a890f7128b..73de5f96a85 100644 --- a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css +++ b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css @@ -438,3 +438,28 @@ .source-jml-keyword { -fx-font-weight: bold; } + +/* ------------------------------------------------------------------ + * proof diff frame (M5, Swing ProofDiffFrame) + * The diff area is a StyleClassedTextArea; deleted/inserted text carries + * the Swing span background colors (#ff8080 / #80ff80) as RichTextFX + * -rtfx-background-color on the styled text segments. + * ------------------------------------------------------------------ */ +.proof-diff-area { + -fx-background-color: -key-surface; +} + +.diff-delete { + -rtfx-background-color: #ff8080; + -fx-fill: -key-text; +} + +.diff-insert { + -rtfx-background-color: #80ff80; + -fx-fill: -key-text; +} + +.diff-help-heading { + -fx-font-weight: bold; + -fx-underline: true; +} From 948c83b2e60c10d58fa9b559b8a60e3194771796 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Thu, 8 Oct 2026 08:48:12 +0200 Subject: [PATCH 037/281] key.ui.fx: M4 settings dialog: shared child providers, dialog theming, duplicate refresh Finishes the three pending fixes of the settings chunk (they were left uncommitted after the last verification run): - StandardUISettingsF keeps its child providers (colors, shortcuts) in one final list: the tree, initializeProviders and the apply process now share the same panel instances; per-call creation would apply fresh, unpopulated panels instead of the edited ones. - SettingsManagerF tracks the dialog scene in the ThemeManager, so the dialog is themed at open and follows theme switches made by its own appearance panel (observed stuck light dialog in the dark run before this fix). - ShortcutSettingsF refreshes the table when a shortcut spec changes, so both rows of a duplicate pair wear the error marking (the marking is evaluated per cell in updateItem). - SettingsManagerF labels the invisible tree root 'KeY Settings' like the Swing SettingsUi (it rendered as an empty row before). --- .../key/gui/fx/keyshortcuts/ShortcutSettingsF.java | 3 +++ .../ilkd/key/gui/fx/settings/SettingsManagerF.java | 11 ++++++++++- .../ilkd/key/gui/fx/settings/StandardUISettingsF.java | 8 +++++++- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/keyshortcuts/ShortcutSettingsF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/keyshortcuts/ShortcutSettingsF.java index 17aa3a0faf7..726eacf3bb3 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/keyshortcuts/ShortcutSettingsF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/keyshortcuts/ShortcutSettingsF.java @@ -102,6 +102,9 @@ private void refresh() { List rows = entries.entrySet().stream().map(e -> new ShortcutRow(e.getKey(), e.getValue())) .toList(); + // re-render all cells when a spec changes, so both rows of a duplicate pair get the + // error marking (the marking is computed per cell in updateItem) + rows.forEach(row -> row.spec.addListener((obs, old, value) -> tblShortcuts.refresh())); tblShortcuts.setItems(FXCollections.observableList(rows)); tblShortcuts.sort(); } diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/SettingsManagerF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/SettingsManagerF.java index a45e6a1e3e9..33d5d309df3 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/SettingsManagerF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/SettingsManagerF.java @@ -31,6 +31,7 @@ import javafx.stage.Stage; import de.uka.ilkd.key.gui.fx.MainWindowF; +import de.uka.ilkd.key.gui.fx.theme.ThemeManager; import de.uka.ilkd.key.proof.Proof; import de.uka.ilkd.key.settings.ChoiceSettings; import de.uka.ilkd.key.settings.ProofSettings; @@ -180,6 +181,9 @@ private Stage createDialog(MainWindowF mainWindow) { root.setPadding(new Insets(8)); root.setBottom(createButtonBar(mainWindow, providers, stage)); stage.setScene(new Scene(root, 900, 600)); + // track the dialog scene so it is styled with the current theme and follows theme + // switches applied by its own appearance panel while it is open + ThemeManager.getInstance().manage(stage.getScene()); stage.setUserData(ui); stage.getScene().setOnKeyPressed(e -> { @@ -333,10 +337,15 @@ private TreeCell createCell( protected void updateItem(SettingsProviderF item, boolean empty) { super.updateItem(item, empty); getStyleClass().remove("settings-tree-match"); - if (empty || item == null) { + if (empty) { setText(null); return; } + if (item == null) { + // the invisible root carries no provider; Swing labels it "KeY Settings" + setText("KeY Settings"); + return; + } setText(item.getDescription()); String search = txtSearch.getText(); if (!search.isEmpty() && item.contains(search)) { diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/StandardUISettingsF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/StandardUISettingsF.java index 45b085baab7..d8d41a6cd9f 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/StandardUISettingsF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/StandardUISettingsF.java @@ -66,6 +66,12 @@ Maximum size (line count) of the tooltips of applicable rules """; private final ToggleGroup themeGroup = new ToggleGroup(); + /** + * The child providers, created once: the tree, the initialization and the apply process must + * share the same instances (a per-call creation would apply fresh, unpopulated panels). + */ + private final java.util.List childProviders = + java.util.List.of(new ColorSettingsProviderF(), new ShortcutSettingsF()); private RadioButton lightRadio; private RadioButton darkRadio; private Spinner spFontSizeGlobal; @@ -151,7 +157,7 @@ public String getDescription() { @Override public java.util.List getChildProviders() { - return java.util.Arrays.asList(new ColorSettingsProviderF(), new ShortcutSettingsF()); + return childProviders; } @Override From bb8b40c52ce38606dd2d2fb21f107b5f1ef3feb5 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Thu, 8 Oct 2026 09:01:21 +0200 Subject: [PATCH 038/281] key.ui.fx: M4 settings dialog: color overrides follow theme switches --- .../java/de/uka/ilkd/key/gui/fx/colors/ColorSettingsF.java | 3 +++ 1 file changed, 3 insertions(+) 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 index 69a31b8f597..a5fdcc4580d 100644 --- 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 @@ -100,6 +100,9 @@ public final class ColorSettingsF { 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( From 87feb04d493eafe4654add885855a9d6d924a894 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Thu, 8 Oct 2026 09:04:48 +0200 Subject: [PATCH 039/281] key.ui.fx: M4 settings dialog: escape closes dialog from any focus --- .../key/gui/fx/settings/SettingsManagerF.java | 32 ++++++++++++------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/SettingsManagerF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/SettingsManagerF.java index 33d5d309df3..a9757b09934 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/SettingsManagerF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/SettingsManagerF.java @@ -186,9 +186,13 @@ private Stage createDialog(MainWindowF mainWindow) { ThemeManager.getInstance().manage(stage.getScene()); stage.setUserData(ui); - stage.getScene().setOnKeyPressed(e -> { - // Swing registers ESCAPE on the root pane (WHEN_IN_FOCUSED_WINDOW) - if (ui.isEscapeConsumed(e)) { + stage.getScene().addEventFilter(javafx.scene.input.KeyEvent.KEY_PRESSED, e -> { + // Swing registers ESCAPE on the root pane (WHEN_IN_FOCUSED_WINDOW): Escape closes + // the dialog from any focus. The event filter catches the key before controls like + // the provider tree consume it; an open cell editor still wins (its Escape cancels + // the edit), like the WHEN_ANCESTOR bindings of the Swing cell editors. + if (e.getCode() == javafx.scene.input.KeyCode.ESCAPE + && !isCellEditing(stage.getScene().getFocusOwner())) { stage.close(); e.consume(); } @@ -196,6 +200,20 @@ private Stage createDialog(MainWindowF mainWindow) { return stage; } + /** + * @return whether the focus owner or one of its ancestors is an editing cell, so the Escape + * key belongs to the cell editor and must not close the dialog + */ + private static boolean isCellEditing(javafx.scene.Node node) { + while (node != null) { + if (node instanceof javafx.scene.control.IndexedCell cell && cell.isEditing()) { + return true; + } + node = node.getParent(); + } + return false; + } + private javafx.scene.Node createButtonBar(MainWindowF mainWindow, List providers, Stage stage) { ButtonBar bar = new ButtonBar(); @@ -406,13 +424,5 @@ private java.util.Optional> findItem( } return java.util.Optional.empty(); } - - /** - * @return whether the given key event is the dialog's escape key (consumed by the tree - * and the search field otherwise) - */ - boolean isEscapeConsumed(javafx.scene.input.KeyEvent event) { - return !event.isConsumed() && event.getCode() == javafx.scene.input.KeyCode.ESCAPE; - } } } From 0e295bf28ddf28dc0bee1b0779713aa1478cff36 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Thu, 8 Oct 2026 09:18:19 +0200 Subject: [PATCH 040/281] key.ui.fx: M4 file dialogs: example chooser (Swing ExampleChooser/Example port) --- .../uka/ilkd/key/gui/fx/ExampleChooserF.java | 422 ++++++++++++++++++ .../java/de/uka/ilkd/key/gui/fx/ExampleF.java | 193 ++++++++ .../de/uka/ilkd/key/gui/fx/theme/key-dark.css | 9 + .../uka/ilkd/key/gui/fx/theme/key-light.css | 9 + 4 files changed, 633 insertions(+) create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/ExampleChooserF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/ExampleF.java 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/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css index 1f0ec45d726..da4e905c771 100644 --- a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css +++ b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css @@ -442,3 +442,12 @@ .source-jml-keyword { -fx-font-weight: bold; } + +/* ------------------------------------------------------------------ + * M4 file/proof dialogs (ExampleChooserF, ProofSelectionDialogF): + * section headings replace the Swing TitledBorder labels + * ------------------------------------------------------------------ */ +.dialog-section-title { + -fx-font-weight: bold; + -fx-text-fill: -key-text-secondary; +} diff --git a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css index 6a890f7128b..d9b3d110373 100644 --- a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css +++ b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css @@ -438,3 +438,12 @@ .source-jml-keyword { -fx-font-weight: bold; } + +/* ------------------------------------------------------------------ + * M4 file/proof dialogs (ExampleChooserF, ProofSelectionDialogF): + * section headings replace the Swing TitledBorder labels + * ------------------------------------------------------------------ */ +.dialog-section-title { + -fx-font-weight: bold; + -fx-text-fill: -key-text-secondary; +} From 092489d1ddae5fe5ccc5de9e83336449c18bf4c2 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Thu, 8 Oct 2026 09:19:40 +0200 Subject: [PATCH 041/281] key.ui.fx: M4 file dialogs: proof selection dialog (.zproof), bundle loading, .java load warning --- .../de/uka/ilkd/key/gui/fx/MainWindowF.java | 99 ++++++- .../key/gui/fx/ProofSelectionDialogF.java | 252 ++++++++++++++++++ 2 files changed, 340 insertions(+), 11 deletions(-) create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/ProofSelectionDialogF.java 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 index d5e6f93214a..54524d20495 100644 --- 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 @@ -12,6 +12,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Properties; import javafx.application.Platform; import javafx.beans.property.ReadOnlyBooleanWrapper; import javafx.concurrent.Task; @@ -19,6 +20,8 @@ import javafx.geometry.Pos; import javafx.scene.Node; import javafx.scene.Scene; +import javafx.scene.control.Alert; +import javafx.scene.control.CheckBox; import javafx.scene.control.CheckMenuItem; import javafx.scene.control.Label; import javafx.scene.control.Menu; @@ -73,6 +76,7 @@ import de.uka.ilkd.key.proof.Proof; import de.uka.ilkd.key.proof.ProofEvent; import de.uka.ilkd.key.proof.io.ProofSaver; +import de.uka.ilkd.key.proof.io.SingleThreadProblemLoader; import de.uka.ilkd.key.settings.PathConfig; import de.uka.ilkd.key.settings.ProofIndependentSettings; import de.uka.ilkd.key.settings.ViewSettings; @@ -82,6 +86,7 @@ import org.key_project.util.javafx.FxUtil; +import org.jspecify.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -348,12 +353,43 @@ private void startDemoProofLoad() { * @param demo whether this is the demo property load (notification prefix "Demo proof") */ private void startProofLoad(Path location, boolean demo) { - // pure .key problems carry no Java source; the source view then shows the problem file - sourceView.setFallbackSourceFile(location); + startProofLoad(location, demo, null); + } + + /** + * 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) { + // 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 = KeYEnvironment.load(location); + 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. + DefaultUserInterfaceControl ui = new DefaultUserInterfaceControl(); + 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 { + env = KeYEnvironment.load(location); + } if (System.getProperty("key.fx.demo.autoprove") != null) { LOGGER.info("Demo: running auto mode on the loaded proof"); env.getProofControl().startAndWaitForAutoMode(env.getLoadedProof()); @@ -431,7 +467,7 @@ protected KeYEnvironment call() throws Exception { } // ------------------------------------------------------------------ - // file menu actions: open / reload / recent files / save (M3) + // file menu actions: open / reload / recent files / save (M3/M4) // ------------------------------------------------------------------ /** @@ -462,22 +498,63 @@ private void openFileChooser() { /** * Loads the given file and registers it in the recent files list (Swing - * {@code WindowUserInterfaceControl.loadProblem} registers before the load starts). Proof - * bundles require the Swing {@code ProofSelectionDialog} flow and are deferred. + * {@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 */ - private void openProofFile(Path file) { - if (file.toString().endsWith(".zproof")) { - LOGGER.info("Proof bundle requested: {} (deferred)", file); - NotificationManagerF.getInstance() - .notify("Proof bundles (.zproof) are not supported yet.", Kind.WARNING); + public void openProofFile(Path file) { + // 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); recentFiles.add(file.toAbsolutePath().toString(), null, false, null); 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. */ 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"); + } +} From c04889a0c35decd09d450432f6806ac5642af884 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Thu, 8 Oct 2026 09:19:54 +0200 Subject: [PATCH 042/281] key.ui.fx: M4 file dialogs: quick save/load (F5/F6), gzip save option, menu wiring --- .../de/uka/ilkd/key/gui/fx/MainWindowF.java | 166 ++++++++++++++++-- .../ilkd/key/gui/fx/actions/QuickSaveF.java | 82 +++++++++ .../key/gui/fx/recentfiles/RecentFilesF.java | 7 + 3 files changed, 242 insertions(+), 13 deletions(-) create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/actions/QuickSaveF.java 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 index 54524d20495..3179d102c5c 100644 --- 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 @@ -52,6 +52,7 @@ 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.configuration.ConfigF; import de.uka.ilkd.key.gui.fx.docking.DockLayoutStore; import de.uka.ilkd.key.gui.fx.docking.DockLocation; @@ -75,6 +76,8 @@ import de.uka.ilkd.key.gui.fx.theme.ThemeManager; import de.uka.ilkd.key.proof.Proof; import de.uka.ilkd.key.proof.ProofEvent; +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.settings.PathConfig; @@ -342,8 +345,9 @@ private void startDemoProofLoad() { * 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). The optional {@code key.fx.demo.autoprove} - * run happens inside the load task, so only demo loads prove automatically. + * #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 @@ -592,6 +596,12 @@ private void saveProofFile() { 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; @@ -600,9 +610,15 @@ private void saveProofFile() { lastSelectedDir = file.getParentFile().toPath(); } Path target = file.toPath().toAbsolutePath(); - // the Swing chooser offers a "compressed" checkbox (GZipProofSaver); deferred, the plain - // saver is the default - ProofSaver saver = new ProofSaver(proof, target, KeYConstants.INTERNAL_VERSION); + // 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) { @@ -643,6 +659,112 @@ private static String initialSaveFileName(Proof proof, String extension) { 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); + } + + /** + * @return the mediator of the main window (Swing {@code MainWindow.getMediator}) + */ + public KeYMediatorF getMediator() { + return mediator; + } + /** * 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 @@ -932,19 +1054,37 @@ private Menu buildFileMenu() { // Swing SaveFileAction: enableWhenProofLoaded; interaction is locked during auto mode saveFile.disableProperty() .bind(mediator.autoModeRunningProperty().or(proofLoaded.not())); - file.getItems().addAll( + MenuItem openExample = menuItem("Open Example…", "de.uka.ilkd.key.gui.actions.OpenExampleAction", - IconFactoryF.Key.OPEN_KEY_FILE, this::notYetImplemented), + 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()); + file.getItems().addAll( + openExample, openFile, reload, new SeparatorMenuItem(), saveFile, - menuItem("Save Bundle…", "de.uka.ilkd.key.gui.actions.SaveBundleAction", - this::notYetImplemented), - menuItem("Quick Save", "de.uka.ilkd.key.gui.actions.QuickSaveAction", - this::notYetImplemented), - menuItem("Quick Load", "de.uka.ilkd.key.gui.actions.QuickLoadAction", - this::notYetImplemented), + saveBundle, + quickSave, + quickLoad, new SeparatorMenuItem(), recentFilesMenu, new SeparatorMenuItem(), 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/recentfiles/RecentFilesF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/recentfiles/RecentFilesF.java index 2a64384356e..2ea96e4df7c 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/recentfiles/RecentFilesF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/recentfiles/RecentFilesF.java @@ -14,6 +14,7 @@ import java.util.Objects; import java.util.Optional; +import de.uka.ilkd.key.gui.fx.actions.QuickSaveF; import de.uka.ilkd.key.nparser.ParsingFacade; import de.uka.ilkd.key.settings.Configuration; import de.uka.ilkd.key.settings.PathConfig; @@ -129,9 +130,15 @@ public void add(String path) { /** * Adds a new file with the load options of the Swing original (profile, single Java file, * additional profile options). + *

+ * The quick save location is never added (Swing {@code RecentFileMenu.addNewToModelAndView}: + * "do not add quick save location to recent files"). */ public void add(String path, String profile, boolean singleJava, Configuration additionalOption) { + if (QuickSaveF.QUICK_SAVE_PATH.endsWith(path)) { + return; + } Optional existing = entries.stream().filter(it -> path.equals(it.path())) .findFirst(); if (existing.isPresent()) { From b5bdc7950a92e8a1f90d16c79efb02ca9f74f509 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Thu, 8 Oct 2026 09:39:06 +0200 Subject: [PATCH 043/281] key.ui.fx: M2 sequent view leftovers search modes, update-highlight overlays and term tooltips --- .../key/gui/fx/fonticons/IconFactoryF.java | 5 +- .../key/gui/fx/nodeviews/SequentViewF.java | 822 +++++++++++++++++- .../de/uka/ilkd/key/gui/fx/theme/key-dark.css | 27 + .../uka/ilkd/key/gui/fx/theme/key-light.css | 25 + 4 files changed, 852 insertions(+), 27 deletions(-) diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/fonticons/IconFactoryF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/fonticons/IconFactoryF.java index f350f405606..11a57b4d303 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/fonticons/IconFactoryF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/fonticons/IconFactoryF.java @@ -64,7 +64,10 @@ public enum Key { PROOF_SEARCH_STRATEGY(FontAwesomeSolid.COG), KEY_HOLE(FontAwesomeSolid.KEY), KEY_HOLE_INTERACTIVE(FontAwesomeSolid.HAND_PAPER), - KEY_HOLE_LINKED(FontAwesomeSolid.LINK); + KEY_HOLE_LINKED(FontAwesomeSolid.LINK), + SEARCH_HIGHLIGHT(FontAwesomeSolid.HIGHLIGHTER), + SEARCH_HIDE(FontAwesomeSolid.LOW_VISION), + SEARCH_REGROUP(FontAwesomeSolid.VIDEO); private final IconFont glyph; diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentViewF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentViewF.java index 36bd12465a3..150f606ecd1 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentViewF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentViewF.java @@ -4,16 +4,22 @@ * SPDX-License-Identifier: GPL-2.0-or-later */ package de.uka.ilkd.key.gui.fx.nodeviews; +import java.util.ArrayDeque; import java.util.ArrayList; +import java.util.Iterator; import java.util.List; import java.util.Objects; import java.util.TreeSet; import java.util.function.Consumer; import java.util.regex.Matcher; import java.util.regex.Pattern; +import javafx.animation.PauseTransition; import javafx.geometry.Insets; import javafx.geometry.Point2D; import javafx.scene.control.Button; +import javafx.scene.control.ComboBox; +import javafx.scene.control.Label; +import javafx.scene.control.ListCell; import javafx.scene.control.ScrollPane; import javafx.scene.control.TextField; import javafx.scene.control.ToggleButton; @@ -25,17 +31,21 @@ 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.shape.HLineTo; import javafx.scene.shape.LineTo; import javafx.scene.shape.MoveTo; +import javafx.scene.shape.Path; import javafx.scene.shape.PathElement; import javafx.scene.shape.VLineTo; import javafx.scene.text.Font; import javafx.scene.text.HitInfo; import javafx.scene.text.Text; import javafx.scene.text.TextFlow; +import javafx.util.Duration; import de.uka.ilkd.key.core.fx.KeYSelectionEvent; import de.uka.ilkd.key.core.fx.KeYSelectionListener; @@ -43,6 +53,7 @@ import de.uka.ilkd.key.gui.fx.configuration.ConfigF; import de.uka.ilkd.key.gui.fx.fonticons.IconFactoryF; import de.uka.ilkd.key.logic.label.TermLabel; +import de.uka.ilkd.key.pp.HideSequentPrintFilter; import de.uka.ilkd.key.pp.IdentitySequentPrintFilter; import de.uka.ilkd.key.pp.IllegalRegexException; import de.uka.ilkd.key.pp.InitialPositionTable; @@ -50,13 +61,22 @@ import de.uka.ilkd.key.pp.PosInSequent; import de.uka.ilkd.key.pp.PosTableLayouter; import de.uka.ilkd.key.pp.Range; +import de.uka.ilkd.key.pp.RegroupSequentPrintFilter; import de.uka.ilkd.key.pp.SearchSequentPrintFilter; +import de.uka.ilkd.key.pp.SequentPrintFilter; +import de.uka.ilkd.key.pp.SequentPrintFilterEntry; import de.uka.ilkd.key.pp.SequentViewLogicPrinter; import de.uka.ilkd.key.pp.VisibleTermLabels; import de.uka.ilkd.key.proof.Node; import de.uka.ilkd.key.proof.Proof; +import de.uka.ilkd.key.settings.ProofIndependentSettings; import org.key_project.logic.Name; +import org.key_project.logic.Term; +import org.key_project.prover.sequent.Semisequent; +import org.key_project.prover.sequent.Sequent; +import org.key_project.prover.sequent.SequentFormula; +import org.key_project.util.collection.ImmutableList; import org.key_project.util.javafx.FxUtil; import org.slf4j.Logger; @@ -81,14 +101,30 @@ * {@code SearchSequentPrintFilter.createPattern}: an all-lowercase query matches * case-insensitively, whitespace runs match line breaks) highlights all matches in the rendered * text; Prev/Next cycle the current match (stronger styling) and scroll it into view. Open with - * {@code Ctrl+Shift+F}, close with {@code Escape}. The Swing modes that change the printed text - * (Hide, Regroup — implemented as sequent print filters) are deferred to the print-filter chunk. + * {@code Ctrl+Shift+F}, close with {@code Escape}. The mode combo ports the Swing modes: Highlight + * keeps the sequent unchanged; Hide only prints the formulas matching the query + * ({@code HideSequentPrintFilter}); Regroup arranges the matching formulas around the sequent + * arrow ({@code RegroupSequentPrintFilter}) — both filters are reused from {@code key.core} and + * drive the printer like in Swing, so the click→position mapping follows the filtered printing. + * While a search filter hides formulas, a warning banner shows the Swing + * {@code SequentHideWarningBorder} message. *

- * Deliberately deferred to later chunks: syntax highlighting of the printed terms, hover - * tooltips, update highlighting (highlighting the formulas changed by a rule application), - * sequent hiding, mediator wiring (shared {@code NotationInfo} and term label visibility), and - * translucent rectangle highlights behind the matches (the search currently colors the matched - * text; inline background spans of a {@link TextFlow} cannot split across wrapped lines). + * Update highlights (Swing {@code CurrentGoalView.updateUpdateHighlights}): the ranges of + * the printed update operators, reported by {@link InitialPositionTable#getUpdateRanges()}, are + * painted as translucent rectangles behind the text ({@link TextFlow#getRangeShape} shapes in an + * overlay pane; Swing paints the same ranges with the HTML +1 offset that the FX view does not + * need). The term under the mouse gets the same treatment as the Swing hover highlight + * ({@code DEFAULT_HIGHLIGHT_COLOR}). + *

+ * Tooltip (Swing {@code SequentView.getToolTipText}): hovering a position shows the + * operator class, operator and sort of the term at that position in a {@link Tooltip} shown after + * a short delay; gated by the shared {@code ViewSettings.isShowSequentViewTooltips()} like in + * Swing. + *

+ * Deliberately deferred to later chunks: mediator wiring (shared {@code NotationInfo} and term + * label visibility), the tooltip strings of the GUI extensions ({@code KeYGuiExtensionFacade} + * has no FX counterpart yet), and the search-mode menu items of the Swing proof-tree popup + * ({@code SearchModeChangeAction}, M3 popups). */ public class SequentViewF extends BorderPane { @@ -115,12 +151,36 @@ public boolean contains(Name name) { private final ScrollPane scrollPane = new ScrollPane(); private final TextFlow textFlow = new TextFlow(); + /** + * Translucent rectangles painted behind the printed text (Swing's highlighter layer): the + * update-operator highlight of the current printing. The pane fills the same area as the + * {@link #textFlow} inside the content {@link StackPane}, so the {@link TextFlow} local + * coordinates of {@link TextFlow#getRangeShape} address it directly. + */ + private final Pane updateOverlay = new Pane(); + /** + * Overlay pane for the term under the mouse (Swing {@code DEFAULT_HIGHLIGHT_COLOR} hover + * highlight), above the update highlights, behind the text. + */ + private final Pane hoverOverlay = new Pane(); private final HBox searchBar = new HBox(4); private final TextField searchField = new TextField(); private final ToggleButton regexToggle = new ToggleButton("RegExp"); + /** the search mode combo (Swing {@code SequentViewSearchBar.searchModeBox}). */ + private final ComboBox searchModeBox = new ComboBox<>(); + + /** + * the warning banner shown while a search filter hides formulas (Swing + * {@code SequentHideWarningBorder} message). + */ + private final Label hideWarning = new Label(WARNING_TEXT); - private final IdentitySequentPrintFilter filter = new IdentitySequentPrintFilter(); + /** + * The current sequent print filter (Swing {@code SequentView.filter}): the identity filter by + * default; the search bar's Hide/Regroup modes install the print filters of {@code key.core}. + */ + private SequentPrintFilter filter = new IdentitySequentPrintFilter(); private SequentViewLogicPrinter printer; private Proof proof; @@ -140,6 +200,25 @@ public boolean contains(Name name) { /** the index of the current match in {@link #searchMatches}, {@code -1} if none. */ private int searchResultPos = -1; + // update highlights (Swing CurrentGoalView.updateUpdateHighlights) + /** number of update-highlight rectangles currently displayed. */ + private int updateRectCount; + + // hover highlight + tooltip (Swing SequentViewInputListener.mouseMoved / getToolTipText) + /** the term range under the mouse, {@code null} when the mouse is over empty space. */ + private Range hoveredRange; + /** shows the term info of the hovered position after the Swing-like display delay. */ + private final Tooltip hoverTooltip = new Tooltip(); + /** delay before the tooltip shows (Swing ToolTipManager initial delay ≈ 500 ms). */ + private final PauseTransition hoverTooltipDelay = + new PauseTransition(Duration.millis(600)); + /** last mouse screen position for placing the tooltip below-right of the cursor. */ + private double hoverScreenX; + private double hoverScreenY; + + /** the warning message painted by the Swing {@code SequentHideWarningBorder}. */ + private static final String WARNING_TEXT = "Some formulas have been hidden (by search phrase)"; + private KeYSelectionModel selectionModel; private final KeYSelectionListener selectionListener = new KeYSelectionListener() { @Override @@ -166,12 +245,36 @@ public SequentViewF() { scrollPane.getStyleClass().add("sequent-view"); scrollPane.setFitToWidth(true); scrollPane.setFitToHeight(true); - scrollPane.setContent(textFlow); + // the overlay panes fill the same area as the text flow, so the TextFlow local + // coordinates of the range shapes can be used directly; they sit behind the text like + // the Swing highlighter layer and never receive mouse events + updateOverlay.getStyleClass().add("sequent-overlay"); + updateOverlay.setMouseTransparent(true); + updateOverlay.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE); + hoverOverlay.getStyleClass().add("sequent-overlay"); + hoverOverlay.setMouseTransparent(true); + hoverOverlay.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE); + StackPane content = new StackPane(updateOverlay, hoverOverlay, textFlow); + scrollPane.setContent(content); setCenter(scrollPane); textFlow.getStyleClass().add("sequent-view-flow"); textFlow.setPadding(new Insets(6)); textFlow.setOnMouseClicked(this::handleMouseClick); - printPlaceholder(); + textFlow.setOnMouseMoved(this::handleMouseMove); + textFlow.setOnMouseExited(this::handleMouseExited); + // recompute the overlay rectangles when the layout changes (viewport resize); the prints + // rebuild them directly + textFlow.layoutBoundsProperty() + .addListener((obs, oldBounds, newBounds) -> FxUtil.runLater(this::rebuildOverlays)); + // the tooltip shows when the mouse pauses over a term (Swing ToolTipManager) + hoverTooltipDelay.setOnFinished(event -> showHoverTooltip()); + + // the warning banner replaces the Swing SequentHideWarningBorder painted around the + // enclosing panel; hidden (and unmanaged) unless a search filter hides formulas + hideWarning.getStyleClass().add("sequent-hide-warning"); + hideWarning.setManaged(false); + hideWarning.setVisible(false); + setTop(hideWarning); createSearchBar(); setBottom(searchBar); @@ -181,6 +284,7 @@ public SequentViewF() { // shortcut fires whenever the keyboard focus is anywhere inside this view (the key // events bubble from the focused control up to this pane). setOnKeyPressed(this::handlePaneKeyPressed); + printPlaceholder(); } /** @@ -230,8 +334,19 @@ private void createSearchBar() { runSearch(); }); + searchModeBox.getStyleClass().add("sequent-search-mode"); + searchModeBox.setMaxWidth(Region.USE_PREF_SIZE); + searchModeBox.getItems().addAll(SearchMode.values()); + searchModeBox.setCellFactory(view -> new SearchModeListCell()); + searchModeBox.setButtonCell(new SearchModeListCell()); + searchModeBox.setTooltip(new Tooltip("Determines search behaviour: Hide only shows " + + "sequent formulas that match the search. Regroup arranges the matching formulas " + + "around the sequent arrow. Highlight leaves the sequent unchanged.")); + searchModeBox.getSelectionModel().select(SearchMode.HIGHLIGHT); + searchModeBox.setOnAction(e -> applySearchMode(searchModeBox.getValue())); + searchBar.getChildren().addAll(searchField, prevButton, nextButton, closeButton, - regexToggle); + regexToggle, searchModeBox); searchBar.setOnKeyPressed(e -> { if (KeyCode.ESCAPE.equals(e.getCode())) { hideSearchBar(); @@ -239,6 +354,56 @@ private void createSearchBar() { }); } + /** + * A combo cell with the mode name and icon (Swing {@code SearchMode} items carry icons). + */ + private static final class SearchModeListCell extends ListCell { + @Override + protected void updateItem(SearchMode item, boolean empty) { + super.updateItem(item, empty); + if (empty || item == null) { + setText(null); + setGraphic(null); + } else { + setText(item.getDisplayName()); + setGraphic(IconFactoryF.createIcon(item.getIcon())); + } + } + } + + /** + * The search modes of the sequent search bar (Swing + * {@code SequentViewSearchBar.SearchMode}): Highlight leaves the sequent unchanged, Hide only + * shows the formulas matching the query and Regroup arranges the matching formulas around the + * sequent arrow. Hide and Regroup are print filters reused from {@code key.core}. + */ + public enum SearchMode { + HIGHLIGHT("Highlight", IconFactoryF.Key.SEARCH_HIGHLIGHT), + HIDE("Hide", IconFactoryF.Key.SEARCH_HIDE), + REGROUP("Regroup", IconFactoryF.Key.SEARCH_REGROUP); + + private final String displayName; + private final IconFactoryF.Key icon; + + SearchMode(String name, IconFactoryF.Key icon) { + this.displayName = name; + this.icon = icon; + } + + public String getDisplayName() { + return displayName; + } + + public IconFactoryF.Key getIcon() { + return icon; + } + + @Override + public String toString() { + return displayName; + } + } + private void handlePaneKeyPressed(KeyEvent event) { if (OPEN_SEARCH.match(event)) { event.consume(); @@ -289,6 +454,11 @@ public void display(Node node) { // TODO(M2c): share the NotationInfo and the term label visibility with the mediator printer = SequentViewLogicPrinter.positionPrinter(new NotationInfo(), node.proof().getServices(), NO_VISIBLE_TERM_LABELS); + if (filter instanceof SearchSequentPrintFilter searchFilter) { + // the search filters print single formulas through the view's printer (Swing + // SequentViewSearchBar.search refreshes it the same way) + searchFilter.setLogicPrinter(printer); + } } selectedNode = node; printSequent(); @@ -314,11 +484,20 @@ public void printSequent() { // TODO(M2): compute the line width from the font metrics and the viewport width printer.update(filter, PosTableLayouter.DEFAULT_LINE_WIDTH); printed = printer.result(); + LOGGER.debug("printSequent: chars={} filter={} antec={} succ={}", printed.length(), filter + .getClass().getSimpleName(), + filter.getFilteredAntec() == null ? -1 : filter.getFilteredAntec().size(), + filter.getFilteredSucc() == null ? -1 : filter.getFilteredSucc().size()); syntaxHighlights = syntaxHighlighting ? SequentSyntaxHighlighterF.highlight(printed, selectedNode) : List.of(); syntaxHighlights.sort((a, b) -> Integer.compare(a.start(), b.start())); + // the reprint invalidates the mouse-dependent state (Swing's setText clears the + // highlights, CurrentGoalView re-paints the update highlights afterwards) + clearHover(); rebuildRuns(); + updateHideWarning(); + rebuildOverlays(); } /** @@ -400,6 +579,178 @@ private void handleMouseClick(MouseEvent event) { onPosSelected.accept(pos); } + // ----------------------------------------------------------------------- + // Hover highlight + tooltip (Swing SequentViewInputListener.mouseMoved / + // SequentView.getToolTipText) + // ----------------------------------------------------------------------- + + /** + * Follows the mouse (Swing {@code mouseMoved}): the term under the cursor gets a translucent + * highlight rectangle and the tooltip shows the term info of the hovered position. + */ + private void handleMouseMove(MouseEvent event) { + InitialPositionTable table = getInitialPositionTable(); + if (table == null || printed == null) { + return; + } + Point2D local = textFlow.sceneToLocal(event.getSceneX(), event.getSceneY()); + HitInfo hit = textFlow.getHitInfo(local); + int charIndex = hit == null ? -1 : hit.getCharIndex(); + PosInSequent pos = + charIndex >= 0 && charIndex < printed.length() ? table.getPosInSequent(charIndex, + filter) : null; + Range bounds = pos != null ? pos.getBounds() : null; + Range hover = bounds != null && bounds.length() > 0 ? bounds : null; + if (!Objects.equals(hover, hoveredRange)) { + hoveredRange = hover; + rebuildHoverOverlay(); + } + updateHoverTooltip(event, pos); + } + + /** + * Clears the hover state when the mouse leaves the view (Swing {@code mouseExited} → + * {@code disableHighlights}). + */ + private void handleMouseExited(MouseEvent event) { + clearHover(); + } + + /** Clears the hovered term range and hides the tooltip. */ + private void clearHover() { + if (hoveredRange != null) { + hoveredRange = null; + rebuildHoverOverlay(); + } + hideHoverTooltip(); + } + + /** Stops the display delay and hides the tooltip (its content would be stale). */ + private void hideHoverTooltip() { + hoverTooltipDelay.stop(); + if (hoverTooltip.isShowing()) { + hoverTooltip.hide(); + } + } + + /** + * Shows the tooltip with the term info of the given position (Swing + * {@code SequentView.getToolTipText}): operator class, operator and sort of the term. The + * tooltip appears when the mouse pauses over a term; while it is visible its content updates + * with the position. + */ + private void updateHoverTooltip(MouseEvent event, PosInSequent pos) { + hoverScreenX = event.getScreenX(); + hoverScreenY = event.getScreenY(); + String text = getTooltipText(pos); + if (text.isEmpty()) { + hideHoverTooltip(); + return; + } + hoverTooltip.setText(text); + if (hoverTooltip.isShowing()) { + // the content updates live while the mouse moves over terms + return; + } + hoverTooltipDelay.playFromStart(); + } + + /** Shows the tooltip below-right of the current mouse position. */ + private void showHoverTooltip() { + if (hoveredRange == null || hoverTooltip.getText() == null + || hoverTooltip.getText().isEmpty()) { + return; + } + var window = textFlow.getScene() != null ? textFlow.getScene().getWindow() : null; + if (window == null || !window.isShowing()) { + return; + } + if (!hoverTooltip.isShowing()) { + hoverTooltip.show(textFlow, hoverScreenX + 16, hoverScreenY + 24); + } + } + + /** + * The tooltip text for the given position (Swing {@code SequentView.getToolTipText} without + * the HTML markup and without the GUI extension strings, which have no FX counterpart yet). + */ + private String getTooltipText(PosInSequent pos) { + if (!ProofIndependentSettings.DEFAULT_INSTANCE.getViewSettings() + .isShowSequentViewTooltips()) { + return ""; + } + if (pos == null || pos.isSequent() || pos.getPosInOccurrence() == null) { + return ""; + } + Term term = pos.getPosInOccurrence().subTerm(); + return "Operator: " + term.op().getClass().getSimpleName() + " (" + term.op() + ")\nSort: " + + term.sort(); + } + + // ----------------------------------------------------------------------- + // Overlay rectangles (Swing paints the same ranges with the highlighter behind the text) + // ----------------------------------------------------------------------- + + /** Rebuilds all overlay rectangles behind the text. */ + private void rebuildOverlays() { + rebuildUpdateOverlays(); + rebuildHoverOverlay(); + } + + /** + * Rebuilds the update-highlight rectangles of the current printing (Swing + * {@code CurrentGoalView.updateUpdateHighlights}): one translucent rectangle per update range + * of the position table, shaped with {@link TextFlow#getRangeShape}. Unlike the Swing view no + * +1 offset is applied — the character indexes of the position table match the TextFlow + * character model exactly. + */ + private void rebuildUpdateOverlays() { + updateOverlay.getChildren().clear(); + updateRectCount = 0; + InitialPositionTable table = getInitialPositionTable(); + if (printed == null || table == null) { + return; + } + // make sure the text flow is laid out before asking for glyph shapes + textFlow.applyCss(); + textFlow.layout(); + for (Range range : table.getUpdateRanges()) { + int start = Math.clamp(range.start(), 0, printed.length()); + int end = Math.clamp(range.end(), start, printed.length()); + if (end <= start) { + continue; + } + updateOverlay.getChildren() + .add(rectForRange(start, end, "sequent-update-highlight")); + updateRectCount++; + } + } + + /** Rebuilds the highlight rectangle of the term under the mouse. */ + private void rebuildHoverOverlay() { + hoverOverlay.getChildren().clear(); + if (hoveredRange == null || printed == null) { + return; + } + int start = Math.clamp(hoveredRange.start(), 0, printed.length()); + int end = Math.clamp(hoveredRange.start() + hoveredRange.length(), start, + printed.length()); + if (end > start) { + hoverOverlay.getChildren().add(rectForRange(start, end, "sequent-hover-highlight")); + } + } + + /** + * @return a filled path over the on-screen shape of the given character range (a per-line + * polygon like the Swing highlighter paints) + */ + private Path rectForRange(int start, int end, String styleClass) { + Path rect = new Path(textFlow.getRangeShape(start, end, true)); + rect.getStyleClass().add(styleClass); + rect.setManaged(false); + return rect; + } + // ----------------------------------------------------------------------- // Search (Swing SequentViewSearchBar) // ----------------------------------------------------------------------- @@ -421,11 +772,15 @@ public void showSearchBar() { /** * Hides the search bar and clears the highlights so the plain sequent is shown again (Swing * {@code setVisible(false)}). The field text is kept like in the Swing view and re-applied on - * the next opening. + * the next opening; the mode resets to Highlight, which reinstalls the identity filter. */ public void hideSearchBar() { searchBar.setVisible(false); searchBar.setManaged(false); + // Swing setVisible(false) resets the mode combo, which re-installs the identity filter + if (searchModeBox.getValue() != SearchMode.HIGHLIGHT) { + searchModeBox.getSelectionModel().select(SearchMode.HIGHLIGHT); + } if (!searchMatches.isEmpty() || searchResultPos >= 0) { searchMatches.clear(); searchResultPos = -1; @@ -436,28 +791,102 @@ public void hideSearchBar() { } /** - * Runs the search for the current field text (Swing {@code search()}): highlights all - * matches of the query in the rendered text. An invalid regular expression and a query - * without matches switch the field to the alert styling. + * Switches the sequent print filter to the given search mode (Swing + * {@code SequentViewSearchBar}'s combo listener): Hide and Regroup install the print filters + * of {@code key.core} on the view's printer, Highlight returns to the identity filter. The + * search itself runs afterwards ({@link #runSearch()}, Swing {@code search()}). + */ + private void applySearchMode(SearchMode mode) { + if (mode == null) { + return; + } + switch (mode) { + case HIDE -> setSequentFilter( + printer == null ? new IdentitySequentPrintFilter() + : new HideSequentPrintFilter(printer, regexToggle.isSelected()), + false); + case REGROUP -> setSequentFilter( + printer == null ? new IdentitySequentPrintFilter() + : new RegroupSequentPrintFilter(printer, regexToggle.isSelected()), + false); + case HIGHLIGHT -> setSequentFilter(new IdentitySequentPrintFilter(), false); + } + LOGGER.debug("applySearchMode: {} filter={}", mode, filter.getClass().getSimpleName()); + runSearch(); + } + + /** + * Sets the sequent print filter used for the next printing (Swing + * {@code SequentView.setFilter}): the filter receives the selected sequent immediately, a + * forced update re-prints. + */ + private void setSequentFilter(SequentPrintFilter newFilter, boolean forceUpdate) { + filter = newFilter; + if (selectedNode != null) { + filter.setSequent(selectedNode.sequent()); + } + if (forceUpdate) { + printSequent(); + } + } + + /** + * Does the active print filter hide formulas from the sequent (Swing + * {@code SequentView.isHiding}). + * + * @return {@code true} iff at least one formula is not shown + */ + public boolean isHiding() { + Sequent originalSequent = filter.getOriginalSequent(); + if (originalSequent == null) { + return false; + } + int filteredSize = + (filter.getFilteredAntec() == null ? 0 : filter.getFilteredAntec().size()) + + (filter.getFilteredSucc() == null ? 0 : filter.getFilteredSucc().size()); + return originalSequent.size() != filteredSize; + } + + /** Shows or hides the hide-warning banner (Swing {@code updateHidingProperty}). */ + private void updateHideWarning() { + boolean hiding = isHiding(); + hideWarning.setVisible(hiding); + hideWarning.setManaged(hiding); + } + + /** + * Runs the search for the current field text (Swing {@code search()}): the sequent is always + * re-printed (Swing {@code SequentViewSearchBar.search}: "search always does a repaint") — an + * active search filter (Hide/Regroup mode) re-filters the sequent with the query, the + * identity filter of the Highlight mode re-prints the plain sequent so that formulas hidden + * by a previous mode reappear. Then all matches of the query are highlighted in the rendered + * text. An invalid regular expression and a query without matches switch the field to the + * alert styling. */ private void runSearch() { - String text = searchField.getText(); searchMatches.clear(); searchResultPos = -1; + String text = searchField.getText(); + if (filter instanceof SearchSequentPrintFilter searchFilter && printer != null + && selectedNode != null) { + searchFilter.setRegex(regexToggle.isSelected()); + searchFilter.setLogicPrinter(printer); + // an invalid regular expression leaves the filter unchanged: the Swing bar + // re-filters with the broken pattern and then crashes printing it (latent Swing + // bug); here the previous filter state stays and the field alerts below + if (createPattern(text) != null) { + searchFilter.setSearchString(text); + } + } + if (printer != null && selectedNode != null) { + printSequent(); + } if (text.isEmpty()) { setAlert(false); rebuildRuns(); return; } - Pattern pattern; - try { - pattern = SearchSequentPrintFilter.createPattern(text, regexToggle.isSelected()); - } catch (IllegalRegexException e) { - LOGGER.debug("runSearch: text={} invalid regex", text); - setAlert(true); - rebuildRuns(); - return; - } + Pattern pattern = createPattern(text); if (pattern == null || printed == null) { setAlert(true); rebuildRuns(); @@ -473,6 +902,20 @@ private void runSearch() { setAlert(searchMatches.isEmpty()); } + /** + * Creates the search pattern for the current field text (Swing + * {@code SearchSequentPrintFilter.createPattern}), {@code null} if it is not a valid regular + * expression. + */ + private Pattern createPattern(String text) { + try { + return SearchSequentPrintFilter.createPattern(text, regexToggle.isSelected()); + } catch (IllegalRegexException e) { + LOGGER.debug("runSearch: text={} invalid regex", text); + return null; + } + } + /** Switches to the next match (Swing {@code searchNext}), wrapping around. */ public void searchNext() { if (searchMatches.isEmpty()) { @@ -641,7 +1084,12 @@ private void addRun(String content, Font font, String... styleClasses) { } private void printPlaceholder() { + clearHover(); + updateHideWarning(); textFlow.getChildren().clear(); + printed = null; + updateOverlay.getChildren().clear(); + updateRectCount = 0; Text placeholder = new Text("No proof loaded.\n" + "Start with -Dkey.fx.demo.sequent= to try the sequent view spike."); placeholder.getStyleClass().add("sequent-placeholder"); @@ -676,7 +1124,13 @@ public String verifySequentSearch(String query) { return "no printed sequent"; } showSearchBar(); - searchField.setText(query); + // the bar was closed before (mode Highlight); make the baseline deterministic + searchModeBox.getSelectionModel().select(SearchMode.HIGHLIGHT); + if (!query.equals(searchField.getText())) { + searchField.setText(query); + } else { + runSearch(); + } int count = searchMatches.size(); boolean rangesOk = true; for (int[] match : searchMatches) { @@ -698,6 +1152,322 @@ public String verifySequentSearch(String query) { + back + "," + wrapped + " rangesOk=" + rangesOk + " " + (pass ? "PASS" : "FAIL"); } + /** + * Development self-test (M2): exercises the search modes like the Swing search bar. In the + * Highlight mode the plain sequent with all matches is the baseline; the Hide mode must print + * only the formulas matching the query (a subset of the baseline formulas, hiding = fewer + * characters); the Regroup mode must print the same formulas with the matching ones grouped + * at the sequent arrow (antecedent: matching at the end, succedent: matching at the start, + * like Swing's {@code RegroupSequentPrintFilter} reorders). The baseline must reproduce the + * original sequent exactly — a half that is empty in the original sequent stays legitimately + * empty (e.g. the Agatha demo has no antecedent formula); with a query that matches every + * formula Hide and Regroup are no-ops and the report marks them {@code vacuous}. Restores the + * plain view. + *

+ * The modes can only be exercised on a sequent with at least two formulas; the displayed node + * of a fresh demo proof (the root) has a single one. The test therefore runs on + * {@link #findModeTestNode()} and restores the display of the original node afterwards, like + * the Swing bar closing leaves the selected node displayed. + * + * @param query the query to search for + * @return a one-line report, {@code "... PASS"} if the modes behave as expected + */ + public String verifySearchModes(String query) { + if (printed == null || selectedNode == null) { + return "no printed sequent"; + } + Node originalNode = selectedNode; + Node testNode = findModeTestNode(); + if (testNode != null && testNode != originalNode) { + display(testNode); + } + showSearchBar(); + // baseline: Highlight mode = the plain printing with the matches highlighted + SequentPrintFilter baselineFilter = new IdentitySequentPrintFilter(); + setSequentFilter(baselineFilter, true); + searchModeBox.getSelectionModel().select(SearchMode.HIGHLIGHT); + if (!query.equals(searchField.getText())) { + searchField.setText(query); + } else { + runSearch(); + } + int highlightChars = printed.length(); + int highlightMatches = searchMatches.size(); + String highlightText = printed; + List highlightAntec = filterFormulas(filter.getFilteredAntec()); + List highlightSucc = filterFormulas(filter.getFilteredSucc()); + + // Hide: only the formulas matching the query are printed + searchModeBox.getSelectionModel().select(SearchMode.HIDE); + int hideChars = printed.length(); + int hideMatches = searchMatches.size(); + boolean hideHiding = isHiding(); + List matchingAntec = filterFormulas(filter.getFilteredAntec()); + List matchingSucc = filterFormulas(filter.getFilteredSucc()); + boolean hideSubset = isSubsequence(highlightAntec, matchingAntec) + && isSubsequence(highlightSucc, matchingSucc); + + // Regroup: the same formulas, the matching ones regrouped around the arrow + searchModeBox.getSelectionModel().select(SearchMode.REGROUP); + int regroupChars = printed.length(); + int regroupMatches = searchMatches.size(); + List regroupAntec = filterFormulas(filter.getFilteredAntec()); + List regroupSucc = filterFormulas(filter.getFilteredSucc()); + boolean regroupSame = sameFormulas(highlightAntec, regroupAntec) + && sameFormulas(highlightSucc, regroupSucc); + boolean regroupOrder = expectedRegroup(highlightAntec, matchingAntec, false) + .equals(regroupAntec) + && expectedRegroup(highlightSucc, matchingSucc, true).equals(regroupSucc); + boolean regrouped = !printed.equals(highlightText); + + // restore the plain view (Highlight mode + closed bar), as the Swing bar closes + hideSearchBar(); + if (testNode != null && testNode != originalNode) { + display(originalNode); + } + // the baseline must be the full original sequent (the identity filter keeps every + // formula; a half that is empty in the original sequent stays legitimately empty — + // e.g. the Agatha demo sequent has no antecedent because KeY does not split the + // top-level implication of the problem statement) + List originalAntec = sequentFormulas(baselineFilter.getOriginalSequent(), + true); + List originalSucc = sequentFormulas(baselineFilter.getOriginalSequent(), + false); + boolean baselineOk = highlightMatches > 0 && sameFormulas(originalAntec, highlightAntec) + && sameFormulas(originalSucc, highlightSucc); + boolean hideOk = hideChars <= highlightChars && hideMatches > 0 + && hideHiding == (hideChars < highlightChars) && hideSubset; + boolean regroupOk = regroupSame && regroupOrder && regroupMatches == highlightMatches; + boolean pass = baselineOk && hideOk && regroupOk; + // vacuous: the query matched every formula, Hide and Regroup cannot change anything + boolean vacuous = hideChars == highlightChars && regroupChars == highlightChars; + return "query=" + query + " node=" + (testNode != null ? testNode.serialNr() : -1) + + " highlight chars=" + highlightChars + " matches=" + + highlightMatches + " antec=" + highlightAntec.size() + " succ=" + + highlightSucc.size() + " | hide chars=" + hideChars + " matches=" + hideMatches + + " antec=" + matchingAntec.size() + " succ=" + matchingSucc.size() + " hiding=" + + hideHiding + " subset=" + hideSubset + " | regroup chars=" + regroupChars + + " matches=" + regroupMatches + " antec=" + regroupAntec.size() + " succ=" + + regroupSucc.size() + " same=" + regroupSame + " order=" + regroupOrder + + " regrouped=" + regrouped + " | baselineOk=" + baselineOk + " hideOk=" + hideOk + + " regroupOk=" + regroupOk + " vacuous=" + vacuous + " " + + (pass ? "PASS" : "FAIL"); + } + + /** + * Finds the node to run the search mode self test on: the node with the most sequent formulas + * in the subtree of the displayed node — or, if even that subtree has no multi-formula node, + * of the whole proof, falling back to the displayed node itself. The richest node is wanted + * because the modes can only be exercised on a sequent with several formulas, and the + * displayed node of a demo proof is often poor (the root has a single formula; after the tree + * search verification the selection rests on a two-formula node). + * + * @return the test node (never {@code null} if a node is displayed) + */ + private Node findModeTestNode() { + if (selectedNode == null) { + return null; + } + Node best = findRichestNode(selectedNode); + if (best == null && proof != null) { + best = findRichestNode(proof.root()); + } + return best != null ? best : selectedNode; + } + + /** + * @return the node with the most sequent formulas in the subtree of the given node where at + * least two formulas exist ({@code null} if there is none; pre-order walk, the first + * node wins on ties) + */ + private static Node findRichestNode(Node node) { + ArrayDeque stack = new ArrayDeque<>(); + stack.push(node); + Node best = null; + int bestSize = 1; + while (!stack.isEmpty()) { + Node current = stack.pop(); + int size = current.sequent().size(); + if (size > bestSize) { + best = current; + bestSize = size; + } + for (int i = 0; i < current.childrenCount(); i++) { + stack.push(current.child(i)); + } + } + return best; + } + + /** + * Development self-test (M2): verifies the update-highlight overlays of the current printing + * (Swing {@code CurrentGoalView.updateUpdateHighlights}): every update range of the position + * table must be displayed as one translucent rectangle whose shape lies inside the text flow + * bounds. Requires a sequent that prints update operators (e.g. a program verification goal + * after symbolic execution); a plain sequent reports {@code ranges=0 ... FAIL}. + * + * @return a one-line report, {@code "... PASS"} if the overlays exist with valid geometry + */ + public String verifyUpdateHighlights() { + if (printed == null) { + return "no printed sequent"; + } + textFlow.applyCss(); + textFlow.layout(); + InitialPositionTable table = getInitialPositionTable(); + Range[] ranges = table == null ? new Range[0] : table.getUpdateRanges(); + boolean geometryOk = ranges.length > 0; + for (Range range : ranges) { + int start = Math.clamp(range.start(), 0, printed.length()); + int end = Math.clamp(range.end(), start, printed.length()); + double minX = Double.POSITIVE_INFINITY; + double minY = Double.POSITIVE_INFINITY; + double maxX = Double.NEGATIVE_INFINITY; + double maxY = Double.NEGATIVE_INFINITY; + for (PathElement element : textFlow.getRangeShape(start, end, true)) { + double x = Double.NaN; + double y = Double.NaN; + if (element instanceof MoveTo m) { + x = m.getX(); + y = m.getY(); + } else if (element instanceof LineTo l) { + x = l.getX(); + y = l.getY(); + } else if (element instanceof HLineTo h) { + x = h.getX(); + } else if (element instanceof VLineTo v) { + y = v.getY(); + } + if (!Double.isNaN(x)) { + minX = Math.min(minX, x); + maxX = Math.max(maxX, x); + } + if (!Double.isNaN(y)) { + minY = Math.min(minY, y); + maxY = Math.max(maxY, y); + } + } + geometryOk &= Double.isFinite(minX) && maxX > minX && maxY > minY + && minX >= -1 && minY >= -1 && maxX <= textFlow.getWidth() + 1 + && maxY <= textFlow.getHeight() + 1; + } + boolean pass = ranges.length > 0 && updateRectCount == ranges.length && geometryOk; + return "ranges=" + ranges.length + " rects=" + updateRectCount + " geometryOk=" + + geometryOk + " " + (pass ? "PASS" : "FAIL"); + } + + /** + * @return the original formulas of the given filter entries, in printing order + */ + private static List filterFormulas( + ImmutableList entries) { + List formulas = new ArrayList<>(); + if (entries != null) { + for (SequentPrintFilterEntry entry : entries) { + formulas.add(entry.getOriginalFormula()); + } + } + return formulas; + } + + /** + * @return the formulas of the given sequent half, in printing order + */ + private static List sequentFormulas(Sequent sequent, boolean antecedent) { + List formulas = new ArrayList<>(); + if (sequent != null) { + Semisequent semisequent = antecedent ? sequent.antecedent() : sequent.succedent(); + for (Iterator it = semisequent.iterator(); it.hasNext();) { + formulas.add(it.next()); + } + } + return formulas; + } + + /** + * @return {@code true} iff {@code sub} is a subsequence of {@code list} (same formula + * objects, same order) — the Hide filter keeps the matching formulas in order + */ + private static boolean isSubsequence(List list, List sub) { + if (sub.size() > list.size()) { + return false; + } + int i = 0; + for (SequentFormula formula : sub) { + while (i < list.size() && list.get(i) != formula) { + i++; + } + if (i == list.size()) { + return false; + } + i++; + } + return true; + } + + /** + * @return {@code true} iff both lists contain the same formula objects (any order) — the + * Regroup filter reorders but never drops formulas + */ + private static boolean sameFormulas(List list, List other) { + if (list.size() != other.size()) { + return false; + } + List copy = new ArrayList<>(other); + for (SequentFormula formula : list) { + if (!removeRef(copy, formula)) { + return false; + } + } + return copy.isEmpty(); + } + + /** + * The regrouped order the Swing {@code RegroupSequentPrintFilter.filterSequent} computes: + * iterating the original order, a matching formula is appended (antecedent) resp. prepended + * (succedent), a non-matching one prepended (antecedent) resp. appended (succedent) — so the + * matching formulas end up grouped at the sequent arrow. + * + * @param base the formulas in original order (the Highlight mode printing) + * @param matching the formulas matching the query (the Hide mode printing), as a subsequence + * of {@code base} + * @param matchingAtStart whether the matching formulas belong at the arrow-adjacent start of + * the list (succedent) or at its end (antecedent) + * @return the expected regrouped formula list + */ + private static List expectedRegroup(List base, + List matching, boolean matchingAtStart) { + ArrayDeque deque = new ArrayDeque<>(); + for (SequentFormula formula : base) { + boolean match = containsRef(matching, formula); + if (match == matchingAtStart) { + deque.addFirst(formula); + } else { + deque.addLast(formula); + } + } + return new ArrayList<>(deque); + } + + private static boolean containsRef(List list, SequentFormula formula) { + for (SequentFormula candidate : list) { + if (candidate == formula) { + return true; + } + } + return false; + } + + private static boolean removeRef(List list, SequentFormula formula) { + for (int i = 0; i < list.size(); i++) { + if (list.get(i) == formula) { + list.remove(i); + return true; + } + } + return false; + } + /** * Development self-test (M2): verifies the syntax highlighting of the current printing — the * intervals must be valid ranges of the printed text, at least the sequent arrow must be diff --git a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css index 1f0ec45d726..dbcb8b5adaf 100644 --- a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css +++ b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css @@ -442,3 +442,30 @@ .source-jml-keyword { -fx-font-weight: bold; } + +/* update highlights of the printed update operators (M2, Swing + CurrentGoalView.updateUpdateHighlights paints InitialPositionTable.getUpdateRanges with + UPDATE_HIGHLIGHT_COLOR behind the text); the rectangles are Path nodes styled with -fx-fill; + the Swing dark color is fully opaque (painted behind the text) */ +.sequent-update-highlight { + -fx-fill: rgba(0, 150, 130, 1.0); +} + +/* hover highlight of the term under the mouse (Swing DEFAULT_HIGHLIGHT_COLOR dark) */ +.sequent-hover-highlight { + -fx-fill: rgba(140, 200, 255, 0.71); +} + +/* warning banner while a search filter hides formulas (Swing SequentHideWarningBorder message; + the Swing border uses the same single color in both themes) */ +.sequent-hide-warning { + -fx-background-color: #ffb2b2; + -fx-text-fill: #000000; + -fx-font-size: 0.9em; + -fx-padding: 2 8 2 8; +} + +/* the search mode combo (Swing SequentViewSearchBar.searchModeBox with the mode icons) */ +.sequent-search-mode { + -fx-padding: 1; +} diff --git a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css index 6a890f7128b..188934181ef 100644 --- a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css +++ b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css @@ -438,3 +438,28 @@ .source-jml-keyword { -fx-font-weight: bold; } + +/* update highlights of the printed update operators (M2, Swing + CurrentGoalView.updateUpdateHighlights paints InitialPositionTable.getUpdateRanges with + UPDATE_HIGHLIGHT_COLOR behind the text); the rectangles are Path nodes styled with -fx-fill */ +.sequent-update-highlight { + -fx-fill: rgba(0, 150, 130, 0.15); +} + +/* hover highlight of the term under the mouse (Swing DEFAULT_HIGHLIGHT_COLOR) */ +.sequent-hover-highlight { + -fx-fill: rgba(70, 100, 170, 0.3); +} + +/* warning banner while a search filter hides formulas (Swing SequentHideWarningBorder message) */ +.sequent-hide-warning { + -fx-background-color: #ffb2b2; + -fx-text-fill: #000000; + -fx-font-size: 0.9em; + -fx-padding: 2 8 2 8; +} + +/* the search mode combo (Swing SequentViewSearchBar.searchModeBox with the mode icons) */ +.sequent-search-mode { + -fx-padding: 1; +} From 37f03d598a88996ae178d13af911ba81da1ae699 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Thu, 8 Oct 2026 09:39:07 +0200 Subject: [PATCH 044/281] key.ui.fx: M2 sequent view leftovers verify hooks for the new features --- .../de/uka/ilkd/key/gui/fx/MainWindowF.java | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) 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 index d5e6f93214a..67a7b607dc9 100644 --- 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 @@ -398,6 +398,14 @@ protected KeYEnvironment call() throws Exception { // 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(); + } if (System.getProperty("key.fx.verify.tree") != null) { String report = proofTreeView.verifyTreeStructure(); LOGGER.info("Proof tree structure verification: {}", report); @@ -629,6 +637,12 @@ public void autoModeStopped(ProofEvent e) { 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); @@ -669,6 +683,41 @@ private void runSequentSearchVerification() { report.endsWith("PASS") ? Kind.INFO : Kind.ERROR); } + /** + * 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); + } + /** * 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 From ffdf9ce8e357b9a2a9b8a6f66289ddaf1e00e1b6 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Thu, 8 Oct 2026 09:39:08 +0200 Subject: [PATCH 045/281] key.ui.fx: M2 sequent view leftovers forward the new verify flags to the run task --- key.ui.fx/build.gradle | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/key.ui.fx/build.gradle b/key.ui.fx/build.gradle index 6c72d647341..86152523631 100644 --- a/key.ui.fx/build.gradle +++ b/key.ui.fx/build.gradle @@ -61,10 +61,15 @@ run { // * key.fx.verify.search = any (M2: run the proof tree search self test after the demo load) // * key.fx.verify.sequentsearch = q (M2: run the sequent search self test with query q) // * key.fx.verify.treefilters = any (M2: run the tree filter self test after auto mode) + // * key.fx.verify.sequentsearchmodes = any (M2: run the search mode self test (Highlight/ + // Hide/Regroup) with the key.fx.verify.sequentsearch query after the demo load) + // * key.fx.verify.updatehighlight = any (M2: run the update-highlight overlay self test; + // needs a sequent printing update operators, e.g. standard_key/queries/useQuery.key) ["key.fx.driver", "key.fx.theme", "key.fx.demo.sequent", "key.fx.verify.sequent", "key.fx.show", "key.fx.demo.autoprove", "key.fx.verify.tree", "key.fx.demo.autoprove.live", "key.fx.demo.maxsteps", "key.fx.verify.goallist", - "key.fx.verify.search", "key.fx.verify.sequentsearch", "key.fx.verify.treefilters"] + "key.fx.verify.search", "key.fx.verify.sequentsearch", "key.fx.verify.treefilters", + "key.fx.verify.sequentsearchmodes", "key.fx.verify.updatehighlight"] .each { name -> def value = System.getProperty(name) if (value != null) { From 49fe1dc6ae5247251ab6c07116d35984ee9fd428 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Thu, 8 Oct 2026 10:09:12 +0200 Subject: [PATCH 046/281] key.ui.fx: fix CSS merge resolution - restore closing braces lost by diff-based section splicing --- .../de/uka/ilkd/key/gui/fx/theme/key-dark.css | 40 ++++++++----------- .../uka/ilkd/key/gui/fx/theme/key-light.css | 38 +++++++----------- 2 files changed, 31 insertions(+), 47 deletions(-) diff --git a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css index 812b54a3c86..173dd4a912f 100644 --- a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css +++ b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css @@ -451,31 +451,21 @@ -fx-font-weight: bold; -fx-text-fill: -key-text-secondary; } - /* - * Configurable colors shared with the Swing UI (colors.json); the file keys are mapped to - * these properties by ColorSettingsF.CSS_VARIABLES and the defaults are the dark defaults - * of the Swing ColorSettings. The sequent prop-logic default is the plain text color - * (white in the Swing UI). - */ - -key-settings-error: #c86464; - -key-hl-java: #cf50a5; - -key-hl-comment: #9fbf9f; - -key-hl-jml: #8888cf; - -key-hl-jml-keyword: #8888cf; - -key-sequent-hl-prop: -key-text; - -key-sequent-hl-dyn: #9696fa; - -key-sequent-hl-progvar: #6464fa; - -key-sequent-hl-arrow: #6464fa; - - -fx-fill: -key-sequent-hl-prop; - -fx-fill: -key-sequent-hl-dyn; - -fx-fill: -key-hl-java; - -fx-fill: -key-sequent-hl-progvar; - -fx-fill: -key-hl-comment; - -fx-fill: -key-hl-jml; - -fx-fill: -key-sequent-hl-arrow; - -fx-fill: -key-hl-java; + +.source-comment { -fx-fill: -key-hl-comment; + -fx-font-style: italic; +} + +.source-string { + -fx-fill: -key-success; +} + +.source-annotation { + -fx-fill: -key-warning; +} + +.source-jml-keyword { -fx-fill: -key-hl-jml-keyword; -fx-font-weight: bold; } @@ -489,6 +479,7 @@ .settings-panel-title { -fx-font-size: 1.3em; + -fx-font-weight: bold; -fx-text-fill: -key-text; } @@ -563,6 +554,7 @@ .key-icon.key-icon-error { -fx-fill: -key-error; +} /* ------------------------------------------------------------------ * proof diff frame (M5, Swing ProofDiffFrame) diff --git a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css index 1135e191b5d..2962a642bf4 100644 --- a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css +++ b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css @@ -448,30 +448,20 @@ -fx-text-fill: -key-text-secondary; } - /* - * Configurable colors shared with the Swing UI (colors.json); the file keys are mapped to - * these properties by ColorSettingsF.CSS_VARIABLES and the defaults are the light defaults - * of the Swing ColorSettings. The sequent prop-logic default is the plain text color - * (black in the Swing UI). - */ - -key-settings-error: #c86464; - -key-hl-java: #7f0055; - -key-hl-comment: #3f7f5f; - -key-hl-jml: #0000c0; - -key-hl-jml-keyword: #0000f0; - -key-sequent-hl-prop: -key-text; - -key-sequent-hl-dyn: #0000d0; - -key-sequent-hl-progvar: #6a3e3e; - -key-sequent-hl-arrow: #6a3e3e; - -fx-fill: -key-sequent-hl-prop; - -fx-fill: -key-sequent-hl-dyn; - -fx-fill: -key-hl-java; - -fx-fill: -key-sequent-hl-progvar; - -fx-fill: -key-hl-comment; - -fx-fill: -key-hl-jml; - -fx-fill: -key-sequent-hl-arrow; - -fx-fill: -key-hl-java; +.source-comment { -fx-fill: -key-hl-comment; + -fx-font-style: italic; +} + +.source-string { + -fx-fill: -key-success; +} + +.source-annotation { + -fx-fill: -key-warning; +} + +.source-jml-keyword { -fx-fill: -key-hl-jml-keyword; -fx-font-weight: bold; } @@ -498,6 +488,7 @@ } .settings-separator-title { + -fx-font-weight: bold; -fx-text-fill: -key-text; } @@ -559,6 +550,7 @@ .key-icon.key-icon-error { -fx-fill: -key-error; +} /* ------------------------------------------------------------------ * proof diff frame (M5, Swing ProofDiffFrame) From dc835936e1473e412a7d5c84b531cc0e1685c15e Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Thu, 8 Oct 2026 21:26:43 +0200 Subject: [PATCH 047/281] key.ui.fx: notification: port the Swing notification event/task/action framework Port of key.ui/src/main/java/de/uka/ilkd/key/gui/notification/ into key.ui.fx/.../gui/fx/notification/: - NotificationEventIDF/NotificationEventF + events/ (proof closed, task abandoned, exit, general failure, exception failure, general information) - NotificationActionF/NotificationTaskF (FX-thread marshalling via Platform.runLater mirrors the Swing EDT invokeLater) - the six tasks (ProofClosed/GeneralFailure/GeneralInformation/Abandon/ ExitKeY/ExceptionFailure notifications) - actions/: ToastActionF (FX toast-instead-of-JOptionPane deviation) and ProofClosedDialogActionF + ProofClosedDialogF (statistics dialog, port of ProofClosedJTextPaneDisplay -> ShowProofStatistics.Window) - NotificationCenterF: port of the Swing NotificationManager (task registry, auto-mode tracking listener, event dispatch) plus the minimal app-level trigger afterAutoModeFinished and the key.fx.verify.notifications self tests Exception routing is owned by the seam agent (TODO-merge comments only). --- .../fx/notification/AbandonNotificationF.java | 20 + .../ExceptionFailureNotificationF.java | 45 ++ .../fx/notification/ExitKeYNotificationF.java | 25 ++ .../GeneralFailureNotificationF.java | 39 ++ .../GeneralInformationNotificationF.java | 30 ++ .../fx/notification/NotificationActionF.java | 24 ++ .../fx/notification/NotificationCenterF.java | 383 ++++++++++++++++++ .../fx/notification/NotificationEventIDF.java | 29 ++ .../fx/notification/NotificationTaskF.java | 90 ++++ .../fx/notification/ProofClosedDialogF.java | 101 +++++ .../ProofClosedNotificationF.java | 41 ++ .../actions/ProofClosedDialogActionF.java | 39 ++ .../fx/notification/actions/ToastActionF.java | 55 +++ .../events/AbandonTaskEventF.java | 25 ++ .../events/ExceptionFailureEventF.java | 31 ++ .../fx/notification/events/ExitKeYEventF.java | 25 ++ .../events/GeneralFailureEventF.java | 42 ++ .../events/GeneralInformationEventF.java | 60 +++ .../events/NotificationEventF.java | 39 ++ .../events/ProofClosedNotificationEventF.java | 35 ++ 20 files changed, 1178 insertions(+) create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/AbandonNotificationF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/ExceptionFailureNotificationF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/ExitKeYNotificationF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/GeneralFailureNotificationF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/GeneralInformationNotificationF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/NotificationActionF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/NotificationCenterF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/NotificationEventIDF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/NotificationTaskF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/ProofClosedDialogF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/ProofClosedNotificationF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/actions/ProofClosedDialogActionF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/actions/ToastActionF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/events/AbandonTaskEventF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/events/ExceptionFailureEventF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/events/ExitKeYEventF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/events/GeneralFailureEventF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/events/GeneralInformationEventF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/events/NotificationEventF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/events/ProofClosedNotificationEventF.java diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/AbandonNotificationF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/AbandonNotificationF.java new file mode 100644 index 00000000000..62c86c3389a --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/AbandonNotificationF.java @@ -0,0 +1,20 @@ +/* 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.notification; + +/** + * Notifies the user when a proof task is abandoned. + *

+ * Port of the Swing original {@code key.ui/src/main/java/de/uka/ilkd/key/gui/notification/ + * AbandonNotification.java}, which declares no actions (the Swing manager only keeps the task + * registered so that callers can add their own actions, e.g. the Eclipse integration). + */ +public class AbandonNotificationF extends NotificationTaskF { + + @Override + public NotificationEventIDF getEventID() { + return NotificationEventIDF.TASK_ABANDONED; + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/ExceptionFailureNotificationF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/ExceptionFailureNotificationF.java new file mode 100644 index 00000000000..38b99063098 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/ExceptionFailureNotificationF.java @@ -0,0 +1,45 @@ +/* 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.notification; + +import de.uka.ilkd.key.gui.fx.notification.actions.ToastActionF; + +/** + * This task notifies the user about an exception causing a failure. + *

+ * Port of the Swing original {@code key.ui/src/main/java/de/uka/ilkd/key/gui/notification/ + * ExceptionFailureNotification.java} (Swing action: + * {@code ExceptionFailureNotificationDialog} → {@code IssueDialog.showExceptionDialog}). + *

+ * TODO-merge (seam agent, branch {@code weigl/ocfx-seam}): exception routing (the Swing + * {@code mediator.notify(new ExceptionFailureEvent(...))} call sites and the exception-dialog + * path) is owned by the exception-seam agent; this task ships with the ERROR-toast action only + * and is not wired into any control construction yet — it is exercised by the + * {@code key.fx.verify.notifications} self-test hook. Runs also during auto mode (Swing parity). + */ +public class ExceptionFailureNotificationF extends NotificationTaskF { + + /** + * creates the notification task with the toast display action + */ + public ExceptionFailureNotificationF() { + addNotificationAction(new ToastActionF()); + } + + /** + * returns if this task should be executed in auto mode + * + * @return if true execute task even if in automode + */ + @Override + protected boolean automodeEnabledTask() { + return true; + } + + @Override + public NotificationEventIDF getEventID() { + return NotificationEventIDF.EXCEPTION_CAUSED_FAILURE; + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/ExitKeYNotificationF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/ExitKeYNotificationF.java new file mode 100644 index 00000000000..046bfaf2050 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/ExitKeYNotificationF.java @@ -0,0 +1,25 @@ +/* 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.notification; + +/** + * This task takes care for a notification when exiting KeY. + *

+ * Port of the Swing original {@code key.ui/src/main/java/de/uka/ilkd/key/gui/notification/ + * ExitKeYNotification.java}. Swing overrides {@code execute} to marshal with + * {@code SwingUtilities.invokeAndWait} (ExitKeYNotification.java:43-56) because the exit flow + * continues after the notification; in JavaFX {@link NotificationTaskF#execute} already runs + * synchronously when called on the FX application thread (where the FX exit flow lives), so no + * override is needed. Declares no actions, like Swing. Note: the FX exit flow itself is not + * ported yet (parity audit {@code parity-mainwindow.md} gap 5), so no trigger fires this event + * today. + */ +public class ExitKeYNotificationF extends NotificationTaskF { + + @Override + public NotificationEventIDF getEventID() { + return NotificationEventIDF.EXIT_KEY; + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/GeneralFailureNotificationF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/GeneralFailureNotificationF.java new file mode 100644 index 00000000000..dda820a47ad --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/GeneralFailureNotificationF.java @@ -0,0 +1,39 @@ +/* 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.notification; + +import de.uka.ilkd.key.gui.fx.notification.actions.ToastActionF; + +/** + * This task notifies the user about an unexpected error. + *

+ * Port of the Swing original {@code key.ui/src/main/java/de/uka/ilkd/key/gui/notification/ + * GeneralFailureNotification.java} (Swing action: {@code GeneralFailureJTextPaneDisplay}, a + * modal error dialog; FX deviation: ERROR toast). Runs also during auto mode. + */ +public class GeneralFailureNotificationF extends NotificationTaskF { + + /** + * creates the notification task + */ + public GeneralFailureNotificationF() { + addNotificationAction(new ToastActionF()); + } + + /** + * returns if this task should be executed in auto mode + * + * @return if true execute task even if in automode + */ + @Override + protected boolean automodeEnabledTask() { + return true; + } + + @Override + public NotificationEventIDF getEventID() { + return NotificationEventIDF.GENERAL_FAILURE; + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/GeneralInformationNotificationF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/GeneralInformationNotificationF.java new file mode 100644 index 00000000000..3dfa6c87ee6 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/GeneralInformationNotificationF.java @@ -0,0 +1,30 @@ +/* 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.notification; + +import de.uka.ilkd.key.gui.fx.notification.actions.ToastActionF; + +/** + * This notification task is used to inform the user about a non-error situation (e.g. + * statistics (how many goals have been closed) etc.). + *

+ * Port of the Swing original {@code key.ui/src/main/java/de/uka/ilkd/key/gui/notification/ + * GeneralInformationNotification.java} (Swing action: {@code GeneralInformationJTextPaneDisplay}, + * a modal information dialog; FX deviation: INFO toast). + */ +public class GeneralInformationNotificationF extends NotificationTaskF { + + /** + * creates the notification task + */ + public GeneralInformationNotificationF() { + addNotificationAction(new ToastActionF()); + } + + @Override + public NotificationEventIDF getEventID() { + return NotificationEventIDF.GENERAL_INFORMATION; + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/NotificationActionF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/NotificationActionF.java new file mode 100644 index 00000000000..74f176b7d8b --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/NotificationActionF.java @@ -0,0 +1,24 @@ +/* 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.notification; + +import de.uka.ilkd.key.gui.fx.notification.events.NotificationEventF; + +/** + * This interface is implemented by notification actions. + *

+ * Port of the Swing original {@code key.ui/src/main/java/de/uka/ilkd/key/gui/notification/ + * NotificationAction.java}. + */ +public interface NotificationActionF { + + /** + * executes the action + * + * @param event the NotificationEvent triggering this action + * @return indicator if action has been executed successfully + */ + boolean execute(NotificationEventF event); +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/NotificationCenterF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/NotificationCenterF.java new file mode 100644 index 00000000000..7649557acc5 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/NotificationCenterF.java @@ -0,0 +1,383 @@ +/* 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.notification; + +import java.util.ArrayList; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; +import javafx.application.Platform; + +import de.uka.ilkd.key.control.AutoModeListener; +import de.uka.ilkd.key.gui.fx.notification.events.AbandonTaskEventF; +import de.uka.ilkd.key.gui.fx.notification.events.ExceptionFailureEventF; +import de.uka.ilkd.key.gui.fx.notification.events.ExitKeYEventF; +import de.uka.ilkd.key.gui.fx.notification.events.GeneralFailureEventF; +import de.uka.ilkd.key.gui.fx.notification.events.GeneralInformationEventF; +import de.uka.ilkd.key.gui.fx.notification.events.NotificationEventF; +import de.uka.ilkd.key.gui.fx.notification.events.ProofClosedNotificationEventF; +import de.uka.ilkd.key.proof.Proof; +import de.uka.ilkd.key.proof.ProofEvent; +import de.uka.ilkd.key.settings.ProofIndependentSettings; +import de.uka.ilkd.key.settings.ViewSettings; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The notification manager controls the list of active notification tasks. It receives KeY + * system events and looks for an appropriate task. + *

+ * Port of the Swing original {@code key.ui/src/main/java/de/uka/ilkd/key/gui/notification/ + * NotificationManager.java}. Naming deviation: the FX toast sink + * {@link NotificationManagerF} already occupies the Swing class name, so the manager of the + * event/task/action framework is called {@code NotificationCenterF}. + *

+ * App-level wiring status (parity audit {@code parity-unported-dialogs.md} §10): the proof-closed + * and "Automated proof search" triggers are wired through + * {@link #afterAutoModeFinished(Proof, boolean)} (called from the FX auto-mode-stop hook); + * the exception routing and the exit/abandon triggers are not ported yet. + */ +public final class NotificationCenterF { + + private static final Logger LOGGER = LoggerFactory.getLogger(NotificationCenterF.class); + + private static final NotificationCenterF INSTANCE = new NotificationCenterF(); + + /** list of notification tasks */ + private final Map notificationTasks = + new EnumMap<>(NotificationEventIDF.class); + + /** true if we are currently in automode */ + private boolean autoMode = false; + + private final NotificationListenerF notificationListener = new NotificationListenerF(); + + private NotificationCenterF() { + setDefaultNotifications(); + } + + /** + * @return the global notification framework instance + */ + public static NotificationCenterF getInstance() { + return INSTANCE; + } + + /** + * installs the default notification tasks, mirroring the Swing + * {@code NotificationManager.setDefaultNotification}. + */ + public void setDefaultNotifications() { + notificationTasks.clear(); + addNotificationTask(new ProofClosedNotificationF()); + addNotificationTask(new GeneralFailureNotificationF()); + addNotificationTask(new GeneralInformationNotificationF()); + addNotificationTask(new AbandonNotificationF()); + addNotificationTask(new ExitKeYNotificationF()); + + // FIXME (DS): Obviously, adding ExceptionFailureNotification at this place leads to a + // double appearance of Dialogs in case of a parser error. However, the user is not + // notified in case of an ExceptionFailure occurring *after* the parsing procedure, so + // for instance at an erroneous BuiltInRule application. This is not desirable, since + // then there might be a strange GUI behavior without even a notification. + // (comment ported from NotificationManager.setDefaultNotification) + // + // TODO-merge (seam agent, branch weigl/ocfx-seam): the exception routing decides + // whether/where ExceptionFailureNotificationF is registered. + } + + /** + * adds a notification task to this manager + * + * @param task the NotificationTaskF to be added + */ + public void addNotificationTask(NotificationTaskF task) { + notificationTasks.put(task.getEventID(), task); + } + + /** + * removes the given notification task from the list of active tasks + * + * @param task the task to be removed + */ + public void removeNotificationTask(NotificationTaskF task) { + removeNotificationTask(task.getEventID()); + } + + /** + * Removes the {@link NotificationTaskF} with the given {@link NotificationEventIDF}. + *

+ * This functionality is used by the Eclipse integration (Swing parity, + * NotificationManager.java:87-93). + * + * @param eventID The {@link NotificationEventIDF} to remove its {@link NotificationTaskF}. + * @return The removed {@link NotificationTaskF} or {@code null} if none was available. + */ + public NotificationTaskF removeNotificationTask(NotificationEventIDF eventID) { + return notificationTasks.remove(eventID); + } + + /** + * @return true if the prover is currently in automode + */ + public boolean inAutoMode() { + return autoMode; + } + + /** + * @return the auto mode listener tracking the auto-mode state; register it on the proof + * control like the Swing {@code NotificationManager} constructor does + * (NotificationManager.java:99-101) + */ + public AutoModeListener notificationListener() { + return notificationListener; + } + + /** + * dispatches the received notification event and triggers the corresponding task + * + * @param event the notification event + */ + public void handleNotificationEvent(NotificationEventF event) { + NotificationTaskF notificationTask = notificationTasks.get(event.getEventID()); + if (notificationTask != null) { + notificationTask.execute(event, this); + } + } + + /** + * The minimal FX wiring of the app-level notification triggers that Swing performs after an + * automatic strategy run (called from the FX auto-mode-stop hook, + * {@code MainWindowF.refreshViewsFromFinalState}): + *

    + *
  • the proof-closed notification (Swing: {@code KeYMediator.KeYMediatorProofTreeListener + * .proofClosed} fires a {@code ProofClosedNotificationEvent}, KeYMediator.java:641-646),
  • + *
  • the "Automated proof search" information (Swing: + * {@code WindowUserInterfaceControl.taskFinishedInternal → showNotification}, + * WindowUserInterfaceControl.java:199-207), gated by the + * {@link ViewSettings#notificationAfterMacro()} setting with the same ALWAYS / "When not + * focused" / NEVER semantics.
  • + *
+ * + * @param proof the proof the automatic run finished on (may be {@code null}) + * @param windowFocused whether the main window is currently focused (Swing parity: + * {@code MainWindow.isActive()} in the NOTIFICATION_UNFOCUSED case) + */ + public void afterAutoModeFinished(Proof proof, boolean windowFocused) { + if (proof == null || proof.isDisposed()) { + return; + } + if (proof.closed()) { + handleNotificationEvent(new ProofClosedNotificationEventF(proof)); + } + String mode = + ProofIndependentSettings.DEFAULT_INSTANCE.getViewSettings().notificationAfterMacro(); + if (mode.equals(ViewSettings.NOTIFICATION_NEVER)) { + return; + } + if (mode.equals(ViewSettings.NOTIFICATION_UNFOCUSED) && windowFocused) { + return; + } + // Swing passes the TaskFinishedInfo.toString() of the strategy run; the FX auto-mode + // stop hook only has the proof, so the final statistics are used as the body instead + // (documented deviation) + String summary; + try { + summary = proof.getStatistics().toString(); + } catch (RuntimeException e) { + summary = "finished"; + } + handleNotificationEvent( + new GeneralInformationEventF("Automated proof search", summary)); + } + + /** + * Development self-test ({@code key.fx.verify.notifications}): exercises the framework the + * way the Swing original is used, without touching the default task registry. + *
    + *
  1. a task dispatches its actions when executed outside auto mode, synchronously on the + * FX application thread (parity with the EDT check in NotificationTask.execute),
  2. + *
  3. a task not marked {@code automodeEnabledTask} is skipped while in auto mode,
  4. + *
  5. a task marked {@code automodeEnabledTask} runs while in auto mode,
  6. + *
  7. the default registry contains the five default tasks (Swing + * {@code setDefaultNotification}),
  8. + *
  9. {@link #handleNotificationEvent} routes a registered and an unregistered event id + * without failure.
  10. + *
+ * The final dispatch of a real information and failure event additionally makes the toast + * sink show the two severity variants, so the run is visible on screen (screenshot check). + * + * @return {@code "PASS"} or {@code "FAIL: "}, in the style of the other + * {@code key.fx.verify.*} self-tests + */ + public String verifyFramework() { + if (!Platform.isFxApplicationThread()) { + return "FAIL: self-test must run on the JavaFX application thread"; + } + List received = new ArrayList<>(); + NotificationTaskF task = new NotificationTaskF() { + @Override + public NotificationEventIDF getEventID() { + return NotificationEventIDF.GENERAL_INFORMATION; + } + }; + task.addNotificationAction(event -> { + received.add(event); + return true; + }); + + // 1. plain dispatch outside auto mode, synchronous on the FX thread + GeneralInformationEventF first = new GeneralInformationEventF("self-test", "dispatch"); + task.execute(first, this); + if (received.size() != 1 || received.get(0) != first) { + return "FAIL: task did not dispatch its action (received " + received.size() + ")"; + } + + boolean savedAutoMode = autoMode; + try { + autoMode = true; + // 2. gated task is skipped during auto mode + task.execute(new GeneralInformationEventF("self-test", "gated"), this); + if (received.size() != 1) { + return "FAIL: non-automode task ran during auto mode"; + } + // 3. automode-enabled task runs during auto mode + NotificationTaskF alwaysTask = new NotificationTaskF() { + @Override + public NotificationEventIDF getEventID() { + return NotificationEventIDF.EXCEPTION_CAUSED_FAILURE; + } + + @Override + protected boolean automodeEnabledTask() { + return true; + } + }; + alwaysTask.addNotificationAction(event -> { + received.add(event); + return true; + }); + alwaysTask.execute( + new GeneralFailureEventF("self-test automode-enabled dispatch"), this); + if (received.size() != 2) { + return "FAIL: automode-enabled task did not run during auto mode"; + } + } finally { + autoMode = savedAutoMode; + } + + // 4. the default registry contains the five default tasks + for (NotificationEventIDF id : List.of(NotificationEventIDF.PROOF_CLOSED, + NotificationEventIDF.GENERAL_FAILURE, NotificationEventIDF.GENERAL_INFORMATION, + NotificationEventIDF.TASK_ABANDONED, NotificationEventIDF.EXIT_KEY)) { + if (!notificationTasks.containsKey(id)) { + return "FAIL: default task for " + id + " missing"; + } + } + + // 5. end-to-end dispatch through the registry (visible as toasts of both severities) + handleNotificationEvent(new GeneralInformationEventF("Notification self-test", + "framework dispatch works")); + handleNotificationEvent(new GeneralFailureEventF( + "Notification self-test: failure toast (expected)")); + // unregistered id must be ignored silently (Swing parity: handleNotificationEvent) + handleNotificationEvent(new AbandonTaskEventF()); + handleNotificationEvent(new ExitKeYEventF()); + LOGGER.info("Notification framework self-test dispatched all events"); + return "PASS"; + } + + /** + * End-to-end self-test ({@code key.fx.verify.notifications}): fires each notification type + * the way the app triggers them and asserts that the notification actions produce their + * visible counterparts (toast / proof-closed dialog). Reports one {@code PASS} or + * {@code FAIL} line per type. + *

+ * The checks must run one {@link Platform#runLater(Runnable)} hop after each dispatch, + * because the toast sink adds its toasts via {@code runLater} as well and {@code runLater} + * tasks run in order — the hop after a dispatch therefore sees the toast. The assertion + * thresholds are deltas over the toast count before the dispatch, so toasts fired by other + * verifications do not falsify the result. + * + * @param proof the loaded proof used for the proof-closed notification (may be {@code null}, + * the proof-closed fallback is exercised then) + */ + public void verifyNotifications(Proof proof) { + List lines = new ArrayList<>(); + // 1. the framework semantics (task/action dispatch, auto-mode gating, default registry) + lines.add("framework: " + verifyFramework()); + + // 2. task-finished notification (Swing parity: WindowUserInterfaceControl + // .taskFinishedInternal -> showNotification("Automated proof search", ...)); the task's + // toast action must produce a visible toast + int before = NotificationManagerF.getInstance().getVisibleToastCount(); + handleNotificationEvent( + new GeneralInformationEventF("Automated proof search", "notification self-test")); + Platform.runLater(() -> { + lines.add("task-finished: " + (NotificationManagerF.getInstance() + .getVisibleToastCount() > before ? "PASS" : "FAIL: info toast not shown")); + + // 3. proof-closed notification (Swing parity: KeYMediator proofClosed listener -> + // ProofClosedNotificationEvent -> ProofClosedJTextPaneDisplay); the dialog action + // must open the statistics dialog (or, without a proof, the toast fallback) + int beforeFallback = NotificationManagerF.getInstance().getVisibleToastCount(); + handleNotificationEvent(new ProofClosedNotificationEventF(proof)); + Platform.runLater(() -> { + boolean dialog = ProofClosedDialogF.anyShowing(); + boolean fallbackToast = + NotificationManagerF.getInstance().getVisibleToastCount() > beforeFallback; + lines.add("proof-closed: " + (dialog || fallbackToast ? "PASS" + : "FAIL: neither dialog nor fallback toast shown")); + + // 4. synthetic exception (Swing parity: ExceptionFailureEvent -> + // ExceptionFailureNotificationDialog; not registered by default, see the FIXME + // in setDefaultNotifications — registered here for the exercise, then removed) + addNotificationTask(new ExceptionFailureNotificationF()); + int beforeError = NotificationManagerF.getInstance().getVisibleToastCount(); + handleNotificationEvent(new ExceptionFailureEventF( + "Synthetic exception (notification self-test)", + new RuntimeException("notification self-test"))); + Platform.runLater(() -> { + lines.add("exception-failure: " + + (NotificationManagerF.getInstance().getVisibleToastCount() > beforeError + ? "PASS" + : "FAIL: error toast not shown")); + removeNotificationTask(NotificationEventIDF.EXCEPTION_CAUSED_FAILURE); + String report = String.join("\n", lines); + LOGGER.info("Notification verification report:\n{}", report); + boolean pass = report.lines().allMatch(line -> line.endsWith("PASS")); + // report visible like the other key.fx.verify.* self tests + NotificationManagerF.getInstance().notify("Notification verification: " + + (pass ? "PASS" : "FAIL") + " (" + report.replace("\n", "; ") + ")", + pass ? NotificationManagerF.Kind.INFO : NotificationManagerF.Kind.ERROR); + }); + }); + }); + } + + /** + * Listener section with inner classes used to receive KeY system events (Swing + * {@code NotificationManager.NotificationListener}). + */ + private class NotificationListenerF implements AutoModeListener { + + /** + * auto mode started + */ + @Override + public void autoModeStarted(ProofEvent e) { + autoMode = true; + } + + /** + * auto mode stopped + */ + @Override + public void autoModeStopped(ProofEvent e) { + autoMode = false; + } + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/NotificationEventIDF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/NotificationEventIDF.java new file mode 100644 index 00000000000..5cbddd044c6 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/NotificationEventIDF.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.notification; + +/** + * Identifiers for the kinds of events handled by the notification framework. + *

+ * Port of the Swing original {@code key.ui/src/main/java/de/uka/ilkd/key/gui/notification/ + * NotificationEventID.java}. The Swing enum lives in the {@code key.ui} module, which is no + * dependency of {@code key.ui.fx}, hence this FX-side copy (name parity deviation: {@code F} + * suffix, as for all ported FX classes). + */ +public enum NotificationEventIDF { + + /** tasks notifying about proof closed events have this ID */ + PROOF_CLOSED, + /** tasks notifying about abandoned tasks have this ID */ + TASK_ABANDONED, + /** tasks notifying about general failures */ + GENERAL_FAILURE, + /** tasks notifying the user when KeY is shutdown have this ID */ + EXIT_KEY, + /** tasks used to inform the user should have this ID */ + GENERAL_INFORMATION, + /** tasks used to inform the user about exceptions causing a failure have this ID */ + EXCEPTION_CAUSED_FAILURE +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/NotificationTaskF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/NotificationTaskF.java new file mode 100644 index 00000000000..2af72e46b65 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/NotificationTaskF.java @@ -0,0 +1,90 @@ +/* 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.notification; + +import java.util.ArrayList; +import java.util.List; +import javafx.application.Platform; + +import de.uka.ilkd.key.gui.fx.notification.events.NotificationEventF; + +/** + * A notification task maps a {@link NotificationEventF} to a list of actions to be performed + * when the event is encountered. + *

+ * Port of the Swing original {@code key.ui/src/main/java/de/uka/ilkd/key/gui/notification/ + * NotificationTask.java}. The EDT marshalling via {@code SwingUtilities.invokeLater} + * (NotificationTask.java:63) maps to {@link Platform#runLater(Runnable)}; like Swing, actions + * already invoked on the FX application thread run synchronously. + */ +public abstract class NotificationTaskF { + + /** + * the list of actions associated with this task + */ + private final List notificationActions = new ArrayList<>(5); + + /** + * @return returns the notification actions belonging to this task + */ + public List getNotificationActions() { + return notificationActions; + } + + /** + * adds a notification action to this task. + * + * @param action the NotificationActionF to be added + */ + public void addNotificationAction(NotificationActionF action) { + this.notificationActions.add(action); + } + + /** + * called to execute the notification task, but this method only takes care that we are on + * the FX application thread + * + * @param event the NotificationEventF triggering this task + * @param manager the NotificationCenterF to which this task belongs to + */ + public void execute(NotificationEventF event, NotificationCenterF manager) { + // if we are in automode execute task only if it is automode enabled + if (manager.inAutoMode() && !automodeEnabledTask()) { + return; + } + // notify thread safe + if (Platform.isFxApplicationThread()) { + executeActions(event, manager); + } else { + Platform.runLater(() -> executeActions(event, manager)); + } + } + + /** + * called to execute the notification task + * + * @param manager the NotificationCenterF to which this task belongs to + * @param event the NotificationEventF triggering this task + */ + protected void executeActions(NotificationEventF event, NotificationCenterF manager) { + for (final NotificationActionF action : getNotificationActions()) { + action.execute(event); + } + } + + /** + * @return the event id of this task + */ + public abstract NotificationEventIDF getEventID(); + + /** + * returns if this task should be executed in auto mode + * + * @return if true execute task even if in automode + */ + protected boolean automodeEnabledTask() { + return false; + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/ProofClosedDialogF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/ProofClosedDialogF.java new file mode 100644 index 00000000000..24cd73e70a6 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/ProofClosedDialogF.java @@ -0,0 +1,101 @@ +/* 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.notification; + +import java.util.ArrayList; +import java.util.List; +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.ScrollPane; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; + +import de.uka.ilkd.key.proof.Proof; +import de.uka.ilkd.key.proof.Statistics; + +import org.key_project.util.collection.Pair; + +/** + * A small non-modal window informing the user about a closed proof together with its final + * statistics. + *

+ * Port of the proof-closed display action of the Swing notification framework: {@code + * ProofClosedJTextPaneDisplay} (actions/ProofClosedJTextPaneDisplay.java) opens the + * {@code ShowProofStatistics.Window} ("Proof Statistics" dialog) for the closed proof. The FX + * port shows the same summary ({@link Statistics#getSummary()} plus the open-goal count) in a + * plain {@link VBox}; unlike Swing the window is not styled with the proof-tree font. + */ +public final class ProofClosedDialogF extends javafx.stage.Stage { + + /** the currently open proof-closed dialogs (self-test hook, see {@link #anyShowing()}) */ + private static final List OPEN_DIALOGS = new ArrayList<>(); + + private ProofClosedDialogF(Proof proof) { + setTitle("Proof closed"); + setResizable(false); + + Statistics stats = proof.getStatistics(); + int openGoals = proof.openGoals().size(); + + Label header = new Label("Proof " + proof.name() + " closed successfully."); + header.getStyleClass().add("notification-dialog-header"); + header.setWrapText(true); + header.setMaxWidth(360); + + VBox statsBox = new VBox(4); + statsBox.getChildren().add(new Label("Open goals: " + openGoals)); + List> summary = stats.getSummary(); + if (summary != null) { + for (Pair entry : summary) { + if ("".equals(entry.second)) { + statsBox.getChildren().add(new Label(entry.first)); + } else { + statsBox.getChildren().add(new Label(entry.first + ": " + entry.second)); + } + } + } + + Button close = new Button("Close"); + close.setOnAction(e -> hide()); + VBox.setVgrow(close, Priority.NEVER); + + ScrollPane scroll = new ScrollPane(statsBox); + scroll.setFitToWidth(true); + scroll.setPrefViewportHeight(Math.min(240, 40 + 22 * statsBox.getChildren().size())); + + VBox root = new VBox(12, header, scroll, close); + root.getStyleClass().add("notification-dialog"); + root.setPadding(new Insets(14)); + root.setAlignment(Pos.CENTER_LEFT); + + Scene scene = new Scene(root, 380, Math.min(480, 160 + 22 * statsBox.getChildren().size())); + setScene(scene); + } + + /** + * Shows the proof-closed dialog for the given proof (Swing parity: + * {@code ProofClosedJTextPaneDisplay.execute} opens the statistics window for the closed + * proof). The dialog is non-modal, like the Swing {@code ShowProofStatistics.Window}. + * + * @param proof the closed proof, must not be {@code null} + */ + public static void show(Proof proof) { + ProofClosedDialogF dialog = new ProofClosedDialogF(proof); + OPEN_DIALOGS.add(dialog); + dialog.setOnHidden(e -> OPEN_DIALOGS.remove(dialog)); + dialog.show(); + } + + /** + * @return whether any proof-closed dialog is currently visible (self-test hook used by + * {@code key.fx.verify.notifications}) + */ + static boolean anyShowing() { + return OPEN_DIALOGS.stream().anyMatch(javafx.stage.Stage::isShowing); + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/ProofClosedNotificationF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/ProofClosedNotificationF.java new file mode 100644 index 00000000000..7bfda6c0751 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/ProofClosedNotificationF.java @@ -0,0 +1,41 @@ +/* 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.notification; + +import de.uka.ilkd.key.gui.fx.notification.actions.ProofClosedDialogActionF; + +/** + * The proof closed notification notifies the user about a successful attempt closing a proof. + *

+ * Port of the Swing original {@code key.ui/src/main/java/de/uka/ilkd/key/gui/notification/ + * ProofClosedNotification.java}. Runs also during auto mode (Swing + * {@code automodeEnabledTask() == true}) since the automatic prover is usually what closes the + * proof. + */ +public class ProofClosedNotificationF extends NotificationTaskF { + + /** + * Creates a proof closed notification task with the FX proof-closed statistics dialog + * (Swing parity: the constructor adding {@code ProofClosedJTextPaneDisplay}). + */ + public ProofClosedNotificationF() { + addNotificationAction(new ProofClosedDialogActionF()); + } + + @Override + public NotificationEventIDF getEventID() { + return NotificationEventIDF.PROOF_CLOSED; + } + + /** + * returns if this task should be executed in auto mode + * + * @return if true execute task even if in automode + */ + @Override + protected boolean automodeEnabledTask() { + return true; + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/actions/ProofClosedDialogActionF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/actions/ProofClosedDialogActionF.java new file mode 100644 index 00000000000..fd15730dfec --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/actions/ProofClosedDialogActionF.java @@ -0,0 +1,39 @@ +/* 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.notification.actions; + +import de.uka.ilkd.key.gui.fx.notification.NotificationActionF; +import de.uka.ilkd.key.gui.fx.notification.NotificationManagerF; +import de.uka.ilkd.key.gui.fx.notification.ProofClosedDialogF; +import de.uka.ilkd.key.gui.fx.notification.events.NotificationEventF; +import de.uka.ilkd.key.gui.fx.notification.events.ProofClosedNotificationEventF; +import de.uka.ilkd.key.proof.Proof; + +/** + * Opens the {@link ProofClosedDialogF} (statistics summary) for a closed proof. + *

+ * Port of the Swing original {@code key.ui/src/main/java/de/uka/ilkd/key/gui/notification/ + * actions/ProofClosedJTextPaneDisplay.java}, which opens the {@code ShowProofStatistics.Window} + * for the closed proof. The "no proof" fallback (a Swing {@code JOptionPane} with "Proof Closed. + * No statistics available.", ProofClosedJTextPaneDisplay.java:70-78) maps to a toast, consistent + * with the FX toast-instead-of-modal-dialog deviation. + */ +public final class ProofClosedDialogActionF implements NotificationActionF { + + @Override + public boolean execute(NotificationEventF event) { + if (event instanceof ProofClosedNotificationEventF pcne) { + Proof proof = pcne.getProof(); + if (proof != null) { + ProofClosedDialogF.show(proof); + } else { + NotificationManagerF.getInstance() + .notify("Proof Closed. No statistics available.", + NotificationManagerF.Kind.INFO); + } + } + return true; + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/actions/ToastActionF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/actions/ToastActionF.java new file mode 100644 index 00000000000..5542010a49e --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/actions/ToastActionF.java @@ -0,0 +1,55 @@ +/* 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.notification.actions; + +import de.uka.ilkd.key.gui.fx.notification.NotificationActionF; +import de.uka.ilkd.key.gui.fx.notification.NotificationManagerF; +import de.uka.ilkd.key.gui.fx.notification.events.AbandonTaskEventF; +import de.uka.ilkd.key.gui.fx.notification.events.ExceptionFailureEventF; +import de.uka.ilkd.key.gui.fx.notification.events.ExitKeYEventF; +import de.uka.ilkd.key.gui.fx.notification.events.GeneralFailureEventF; +import de.uka.ilkd.key.gui.fx.notification.events.GeneralInformationEventF; +import de.uka.ilkd.key.gui.fx.notification.events.NotificationEventF; +import de.uka.ilkd.key.gui.fx.notification.events.ProofClosedNotificationEventF; +import de.uka.ilkd.key.proof.Proof; + +/** + * Displays a notification event as a toast via the {@link NotificationManagerF}. + *

+ * This is the FX counterpart of the Swing {@code JTextPane}/display actions of the + * {@code key.ui} notification package ({@code GeneralInformationJTextPaneDisplay}, + * {@code GeneralFailureJTextPaneDisplay}, {@code ExceptionFailureNotificationDialog}, + * {@code ShowDisplayPane}): the Swing originals show modal {@code JOptionPane} message dialogs, + * which the FX UI deliberately replaces by toasts (documented deviation, same as the Swing + * {@code popupWarning} mapping in {@code MainWindowF}). + */ +public final class ToastActionF implements NotificationActionF { + + @Override + public boolean execute(NotificationEventF event) { + NotificationManagerF manager = NotificationManagerF.getInstance(); + // ExceptionFailureEventF extends GeneralFailureEventF, so check it first + if (event instanceof ExceptionFailureEventF ex) { + manager.notify(ex.getErrorMessage(), NotificationManagerF.Kind.ERROR); + } else if (event instanceof GeneralFailureEventF failure) { + manager.notify(failure.getErrorMessage(), NotificationManagerF.Kind.ERROR); + } else if (event instanceof GeneralInformationEventF info) { + // Swing uses the event context (e.g. "Automated proof search") as dialog title + manager.notify(info.getContext() + ": " + info.getMessage(), + NotificationManagerF.Kind.INFO); + } else if (event instanceof ProofClosedNotificationEventF closed) { + Proof proof = closed.getProof(); + manager.notify("Proof closed: " + (proof == null ? "" : proof.name()), + NotificationManagerF.Kind.INFO); + } else if (event instanceof AbandonTaskEventF) { + manager.notify("Proof task abandoned.", NotificationManagerF.Kind.WARNING); + } else if (event instanceof ExitKeYEventF) { + manager.notify("KeY is shutting down.", NotificationManagerF.Kind.INFO); + } else { + manager.notify("Notification: " + event, NotificationManagerF.Kind.INFO); + } + return true; + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/events/AbandonTaskEventF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/events/AbandonTaskEventF.java new file mode 100644 index 00000000000..3b79250bf24 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/events/AbandonTaskEventF.java @@ -0,0 +1,25 @@ +/* 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.notification.events; + +import de.uka.ilkd.key.gui.fx.notification.NotificationEventIDF; + +/** + * Emitted after removing a proof task. + *

+ * Port of the Swing original {@code key.ui/src/main/java/de/uka/ilkd/key/gui/notification/ + * events/AbandonTaskEvent.java}. Note: the abandon-task action is not ported yet (parity audit + * {@code parity-mainwindow.md} gap 12), so no app-level trigger fires this event today; the + * class completes the framework for future wiring. + */ +public class AbandonTaskEventF extends NotificationEventF { + + /** + * creates an event + */ + public AbandonTaskEventF() { + super(NotificationEventIDF.TASK_ABANDONED); + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/events/ExceptionFailureEventF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/events/ExceptionFailureEventF.java new file mode 100644 index 00000000000..7b22787d5c4 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/events/ExceptionFailureEventF.java @@ -0,0 +1,31 @@ +/* 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.notification.events; + +import de.uka.ilkd.key.gui.fx.notification.NotificationEventIDF; + +/** + * A failure notification event caused by an exception. + *

+ * Port of the Swing original {@code key.ui/src/main/java/de/uka/ilkd/key/gui/notification/ + * events/ExceptionFailureEvent.java}. + *

+ * TODO-merge (seam agent, branch {@code weigl/ocfx-seam}): exception routing into this + * framework is owned by the exception-seam agent; do not wire report paths here yet — the + * framework side ({@code ExceptionFailureNotificationF}) is ready to receive the events. + */ +public class ExceptionFailureEventF extends GeneralFailureEventF { + + private final Throwable error; + + public ExceptionFailureEventF(String string, Throwable throwable) { + super(NotificationEventIDF.EXCEPTION_CAUSED_FAILURE); + this.error = throwable; + } + + public Throwable getException() { + return error; + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/events/ExitKeYEventF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/events/ExitKeYEventF.java new file mode 100644 index 00000000000..932e5e619c6 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/events/ExitKeYEventF.java @@ -0,0 +1,25 @@ +/* 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.notification.events; + +import de.uka.ilkd.key.gui.fx.notification.NotificationEventIDF; + +/** + * An exit key event indicating that KeY is currently shut down. + *

+ * Port of the Swing original {@code key.ui/src/main/java/de/uka/ilkd/key/gui/notification/ + * events/ExitKeYEvent.java}. Note: the FX exit flow itself is not ported yet (parity audit + * {@code parity-mainwindow.md} gap 5), so no app-level trigger fires this event today; the + * class completes the framework for future wiring. + */ +public class ExitKeYEventF extends NotificationEventF { + + /** + * creates an event fired when KeY is shutdown + */ + public ExitKeYEventF() { + super(NotificationEventIDF.EXIT_KEY); + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/events/GeneralFailureEventF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/events/GeneralFailureEventF.java new file mode 100644 index 00000000000..7d2dcca899b --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/events/GeneralFailureEventF.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.notification.events; + +import de.uka.ilkd.key.gui.fx.notification.NotificationEventIDF; + +/** + * A notification event caused by a general unexpected failure (usually caused by a bug of the + * system). + *

+ * Port of the Swing original {@code key.ui/src/main/java/de/uka/ilkd/key/gui/notification/ + * events/GeneralFailureEvent.java} (which is {@code @Deprecated} there in favor of + * {@code IssueDialog}; the FX {@code IssueDialogF} is a future dialog-catalog item, until then + * the framework keeps the same event shape). + */ +public class GeneralFailureEventF extends NotificationEventF { + + private String errorMessage = "Unknown Error."; + + protected GeneralFailureEventF(NotificationEventIDF id) { + super(id); + } + + /** + * creates an instance of this event + * + * @param errorMessage a String describing the failure + */ + public GeneralFailureEventF(String errorMessage) { + super(NotificationEventIDF.GENERAL_FAILURE); + this.errorMessage = errorMessage; + } + + /** + * @return the error message describing the reason for this event + */ + public String getErrorMessage() { + return errorMessage; + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/events/GeneralInformationEventF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/events/GeneralInformationEventF.java new file mode 100644 index 00000000000..e05842f1fdc --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/events/GeneralInformationEventF.java @@ -0,0 +1,60 @@ +/* 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.notification.events; + +import de.uka.ilkd.key.gui.fx.notification.NotificationEventIDF; + +/** + * If the system wants to inform the user it may emit this event. If the flavour of the + * information is that of an error message, one should use the + * {@link GeneralFailureEventF} instead. + *

+ * Port of the Swing original {@code key.ui/src/main/java/de/uka/ilkd/key/gui/notification/ + * events/GeneralInformationEvent.java}. + */ +public class GeneralInformationEventF extends NotificationEventF { + + /** the message with the information to be displayed */ + private final String informationMessage; + + /** the context/ category of this message (e.g. used as window title) */ + private final String context; + + /** + * Creates an event informing the user about the fact given as string + * + * @param context the String describing the context/category of the information (e.g. used as + * window title, head line etc.) + * @param informationMessage the String containing the information + */ + public GeneralInformationEventF(String context, String informationMessage) { + super(NotificationEventIDF.GENERAL_INFORMATION); + this.context = context; + this.informationMessage = informationMessage; + } + + /** + * Creates an event informing the user about the fact given as string + * + * @param informationMessage the String containing the information + */ + public GeneralInformationEventF(String informationMessage) { + this("Information", informationMessage); + } + + /** + * returns the context as string + */ + public String getContext() { + return context; + } + + /** + * returns the information as string + */ + public String getMessage() { + return informationMessage; + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/events/NotificationEventF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/events/NotificationEventF.java new file mode 100644 index 00000000000..579c4badbc7 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/events/NotificationEventF.java @@ -0,0 +1,39 @@ +/* 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.notification.events; + +import de.uka.ilkd.key.gui.fx.notification.NotificationEventIDF; + +/** + * A {@code NotificationEventF} is triggered if the system wants to notify the user about a + * certain situation. Each kind of event is assigned a unique id which is declared in + * {@link NotificationEventIDF}. + *

+ * Port of the Swing original {@code key.ui/src/main/java/de/uka/ilkd/key/gui/notification/ + * events/NotificationEvent.java}. + */ +public abstract class NotificationEventF { + + /** the unique id identifying the kind of this event */ + private final NotificationEventIDF eventID; + + /** + * creates an instance of this event + * + * @param eventID the id identifying the kind of this event + * @see NotificationEventIDF + */ + protected NotificationEventF(NotificationEventIDF eventID) { + this.eventID = eventID; + } + + /** + * @return returns the eventID + * @see NotificationEventIDF + */ + public NotificationEventIDF getEventID() { + return eventID; + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/events/ProofClosedNotificationEventF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/events/ProofClosedNotificationEventF.java new file mode 100644 index 00000000000..38643ed5ac3 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/events/ProofClosedNotificationEventF.java @@ -0,0 +1,35 @@ +/* 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.notification.events; + +import de.uka.ilkd.key.gui.fx.notification.NotificationEventIDF; +import de.uka.ilkd.key.proof.Proof; + +/** + * NotificationEvent used to inform the user about a closed proof. + *

+ * Port of the Swing original {@code key.ui/src/main/java/de/uka/ilkd/key/gui/notification/ + * events/ProofClosedNotificationEvent.java}. + */ +public class ProofClosedNotificationEventF extends NotificationEventF { + + /** the closed proof */ + private final Proof proof; + + /** + * creates a proof closed notification event + */ + public ProofClosedNotificationEventF(Proof proof) { + super(NotificationEventIDF.PROOF_CLOSED); + this.proof = proof; + } + + /** + * @return the proof that has been closed + */ + public Proof getProof() { + return proof; + } +} From fba4be09babca62b4eedff606f6d346ce5510075 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Thu, 8 Oct 2026 21:26:45 +0200 Subject: [PATCH 048/281] key.ui.fx: notification: wire app-level triggers + key.fx.verify.notifications self test MainWindowF (minimal, // notification: marked): - register the framework auto-mode tracker on the proof control after attach (Swing parity: NotificationManager constructor, NotificationManager.java:99-101) - fire proof-closed + "Automated proof search" notifications at the auto-mode stop hook refreshViewsFromFinalState (covers interactive and demo-live runs; Swing parity: KeYMediator.proofClosed / WUIC.taskFinishedInternal, gated by ViewSettings.notificationAfterMacro) - load-failure path carries a TODO-merge marker for the seam agent's exception routing (ExceptionFailureEventF -> NotificationCenterF) - key.fx.verify.notifications=1 hook runs NotificationCenterF.verifyNotifications (framework semantics + one PASS/FAIL line per type, report logged and toasted) NotificationManagerF: getVisibleToastCount() self-test accessor. CSS: appended .notification-dialog styles to key-light.css and key-dark.css (existing -key-* variables only). build.gradle: forward key.fx.verify.notifications to the run task. --- key.ui.fx/build.gradle | 6 +++- .../de/uka/ilkd/key/gui/fx/MainWindowF.java | 24 ++++++++++++++++ .../fx/notification/NotificationManagerF.java | 8 ++++++ .../de/uka/ilkd/key/gui/fx/theme/key-dark.css | 28 +++++++++++++++++++ .../uka/ilkd/key/gui/fx/theme/key-light.css | 28 +++++++++++++++++++ 5 files changed, 93 insertions(+), 1 deletion(-) diff --git a/key.ui.fx/build.gradle b/key.ui.fx/build.gradle index 86152523631..311e13d3624 100644 --- a/key.ui.fx/build.gradle +++ b/key.ui.fx/build.gradle @@ -65,11 +65,15 @@ run { // Hide/Regroup) with the key.fx.verify.sequentsearch query after the demo load) // * key.fx.verify.updatehighlight = any (M2: run the update-highlight overlay self test; // needs a sequent printing update operators, e.g. standard_key/queries/useQuery.key) + // * key.fx.verify.notifications = any (run the notification framework self test after the + // demo load: framework semantics plus one PASS/FAIL line per type (task-finished, + // proof-closed, synthetic exception); report logged and toasted) ["key.fx.driver", "key.fx.theme", "key.fx.demo.sequent", "key.fx.verify.sequent", "key.fx.show", "key.fx.demo.autoprove", "key.fx.verify.tree", "key.fx.demo.autoprove.live", "key.fx.demo.maxsteps", "key.fx.verify.goallist", "key.fx.verify.search", "key.fx.verify.sequentsearch", "key.fx.verify.treefilters", - "key.fx.verify.sequentsearchmodes", "key.fx.verify.updatehighlight"] + "key.fx.verify.sequentsearchmodes", "key.fx.verify.updatehighlight", + "key.fx.verify.notifications"] .each { name -> def value = System.getProperty(name) if (value != null) { 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 index 8a46e0ba13f..2abf2ab8f44 100644 --- 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 @@ -64,6 +64,7 @@ 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.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.proofdiff.ProofDiffFrameF; @@ -421,6 +422,11 @@ protected KeYEnvironment call() throws Exception { // the UI's own listener refreshes the views after interactive auto mode runs mediator.attach(env.getProofControl()); 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()); // 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. @@ -482,12 +488,22 @@ protected KeYEnvironment call() throws Exception { .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.demo.autoprove.live") != null) { startLiveAutoMode(env); } }); loadTask.setOnFailed(event -> { Throwable error = loadTask.getException(); + // notification: TODO-merge wire into WindowUserInterfaceControlF — the seam agent + // routes exceptions through NotificationCenterF.handleNotificationEvent( + // new ExceptionFailureEventF(...)) there (Swing parity: + // IssueDialog.showExceptionDialog) LOGGER.error((demo ? "Demo proof" : "Proof") + " loading failed", error); NotificationManagerF.getInstance() .notify((demo ? "Demo proof" : "Proof") + " loading failed: " @@ -958,6 +974,11 @@ private void refreshViewsFromFinalState() { 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()); } /** @@ -996,6 +1017,9 @@ public void autoModeStopped(ProofEvent e) { } 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"); }); } diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/NotificationManagerF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/NotificationManagerF.java index a1c341368e2..74e83d26ac3 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/NotificationManagerF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/NotificationManagerF.java @@ -126,4 +126,12 @@ private void dismiss(Label toast) { fade.play(); } } + + /** + * @return the number of currently visible toasts (self-test hook used by the + * {@code key.fx.verify.notifications} verification) + */ + public int getVisibleToastCount() { + return toastBox.getChildren().size(); + } } diff --git a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css index 173dd4a912f..dc3025aebbd 100644 --- a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css +++ b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css @@ -607,3 +607,31 @@ .sequent-search-mode { -fx-padding: 1; } + +/* proof-closed notification dialog (port of the Swing ProofClosedJTextPaneDisplay dialog) */ +.notification-dialog { + -fx-background-color: -key-background; +} + +.notification-dialog-header { + -fx-font-size: 1.1em; + -fx-text-fill: -key-text; + -fx-padding: 0 0 4 0; +} + +/* notification framework proof-closed dialog (Swing ProofClosedJTextPaneDisplay -> + * ShowProofStatistics.Window port; FX ProofClosedDialogF) */ +.notification-dialog { + -fx-background-color: -key-surface; + -fx-border-color: -key-border; + -fx-text-fill: -key-text; +} + +.notification-dialog .label { + -fx-text-fill: -key-text; +} + +.notification-dialog-header { + -fx-text-fill: -key-success; + -fx-font-weight: bold; +} diff --git a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css index 2962a642bf4..a0e9c14abf6 100644 --- a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css +++ b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css @@ -601,3 +601,31 @@ .sequent-search-mode { -fx-padding: 1; } + +/* proof-closed notification dialog (port of the Swing ProofClosedJTextPaneDisplay dialog) */ +.notification-dialog { + -fx-background-color: -key-background; +} + +.notification-dialog-header { + -fx-font-size: 1.1em; + -fx-text-fill: -key-text; + -fx-padding: 0 0 4 0; +} + +/* notification framework proof-closed dialog (Swing ProofClosedJTextPaneDisplay -> + * ShowProofStatistics.Window port; FX ProofClosedDialogF) */ +.notification-dialog { + -fx-background-color: -key-surface; + -fx-border-color: -key-border; + -fx-text-fill: -key-text; +} + +.notification-dialog .label { + -fx-text-fill: -key-text; +} + +.notification-dialog-header { + -fx-text-fill: -key-success; + -fx-font-weight: bold; +} From 04a2a68924a4a75814f66c86f1cb7ad3204f24bf Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Thu, 8 Oct 2026 21:30:50 +0200 Subject: [PATCH 049/281] key.ui.fx: docking: tab context menu, maximize, named layout slots with F10-F12/Ctrl+F10, docking self test --- key.ui.fx/build.gradle | 5 +- .../de/uka/ilkd/key/gui/fx/MainWindowF.java | 29 +- .../key/gui/fx/docking/DockLayoutStore.java | 111 +++++-- .../ilkd/key/gui/fx/docking/DockTabPane.java | 93 +++++- .../key/gui/fx/docking/DockTitleActionF.java | 37 +++ .../key/gui/fx/docking/DockWorkspace.java | 264 +++++++++++++++- .../uka/ilkd/key/gui/fx/docking/Dockable.java | 14 + .../key/gui/fx/docking/DockingLayoutF.java | 282 ++++++++++++++++++ .../fx/keyshortcuts/KeyStrokeManagerF.java | 12 + 9 files changed, 810 insertions(+), 37 deletions(-) create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/docking/DockTitleActionF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/docking/DockingLayoutF.java diff --git a/key.ui.fx/build.gradle b/key.ui.fx/build.gradle index 86152523631..c3b5ff5a343 100644 --- a/key.ui.fx/build.gradle +++ b/key.ui.fx/build.gradle @@ -65,11 +65,14 @@ run { // Hide/Regroup) with the key.fx.verify.sequentsearch query after the demo load) // * key.fx.verify.updatehighlight = any (M2: run the update-highlight overlay self test; // needs a sequent printing update operators, e.g. standard_key/queries/useQuery.key) + // * key.fx.verify.docking = any (docking: run the docking interaction self test (named + // layout slots, maximize/restore, close-while-maximized) after the demo load) ["key.fx.driver", "key.fx.theme", "key.fx.demo.sequent", "key.fx.verify.sequent", "key.fx.show", "key.fx.demo.autoprove", "key.fx.verify.tree", "key.fx.demo.autoprove.live", "key.fx.demo.maxsteps", "key.fx.verify.goallist", "key.fx.verify.search", "key.fx.verify.sequentsearch", "key.fx.verify.treefilters", - "key.fx.verify.sequentsearchmodes", "key.fx.verify.updatehighlight"] + "key.fx.verify.sequentsearchmodes", "key.fx.verify.updatehighlight", + "key.fx.verify.docking"] .each { name -> def value = System.getProperty(name) if (value != null) { 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 index 8a46e0ba13f..aa588811cf4 100644 --- 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 @@ -58,6 +58,7 @@ 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.SimpleDockable; import de.uka.ilkd.key.gui.fx.fonticons.IconFactoryF; import de.uka.ilkd.key.gui.fx.goallist.GoalListViewF; @@ -122,6 +123,10 @@ public final class MainWindowF { 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(); @@ -225,6 +230,8 @@ public void selectedNodeChanged(KeYSelectionEvent ev public MainWindowF(Stage stage) { 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); } /** @@ -249,6 +256,9 @@ public void initialize() { // 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 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); stage.setScene(scene); stage.show(); @@ -274,6 +284,13 @@ public DockWorkspace getWorkspace() { return workspace; } + /** + * @return the docking layout store (docking: needed by the {@link DockingLayoutF} slots) + */ + public DockLayoutStore getLayoutStore() { + return layoutStore; + } + /** * @return the registered dockables (id → dockable) */ @@ -482,6 +499,13 @@ protected KeYEnvironment call() throws Exception { .notify("Proof diff verification: " + report, report.endsWith("PASS") ? Kind.INFO : Kind.ERROR); } + 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); + } if (System.getProperty("key.fx.demo.autoprove.live") != null) { startLiveAutoMode(env); } @@ -1191,7 +1215,10 @@ private Menu buildViewMenu() { view.getItems().addAll(prettyPrint, unicode, syntaxHighlighting, new SeparatorMenuItem(), themeMenu, fontSize, new SeparatorMenuItem(), menuItem("Visual Node Diff", "de.uka.ilkd.key.gui.proofdiff.ProofDiffFrame$Action", - this::showProofDiffFrame)); + this::showProofDiffFrame), + new SeparatorMenuItem(), + // docking: named layout slots (Swing DockingLayout, menu path View > Layout) + dockingLayout.layoutMenu()); return view; } diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/docking/DockLayoutStore.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/docking/DockLayoutStore.java index 5164031052b..19bd2e34496 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/docking/DockLayoutStore.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/docking/DockLayoutStore.java @@ -11,6 +11,7 @@ import java.util.EnumMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Properties; /** @@ -21,13 +22,27 @@ * {@code key.ui}; the file format is deliberately simple and self-contained. The actual * sequence of "which dockable belongs to which role" is the persisted content; divider positions * and floating windows are not persisted (yet). + *

+ * Beyond the plain (last-state) layout the store keeps named layout slots — the counter + * part of the Swing {@code DockingLayout} arrangements {@code Default}/{@code Slot 1}/{@code + * Slot 2}, which the user fills via {@code View > Layout > Save ...} (Swing {@code + * SaveLayoutAction}: {@code CControl.save(layoutName)}) and recalls via {@code Load ...} ({@code + * CControl.load(layoutName)}). The slots share the properties file, their keys are prefixed with + * {@code dock.slot..}. */ public final class DockLayoutStore { /** The name of the properties file inside the config directory. */ public static final String FILE_NAME = "layout.properties"; + /** + * The name of the slot applied at startup (Swing {@code DockingLayout.LAYOUT_NAMES[0]}: if + * the user ever saved the {@code Default} arrangement, the UI starts with it). + */ + public static final String DEFAULT_SLOT = "Default"; + private static final String KEY_PREFIX = "dock."; + private static final String SLOT_PREFIX = "slot."; private final Path file; @@ -48,46 +63,67 @@ public Path file() { } /** - * Stores the open dockables per role. + * Stores the open dockables per role, preserving any named layout slots already saved in the + * file (read-modify-write — the Swing {@code CControl.writeXML} keeps all saved layouts, too). * * @param openDockables the ordered dockable ids per role * @throws IOException on I/O errors */ public void save(Map> openDockables) throws IOException { - if (file.getParent() != null) { - Files.createDirectories(file.getParent()); - } - Properties properties = new Properties(); + Properties properties = readProperties(); for (DockLocation location : DockLocation.values()) { List ids = openDockables.getOrDefault(location, List.of()); properties.setProperty(KEY_PREFIX + location.name(), String.join(",", ids)); } - try (var out = Files.newOutputStream(file)) { - properties.store(out, "KeY JavaFX UI docking layout"); - } + writeProperties(properties); } /** - * Loads the stored layout. + * Stores the given arrangement in the named layout slot, preserving the rest of the file + * (Swing {@code SaveLayoutAction.actionPerformed}: {@code mainWindow.getDockControl() + * .save(layoutName)}). * - * @return the ordered dockable ids per role; roles absent from the file (or missing file) - * yield empty lists + * @param name the slot name ({@code Default}, {@code Slot 1}, ...) + * @param openDockables the ordered dockable ids per role * @throws IOException on I/O errors */ - public Map> load() throws IOException { - EnumMap> result = new EnumMap<>(DockLocation.class); + public void saveSlot(String name, Map> openDockables) + throws IOException { + Properties properties = readProperties(); for (DockLocation location : DockLocation.values()) { - result.put(location, List.of()); + List ids = openDockables.getOrDefault(location, List.of()); + properties.setProperty(KEY_PREFIX + SLOT_PREFIX + name + "." + location.name(), + String.join(",", ids)); } + writeProperties(properties); + } + + /** + * Loads the named layout slot, if the user ever saved one (Swing {@code LoadLayoutAction} + * checks {@code CControl.layouts()} for the name before loading; the status line reports + * "Layout <name> could not be found." otherwise). + * + * @param name the slot name + * @return the ordered dockable ids per role, or empty if the slot was never saved + * @throws IOException on I/O errors + */ + public Optional>> loadSlot(String name) throws IOException { if (!Files.isRegularFile(file)) { - return result; + return Optional.empty(); } - Properties properties = new Properties(); - try (var in = Files.newInputStream(file)) { - properties.load(in); + Properties properties = readProperties(); + // a slot is "defined" once its save wrote its keys (all three role keys are always written) + if (properties.getProperty(KEY_PREFIX + SLOT_PREFIX + name + "." + + DockLocation.LEFT.name()) == null) { + return Optional.empty(); } + return Optional.of(parse(properties, KEY_PREFIX + SLOT_PREFIX + name + ".")); + } + + private Map> parse(Properties properties, String keyPrefix) { + EnumMap> result = new EnumMap<>(DockLocation.class); for (DockLocation location : DockLocation.values()) { - String value = properties.getProperty(KEY_PREFIX + location.name(), ""); + String value = properties.getProperty(keyPrefix + location.name(), ""); List ids = Arrays.stream(value.split(",")).map(String::trim) .filter(s -> !s.isEmpty()).toList(); result.put(location, ids); @@ -95,6 +131,43 @@ public Map> load() throws IOException { return result; } + private Properties readProperties() throws IOException { + Properties properties = new Properties(); + if (Files.isRegularFile(file)) { + try (var in = Files.newInputStream(file)) { + properties.load(in); + } + } + return properties; + } + + private void writeProperties(Properties properties) throws IOException { + if (file.getParent() != null) { + Files.createDirectories(file.getParent()); + } + try (var out = Files.newOutputStream(file)) { + properties.store(out, "KeY JavaFX UI docking layout"); + } + } + + /** + * Loads the stored (last-state) layout. + * + * @return the ordered dockable ids per role; roles absent from the file (or missing file) + * yield empty lists + * @throws IOException on I/O errors + */ + public Map> load() throws IOException { + if (!Files.isRegularFile(file)) { + EnumMap> result = new EnumMap<>(DockLocation.class); + for (DockLocation location : DockLocation.values()) { + result.put(location, List.of()); + } + return result; + } + return parse(readProperties(), KEY_PREFIX); + } + /** * Deletes the persisted layout, if present. * diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/docking/DockTabPane.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/docking/DockTabPane.java index 107db85de28..c305fd1beaf 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/docking/DockTabPane.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/docking/DockTabPane.java @@ -8,10 +8,14 @@ import java.util.Map; import java.util.Optional; import java.util.function.Consumer; +import java.util.function.Function; import javafx.scene.Node; +import javafx.scene.Parent; +import javafx.scene.control.ContextMenu; import javafx.scene.control.Tab; import javafx.scene.control.TabPane; +import javafx.scene.input.MouseButton; import javafx.scene.layout.StackPane; /** @@ -28,9 +32,73 @@ public class DockTabPane extends TabPane { private Consumer onDockableClosed = dockable -> { }; + /** + * Invoked when the user double-clicks a tab (Swing bibliothek: the default {@code + * LocationModeManager.DOUBLE_CLICK_STRATEGY} switches the double-clicked dockable between + * {@code ExtendedMode.NORMALIZED} and {@code ExtendedMode.MAXIMIZED}). + */ + private Consumer onMaximizeRequested = dockable -> { + }; + + /** + * Builds the context menu shown on a right-click into a tab (Swing bibliothek: the title + * popup menu with the close/maximize/externalize actions plus the dockable's title actions). + */ + private Function contextMenuFactory = dockable -> null; + public DockTabPane() { getStyleClass().add("key-dock-tab-pane"); setTabDragPolicy(TabDragPolicy.REORDER); + // Swing bibliothek: a double click on the dockable (title or content — the + // DoubleClickController listens on the whole dockable) switches between the normalized + // and the maximized mode (LocationModeManager.DOUBLE_CLICK_STRATEGY) + setOnMouseClicked(event -> { + if (event.getClickCount() == 2 && event.getButton() == MouseButton.PRIMARY) { + Tab selected = getSelectionModel().getSelectedItem(); + if (selected instanceof DockTab dockTab) { + onMaximizeRequested.accept(dockTab.dockable); + } + } + }); + // Swing bibliothek: a right click on the title opens the action popup menu; a click on + // another tab header addresses that dockable, a click on the content the selected one + setOnContextMenuRequested(event -> { + Tab hit = tabHeaderAt(event.getScreenX(), event.getScreenY()); + Tab tab = hit != null ? hit : getSelectionModel().getSelectedItem(); + if (tab instanceof DockTab dockTab) { + getSelectionModel().select(dockTab); + ContextMenu menu = contextMenuFactory.apply(dockTab.dockable); + if (menu != null) { + menu.show(this, event.getScreenX(), event.getScreenY()); + event.consume(); + } + } + }); + } + + /** + * @param screenX the screen x coordinate + * @param screenY the screen y coordinate + * @return the tab whose header contains the given screen position, or {@code null} (the tab + * header regions live in the skin; the lookup is defensive and yields {@code null} + * whenever the skin structure differs) + */ + private Tab tabHeaderAt(double screenX, double screenY) { + if (!(lookup(".tab-header-area > .headers-region") instanceof Parent headersRegion)) { + return null; + } + int index = 0; + for (Node header : headersRegion.getChildrenUnmodifiable()) { + if (!header.getStyleClass().contains("tab")) { + continue; + } + var bounds = header.localToScreen(header.getBoundsInLocal()); + if (bounds != null && bounds.contains(screenX, screenY)) { + return index < getTabs().size() ? getTabs().get(index) : null; + } + index++; + } + return null; } /** @@ -44,6 +112,27 @@ public void setOnDockableClosed(Consumer handler) { } : handler; } + /** + * Registers the handler invoked when the user double-clicks a tab (toggle maximize; see the + * bibliothek double-click strategy cited above). {@link DockWorkspace} installs the handler. + * + * @param handler the handler, or {@code null} to disable + */ + public void setOnMaximizeRequested(Consumer handler) { + this.onMaximizeRequested = handler == null ? dockable -> { + } : handler; + } + + /** + * Registers the factory building the context menu of the tabs. {@link DockWorkspace} installs + * the factory. + * + * @param factory the factory, or {@code null} to disable the context menu + */ + public void setContextMenuFactory(Function factory) { + this.contextMenuFactory = factory == null ? dockable -> null : factory; + } + /** * @param id the dockable id * @return whether a tab for the given dockable is present @@ -69,7 +158,7 @@ public boolean addDockable(Dockable dockable) { if (tabByDockableId.containsKey(dockable.getId())) { return false; } - DockTab tab = new DockTab(dockable); + DockTab tab = new DockTab(dockable, this); tab.setOnClosed(ignored -> onDockableClosed.accept(dockable)); tabByDockableId.put(dockable.getId(), tab); getTabs().add(tab); @@ -131,7 +220,7 @@ private static final class DockTab extends Tab { private final Dockable dockable; - DockTab(Dockable dockable) { + DockTab(Dockable dockable, DockTabPane pane) { this.dockable = dockable; Node content = dockable.getContent(); setContent(content != null ? content : new StackPane()); diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/docking/DockTitleActionF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/docking/DockTitleActionF.java new file mode 100644 index 00000000000..8a103f2dbd1 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/docking/DockTitleActionF.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.docking; + +/** + * A custom action contributed by a {@link Dockable} to its tab header / tab context menu. + *

+ * Counter-part of the Swing title actions of the module {@code key.ui}: {@code + * TabPanel.getTitleActions()} (plain Swing {@code Action}s, mapped to bibliothek + * {@code CButton}s by {@code DockingHelper.translateAction}) and {@code + * TabPanel.getTitleCActions()} (pre-built bibliothek {@code CAction}s). The Swing actions appear + * in the dockable's title bar and in its title popup menu; the JavaFX port shows them at the top + * of the tab context menu (see {@code DockWorkspace#createTabContextMenu}). + *

+ * The toggle flavour of the Swing mapping ({@code DockingHelper.createCheckBox} for actions with + * {@code Action.SELECTED_KEY}) is not needed yet and intentionally left out; add a boolean field + * when the first check-style title action (e.g. the KeyboardTaclet options) is ported. + * + * @param text the menu label (Swing {@code Action.NAME}) + * @param tooltip the description (Swing {@code Action.SHORT_DESCRIPTION}; unused in the menu, kept + * for future tab-header buttons) + * @param action the action to run (Swing {@code Action.actionPerformed}) + */ +public record DockTitleActionF(String text, String tooltip, Runnable action) { + + /** + * Creates a title action without a tooltip. + * + * @param text the menu label + * @param action the action to run + */ + public DockTitleActionF(String text, Runnable action) { + this(text, null, action); + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/docking/DockWorkspace.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/docking/DockWorkspace.java index c4f5f1f5381..31fcf2dc6ce 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/docking/DockWorkspace.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/docking/DockWorkspace.java @@ -16,12 +16,18 @@ import javafx.scene.Node; import javafx.scene.Parent; import javafx.scene.Scene; +import javafx.scene.control.ContextMenu; +import javafx.scene.control.MenuItem; +import javafx.scene.control.SeparatorMenuItem; import javafx.scene.control.SplitPane; import javafx.scene.layout.StackPane; import javafx.stage.Stage; import de.uka.ilkd.key.gui.fx.theme.ThemeManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + /** * The docking workspace of the JavaFX UI: three role areas (LEFT / MAIN / RIGHT) à la * {@code DockingHelper}'s {@code CGrid} roles of the Swing module {@code key.ui}, realized with @@ -37,6 +43,8 @@ */ public final class DockWorkspace { + private static final Logger LOGGER = LoggerFactory.getLogger(DockWorkspace.class); + /** * An entry of the factory-default layout: open the given dockable in the given role. * @@ -68,6 +76,21 @@ public interface DockableFactory { private List defaultLayout = List.of(); + /** + * The dockable currently shown in the maximized mode (bibliothek {@code + * ExtendedMode.MAXIMIZED}: the dockable fills the whole content area while the others keep + * their positions and are merely hidden), or {@code null} if the workspace is not maximized. + */ + private Dockable maximizedDockable; + + /** + * The arrangement hidden by the maximized dockable: the open dockables per role and the + * selected dockable per role, restored by {@link #restoreMaximized()} (bibliothek keeps the + * "last maximized location" of a dockable in {@code MaximizedMode}). + */ + private EnumMap> preMaximizedDockables; + private EnumMap preMaximizedSelection; + /** * Creates an empty workspace with three role areas. */ @@ -76,6 +99,10 @@ public DockWorkspace() { for (DockLocation location : DockLocation.values()) { DockTabPane pane = new DockTabPane(); pane.setOnDockableClosed(this::close); + // docking interactions ported from the Swing bibliothek framework: + // double-click toggles maximize, the right-click popup offers the title actions + pane.setOnMaximizeRequested(this::toggleMaximize); + pane.setContextMenuFactory(this::createTabContextMenu); panes.put(location, pane); ObservableList list = FXCollections.observableArrayList(); list.addListener(dockablesChanged(location)); @@ -158,6 +185,12 @@ public Optional locationOf(String id) { public void open(Dockable dockable, DockLocation location) { String id = dockable.getId(); + // bibliothek: making another dockable visible while one is maximized returns the + // workspace to the normalized state (the maximized area covers the others otherwise) + if (maximizedDockable != null && !maximizedDockable.getId().equals(id)) { + restoreMaximized(); + } + Stage floatStage = floatStages.remove(id); if (floatStage != null) { floatStage.close(); @@ -190,6 +223,11 @@ public void close(Dockable dockable) { * @param id the dockable id */ public void close(String id) { + // bibliothek: closing the maximized dockable returns the workspace to the previous + // arrangement (the hidden dockables reappear, the closed one is then removed below) + if (maximizedDockable != null && maximizedDockable.getId().equals(id)) { + restoreMaximized(); + } Stage floatStage = floatStages.remove(id); if (floatStage != null) { floatStage.close(); @@ -217,6 +255,10 @@ public void select(Dockable dockable) { */ public void floatDockable(Dockable dockable) { String id = dockable.getId(); + // bibliothek: externalizing returns the workspace to the normalized state first + if (maximizedDockable != null && !maximizedDockable.getId().equals(id)) { + restoreMaximized(); + } locationOf(id).ifPresent(location -> dockables.get(location).remove(dockable)); Stage floatStage = floatStages.get(id); @@ -244,6 +286,149 @@ public void floatDockable(Dockable dockable) { stage.show(); } + // ------------------------------------------------------------------ + // maximize (Swing bibliothek: ExtendedMode.MAXIMIZED / CMaximizeAction) + // ------------------------------------------------------------------ + + /** + * @param id the dockable id + * @return whether the dockable with the given id currently fills the whole workspace (the + * bibliothek {@code ExtendedMode.MAXIMIZED} state) + */ + public boolean isMaximized(String id) { + return maximizedDockable != null && maximizedDockable.getId().equals(id); + } + + /** + * @return the dockable currently shown maximized, if any + */ + public Optional maximized() { + return Optional.ofNullable(maximizedDockable); + } + + /** + * Toggles the maximized state of the given dockable (the bibliothek default double-click + * strategy and {@code CControl.KEY_MAXIMIZE_CHANGE} switch between the normalized and the + * maximized mode). + * + * @param dockable the dockable to toggle + */ + public void toggleMaximize(Dockable dockable) { + if (isMaximized(dockable.getId())) { + restoreMaximized(); + } else { + maximize(dockable); + } + } + + /** + * Maximizes the given dockable: it fills the whole workspace while the other dockables are + * hidden (their arrangement is remembered and brought back by {@link #restoreMaximized()}, + * like the bibliothek {@code CMaximizeAction} "Maximize" / {@code maximize.in}). Floating + * dockables are not maximizable (bibliothek: the maximized mode is not available for + * externalized dockables). + * + * @param dockable the dockable to maximize + */ + public void maximize(Dockable dockable) { + if (maximizedDockable == dockable) { + return; + } + if (locationOf(dockable.getId()).isEmpty()) { + LOGGER.info("Cannot maximize floating dockable {}", dockable.getId()); + return; + } + if (maximizedDockable != null) { + restoreMaximized(); // maximize the other dockable from the restored arrangement + } + snapshotState(); + String id = dockable.getId(); + for (DockLocation location : DockLocation.values()) { + dockables.get(location).removeIf(d -> !d.getId().equals(id)); + } + select(dockable); + maximizedDockable = dockable; + LOGGER.info("Dockable '{}' maximized", id); + } + + /** + * Restores the arrangement hidden by {@link #maximize(Dockable)} (bibliothek {@code + * CNormalizeAction}/{@code maximize.out} "Return": restores the former state). Does nothing + * if the workspace is not maximized. + */ + public void restoreMaximized() { + if (maximizedDockable == null) { + return; + } + maximizedDockable = null; + closeAllFloating(); + for (DockLocation location : DockLocation.values()) { + dockables.get(location).clear(); + for (Dockable dockable : preMaximizedDockables.get(location)) { + dockables.get(location).add(dockable); + } + } + for (DockLocation location : DockLocation.values()) { + String selected = preMaximizedSelection.get(location); + if (selected != null) { + panes.get(location).select(selected); + } + } + LOGGER.info("Maximized state restored"); + } + + private void snapshotState() { + preMaximizedDockables = new EnumMap<>(DockLocation.class); + preMaximizedSelection = new EnumMap<>(DockLocation.class); + for (DockLocation location : DockLocation.values()) { + preMaximizedDockables.put(location, List.copyOf(dockables.get(location))); + preMaximizedSelection.put(location, + panes.get(location).getSelectedDockable().map(Dockable::getId).orElse(null)); + } + } + + // ------------------------------------------------------------------ + // tab context menu (the bibliothek title popup menu) + // ------------------------------------------------------------------ + + /** + * Builds the context menu of a dock tab, the counter-part of the bibliothek title popup + * menu: the dockable's custom title actions (Swing {@code DockingHelper.getTitleActions()}), + * then the framework actions Maximize (or Return in the maximized state), + * Externalize and Close (labels from the bibliothek resource bundle + * {@code common.properties}: {@code maximize.in}/{@code maximize.out}, {@code + * externalize.in}, {@code preference.shortcut.close.label}). Swing offers no "close others" + * action and no Minimize equivalent exists in the FX role layout (the bibliothek + * minimized bar has no port); both are omitted on purpose. + * + * @param dockable the dockable owning the clicked tab + * @return the context menu to show + */ + private ContextMenu createTabContextMenu(Dockable dockable) { + ContextMenu menu = new ContextMenu(); + for (DockTitleActionF action : dockable.getTitleActions()) { + MenuItem item = new MenuItem(action.text()); + item.setOnAction(e -> action.action().run()); + menu.getItems().add(item); + } + if (!dockable.getTitleActions().isEmpty()) { + menu.getItems().add(new SeparatorMenuItem()); + } + MenuItem maximizeItem = new MenuItem(isMaximized(dockable.getId()) ? "Return" : "Maximize"); + maximizeItem.setOnAction(e -> toggleMaximize(dockable)); + MenuItem externalizeItem = new MenuItem("Externalize"); + externalizeItem.setOnAction(e -> floatDockable(dockable)); + MenuItem closeItem = new MenuItem("Close"); + closeItem.setDisable(!dockable.isClosable()); + closeItem.setOnAction(e -> close(dockable)); + menu.getItems().addAll(maximizeItem, externalizeItem, closeItem); + return menu; + } + + // ------------------------------------------------------------------ + // named layout slots (Swing DockingLayout save/load actions) + // ------------------------------------------------------------------ + /** * Sets the factory-default layout used by {@link #restoreFactoryDefault()}. * @@ -274,44 +459,95 @@ public void restoreFactoryDefault() { } /** - * Persists the current layout through the given store. - * - * @param store the store to write to - * @throws IOException on I/O errors + * @return the ids of the currently open dockables per role (a snapshot of the model, e.g. + * for persisting the layout or comparing arrangements) */ - public void saveLayout(DockLayoutStore store) throws IOException { + public Map> snapshotIds() { EnumMap> ids = new EnumMap<>(DockLocation.class); for (DockLocation location : DockLocation.values()) { ids.put(location, dockables.get(location).stream().map(Dockable::getId).toList()); } - store.save(ids); + return ids; + } + + /** + * Persists the current layout through the given store. + * + * @param store the store to write to + * @throws IOException on I/O errors + */ + public void saveLayout(DockLayoutStore store) throws IOException { + store.save(snapshotIds()); } /** - * Restores a previously persisted layout. If the store contains no open dockables, the - * factory-default layout is opened instead. + * Saves the current arrangement into the named layout slot (Swing {@code SaveLayoutAction}: + * {@code CControl.save(layoutName)}). + * + * @param store the store to write to + * @param name the slot name ({@code Default}, {@code Slot 1}, ...) + * @throws IOException on I/O errors + */ + public void saveSlot(DockLayoutStore store, String name) throws IOException { + store.saveSlot(name, snapshotIds()); + } + + /** + * Restores a previously persisted layout. If neither the {@code Default} slot nor the + * last-state layout contains open dockables, the factory-default layout is opened instead. + *

+ * Like the Swing {@code DockingLayout.init}, the startup prefers the {@code Default} slot if + * the user ever saved one ({@code setLayout(LAYOUT_NAMES[0])} applies it when defined); the + * fallback to the plain last-state layout is the pre-existing FX behavior. * * @param store the store to read from * @param factory recreates dockables from their persisted ids * @throws IOException on I/O errors */ public void restoreLayout(DockLayoutStore store, DockableFactory factory) throws IOException { - Map> saved = store.load(); + Optional>> savedSlot = + store.loadSlot(DockLayoutStore.DEFAULT_SLOT); + Map> saved = savedSlot.isPresent() ? savedSlot.get() + : store.load(); boolean anyOpen = saved.values().stream().anyMatch(list -> !list.isEmpty()); - closeAllFloating(); - for (ObservableList list : dockables.values()) { - list.clear(); - } - if (!anyOpen) { + closeAllFloating(); + for (ObservableList list : dockables.values()) { + list.clear(); + } for (Default entry : defaultLayout) { open(entry.dockable(), entry.location()); } return; } + applyLayout(saved, factory); + } + + /** + * Restores the given named-slot arrangement (Swing {@code LoadLayoutAction}: {@code + * CControl.load(layoutName)} — unlike {@link #restoreLayout} there is no factory-default + * fallback; the caller checks that the slot is defined first). + * + * @param saved the ordered dockable ids per role to restore + * @param factory recreates dockables from their persisted ids + */ + public void restoreSlot(Map> saved, DockableFactory factory) { + applyLayout(saved, factory); + } + + /** + * Replaces the current arrangement with the given one (close everything, then re-open the + * persisted dockables, skipping unknown ids — the FX counterpart of the Swing {@code + * DockingHelper.restoreMissingPanels} completing a loaded layout). + */ + private void applyLayout(Map> saved, DockableFactory factory) { + closeAllFloating(); + for (ObservableList list : dockables.values()) { + list.clear(); + } for (DockLocation location : DockLocation.values()) { for (String id : saved.getOrDefault(location, List.of())) { if (isOpen(id)) { diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/docking/Dockable.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/docking/Dockable.java index 326bf5c1c1b..029f6972209 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/docking/Dockable.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/docking/Dockable.java @@ -4,6 +4,10 @@ * SPDX-License-Identifier: GPL-2.0-or-later */ package de.uka.ilkd.key.gui.fx.docking; +import java.util.List; + +import java.util.List; + import javafx.beans.property.BooleanProperty; import javafx.beans.property.ObjectProperty; import javafx.beans.property.StringProperty; @@ -90,4 +94,14 @@ default boolean isClosable() { default void setClosable(boolean closable) { closableProperty().set(closable); } + + /** + * @return the custom actions shown in this dockable's tab context menu (and later in its tab + * header), like the Swing {@code TabPanel.getTitleActions()}/{@code + * TabPanel.getTitleCActions()} rendered by {@code DockingHelper} into the dockable's + * title bar and title popup menu + */ + default List getTitleActions() { + return List.of(); + } } diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/docking/DockingLayoutF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/docking/DockingLayoutF.java new file mode 100644 index 00000000000..38a7cb4699c --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/docking/DockingLayoutF.java @@ -0,0 +1,282 @@ +/* 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.docking; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import javafx.scene.Node; +import javafx.scene.Scene; +import javafx.scene.control.Menu; +import javafx.scene.control.MenuItem; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyEvent; + +import de.uka.ilkd.key.gui.fx.MainWindowF; +import de.uka.ilkd.key.gui.fx.docking.DockWorkspace.DockableFactory; +import de.uka.ilkd.key.gui.fx.fonticons.IconFactoryF; +import de.uka.ilkd.key.gui.fx.keyshortcuts.KeyStrokeManagerF; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The FX counter-part of the Swing {@code key.ui} extension {@code + * de.uka.ilkd.key.gui.docking.DockingLayout}: the named layout slots menu ({@code View > + * Layout}), the global maximize toggle and the shutdown layout persistence. + *

    + *
  • Layout slots: the three Swing arrangements {@code Default}, {@code Slot 1}, {@code Slot 2} + * are saved with Ctrl+Shift+F10..F12 and loaded with Ctrl+F10..F12 (Swing + * {@code SaveLayoutAction}: {@code KeyStroke.getKeyStroke(key, SHORTCUT_KEY_MASK | + * SHIFT_DOWN_MASK)}, {@code LoadLayoutAction}: {@code KeyStroke.getKeyStroke(key, + * CTRL_DOWN_MASK)}; the defaults are registered in {@link KeyStrokeManagerF} under the Swing + * action ids, so the shared {@code keystrokes.json} overrides apply).
  • + *
  • Shutdown: the current arrangement is written to the {@code DockLayoutStore}, like the Swing + * {@code GUIListener.shutDown} writing {@code CControl.writeXML}.
  • + *
  • Maximize: Ctrl+M toggles the maximized state of the dockable focused by the user — + * the bibliothek {@code CControl.KEY_MAXIMIZE_CHANGE} default ({@code KeyStroke.getKeyStroke( + * VK_M, CTRL_MASK)}; the JavaFX equivalent of the "focused dockable" is the selected tab of the + * {@link DockTabPane} containing the focus owner).
  • + *
+ * All menu labels and status-line messages follow the Swing original verbatim. + */ +public final class DockingLayoutF { + + private static final Logger LOGGER = LoggerFactory.getLogger(DockingLayoutF.class); + + /** The slot names of the Swing {@code DockingLayout.LAYOUT_NAMES}. */ + private static final String[] LAYOUT_NAMES = { DockLayoutStore.DEFAULT_SLOT, "Slot 1", + "Slot 2" }; + + /** + * Dedicated slot of the {@code key.fx.verify.docking} self test; not part of the user + * interface. + */ + private static final String SELF_TEST_SLOT = "SelfTest"; + + private final MainWindowF window; + private final DockWorkspace workspace; + private final DockLayoutStore store; + + private Scene scene; + + /** Creates the layout extension for the given main window (Swing: {@code init(MainWindow)}). */ + public DockingLayoutF(MainWindowF window) { + this.window = window; + this.workspace = window.getWorkspace(); + this.store = window.getLayoutStore(); + } + + /** + * Installs the global parts: the Ctrl+M maximize toggle ({@code + * CControl.KEY_MAXIMIZE_CHANGE}) as a scene filter and the shutdown persistence ({@code + * GUIListener.shutDown}). + * + * @param scene the main window scene + */ + public void install(Scene scene) { + this.scene = scene; + scene.addEventFilter(KeyEvent.KEY_PRESSED, this::handleKeyPressed); + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + try { + workspace.saveLayout(store); + LOGGER.info("Docking layout saved to {}", store.file()); + } catch (IOException e) { + LOGGER.warn("Failed to save the docking layout", e); + } + }, "fx-docking-shutdown")); + } + + /** + * @return the {@code View > Layout} submenu with the Swing {@code DockingLayout + * .getMainMenuActions} items (all load actions, then all save actions, then the reset). + */ + public Menu layoutMenu() { + Menu layout = new Menu("Layout"); + for (String name : LAYOUT_NAMES) { + layout.getItems().add(layoutItem("Load " + name, name, + "de.uka.ilkd.key.gui.docking.LoadLayoutAction$" + name, + IconFactoryF.Key.OPEN_KEY_FILE, () -> loadLayout(name))); + } + for (String name : LAYOUT_NAMES) { + layout.getItems().add(layoutItem("Save " + name, name, + "de.uka.ilkd.key.gui.docking.SaveLayoutAction$" + name, + IconFactoryF.Key.SAVE_FILE, () -> saveLayout(name))); + } + layout.getItems().add(layoutItem("Reset Layout", null, null, null, this::resetLayout)); + return layout; + } + + /** + * Creates one {@code View > Layout} item, registering its accelerator with the + * {@link KeyStrokeManagerF} (Swing {@code setAcceleratorKey} + {@code + * KeyStrokeManager.lookupAndOverride}), so user overrides from the shared {@code + * keystrokes.json} apply. + */ + private MenuItem layoutItem(String text, String slotName, 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; + } + + /** + * The Ctrl+M maximize toggle (bibliothek {@code CControl.KEY_MAXIMIZE_CHANGE}: "Change + * maximize state" of the focused dockable). + */ + private void handleKeyPressed(KeyEvent event) { + if (event.getCode() == KeyCode.M && event.isControlDown() && !event.isShiftDown() + && !event.isAltDown()) { + event.consume(); + focusedPane().flatMap(DockTabPane::getSelectedDockable) + .ifPresent(workspace::toggleMaximize); + } + } + + /** + * @return the tab pane containing the scene's focus owner, if any (the FX equivalent of the + * bibliothek "focused dockable") + */ + private Optional focusedPane() { + if (scene == null) { + return Optional.empty(); + } + Node node = scene.getFocusOwner(); + while (node != null) { + if (node instanceof DockTabPane pane) { + return Optional.of(pane); + } + node = node.getParent(); + } + return Optional.empty(); + } + + /** + * Loads the named slot into the workspace (Swing {@code LoadLayoutAction.actionPerformed}); + * an undefined slot keeps the current arrangement and reports it in the status line. + */ + private void loadLayout(String name) { + try { + Optional>> saved = store.loadSlot(name); + if (saved.isEmpty()) { + LOGGER.info("Layout {} could not be found", name); + window.setStatusLine("Layout " + name + " could not be found."); + return; + } + workspace.restoreSlot(saved.get(), factory()); + LOGGER.info("Layout {} loaded", name); + window.setStatusLine("Layout loaded from " + name); + } catch (IOException e) { + LOGGER.warn("Could not load layout {}", name, e); + } + } + + /** + * Saves the current arrangement into the named slot (Swing {@code SaveLayoutAction + * .actionPerformed}). + */ + private void saveLayout(String name) { + try { + workspace.saveSlot(store, name); + LOGGER.info("Layout {} saved", name); + window.setStatusLine("Layout saved to " + name); + } catch (IOException e) { + LOGGER.warn("Could not save layout {}", name, e); + } + } + + /** The reset action (Swing {@code ResetLayoutAction}). */ + private void resetLayout() { + workspace.restoreFactoryDefault(); + window.setStatusLine("Factory reset of the layout."); + } + + private DockableFactory factory() { + return id -> Optional.ofNullable(window.getDockables().get(id)); + } + + /** + * Scripted self test ({@code key.fx.verify.docking}): exercises the docking interactions + * ported from the Swing module — slot save/recall (Swing {@code SaveLayoutAction}/{@code + * LoadLayoutAction}), maximize/restore (bibliothek {@code CMaximizeAction}) and close while + * maximized — on the live workspace and compares the arrangements. Leaves the workspace in + * the state it had before the test. + * + * @return {@code "PASS ..."} or {@code "FAIL: "}, like the other verify hooks + */ + public String runSelfTest() { + List failures = new ArrayList<>(); + try { + Map> baseline = workspace.snapshotIds(); + Dockable sequent = window.getDockables().get(MainWindowF.ID_SEQUENT); + + // 1) named slots: save the arrangement, rearrange, recall, compare + store.saveSlot(SELF_TEST_SLOT, baseline); + workspace.close(MainWindowF.ID_SOURCE_VIEW); + workspace.close(MainWindowF.ID_INFO_VIEW); + if (workspace.isOpen(MainWindowF.ID_SOURCE_VIEW)) { + failures.add("close did not hide the source view"); + } + workspace.restoreSlot(store.loadSlot(SELF_TEST_SLOT).orElseThrow(), factory()); + if (!workspace.snapshotIds().equals(baseline)) { + failures.add("slot recall did not restore the saved arrangement"); + } + + // 2) maximize/restore (bibliothek: the maximized dockable hides the others) + if (workspace.isOpen(sequent.getId())) { + workspace.maximize(sequent); + if (workspace.snapshotIds().values().stream().mapToInt(List::size).sum() != 1 + || !workspace.isMaximized(sequent.getId())) { + failures.add("maximize did not reduce the workspace to the one dockable"); + } + workspace.restoreMaximized(); + if (!workspace.snapshotIds().equals(baseline)) { + failures.add("restore did not return the previous arrangement"); + } + + // 3) closing the maximized dockable restores the others (bibliothek behavior) + workspace.maximize(sequent); + workspace.close(sequent.getId()); + if (workspace.isMaximized(sequent.getId())) { + failures.add("closing the maximized dockable left the maximized state"); + } + if (!workspace.snapshotIds().equals(without(baseline, sequent.getId()))) { + failures.add("closing the maximized dockable did not restore the others"); + } + } else { + failures.add("prerequisite: the sequent dockable is not open"); + } + + // bring the pre-test arrangement back + workspace.restoreSlot(store.loadSlot(SELF_TEST_SLOT).orElseThrow(), factory()); + if (!workspace.snapshotIds().equals(baseline)) { + failures.add("final slot recall did not restore the pre-test arrangement"); + } + } catch (Exception e) { + failures.add("unexpected exception: " + e); + } + return failures.isEmpty() ? "PASS (slots, maximize, restore, close-while-maximized)" + : "FAIL: " + String.join("; ", failures); + } + + private static Map> without(Map> ids, + String dockableId) { + EnumMap> copy = new EnumMap<>(DockLocation.class); + ids.forEach((location, list) -> copy.put(location, + list.stream().filter(id -> !id.equals(dockableId)).toList())); + return copy; + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/keyshortcuts/KeyStrokeManagerF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/keyshortcuts/KeyStrokeManagerF.java index 960bc459ff9..1b4eef9207e 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/keyshortcuts/KeyStrokeManagerF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/keyshortcuts/KeyStrokeManagerF.java @@ -100,6 +100,18 @@ private void registerDefaults() { defineDefault("de.uka.ilkd.key.gui.actions.QuickSaveAction", "F5"); defineDefault("de.uka.ilkd.key.gui.actions.QuickLoadAction", "F6"); + // docking layout slots (Swing DockingLayout: the save actions use the shortcut mask + + // SHIFT (Ctrl+Shift+F10..F12), the load actions the shortcut mask (Ctrl+F10..F12)) + defineDefault("de.uka.ilkd.key.gui.docking.SaveLayoutAction$Default", + modifier() + "SHIFT+F10"); + defineDefault("de.uka.ilkd.key.gui.docking.SaveLayoutAction$Slot 1", + modifier() + "SHIFT+F11"); + defineDefault("de.uka.ilkd.key.gui.docking.SaveLayoutAction$Slot 2", + modifier() + "SHIFT+F12"); + defineDefault("de.uka.ilkd.key.gui.docking.LoadLayoutAction$Default", modifier() + "F10"); + defineDefault("de.uka.ilkd.key.gui.docking.LoadLayoutAction$Slot 1", modifier() + "F11"); + defineDefault("de.uka.ilkd.key.gui.docking.LoadLayoutAction$Slot 2", modifier() + "F12"); + defineDefault("de.uka.ilkd.key.gui.actions.IncreaseFontSizeAction", modifier() + "PLUS"); defineDefault("de.uka.ilkd.key.gui.actions.DecreaseFontSizeAction", modifier() + "MINUS"); defineDefault("de.uka.ilkd.key.gui.actions.AbandonTaskAction", modifier() + "W"); From b303dc97094c600bdfe0057fb36555a8b1a18b34 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Thu, 8 Oct 2026 21:41:13 +0200 Subject: [PATCH 050/281] key.ui.fx: proofmgmt: ProofManagementDialogF (port of the Swing ProofManagementDialog) with ClassTreeF and ContractSelectionPanelF --- .../gui/fx/proofmanagement/ClassTreeF.java | 364 ++++++++ .../ContractSelectionPanelF.java | 190 ++++ .../ProofManagementDialogF.java | 874 ++++++++++++++++++ 3 files changed, 1428 insertions(+) create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/proofmanagement/ClassTreeF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/proofmanagement/ContractSelectionPanelF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/proofmanagement/ProofManagementDialogF.java diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/proofmanagement/ClassTreeF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/proofmanagement/ClassTreeF.java new file mode 100644 index 00000000000..a7a81df7790 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/proofmanagement/ClassTreeF.java @@ -0,0 +1,364 @@ +/* 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.proofmanagement; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.Deque; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; +import javafx.scene.control.TreeItem; +import javafx.scene.control.TreeView; + +import de.uka.ilkd.key.java.Services; +import de.uka.ilkd.key.java.ast.abstraction.KeYJavaType; +import de.uka.ilkd.key.java.ast.declaration.ClassDeclaration; +import de.uka.ilkd.key.java.ast.declaration.InterfaceDeclaration; +import de.uka.ilkd.key.java.ast.declaration.TypeDeclaration; +import de.uka.ilkd.key.ldt.HeapLDT; +import de.uka.ilkd.key.logic.ProgramElementName; +import de.uka.ilkd.key.logic.op.IObserverFunction; +import de.uka.ilkd.key.logic.op.IProgramMethod; +import de.uka.ilkd.key.logic.op.ObserverFunction; +import de.uka.ilkd.key.util.KeYTypeUtil; + +import org.key_project.util.collection.ImmutableSet; + +/** + * JavaFX port of the Swing {@code de.uka.ilkd.key.gui.ClassTree} (module {@code key.ui}): a + * {@link TreeView} of the loaded Java types (full class names compressed at linear path + * segments) with their contract targets as leaves. Used by the + * {@link ProofManagementDialogF} "By Target" tab to select a {@link KeYJavaType} and an + * {@link IObserverFunction} whose contracts are shown in the contract panel. + *

+ * The tree building ({@link #createTree}, {@link #insertIntoTree}, + * {@link #compressLinearPaths}) and the target display name ({@link #getDisplayName}) are ported + * 1:1 from the Swing original; the Swing {@code DefaultTreeModel}/{@code TreePath} mechanics are + * replaced by {@link TreeItem} traversal. + */ +public class ClassTreeF extends TreeView { + + /** + * The user object of the tree nodes (Swing {@code ClassTree.Entry}): a path segment, a Java + * type or a contract target, depending on which fields are set. + */ + public static class Entry { + public String string; + public KeYJavaType kjt = null; + public IObserverFunction target = null; + public int numMembers = 0; + public int numSelectedMembers = 0; + + public Entry(String string) { + this.string = string; + } + + @Override + public String toString() { + return string; + } + } + + private final Services services; + + /** + * Creates the class tree for the given services. + * + * @param addContractTargets whether the contract targets (methods/observers) are added as + * leaves below each class node + * @param skipLibraryClasses whether library classes are hidden + * @param services the services providing the Java information and the specification + * repository + */ + public ClassTreeF(boolean addContractTargets, boolean skipLibraryClasses, Services services) { + this.services = services; + setRoot(createTree(addContractTargets, skipLibraryClasses, services)); + // the Swing tree hides the invisible root as well + setShowRoot(false); + } + + // ------------------------------------------------------------------------- + // internal methods (ported from the Swing original) + // ------------------------------------------------------------------------- + + private static TreeItem getChildByString(TreeItem parentNode, + String childString) { + for (TreeItem childNode : parentNode.getChildren()) { + if (childString.equals(childNode.getValue().string)) { + return childNode; + } + } + return null; + } + + private static TreeItem getChildByTarget(TreeItem parentNode, + IObserverFunction target) { + for (TreeItem childNode : parentNode.getChildren()) { + if (target.equals(childNode.getValue().target)) { + return childNode; + } + } + return null; + } + + private static void insertIntoTree(TreeItem rootNode, KeYJavaType kjt, + boolean addContractTargets, Services services) { + String fullClassName = kjt.getFullName(); + int length = fullClassName.length(); + int index = -1; + TreeItem node = rootNode; + do { + // get next part of the name + int lastIndex = index; + index = fullClassName.indexOf('.', ++index); + if (index == -1) { + index = length; + } + String namePart = fullClassName.substring(lastIndex + 1, index); + + // try to get child node; otherwise, create and insert it + TreeItem childNode = getChildByString(node, namePart); + if (childNode == null) { + childNode = new TreeItem<>(new Entry(namePart)); + node.getChildren().add(childNode); + } + + // go down to child node + node = childNode; + } while (index != length); + + // save kjt in leaf + node.getValue().kjt = kjt; + + // add all contract targets of kjt + if (addContractTargets) { + final ImmutableSet targets = + services.getSpecificationRepository().getContractTargets(kjt); + + // sort targets alphabetically + final IObserverFunction[] targetsArr = targets.toArray(new IObserverFunction[0]); + Arrays.sort(targetsArr, (o1, o2) -> { + if (o1 instanceof IProgramMethod && !(o2 instanceof IProgramMethod)) { + return -1; + } else if (!(o1 instanceof IProgramMethod) && o2 instanceof IProgramMethod) { + return 1; + } else { + String s1 = o1.name() instanceof ProgramElementName + ? ((ProgramElementName) o1.name()).getProgramName() + : o1.name().toString(); + String s2 = o2.name() instanceof ProgramElementName + ? ((ProgramElementName) o2.name()).getProgramName() + : o2.name().toString(); + return s1.compareTo(s2); + } + }); + + for (IObserverFunction target : targetsArr) { + Entry te = new Entry(getDisplayName(services, target)); + TreeItem childNode = new TreeItem<>(te); + te.kjt = kjt; + te.target = target; + node.getChildren().add(childNode); + } + } + } + + private static void compressLinearPaths(TreeItem root) { + + int numChildren = root.getChildren().size(); + for (int i = 0; i < numChildren; i++) { + TreeItem child = root.getChildren().get(i); + int numGrandChildren = child.getChildren().size(); + if (numGrandChildren == 1) { + TreeItem grandChild = child.getChildren().get(0); + // stop compressing at method name + if (grandChild.getValue().target != null) { + continue; + } + child.getChildren().remove(grandChild); + root.getChildren().set(i, grandChild); + Entry e1 = child.getValue(); + Entry e2 = grandChild.getValue(); + e2.string = e1.string + "." + e2.string; + compressLinearPaths(root); + } + } + } + + /** + *

+ * Returns a human readable display name for the given {@link ObserverFunction} with use of + * the given {@link Services}. + *

+ *

+ * Ported verbatim from {@code ClassTree.getDisplayName} (also used by other products). + *

+ * + * @param services The {@link Services} to use. + * @param ov The {@link ObserverFunction} for that a display name is needed. + * @return The display name for the given {@link ObserverFunction}. + */ + public static String getDisplayName(Services services, IObserverFunction ov) { + StringBuilder sb = new StringBuilder(); + String prettyName = HeapLDT.getPrettyFieldName(ov); + if (prettyName != null) { + sb.append(prettyName); + } else if (ov.name() instanceof ProgramElementName) { + sb.append(((ProgramElementName) ov.name()).getProgramName()); + } else { + sb.append(ov.name()); + } + if (ov.getNumParams() > 0 || ov instanceof IProgramMethod) { + sb.append("("); + } + for (KeYJavaType paramType : ov.getParamTypes()) { + sb.append(paramType.getSort().name()).append(", "); + } + if (ov.getNumParams() > 0) { + sb.setLength(sb.length() - 2); + } + if (ov.getNumParams() > 0 || ov instanceof IProgramMethod) { + sb.append(")"); + } + return sb.toString(); + } + + private static TreeItem createTree(boolean addContractTargets, + boolean skipLibraryClasses, Services services) { + // get all classes + var types = new ArrayList<>(services.getJavaInfo().getAllKeYJavaTypes()); + types.removeIf(kjt -> !(kjt.getJavaType() instanceof ClassDeclaration + || kjt.getJavaType() instanceof InterfaceDeclaration) + || (((TypeDeclaration) kjt.getJavaType()).isLibraryClass() && skipLibraryClasses)); + + // sort classes alphabetically + types.sort(Comparator.comparing(KeYJavaType::getFullName)); + + // build tree + TreeItem rootNode = new TreeItem<>(new Entry("")); + for (KeYJavaType keYJavaType : types) { + insertIntoTree(rootNode, keYJavaType, addContractTargets, services); + } + + compressLinearPaths(rootNode); + return rootNode; + } + + // ------------------------------------------------------------------------- + // public interface + // ------------------------------------------------------------------------- + + /** + * Selects the given Java type (and optionally its contract target) and expands the path to + * it (Swing {@code ClassTree.select} + {@code open}). Inner types are reached via their + * root type node like in the Swing original. + * + * @param kjt the Java type to select + * @param target the contract target to select, may be {@code null} + */ + public void select(KeYJavaType kjt, IObserverFunction target) { + // get tree path to class + List> path = new ArrayList<>(); + TreeItem node = getRoot(); + path.add(node); + // Collect inner classes + Deque types = new ArrayDeque<>(); + KeYJavaType currentKjt = kjt; + types.addFirst(currentKjt); + while (KeYTypeUtil.isInnerType(services, currentKjt)) { + String parentFullName = KeYTypeUtil.getParentName(services, kjt); + currentKjt = KeYTypeUtil.getType(services, parentFullName); + types.addFirst(currentKjt); + } + // extend tree path to root class + Iterator typesIter = types.iterator(); + KeYJavaType rootType = typesIter.next(); + TreeItem fullQualifiedNode = searchNode(node, rootType.getFullName()); + if (fullQualifiedNode != null) { + path.add(fullQualifiedNode); + node = fullQualifiedNode; + } else { + final String[] segments = rootType.getFullName().split("\\."); + String accumulatedSegment = null; + for (final String segment : segments) { + accumulatedSegment = + accumulatedSegment == null ? segment : accumulatedSegment + "." + segment; + final TreeItem resNode = searchNode(node, accumulatedSegment); + if (resNode != null) { + node = resNode; + path.add(node); + accumulatedSegment = null; + } + } + } + // extend tree path to inner classes + while (typesIter.hasNext()) { + KeYJavaType innerType = typesIter.next(); + node = searchNode(node, innerType.getName()); + path.add(node); + } + + // extend tree path to method + TreeItem methodNode = null; + if (target != null) { + methodNode = getChildByTarget(node, target); + } + + // open and select + // expand all ancestors of the target node (the Swing code expands the incomplete path) + for (TreeItem item : path) { + item.setExpanded(true); + } + TreeItem selection = methodNode != null ? methodNode : node; + selection.setExpanded(true); + getSelectionModel().clearSelection(); + getSelectionModel().select(selection); + scrollTo(getRow(selection)); + } + + /** + * Selects the given Java type (no contract target). + * + * @param kjt the Java type to select + */ + public void select(KeYJavaType kjt) { + select(kjt, null); + } + + /** + * @return the root node of the tree + */ + public TreeItem getRootNode() { + return getRoot(); + } + + /** + * @return the entry of the currently selected node or {@code null} if nothing is selected + */ + public Entry getSelectedEntry() { + TreeItem node = getSelectionModel().getSelectedItem(); + return node != null ? node.getValue() : null; + } + + /** + * Searches the child of {@code parent} with the given text. + * + * @param parent the node to search in + * @param text the text of the child to search for + * @return the first found child with the given text or {@code null} if there is none + */ + protected TreeItem searchNode(TreeItem parent, String text) { + for (TreeItem childNode : parent.getChildren()) { + Entry e = childNode.getValue(); + if (Objects.equals(text, e.string)) { + return childNode; + } + } + return null; + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/proofmanagement/ContractSelectionPanelF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/proofmanagement/ContractSelectionPanelF.java new file mode 100644 index 00000000000..a2f11ea89e2 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/proofmanagement/ContractSelectionPanelF.java @@ -0,0 +1,190 @@ +/* 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.proofmanagement; + +import java.util.Comparator; +import java.util.List; +import javafx.beans.value.ChangeListener; +import javafx.beans.value.ObservableValue; +import javafx.geometry.Insets; +import javafx.scene.control.Label; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; + +import de.uka.ilkd.key.java.Services; +import de.uka.ilkd.key.speclang.Contract; + +import org.key_project.util.collection.ImmutableSet; + +import org.jspecify.annotations.Nullable; + +/** + * A titled list panel for selecting a single {@link Contract}, port of {@code + * de.uka.ilkd.key.gui.ContractSelectionPanel} (key.ui) reduced to the behavior the proof + * management dialog needs (ContractSelectionPanel.java:66-127, 250-320): + *
    + *
  • {@link #setContracts(ImmutableSet, String)} sorts by contract type, then display name, then + * id (ContractSelectionPanel.java:36-43, 260-287) and preselects the first entry + * (ContractSelectionPanel.java:284);
  • + *
  • {@link #getContract()} returns the selected contract or {@code null} if the selection is + * empty (ContractSelectionPanel.java:314-320; the multi-selection combination of + * ContractSelectionPanel.java:297-306 is not ported — the proof management dialog always uses + * single selection);
  • + *
  • {@link #selectContract(Contract)} selects the given contract + * (ContractSelectionPanel.java:308-312);
  • + *
  • each cell shows the display name as the heading and the plain-text rendering of the + * contract (ContractSelectionPanel.java:154-190 uses {@code getHTMLText}; + * {@code getPlainText} is the HTML-free equivalent).
  • + *
+ * Not ported: the auxiliary-contract graying (ContractSelectionPanel.java:104-142, 171-183 — + * needs the closed-proof/used-contract fixpoint computation) and the multiple selection mode. + */ +public final class ContractSelectionPanelF extends VBox { + + /** + * The contract sort order of the Swing original: by contract type + * (CONTRACT_TYPE_ORDER, ContractSelectionPanel.java:36-43), then display name, then id + * (ContractSelectionPanel.java:260-287). + */ + private static final Comparator CONTRACT_ORDER = Comparator + .comparingInt(ContractSelectionPanelF::typeOrder) + .thenComparing(Contract::getDisplayName) + .thenComparingInt(Contract::id); + + /** ranks the contract types (Swing CONTRACT_TYPE_ORDER, ContractSelectionPanel.java:36-43). */ + private static int typeOrder(Contract c) { + // Contract.getTypeName() returns e.g. "JML operation contract"; the ranking mirrors + // Swing's class-based map (FunctionalOperation < InformationFlow < Dependency < + // BlockContract < LoopContract) + return switch (c.getTypeName()) { + case "JML operation contract" -> 0; + case "JML information flow contract" -> 1; + case "JML dependency contract" -> 2; + case "block contract" -> 3; + case "loop contract" -> 4; + default -> 100; + }; + } + + /** the services used to render the contract text (Swing {@code services}, :45). */ + private final Services services; + + /** the bordered title above the list (Swing TitledBorder, :72). */ + private final Label title = new Label("Contracts"); + + /** the contract list (Swing JList<Contract>, :80). */ + private final ListView contractList = new ListView<>(); + + /** notified on every selection change (Swing addListSelectionListener, :252-254). */ + private final ChangeListener selectionListener; + + /** the currently shown contracts (Swing {@code contracts}, :48). */ + private List contracts = List.of(); + + /** + * Creates the panel with the default title "Contracts". + * + * @param services the services of the proof environment (used to render contract text) + * @param selectionListener notified on every selection change + */ + public ContractSelectionPanelF(Services services, + ChangeListener selectionListener) { + this.services = services; + this.selectionListener = selectionListener; + setSpacing(2); + setPadding(new Insets(4, 8, 4, 8)); + title.getStyleClass().add("dialog-section-title"); + contractList.setPlaceholder(new Label("(none)")); + contractList.setCellFactory(view -> new ListCell<>() { + @Override + protected void updateItem(Contract item, boolean empty) { + super.updateItem(item, empty); + if (empty || item == null) { + setText(null); + setGraphic(null); + } else { + // the display name as the bold heading, the plain text rendering below + // (Swing cell renderer, ContractSelectionPanel.java:154-190) + var heading = new Label(item.getDisplayName()); + heading.getStyleClass().add("contract-cell-name"); + var text = new Label(item.getPlainText(services)); + text.setWrapText(true); + text.getStyleClass().add("contract-cell-text"); + text.setMaxWidth(Double.MAX_VALUE); + var box = new VBox(2, heading, text); + box.getStyleClass().add("contract-cell"); + setText(null); + setGraphic(box); + } + } + }); + contractList.getSelectionModel().selectedItemProperty() + .addListener(this::fireSelection); + getChildren().addAll(title, contractList); + VBox.setVgrow(contractList, Priority.ALWAYS); + } + + /** forwards the selection change to the listener (Swing list selection event). */ + private void fireSelection(ObservableValue obs, Contract oldV, + Contract newV) { + selectionListener.changed(obs, oldV, newV); + } + + /** + * Shows the given contracts under the given title (Swing + * {@code setContracts(ImmutableSet, String)}, ContractSelectionPanel.java:290-293): sorted + * and with the first entry preselected (ContractSelectionPanel.java:284). + */ + public void setContracts(ImmutableSet newContracts, @Nullable String newTitle) { + contracts = newContracts == null ? List.of() + : newContracts.stream().sorted(CONTRACT_ORDER).toList(); + title.setText(newTitle == null ? "Contracts" : newTitle); + contractList.getSelectionModel().clearSelection(); + contractList.getItems().setAll(contracts); + if (!contracts.isEmpty()) { + contractList.getSelectionModel().selectFirst(); + } + } + + /** + * @return the selected contract or {@code null} if the selection is empty (Swing + * {@code getContract}, ContractSelectionPanel.java:314-320 — single selection only) + */ + public @Nullable Contract getContract() { + return contractList.getSelectionModel().getSelectedItem(); + } + + /** + * Selects the given contract in the list (Swing {@code selectContract}, + * ContractSelectionPanel.java:308-312); {@code null} clears the selection. + */ + public void selectContract(@Nullable Contract contract) { + if (contract == null) { + contractList.getSelectionModel().clearSelection(); + } else { + contractList.getSelectionModel().select(contract); + } + } + + /** + * Selects the given contract and reports it to the selection listener — used when the dialog + * restores a remembered selection while the panel is still empty and the change must also + * refresh the start button (Swing ProofManagementDialog.select(ContractId) relies on the + * JList's selection events, ProofManagementDialog.java:403-408). + */ + public void selectContractAndNotify(@Nullable Contract contract) { + selectContract(contract); + if (contract != null) { + selectionListener.changed(null, null, contract); + } + } + + /** @return the contracts currently shown (empty if none) */ + public List getContracts() { + return contracts; + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/proofmanagement/ProofManagementDialogF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/proofmanagement/ProofManagementDialogF.java new file mode 100644 index 00000000000..e9a93435fad --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/proofmanagement/ProofManagementDialogF.java @@ -0,0 +1,874 @@ +/* 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.proofmanagement; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.stream.Stream; +import javafx.concurrent.Task; +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.Tab; +import javafx.scene.control.TabPane; +import javafx.scene.control.Tooltip; +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.Window; + +import de.uka.ilkd.key.control.DefaultUserInterfaceControl; +import de.uka.ilkd.key.gui.fx.fonticons.IconFactoryF; +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 de.uka.ilkd.key.java.Services; +import de.uka.ilkd.key.java.ast.abstraction.KeYJavaType; +import de.uka.ilkd.key.java.ast.declaration.InterfaceDeclaration; +import de.uka.ilkd.key.java.ast.declaration.TypeDeclaration; +import de.uka.ilkd.key.logic.ProgramElementName; +import de.uka.ilkd.key.logic.op.IObserverFunction; +import de.uka.ilkd.key.logic.op.IProgramMethod; +import de.uka.ilkd.key.proof.Proof; +import de.uka.ilkd.key.proof.ProofAggregate; +import de.uka.ilkd.key.proof.init.InitConfig; +import de.uka.ilkd.key.proof.init.ProblemInitializer; +import de.uka.ilkd.key.proof.init.ProofOblInput; +import de.uka.ilkd.key.proof.mgt.ProofEnvironment; +import de.uka.ilkd.key.proof.mgt.ProofStatus; +import de.uka.ilkd.key.proof.mgt.SpecificationRepository; +import de.uka.ilkd.key.speclang.Contract; + +import org.key_project.util.collection.DefaultImmutableSet; +import org.key_project.util.collection.ImmutableSet; + +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The JavaFX proof management dialog, counter-part of {@code de.uka.ilkd.key.gui. + * ProofManagementDialog} (key.ui, ProofManagementDialog.java:52-681). The dialog shows the + * contracts of the loaded Java classpath and starts or navigates to the proof of the selected + * contract: + *
    + *
  • By Target tab: the {@link ClassTreeF} (the ported Swing {@code ClassTree}) of the + * classpath types and their contract targets, each target leaf annotated with the proof status of + * its non-auxiliary contracts (the {@code targetIcons} computation, + * ProofManagementDialog.java:565-617), and a {@link ContractSelectionPanelF} with the contracts of + * the selected target (ProofManagementDialog.java:540-552);
  • + *
  • By Proof tab: the list of all proofs registered with the specification repository, + * newest first (ProofManagementDialog.java:620-640), with the contracts used in the selected proof + * (ProofManagementDialog.java:553-557);
  • + *
  • Start Proof / Go to Proof and Cancel buttons with the Swing enablement + * ({@code updateStartButton}, ProofManagementDialog.java:502-530) and load semantics + * ({@code findOrStartProof}, ProofManagementDialog.java:466-500): an existing proof for the + * selected contract is activated ("Go to Proof"), otherwise a new proof is created via the core + * {@link ProblemInitializer} on a background thread and registered in the proof environment;
  • + *
  • double-click shortcuts on tree targets with exactly one contract, contract panels and proof + * rows (ProofManagementDialog.java:91-113, 169-190);
  • + *
  • the last contract is remembered across dialog invocations + * ({@code previouslySelectedContracts}, ProofManagementDialog.java:59-61, 343-345, 364-370).
  • + *
+ *

+ * Deliberate deviations from the Swing original: the auxiliary-contract graying of + * {@code ContractSelectionPanel} is not ported (needs the closed-proof/used-contract fixpoint), + * the {@code mediator.startInterface(true)} call (ProofManagementDialog.java:497-499) has no FX + * counterpart (the FX mediator is always interactive), and proof starting runs on a background + * thread ({@link Task}) instead of the EDT. + *

+ * proofmgmt: TODO-merge hook into WindowUserInterfaceControlF load flow — currently + * standalone: the caller hands in the {@link DefaultUserInterfaceControl} of the environment the + * problem was loaded with (MainWindowF keeps the most recent one). Once + * {@code WindowUserInterfaceControlF} exists: (a) pass {@code mediator.getUI()} instead (Swing + * ProofManagementDialog.java:469), (b) let + * {@code WindowUserInterfaceControlF.selectProofObligation} + * (counterpart of {@code WindowUserInterfaceControl.java:516}) call {@link #showInstance} so bare + * {@code .java} loads route into this dialog, and (c) route {@code registerProofAggregate} of the + * proofs started here into the {@code ProofManagerF} of the Loaded Proofs view (the + * {@code proofSelector} callback of this dialog is the seam). + */ +public final class ProofManagementDialogF { + + private static final Logger LOGGER = LoggerFactory.getLogger(ProofManagementDialogF.class); + + /** + * The visual proof status (Swing key-hole icons of the renderers in TaskTree.java:348-394 and + * ProofManagementDialog.java:118-145, expressed as CSS-colored font icons). Shared with the + * Loaded Proofs view ({@code TaskTreeF}). + */ + public enum StatusKind { + /** open proof (Swing key-hole icon {@code keyHole}). */ + OPEN("proof-status-open", "Open proof"), + /** closed proof (Swing {@code keyHoleClosed}). */ + CLOSED("proof-status-closed", "Closed proof"), + /** + * closed proof that still depends on other contracts (Swing + * {@code keyHoleAlmostClosed}). + */ + ALMOST_CLOSED("proof-status-almost-closed", + "Closed proof (depends on other contracts)"), + /** closed proof via the proof cache (Swing {@code keyCachedClosed}). */ + CLOSED_BY_CACHE("proof-status-closed-cache", "Closed proof (using proof cache)"), + /** no status (not shown). */ + NONE(null, null); + + private final @Nullable String styleClass; + private final @Nullable String tooltip; + + StatusKind(@Nullable String styleClass, @Nullable String tooltip) { + this.styleClass = styleClass; + this.tooltip = tooltip; + } + + /** + * Maps a {@link ProofStatus} onto the visual state; the precedence mirrors the Swing + * renderer (TaskTree.java:373-389: almost closed / cache / closed / open). + */ + public static StatusKind fromProofStatus(@Nullable ProofStatus ps) { + if (ps == null) { + return NONE; + } + if (ps.getProofClosedButLemmasLeft()) { + return ALMOST_CLOSED; + } else if (ps.getProofClosedByCache()) { + return CLOSED_BY_CACHE; + } else if (ps.getProofClosed()) { + return CLOSED; + } else { + return OPEN; + } + } + + /** @return the CSS style class coloring the status icon, {@code null} = not shown */ + public @Nullable String styleClass() { + return styleClass; + } + + /** @return the tooltip describing the status, {@code null} = no tooltip */ + public @Nullable String tooltip() { + return tooltip; + } + } + + /** + * The last contract for which a proof was started or selected, stored by type name, method + * name, and contract name to avoid keeping environments alive (Swing + * {@code previouslySelectedContracts}, ProofManagementDialog.java:57-61). + */ + private static @Nullable ContractId previouslySelectedContracts; + + /** the dialog window (Swing JDialog, :52). */ + private final javafx.stage.Stage stage = new javafx.stage.Stage(); + /** the initial configuration of the loaded problem (Swing {@code initConfig}, :77). */ + private final InitConfig initConfig; + /** the services of {@link #initConfig}. */ + private final Services services; + /** + * The user interface control proofs are started with (Swing uses {@code mediator.getUI()}, + * ProofManagementDialog.java:469). The caller passes the control of the environment the + * problem was loaded with — the TODO-merge seam noted in the class javadoc. + */ + private final DefaultUserInterfaceControl ui; + /** + * activates a proof in the main window (the Swing dialog calls + * {@code mediator.getSelectionModel().setSelectedProof}, ProofManagementDialog.java:486, :492). + */ + private final Consumer proofSelector; + + /** the target tree of the "By Target" tab (Swing {@code classTree}, :70). */ + private final ClassTreeF classTree; + /** the proof status per target leaf (Swing {@code targetIcons}, :69). */ + private final Map targetIcons = new LinkedHashMap<>(); + /** the proof list of the "By Proof" tab (Swing {@code proofList}, :71). */ + private final ListView proofList = new ListView<>(); + /** the contract panel of the "By Target" tab (Swing {@code contractPanelByMethod}, :72). */ + private final ContractSelectionPanelF contractPanelByMethod; + /** the contract panel of the "By Proof" tab (Swing {@code contractPanelByProof}, :73). */ + private final ContractSelectionPanelF contractPanelByProof; + /** the tabs (Swing {@code tabbedPane}, :68). */ + private final TabPane tabbedPane = new TabPane(); + /** the "Start Proof"/"Go to Proof" button (Swing {@code startButton}, :74). */ + private final Button startButton = new Button(); + /** whether a proof was started or selected during this dialog invocation (:67). */ + private boolean startedProof; + + /** + * The proof environment to register started proofs in (Swing {@code env}, :78): the + * environment of the selected proof, or {@code null} (a new environment is created then). + */ + private @Nullable ProofEnvironment env; + + /** + * Creates and lays out the dialog (Swing constructor, ProofManagementDialog.java:83-240). + * + * @param owner the owner window (the main window stage), may be {@code null} + * @param initConfig the initial configuration of the loaded problem + * @param ui the user interface control proofs are started with + * @param proofSelector called with the proof to activate in the main window + * @param selectedProof the proof to preselect on the "By Proof" tab, may be {@code null} + */ + public ProofManagementDialogF(@Nullable Window owner, InitConfig initConfig, + DefaultUserInterfaceControl ui, Consumer proofSelector, + @Nullable Proof selectedProof) { + this.initConfig = initConfig; + this.services = initConfig.getServices(); + this.ui = ui; + this.proofSelector = proofSelector; + if (selectedProof != null) { + this.env = selectedProof.getEnv(); + } + + stage.setTitle("Proof Management"); + stage.initModality(Modality.WINDOW_MODAL); + if (owner != null) { + stage.initOwner(owner); + } + + contractPanelByMethod = + new ContractSelectionPanelF(services, (obs, oldV, newV) -> updateStartButton()); + contractPanelByProof = + new ContractSelectionPanelF(services, (obs, oldV, newV) -> updateStartButton()); + + // --- "By Target" tab: class tree + contract panel --------------------- + // create class tree (ProofManagementDialog.java:88-90); the target status icons are + // computed in updateGlobalStatus + classTree = new ClassTreeF(true, true, services); + classTree.setCellFactory(view -> new javafx.scene.control.TreeCell() { + @Override + protected void updateItem(ClassTreeF.Entry item, boolean empty) { + super.updateItem(item, empty); + if (empty || item == null) { + setText(null); + setGraphic(null); + setTooltip(null); + return; + } + setText(item.string); + setStyle(item.target == null ? "-fx-font-weight: bold;" : null); + StatusKind kind = targetIcons.get(item); + if (kind != null && kind.styleClass() != null) { + var icon = IconFactoryF.createIcon(IconFactoryF.Key.KEY_HOLE, 14); + icon.getStyleClass().add(kind.styleClass()); + setGraphic(icon); + setTooltip(new Tooltip(kind.tooltip())); + } else { + setGraphic(null); + setTooltip(null); + } + } + }); + // selection refreshes the contract panel (ProofManagementDialog.java:114) + classTree.getSelectionModel().selectedItemProperty() + .addListener((obs, oldV, item) -> updateContractPanel()); + // double click on a target with exactly one contract starts the proof + // (ProofManagementDialog.java:91-113) + classTree.setOnMouseClicked(e -> { + if (e.getClickCount() == 2) { + ClassTreeF.Entry entry = classTree.getSelectedEntry(); + if (entry != null && entry.kjt != null && entry.target != null) { + ImmutableSet contracts = + services.getSpecificationRepository().getContracts(entry.kjt, entry.target); + Contract c = contracts.isEmpty() ? null : contracts.iterator().next(); + if (contracts.size() == 1 && c == contractPanelByMethod.getContract()) { + startSelectedContract(); + } + } + } + }); + + // --- "By Proof" tab: proof list + contract panel ---------------------- + // the proof list renderer (ProofManagementDialog.java:118-145): the proof name with the + // proof status icon + proofList.setCellFactory(view -> new ListCell<>() { + @Override + protected void updateItem(Proof item, boolean empty) { + super.updateItem(item, empty); + if (empty || item == null) { + setText(null); + setGraphic(null); + setTooltip(null); + } else { + setText(item.name().toString()); + StatusKind kind = StatusKind.fromProofStatus(item.mgt().getStatus()); + if (kind.styleClass() != null) { + var icon = IconFactoryF.createIcon(IconFactoryF.Key.KEY_HOLE, 14); + icon.getStyleClass().add(kind.styleClass()); + setGraphic(icon); + setTooltip(new Tooltip(kind.tooltip())); + } else { + setGraphic(null); + setTooltip(null); + } + } + } + }); + // selection refreshes the contract panel (ProofManagementDialog.java:146) + proofList.getSelectionModel().selectedItemProperty() + .addListener((obs, oldV, proof) -> updateContractPanel()); + // double click on a proof row starts the proof of the selected contract (:182-190) + proofList.setOnMouseClicked(e -> { + if (e.getClickCount() == 2) { + startSelectedContract(); + } + }); + + // --- tabs + buttons --------------------------------------------------- + Tab byTarget = new Tab("By Target", buildTabContent(classTree, contractPanelByMethod)); + byTarget.setClosable(false); + Tab byProof = new Tab("By Proof", buildTabContent(proofList, contractPanelByProof)); + byProof.setClosable(false); + tabbedPane.getTabs().addAll(byTarget, byProof); + // tab change updates the start button and preselects the first proof + // (ProofManagementDialog.java:198-203) + tabbedPane.getSelectionModel().selectedItemProperty().addListener((obs, oldV, tab) -> { + updateStartButton(); + if (proofList.getSelectionModel().isEmpty() && !proofList.getItems().isEmpty()) { + proofList.getSelectionModel().selectFirst(); + } + }); + + startButton.setPrefWidth(140); + startButton.setDisable(true); + startButton.setOnAction(e -> startSelectedContract()); + Button cancelButton = new Button("Cancel"); + cancelButton.setPrefWidth(140); + cancelButton.setCancelButton(true); + cancelButton.setOnAction(e -> stage.close()); + Region spacer = new Region(); + HBox.setHgrow(spacer, Priority.ALWAYS); + HBox buttonPanel = new HBox(5, spacer, startButton, cancelButton); + buttonPanel.setAlignment(Pos.CENTER_RIGHT); + buttonPanel.setPadding(new Insets(8, 8, 8, 8)); + + BorderPane root = new BorderPane(); + root.setCenter(tabbedPane); + root.setBottom(buttonPanel); + + Scene scene = new Scene(root, 950, 620); + ThemeManager.getInstance().style(scene); + stage.setScene(scene); + } + + /** + * Builds one tab's content: the selection list on the left, the contract panel on the right + * (Swing BoxLayout list panels, ProofManagementDialog.java:149-192). + */ + private BorderPane buildTabContent(javafx.scene.control.Control list, + ContractSelectionPanelF contractPanel) { + BorderPane pane = new BorderPane(); + pane.setPadding(new Insets(8)); + Label heading = new Label(list == proofList ? "Proofs" : "Contract Targets"); + heading.getStyleClass().add("dialog-section-title"); + VBox left = new VBox(2, heading, list); + VBox.setVgrow(list, Priority.ALWAYS); + left.setPrefWidth(330); + pane.setCenter(left); + pane.setRight(contractPanel); + BorderPane.setMargin(contractPanel, new Insets(0, 0, 0, 8)); + return pane; + } + + // ------------------------------------------------------------------ + // status computation + // ------------------------------------------------------------------ + + /** + * Computes the status icons of all contract targets and fills the proof list (ProofManagement + * Dialog.{@code updateGlobalStatus}, :565-645). + */ + private void updateGlobalStatus() { + // target icons (:565-617) + targetIcons.clear(); + SpecificationRepository specRepos = services.getSpecificationRepository(); + var kjts = services.getJavaInfo().getAllKeYJavaTypes(); + for (KeYJavaType kjt : kjts) { + // skip library classes, the user isn't shown contracts for them + if (kjt.getJavaType() instanceof TypeDeclaration + && ((TypeDeclaration) kjt.getJavaType()).isLibraryClass()) { + continue; + } + ImmutableSet targets = specRepos.getContractTargets(kjt); + for (IObserverFunction target : targets) { + if (isInstanceMethodOfAbstractClass(kjt, target)) { + continue; + } + targetIcons.put(findTargetEntry(kjt, target), + computeTargetStatus(kjt, target, specRepos)); + } + } + classTree.refresh(); + + // proof list: all proofs, newest first (:620-640) + List proofs = new ArrayList<>(); + for (Proof p : specRepos.getAllProofs()) { + proofs.add(0, p); + } + proofList.getItems().setAll(proofs); + } + + /** @return the tree leaf entry of the given type/target, or a fresh dummy entry. */ + private ClassTreeF.Entry findTargetEntry(KeYJavaType kjt, IObserverFunction target) { + ClassTreeF.Entry entry = searchTargetEntry(classTree.getRootNode(), kjt, target); + return entry != null ? entry : new ClassTreeF.Entry(""); + } + + /** depth-first search for the leaf entry with the given type/target. */ + private static ClassTreeF.@Nullable Entry searchTargetEntry( + javafx.scene.control.TreeItem item, KeYJavaType kjt, + IObserverFunction target) { + ClassTreeF.Entry value = item.getValue(); + if (value.target != null && target.equals(value.target) && kjt.equals(value.kjt)) { + return value; + } + for (javafx.scene.control.TreeItem child : item.getChildren()) { + ClassTreeF.Entry found = searchTargetEntry(child, kjt, target); + if (found != null) { + return found; + } + } + return null; + } + + /** + * Computes the status icon of one contract target (ProofManagementDialog.java:581-614): the + * aggregate over the non-auxiliary contracts, "not started" (no icon) if no contract has a + * proof. + */ + private StatusKind computeTargetStatus(KeYJavaType kjt, IObserverFunction target, + SpecificationRepository specRepos) { + boolean startedProving = false; + boolean allClosed = true; + boolean lemmasLeft = false; + boolean cached = false; + for (Contract contract : specRepos.getContracts(kjt, target)) { + // skip auxiliary contracts like block/loop contracts (ProofManagementDialog.java:588) + if (contract.isAuxiliary()) { + continue; + } + Proof proof = findPreferablyClosedProof(contract); + if (proof == null) { + allClosed = false; + } else { + startedProving = true; + ProofStatus status = proof.mgt().getStatus(); + if (status.getProofOpen()) { + allClosed = false; + } else if (status.getProofClosedButLemmasLeft()) { + lemmasLeft = true; + } + if (status.getProofClosedByCache()) { + cached = true; + } + } + } + if (!startedProving) { + return StatusKind.NONE; + } + if (!allClosed) { + return StatusKind.OPEN; + } + if (cached) { + return StatusKind.CLOSED_BY_CACHE; + } + return lemmasLeft ? StatusKind.ALMOST_CLOSED : StatusKind.CLOSED; + } + + /** + * Finds a proof for the given contract, preferring a closed proof, then one that just misses + * lemmas (ProofManagementDialog.findPreferablyClosedProof, :444-464). + * + * @return the proof or {@code null} if there is no proof for the contract + */ + private @Nullable Proof findPreferablyClosedProof(@NonNull Contract contract) { + ImmutableSet proofs = services.getSpecificationRepository().getProofs(contract); + if (proofs.isEmpty()) { + return null; + } + Proof fallback = null; + for (Proof proof : proofs) { + final ProofStatus status = proof.mgt().getStatus(); + if (status.getProofClosed()) { + return proof; + } else if (fallback == null || status.getProofClosedButLemmasLeft()) { + fallback = proof; + } + } + return fallback; + } + + /** Swing {@code ProofManagementDialog.isInstanceMethodOfAbstractClass}, :533-536. */ + private static boolean isInstanceMethodOfAbstractClass(KeYJavaType kjt, + IObserverFunction obs) { + return kjt.getJavaType() instanceof InterfaceDeclaration + || (kjt.getSort().isAbstract() && !obs.isStatic()); + } + + /** the target sort order of ProofManagementDialog.java:273-287 (ClassTree.java:157-172). */ + private static Comparator targetComparator() { + return (o1, o2) -> { + if (o1 instanceof IProgramMethod && !(o2 instanceof IProgramMethod)) { + return -1; + } else if (!(o1 instanceof IProgramMethod) && o2 instanceof IProgramMethod) { + return 1; + } + return targetSortName(o1).compareTo(targetSortName(o2)); + }; + } + + private static String targetSortName(IObserverFunction o) { + return o.name() instanceof ProgramElementName + ? ((ProgramElementName) o.name()).getProgramName() + : o.name().toString(); + } + + // ------------------------------------------------------------------ + // selection / panel updates + // ------------------------------------------------------------------ + + /** + * Refreshes the contract panel of the active tab (ProofManagementDialog. + * {@code updateContractPanel}, :538-563). + */ + private void updateContractPanel() { + if (isByTargetTab()) { + ClassTreeF.Entry entry = classTree.getSelectedEntry(); + if (entry != null && entry.target != null + && !isInstanceMethodOfAbstractClass(entry.kjt, entry.target)) { + ImmutableSet contracts = + services.getSpecificationRepository().getContracts(entry.kjt, entry.target); + contractPanelByMethod.setContracts(contracts, "Contracts"); + // auxiliary contracts are grayed out if the target's proof is already closed + // (ProofManagementDialog.java:548-549) — not ported, see the class javadoc + } else { + contractPanelByMethod.setContracts(DefaultImmutableSet.nil(), "Contracts"); + } + } else { + Proof proof = proofList.getSelectionModel().getSelectedItem(); + if (proof != null) { + contractPanelByProof.setContracts(proof.mgt().getUsedContracts(), + "Contracts used in proof \"" + proof.name() + "\""); + } else { + contractPanelByProof.setContracts(DefaultImmutableSet.nil(), "Contracts"); + } + } + updateStartButton(); + } + + /** + * Updates the start button text and enablement (ProofManagementDialog. + * {@code updateStartButton}, :502-530): "No Contract" (disabled) / "Start Proof" / "Go to + * Proof" (a proof for the selected contract already exists). + */ + private void updateStartButton() { + Contract contract = getSelectedContract(); + if (contract == null) { + startButton.setText("No Contract"); + startButton.setDisable(true); + } else { + Proof proof = findPreferablyClosedProof(contract); + startButton.setText(proof == null ? "Start Proof" : "Go to Proof"); + startButton.setDisable(false); + } + } + + /** @return the contract of the active tab's panel (Swing {@code getSelectedContract}, :432). */ + private @Nullable Contract getSelectedContract() { + return isByTargetTab() ? contractPanelByMethod.getContract() + : contractPanelByProof.getContract(); + } + + private boolean isByTargetTab() { + return tabbedPane.getSelectionModel().getSelectedIndex() == 0; + } + + // ------------------------------------------------------------------ + // start proof + // ------------------------------------------------------------------ + + /** + * The start button action (ProofManagementDialog.java:219-225): hides the dialog and starts + * or selects the proof of the selected contract. + */ + private void startSelectedContract() { + Contract contract = getSelectedContract(); + if (contract == null) { + return; + } + stage.close(); + findOrStartProof(contract); + } + + /** + * Starts or selects the proof for the given contract (ProofManagementDialog. + * {@code findOrStartProof}, :466-500): an existing proof is activated in the main window; + * otherwise a new proof is created with the core {@link ProblemInitializer} on a background + * thread and registered in the proof environment. + *

+ * proofmgmt: TODO-merge hook into WindowUserInterfaceControlF load flow — the Swing dialog + * uses {@code mediator.getUI()} here (ProofManagementDialog.java:469); the standalone port + * uses the environment's UI control handed in by the caller. Merge: route through + * {@code WindowUserInterfaceControlF} and report the new proof aggregate to the + * {@code ProofManagerF} of the Loaded Proofs view. + */ + private void findOrStartProof(@NonNull Contract contract) { + Proof proof = findPreferablyClosedProof(contract); + if (proof != null) { + proofSelector.accept(proof); + startedProof = true; + rememberSelectedContract(contract); + return; + } + + Task startTask = new Task<>() { + @Override + protected Proof call() throws Exception { + ProblemInitializer pi = new ProblemInitializer(ui, services, ui); + // enables to access the FileRepo created in AbstractProblemLoader + // (ProofManagementDialog.java:473-474) + pi.setFileRepo(initConfig.getFileRepo()); + ProofOblInput po = + contract.createProofObl(initConfig.copyWithServices(initConfig.getServices())); + ProofAggregate pl = pi.startProver(initConfig, po); + + // register the new proof in the environment of the selected proof, or in a fresh + // one (ProofManagementDialog.java:481-485) + if (env == null) { + env = ui.createProofEnvironmentAndRegisterProof(po, pl, initConfig); + } else { + env.registerProof(po, pl); + } + return pl.getFirstProof(); + } + }; + startTask.setOnSucceeded(event -> { + Proof newProof = startTask.getValue(); + LOGGER.info("Proof management: started proof for contract {}", contract.getName()); + NotificationManagerF.getInstance() + .notify("Started proof for contract " + contract.getName(), Kind.INFO); + proofSelector.accept(newProof); + }); + startTask.setOnFailed(event -> { + Throwable error = startTask.getException(); + LOGGER.error("Proof management: starting proof for contract " + contract.getName() + + " failed", error); + NotificationManagerF.getInstance() + .notify("Starting proof for contract " + contract.getName() + + " failed: " + error.getMessage(), Kind.ERROR); + }); + Thread worker = new Thread(startTask, "fx-proofmgmt-contract-prover"); + worker.setDaemon(true); + worker.start(); + startedProof = true; + rememberSelectedContract(contract); + } + + /** Stores the started contract (ProofManagementDialog.java:364-370). */ + private void rememberSelectedContract(Contract contract) { + previouslySelectedContracts = new ContractId(contract.getKJT().getFullName(), + contract.getTarget().name().toString(), contract.getName()); + } + + // ------------------------------------------------------------------ + // defaults & selection before showing + // ------------------------------------------------------------------ + + /** + * Selects the first contract of the first type sorted by name (ProofManagementDialog. + * {@code selectKJTandTarget}, :262-301). + */ + private void selectKJTandTarget() { + List allJavaTypes = services.getJavaInfo().getAllKeYJavaTypes().stream() + .sorted(Comparator.comparing(KeYJavaType::getFullName)) + .filter(kjtTmp -> !(kjtTmp.getJavaType() instanceof TypeDeclaration + && ((TypeDeclaration) kjtTmp.getJavaType()).isLibraryClass())) + .toList(); + for (KeYJavaType javaType : allJavaTypes) { + Stream targets = services.getSpecificationRepository() + .getContractTargets(javaType).stream().sorted(targetComparator()) + .filter(targetTmp -> !services.getSpecificationRepository() + .getContracts(javaType, targetTmp).isEmpty()); + Optional t = targets.findFirst(); + if (t.isPresent()) { + select(javaType, t.get()); + break; + } + } + } + + /** + * Selects the given type/target on the "By Target" tab (ProofManagementDialog.java:415-420). + */ + private void select(KeYJavaType kjt, IObserverFunction target) { + tabbedPane.getSelectionModel().select(0); + classTree.select(kjt, target); + } + + /** + * Selects the given proof on the "By Proof" tab (ProofManagementDialog.java:422-430). + */ + private void select(Proof proof) { + for (int i = 0, n = proofList.getItems().size(); i < n; i++) { + if (proofList.getItems().get(i).equals(proof)) { + tabbedPane.getSelectionModel().select(1); + proofList.getSelectionModel().select(i); + break; + } + } + } + + /** + * Selects the remembered contract (ProofManagementDialog.{@code select(ContractId)}, + * :377-409); no-op if the contract is no longer present. + */ + private void selectPreviouslySelectedContract() { + ContractId cid = previouslySelectedContracts; + if (cid == null) { + return; + } + Optional kjt = services.getJavaInfo().getAllKeYJavaTypes().stream() + .filter(it -> it.getFullName().equals(cid.keyJavaTypeName)).findAny(); + if (kjt.isEmpty()) { + return; + } + Optional target = services.getSpecificationRepository() + .getContractTargets(kjt.get()).stream() + .filter(it -> it.name().toString().equals(cid.methodName)).findAny(); + if (target.isEmpty()) { + return; + } + select(kjt.get(), target.get()); + if (!isInstanceMethodOfAbstractClass(kjt.get(), target.get())) { + services.getSpecificationRepository().getContracts(kjt.get(), target.get()).stream() + .filter(it -> it.getName().equals(cid.contractName)).findAny() + .ifPresent(contractPanelByMethod::selectContractAndNotify); + } + } + + // ------------------------------------------------------------------ + // showing + // ------------------------------------------------------------------ + + /** Fills the lists and applies the defaults before the dialog becomes visible. */ + private void prepareAndShow() { + updateGlobalStatus(); + // determine own defaults if not given (ProofManagementDialog.java:341-346) + selectKJTandTarget(); + selectPreviouslySelectedContract(); + updateContractPanel(); + updateStartButton(); + } + + /** + * Fills the lists and opens the dialog modally (ProofManagementDialog. + * {@code showInstance}, :336-372). + * + * @return {@code true} if a proof was started or selected + */ + public boolean showAndWaitInstance() { + prepareAndShow(); + stage.showAndWait(); + return startedProof; + } + + /** + * Fills the lists and shows the dialog without blocking (the self test shows the + * dialog, inspects it and closes it; Swing {@code setVisible(true)}). + */ + public void showForVerification() { + prepareAndShow(); + stage.show(); + } + + /** Closes the dialog (Swing {@code setVisible(false)}, ProofManagementDialog.java:233). */ + public void close() { + stage.close(); + } + + // ------------------------------------------------------------------ + // static entry points + // ------------------------------------------------------------------ + + /** + * Shows the dialog modally and returns whether a proof was started or selected (Swing + * {@code ProofManagementDialog.showInstance(InitConfig, Proof)}, + * ProofManagementDialog.java:306-308, 336-372). + * + * @param owner the owner window (the main window stage), may be {@code null} + * @param initConfig the initial configuration of the loaded problem + * @param ui the user interface control proofs are started with + * @param proofSelector called with the proof to activate in the main window + * @param selectedProof the proof to preselect, may be {@code null} + */ + public static boolean showInstance(@Nullable Window owner, InitConfig initConfig, + DefaultUserInterfaceControl ui, Consumer proofSelector, + @Nullable Proof selectedProof) { + ProofManagementDialogF dialog = + new ProofManagementDialogF(owner, initConfig, ui, proofSelector, selectedProof); + return dialog.showAndWaitInstance(); + } + + /** + * Creates a dialog for the self test ({@code key.fx.verify.proofmgmt}): the caller inspects + * it with {@link #verifyDialog()} and shows it with {@link #showForVerification()}. + */ + public static ProofManagementDialogF createForVerification(@Nullable Window owner, + InitConfig initConfig, DefaultUserInterfaceControl ui, + Consumer proofSelector) { + return new ProofManagementDialogF(owner, initConfig, ui, proofSelector, null); + } + + // ------------------------------------------------------------------ + // self test support + // ------------------------------------------------------------------ + + /** + * Runs the structural self test of the filled dialog: the "By Target" tree contains the + * contract targets with statuses, the "By Proof" list contains all proofs, a contract is + * selected and the start button enabled. Logging is left to the caller. Must be called after + * {@link #showForVerification()} (or the internal {@code prepareAndShow}). + * + * @return a report string ending in PASS or FAIL + */ + public String verifyDialog() { + var checks = new ArrayList(); + long targets = targetIcons.size(); + long targetsWithProof = targetIcons.values().stream().filter(k -> k != StatusKind.NONE) + .count(); + check(checks, "target tree has " + targets + " contract target(s)", targets > 0); + check(checks, targetsWithProof + " target(s) with a proof status", + targetsWithProof > 0); + check(checks, "proof list shows " + proofList.getItems().size() + " proof(s)", + !proofList.getItems().isEmpty()); + Contract selected = getSelectedContract(); + check(checks, "a contract is selected (" + (selected == null ? "-" : selected.getName()) + + ")", selected != null); + check(checks, "start button enabled", !startButton.isDisable()); + boolean ok = checks.stream().allMatch(c -> c.startsWith("PASS")); + return String.join("; ", checks) + (ok ? " PASS" : " FAIL"); + } + + /** appends a PASS/FAIL line for one check. */ + private static void check(List checks, String what, boolean ok) { + checks.add((ok ? "PASS" : "FAIL") + " [" + what + "]"); + } + + /** Records the identification of a contract (ProofManagementDialog.java:670-681). */ + private record ContractId(String keyJavaTypeName, String methodName, String contractName) { + } +} From 3e80272db0e68094ab097ed828f419e3d4dc4cdc Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Thu, 8 Oct 2026 21:43:57 +0200 Subject: [PATCH 051/281] key.ui.fx: help: port HelpFacade/HelpInfo as HelpFacadeF with F1 context help and self test --- key.ui.fx/build.gradle | 5 +- .../uka/ilkd/key/gui/fx/MainApplication.java | 4 + .../de/uka/ilkd/key/gui/fx/MainWindowF.java | 20 +- .../uka/ilkd/key/gui/fx/help/HelpFacadeF.java | 342 ++++++++++++++++++ .../uka/ilkd/key/gui/fx/help/HelpInfoF.java | 35 ++ 5 files changed, 404 insertions(+), 2 deletions(-) create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/help/HelpFacadeF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/help/HelpInfoF.java diff --git a/key.ui.fx/build.gradle b/key.ui.fx/build.gradle index 86152523631..2ee5d3f48f3 100644 --- a/key.ui.fx/build.gradle +++ b/key.ui.fx/build.gradle @@ -65,11 +65,14 @@ run { // Hide/Regroup) with the key.fx.verify.sequentsearch query after the demo load) // * key.fx.verify.updatehighlight = any (M2: run the update-highlight overlay self test; // needs a sequent printing update operators, e.g. standard_key/queries/useQuery.key) + // * key.fx.verify.help = any (smalldialogs: run the F1 context help resolution self test; + // no browser window is opened) ["key.fx.driver", "key.fx.theme", "key.fx.demo.sequent", "key.fx.verify.sequent", "key.fx.show", "key.fx.demo.autoprove", "key.fx.verify.tree", "key.fx.demo.autoprove.live", "key.fx.demo.maxsteps", "key.fx.verify.goallist", "key.fx.verify.search", "key.fx.verify.sequentsearch", "key.fx.verify.treefilters", - "key.fx.verify.sequentsearchmodes", "key.fx.verify.updatehighlight"] + "key.fx.verify.sequentsearchmodes", "key.fx.verify.updatehighlight", + "key.fx.verify.help"] .each { name -> def value = System.getProperty(name) if (value != null) { 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 index da20fbed1f3..953798f68ec 100644 --- 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 @@ -7,6 +7,8 @@ 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}. @@ -21,6 +23,8 @@ 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); 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 index 8a46e0ba13f..485e7704fdd 100644 --- 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 @@ -61,6 +61,7 @@ import de.uka.ilkd.key.gui.fx.docking.SimpleDockable; 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.nodeviews.SequentViewF; @@ -247,8 +248,12 @@ public void initialize() { Scene scene = new Scene(root, 1100, 800); ThemeManager.getInstance().manage(scene); // 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 + // 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); + // smalldialogs: F1 context help (Swing MainWindow.java:300-302 registers the F1 action) + HelpFacadeF.installAccelerator(scene); stage.setScene(scene); stage.show(); @@ -262,6 +267,17 @@ public void initialize() { recentFiles.load(); startDemoProofLoad(); + // smalldialogs: startup self test of the F1 help URL resolution; reports to the log and + // toast (same pattern as the key.fx.verify.* hooks in startProofLoad). The help facade is + // proof-independent, so the hook runs 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); + } + NotificationManagerF.getInstance() .notify("KeY (JavaFX) started. Docking layout restored from " + layoutStore.file() + "."); @@ -975,6 +991,8 @@ private void handleMainWindowKeyPressed(KeyEvent event) { 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. } /** diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/help/HelpFacadeF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/help/HelpFacadeF.java new file mode 100644 index 00000000000..8f70aed648f --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/help/HelpFacadeF.java @@ -0,0 +1,342 @@ +/* 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.help; + +import java.awt.Desktop; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.function.Consumer; +import javafx.application.Application; +import javafx.scene.Node; +import javafx.scene.Scene; +import javafx.scene.input.KeyCombination; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A gate to the KeY documentation system. + *

+ * JavaFX port of {@code de.uka.ilkd.key.gui.help.HelpFacade} (Swing {@code HelpFacade.java}). + * Provides the facility to open the documentation at the press of F1. The opened page is + * determined context-sensitively by the currently focused node and its parent chain: the first + * class carrying a {@link HelpInfoF} annotation wins; without any annotation the main + * documentation page is opened. + *

+ * Port notes and deviations from the Swing original: + *

    + *
  • The browser is opened via the JavaFX {@link Application#getHostServices()} (handed over by + * {@code MainApplication}) with a {@link Desktop#browse(URI)} fallback — the Swing original uses + * {@code SwingUtil.browse} ({@code HelpFacade.java:60-66}), which lives in the Swing-only module + * {@code key.ui} and is therefore not reachable from {@code key.ui.fx}.
  • + *
  • The Swing base-URL override reads {@code System.getProperty("KEY_HELP_URL")} for the null + * check but {@code System.getProperty(KEY_HELP_URL)} (i.e. {@code "key.help.url"}) for the value + * ({@code HelpFacade.java:54-58}) — with the documented property {@code key.help.url} the check + * never fires. The port reads {@code "key.help.url"} consistently.
  • + *
  • The Swing {@code OpenHelpAction} carries a TODO: its {@code ActionEvent} source is always + * the root pane, so F1 always opened the main page ({@code HelpFacade.java:154-158} comment). + * The FX F1 handler (registered in {@code MainWindowF.handleMainWindowKeyPressed}) instead walks + * the focus owner's parent chain and only falls back to the main page — the intended behavior, + * without the Swing bug.
  • + *
  • {@code HelpFacade.createHelpButton} / {@code createHelpAction} + * ({@code HelpFacade.java:133-152}) + * create dockable-title buttons — the FX docking framework has no title-action API yet, so they + * are deliberately not ported (the parallel dock-actions work will host them).
  • + *
+ */ +public final class HelpFacadeF { + private static final Logger LOGGER = LoggerFactory.getLogger(HelpFacadeF.class); + + /** + * System property key for setting the base url of the help system (Swing + * {@code HelpFacade.KEY_HELP_URL}). + */ + public static final String KEY_HELP_URL = "key.help.url"; + + /** + * The base url of the help system (Swing {@code HelpFacade.HELP_BASE_URL}, {@code + * HelpFacade.java:52}); overridable via the {@link #KEY_HELP_URL} property. + */ + public static String HELP_BASE_URL = "https://keyproject.github.io/key-docs/"; + + static { + if (System.getProperty(KEY_HELP_URL) != null) { + HELP_BASE_URL = System.getProperty(KEY_HELP_URL); + } + } + + /** + * The running application handed over by the {@code MainApplication} (see the class javadoc); + * {@code null} until then, in which case {@link Desktop} is used. + */ + private static Application application; + + /** + * The browser-opening seam; replaceable for the self test so no real browser is launched + * during verification. + */ + private static Consumer browserOpener = HelpFacadeF::openInSystemBrowser; + + private HelpFacadeF() { + } + + /** + * Hands the application's host services to this facade; called by {@code MainApplication} + * during startup. + * + * @param application the running {@link Application} + */ + public static void setHostServices(Application runningApplication) { + application = runningApplication; + } + + private static void openInSystemBrowser(String url) { + if (application != null) { + application.getHostServices().showDocument(url); + return; + } + try { + Desktop.getDesktop().browse(new URI(url)); + } catch (IOException | URISyntaxException | UnsupportedOperationException + | IllegalStateException e) { + LOGGER.warn("Failed to open help in browser", e); + } + } + + /** + * Opens the key documentation website in the default system browser (Swing + * {@code HelpFacade.openHelp}, {@code HelpFacade.java:71-73}). + */ + public static void openHelp() { + openHelpInBrowser(HELP_BASE_URL); + } + + /** + * Opens the specified subpage of the KeY documentation website in the default system browser + * (Swing {@code HelpFacade.openHelp(String)}, {@code HelpFacade.java:80-89}). + * + * @param path a valid suffix to the current URI + */ + public static void openHelp(String path) { + if (path.startsWith("https://")) { + openHelpInBrowser(path); + return; + } + if (path.startsWith("/")) { + path = path.substring(1); + } + openHelpInBrowser(HELP_BASE_URL + path); + } + + /** + * Tries to find the documentation of the given node and opens it (Swing + * {@code HelpFacade.openHelp(Component)}, {@code HelpFacade.java:99-107}, the component + * counterpart being the parent chain {@code Component.getParent()}). + *

+ * The documentation is determined by following the parents to the root and checking for + * {@link HelpInfoF} on the node classes. Without any annotated class the main documentation + * page is opened. + * + * @param node the focused node, may be {@code null} + */ + public static void openHelp(Node node) { + while (node != null) { + if (openHelpOfClass(node.getClass())) { + return; + } + node = node.getParent(); + } + openHelp(); + } + + /** + * Opens documentation given for the given class (Swing + * {@code HelpFacade.openHelpOfClass}, {@code HelpFacade.java:117-124}). The class needs to + * be annotated with {@link HelpInfoF}. + * + * @param clazz non-null class instance + * @return whether documentation was found and opened + */ + public static boolean openHelpOfClass(Class clazz) { + HelpInfoF help = clazz.getAnnotation(HelpInfoF.class); + if (help != null) { + openHelpInBrowser(HELP_BASE_URL + help.path()); + return true; + } + return false; + } + + /** + * Opens the documentation for the currently focused node at the press of F1 (Swing trigger: + * {@code MainWindow.java:300-302} registers {@code HelpFacade.ACTION_OPEN_HELP} on the root + * pane with the F1 accelerator set in {@code HelpFacade.OpenHelpAction}, {@code + * HelpFacade.java:159-174}). + *

+ * Registration note: {@code MainWindowF} handles F1 in its scene key handler + * ({@code handleMainWindowKeyPressed}, next to the Ctrl+Space/Escape action keys); this + * alternative registration stays available for additional scenes (e.g. floating dock + * stages) that lack a shared key handler. Register F1 at most once per scene, otherwise the + * accelerator (which is processed before the normal key dispatch) would shadow the handler. + * + * @param scene the scene to register the help accelerator for + */ + public static void installAccelerator(Scene scene) { + // Scene.getAccelerators() is an ObservableMap — the runnable is + // registered under its F1 key combination (Swing OpenHelpAction accelerator + // KeyStroke.getKeyStroke(VK_F1, 0), HelpFacade.java:164) + scene.getAccelerators().put(KeyCombination.valueOf("F1"), + () -> openHelp(scene.getFocusOwner())); + } + + /** + * Resolves the documentation URL for the given class (the URL-returning variant of + * {@link #openHelpOfClass(Class)}): the {@link HelpInfoF} path appended to the base URL, or + * {@code null} when the class carries no annotation. + * + * @param clazz non-null class instance + * @return the documentation URL or {@code null} + */ + public static String helpUrlOfClass(Class clazz) { + HelpInfoF help = clazz.getAnnotation(HelpInfoF.class); + if (help == null) { + return null; + } + String path = help.path(); + if (path.startsWith("/")) { + path = path.substring(1); + } + return HELP_BASE_URL + path; + } + + /** + * Resolves the documentation URL along the given node's parent chain (the URL-returning + * variant of {@link #openHelp(Node)}); the fallback URL is returned when no annotated + * ancestor is found. + * + * @param node the node to start the walk at, may be {@code null} + * @param fallback the URL to return when nothing is annotated + * @return the resolved documentation URL + */ + public static String resolveContextHelpUrl(Node node, String fallback) { + while (node != null) { + String url = helpUrlOfClass(node.getClass()); + if (url != null) { + return url; + } + node = node.getParent(); + } + return fallback; + } + + private static void openHelpInBrowser(String url) { + LOGGER.info("Opening help in browser: {}", url); + browserOpener.accept(url); + } + + /** + * Replaces the browser-opening seam (self tests only). + * + * @param opener the new opener + */ + static void setBrowserOpener(Consumer opener) { + browserOpener = opener; + } + + /** + * Self test of the help URL resolution logic (system property {@code key.fx.verify.help}), + * run at startup from {@code MainWindowF}. Exercises the pure logic of the Swing original + * without launching a browser: the seam records the URLs instead. + * + * @return a self-test report ending in {@code PASS} or {@code FAIL} + */ + public static String verifyHelp() { + StringBuilder report = new StringBuilder(); + Consumer realOpener = browserOpener; + try { + // record instead of browsing + String[] lastUrl = new String[1]; + browserOpener = url -> lastUrl[0] = url; + + // 1. default base URL and plain openHelp (HelpFacade.java:71-73) + openHelp(); + check(report, "main page", HELP_BASE_URL, lastUrl[0]); + + // 2. relative path (HelpFacade.java:80-89) + openHelp("user/ProofCaching/"); + check(report, "relative path", HELP_BASE_URL + "user/ProofCaching/", lastUrl[0]); + + // 3. leading slash is stripped + openHelp("/user/Exploration/"); + check(report, "leading slash", HELP_BASE_URL + "user/Exploration/", lastUrl[0]); + + // 4. absolute https URL passes through unchanged + openHelp("https://example.org/doc/"); + check(report, "absolute url", "https://example.org/doc/", lastUrl[0]); + + // 5. annotated class resolution (HelpFacade.java:117-124) + lastUrl[0] = null; + boolean found = openHelpOfClass(AnnotatedDummy.class); + check(report, "annotated class", "true:" + HELP_BASE_URL + "/user/Annotated/", + found + ":" + lastUrl[0]); + + // 6. unannotated class is rejected + check(report, "unannotated class", "false", + String.valueOf(openHelpOfClass(Dummy.class))); + + // 6b. the URL-returning resolution variants + check(report, "helpUrlOfClass", HELP_BASE_URL + "user/Annotated/", + helpUrlOfClass(AnnotatedDummy.class)); + check(report, "helpUrlOfClass unannotated", "null", + String.valueOf(helpUrlOfClass(Dummy.class))); + + // 7. parent-chain resolution (HelpFacade.java:99-107): the annotated parent is + // found from the child + lastUrl[0] = null; + Dummy child = new Dummy(); + AnnotatedDummy parent = new AnnotatedDummy(); + parent.getChildren().add(child); + openHelp(child); + check(report, "parent chain", HELP_BASE_URL + "/user/Annotated/", lastUrl[0]); + check(report, "resolveContextHelpUrl parent", HELP_BASE_URL + "user/Annotated/", + resolveContextHelpUrl(child, HELP_BASE_URL)); + + // 8. a node without any annotated ancestor falls back to the main page (the FX + // counterpart of the Swing OpenHelpAction behavior, see the class javadoc) + lastUrl[0] = null; + Dummy orphan = new Dummy(); + openHelp(orphan); + check(report, "fallback", HELP_BASE_URL, lastUrl[0]); + check(report, "resolveContextHelpUrl fallback", HELP_BASE_URL, + resolveContextHelpUrl(orphan, HELP_BASE_URL)); + } catch (RuntimeException e) { + report.append("exception ").append(e).append("; "); + } finally { + browserOpener = realOpener; + } + report.append(report.toString().contains("FAIL") || report.toString().contains("exception") + ? "FAIL" + : "PASS"); + return report.toString(); + } + + private static void check(StringBuilder report, String what, String expected, + String actual) { + boolean ok = java.util.Objects.equals(expected, actual); + report.append(what).append("=").append(ok ? "ok" + : "FAIL(expected <" + expected + + "> got <" + actual + ">)") + .append("; "); + } + + /** Unannotated node for the self test. */ + private static class Dummy extends javafx.scene.layout.VBox { + } + + /** Annotated node for the self test. */ + @HelpInfoF(path = "/user/Annotated/") + private static class AnnotatedDummy extends javafx.scene.layout.VBox { + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/help/HelpInfoF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/help/HelpInfoF.java new file mode 100644 index 00000000000..24c2c8981f6 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/help/HelpInfoF.java @@ -0,0 +1,35 @@ +/* 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.help; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotate the help page for your component. + *

+ * JavaFX port of {@code de.uka.ilkd.key.gui.help.HelpInfo} (Swing {@code HelpInfo.java:1-31}), + * used by {@link HelpFacadeF} to resolve the documentation page of the focused component. The + * Swing annotation is applied to {@code keyext.*} classes and to {@code MainWindow} ({@code + * MainWindow.java:90}) — the FX main window is covered by {@link HelpFacadeF}'s main-page + * fallback instead (see the port notes there). + * + * @see HelpFacadeF + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.TYPE }) +public @interface HelpInfoF { + /** + * The relative part of the URL to the {@link HelpFacadeF#HELP_BASE_URL}. + *

+ * May also be an absolute URL starting with {@code https://}. + *

+ * + * @return non-null string + */ + String path() default ""; +} From 8276c151c03a7442f8a2c81aa50c64bcbb15dad6 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Thu, 8 Oct 2026 21:53:47 +0200 Subject: [PATCH 052/281] key.ui.fx: docking: remove duplicated java.util.List import --- .../src/main/java/de/uka/ilkd/key/gui/fx/docking/Dockable.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/docking/Dockable.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/docking/Dockable.java index 029f6972209..d1e1c9ce263 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/docking/Dockable.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/docking/Dockable.java @@ -6,8 +6,6 @@ import java.util.List; -import java.util.List; - import javafx.beans.property.BooleanProperty; import javafx.beans.property.ObjectProperty; import javafx.beans.property.StringProperty; From e6d21c215286b6f20b07bdef501cd1df54396ff8 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Thu, 8 Oct 2026 21:54:15 +0200 Subject: [PATCH 053/281] key.ui.fx: proofmgmt: Loaded Proofs dockable (TaskTreeF port) with ProofManagerF multi-proof state and main window wiring --- .../de/uka/ilkd/key/gui/fx/MainWindowF.java | 253 +++++++++++- .../gui/fx/proofmanagement/ProofManagerF.java | 140 +++++++ .../ilkd/key/gui/fx/tasktree/TaskTreeF.java | 371 ++++++++++++++++++ 3 files changed, 757 insertions(+), 7 deletions(-) create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/proofmanagement/ProofManagerF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/tasktree/TaskTreeF.java 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 index 8a46e0ba13f..822bba91dd2 100644 --- 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 @@ -13,6 +13,7 @@ import java.util.Map; import java.util.Optional; import java.util.Properties; +import java.util.function.Consumer; import javafx.application.Platform; import javafx.beans.property.ReadOnlyBooleanWrapper; import javafx.concurrent.Task; @@ -67,11 +68,14 @@ 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.proofdiff.ProofDiffFrameF; +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.SettingsManagerF; 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.proof.Proof; @@ -181,6 +185,26 @@ public final class MainWindowF { */ 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); + + /** + * 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. @@ -261,6 +285,14 @@ public void initialize() { recentFiles.setOnChange(this::updateRecentFilesMenu); recentFiles.load(); 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); + // 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(); + } NotificationManagerF.getInstance() .notify("KeY (JavaFX) started. Docking layout restored from " @@ -320,6 +352,7 @@ private void wireSequentView() { goalListView.attach(selectionModel); strategyView.attach(selectionModel); sourceView.attach(selectionModel); + loadedProofs.attach(selectionModel); // proofmgmt: row highlight follows the active proof // the hook doubles as the :99 verification signal (same line as the standalone driver) sourceView.setOnContentLoaded( () -> LOGGER.info("Source self test: {}", sourceView.verifySourceView())); @@ -425,6 +458,10 @@ protected KeYEnvironment call() throws Exception { // 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); String target = ID_PROOF_TREE.equalsIgnoreCase(show) ? ID_PROOF_TREE : ID_SEQUENT; LOGGER.info("Selecting dockable '{}' (key.fx.show={})", target, show); @@ -1014,7 +1051,10 @@ private void setWindowIcons() { // ------------------------------------------------------------------ private void buildDockables() { - registerDockable(ID_LOADED_PROOFS, "Loaded Proofs"); // TaskTree + // proofmgmt: the Loaded Proofs view replaces the M1 placeholder dockable (JavaFX port of + // the Swing TaskTree) + dockables.put(ID_LOADED_PROOFS, + new SimpleDockable(ID_LOADED_PROOFS, "Loaded Proofs", loadedProofs)); dockables.put(ID_GOAL_LIST, new SimpleDockable(ID_GOAL_LIST, "Goal List", goalListView)); dockables.put(ID_PROOF_TREE, @@ -1039,11 +1079,6 @@ private Node buildSourceViewContent() { return new BorderPane(sourceView, header, null, null, null); } - private void registerDockable(String id, String title) { - dockables.put(id, - new SimpleDockable(id, title, placeholderContent(title))); - } - private List defaultLayout() { return List.of(new DockWorkspace.Default(DockLocation.LEFT, requireDockable(ID_LOADED_PROOFS)), @@ -1138,11 +1173,18 @@ private Menu buildFileMenu() { // 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()); file.getItems().addAll( openExample, openFile, reload, new SeparatorMenuItem(), + proofManagement, saveFile, saveBundle, quickSave, @@ -1250,7 +1292,12 @@ private ToolBar buildFileToolBar() { toolbarButton("Save current proof", IconFactoryF.Key.SAVE_FILE, this::saveProofFile); saveFile.disableProperty() .bind(mediator.autoModeRunningProperty().or(proofLoaded.not())); - bar.getItems().addAll(openFile, reload, saveFile); + // proofmgmt: Swing file toolbar has the Proof Management button as well + javafx.scene.control.Button proofManagement = + toolbarButton("Proof Management", IconFactoryF.Key.PROOF_MANAGEMENT, + this::openProofManagement); + proofManagement.disableProperty().bind(mediator.autoModeRunningProperty()); + bar.getItems().addAll(openFile, reload, saveFile, proofManagement); return bar; } @@ -1413,4 +1460,196 @@ 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(); + } + + /** + * 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/proofmanagement/ProofManagerF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/proofmanagement/ProofManagerF.java new file mode 100644 index 00000000000..c801c9b20b7 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/proofmanagement/ProofManagerF.java @@ -0,0 +1,140 @@ +/* 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.proofmanagement; + +import java.util.function.Consumer; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.ReadOnlyObjectProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; + +import de.uka.ilkd.key.proof.Proof; + +import org.jspecify.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The multi-proof state of the JavaFX main window (proofmgmt): the list of currently loaded + * proofs and the active one. The Swing UI keeps this state in the {@code TaskTreeModel} of the + * {@code TaskTree} view plus {@code MainWindow.getCurrentlyOpenedProofs} (TaskTree.java:112-123 + * registers every proof of a loaded aggregate); here the state lives in its own small class so + * that {@code MainWindowF} and the {@code TaskTreeF} view both consult it, and so that proofs + * started by the {@link ProofManagementDialogF} are registered at the same place. + *

+ * The list holds the individual {@link Proof}s (for the common single-proof aggregates, Swing + * {@code TaskTree.addProof} iterates {@code ProofAggregate.getProofs}, TaskTree.java:112-123). + * Proof removal (Swing {@code TaskTree.removeProof}/{@code removeTask}, TaskTree.java:125-150) + * is deferred until the Abandon-Task flow lands; {@link #removeProof(Proof)} is provided for the + * future caller. + */ +public final class ProofManagerF { + + private static final Logger LOGGER = LoggerFactory.getLogger(ProofManagerF.class); + + /** the loaded proofs, in load order (Swing TaskTreeModel, newest task last). */ + private final ObservableList proofs = FXCollections.observableArrayList(); + + /** + * The active proof. This mirrors the selection model of the mediator (the Swing + * {@code TaskTree} highlights the row of {@code mediator.getSelectedProof}, + * TaskTree.java:397-415): updated by {@link #setActive(Proof)} (user clicks in the view / + * the proof management dialog) and by the selection listener of the view. + */ + private final ObjectProperty activeProof = new SimpleObjectProperty<>(this, + "activeProof"); + + /** + * Activates a proof in the main window; set by {@code MainWindowF} to + * {@code selectionModel::setSelectedProof} (the Swing TaskTree click routes through + * {@code KeYSelectionModel.setSelectedProof} too, TaskTree.java:194-200). Kept as a + * callback so this class has no dependency on the FX mediator. + */ + private @Nullable Consumer activationHandler; + + /** + * @return the loaded proofs (observable; the Loaded Proofs view binds to it) + */ + public ObservableList proofs() { + return proofs; + } + + /** @return the active proof or {@code null} */ + public @Nullable Proof getActiveProof() { + return activeProof.get(); + } + + /** @return the observable active proof */ + public ReadOnlyObjectProperty activeProofProperty() { + return activeProof; + } + + /** + * Sets the handler activating a proof in the main window (the selection model switch). + * + * @param handler called by {@link #setActive(Proof)} + */ + public void setActivationHandler(@Nullable Consumer handler) { + this.activationHandler = handler; + } + + /** + * Registers a loaded proof (Swing {@code TaskTree.addProof}, TaskTree.java:112-123). + * No-op if the proof is already registered (a re-loaded quick save re-registers). + * + * @param proof the loaded proof + */ + public void addProof(Proof proof) { + if (proofs.contains(proof)) { + LOGGER.debug("Proof management: proof {} already registered", proof.name()); + return; + } + proofs.add(proof); + LOGGER.info("Proof management: registered proof {} ({} total)", proof.name(), + proofs.size()); + } + + /** + * Unregisters a proof (Swing {@code TaskTree.removeProof}, TaskTree.java:241-267); no caller + * yet (the Abandon-Task flow is deferred). + * + * @param proof the proof to remove, may be {@code null} + */ + public void removeProof(@Nullable Proof proof) { + if (proof != null) { + proofs.remove(proof); + } + } + + /** @return whether the given proof is registered */ + public boolean contains(Proof proof) { + return proofs.contains(proof); + } + + /** + * Activates the given proof (Swing {@code TaskTree.problemChosen}: the click switches the + * mediator's selected proof, TaskTree.java:194-200): updates {@link #activeProofProperty()} + * and routes the switch through the activation handler. + * + * @param proof the proof to make the active one + */ + public void setActive(Proof proof) { + activeProof.set(proof); + if (activationHandler != null) { + activationHandler.accept(proof); + } + } + + /** + * Mirrors an activation that happened elsewhere (e.g. a load selecting the new proof) + * without routing back through the activation handler. + * + * @param proof the newly active proof + */ + public void setActiveFromSelection(@Nullable Proof proof) { + activeProof.set(proof); + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/tasktree/TaskTreeF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/tasktree/TaskTreeF.java new file mode 100644 index 00000000000..3dd326d2b16 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/tasktree/TaskTreeF.java @@ -0,0 +1,371 @@ +/* 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.tasktree; + +import java.util.IdentityHashMap; +import java.util.Map; +import java.util.Objects; +import javafx.collections.ListChangeListener; +import javafx.scene.control.Label; +import javafx.scene.control.Tooltip; +import javafx.scene.control.TreeCell; +import javafx.scene.control.TreeItem; +import javafx.scene.control.TreeView; +import javafx.scene.layout.BorderPane; + +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.fonticons.IconFactoryF; +import de.uka.ilkd.key.gui.fx.proofmanagement.ProofManagementDialogF.StatusKind; +import de.uka.ilkd.key.gui.fx.proofmanagement.ProofManagerF; +import de.uka.ilkd.key.proof.Proof; +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.mgt.ProofEnvironment; + +import org.key_project.util.javafx.FxUtil; + +import org.jspecify.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * JavaFX port of the Swing "Loaded Proofs" view ({@code de.uka.ilkd.key.gui.TaskTree}, module + * {@code key.ui}): a tree of the currently loaded proofs, grouped by their proof environment + * (the Swing {@code TaskTreeModel} {@code EnvNode}s). Each proof row shows the proof name, the + * number of open goals and the proof status icon (the key-hole icons of the Swing + * {@code TaskTreeIconCellRenderer}, TaskTree.java:348-394, expressed as CSS-colored font icons). + * Clicking a proof row activates it (Swing {@code problemChosen}, + * TaskTree.java:194-200: {@code selectionModel.setSelectedProof}, routed through + * {@link ProofManagerF#setActive(Proof)}), a change of the selected proof highlights the + * corresponding row (Swing {@code TaskTreeSelectionListener}, TaskTree.java:397-415). + *

+ * The multi-proof state (the list of proofs and the active one) lives in + * {@link ProofManagerF}, which the main window consults on every load and the Proof Management + * dialog reports started proofs to; this view binds to the manager's observable list. + *

+ * This first version is a flat two-level tree (environment → proofs); the full Swing model port + * ({@code TaskTreeModel} with {@code BasicTask}/{@code ProofAggregateTask} compound tasks and + * the right-click context menu with close/prune/save actions, TaskTree.java:273-312) is + * deferred. Like the Swing view, proof structure events ({@code proofClosed}/{@code + * proofPruned}/{@code proofStructureChanged}) refresh the affected row + * (TaskTree.java:318-345). + */ +public class TaskTreeF extends BorderPane { + + private static final Logger LOGGER = LoggerFactory.getLogger(TaskTreeF.class); + + /** the multi-proof state consulted by the main window (proofmgmt). */ + private final ProofManagerF proofManager; + + /** tree item values: either a {@link ProofEnvironment} group or a {@link Proof} */ + private final TreeView tree = new TreeView<>(); + + /** environment group items by environment (identity, environments are singletons per env) */ + private final Map> envItems = new IdentityHashMap<>(); + + /** proof items by proof */ + private final Map> proofItems = new IdentityHashMap<>(); + + /** proof tree listeners per registered proof (row refresh on structural changes) */ + private final Map proofListeners = new IdentityHashMap<>(); + + private @Nullable KeYSelectionModel selectionModel; + + /** true while this view mutates the tree selection programmatically */ + private boolean updatingSelection; + + /** + * The selection listener keeping the tree selection in sync with the mediator's selected + * proof (Swing {@code TaskTreeSelectionListener}). + */ + private final KeYSelectionListener selectionListener = new KeYSelectionListener() { + @Override + public void selectedNodeChanged(KeYSelectionEvent event) { + // empty (like the Swing listener) + } + + @Override + public void selectedProofChanged(KeYSelectionEvent event) { + Proof selected = event.getSource().getSelectedProof(); + proofManager.setActiveFromSelection(selected); + syncSelection(selected); + } + }; + + /** + * Creates the loaded proofs view bound to the given multi-proof state. + * + * @param proofManager the proof manager of the main window + */ + public TaskTreeF(ProofManagerF proofManager) { + this.proofManager = proofManager; + tree.getStyleClass().add("loaded-proofs"); + tree.setShowRoot(false); + tree.setRoot(new TreeItem<>("Tasks")); + tree.setCellFactory(view -> new TaskTreeCell()); + tree.getSelectionModel().selectedItemProperty().addListener((obs, old, value) -> { + if (updatingSelection || value == null || !(value.getValue() instanceof Proof proof)) { + return; + } + // a user click activates the proof (Swing TaskTreeSelectionListener → problemChosen) + if (selectionModel != null && selectionModel.getSelectedProof() != proof) { + LOGGER.debug("Loaded Proofs: activating proof {}", proof.name()); + proofManager.setActive(proof); + } + }); + Label placeholder = new Label("No proofs loaded."); + placeholder.getStyleClass().add("loaded-proofs-empty"); + setCenter(tree); + setBottom(placeholder); + + // the view follows the multi-proof state of the manager: every proof registered by the + // main window (or the Proof Management dialog) gets a row + proofManager.proofs().addListener((ListChangeListener) change -> { + while (change.next()) { + if (change.wasAdded()) { + change.getAddedSubList().forEach(this::addProof); + } + if (change.wasRemoved()) { + change.getRemoved().forEach(this::removeProof); + } + } + }); + } + + /** + * Registers this view as a selection listener on the given model and highlights the + * currently selected proof. + * + * @param model the selection model of the mediator + */ + public void attach(KeYSelectionModel model) { + Objects.requireNonNull(model); + if (selectionModel == model) { + return; + } + if (selectionModel != null) { + selectionModel.removeKeYSelectionListener(selectionListener); + } + selectionModel = model; + model.addKeYSelectionListenerChecked(selectionListener); + syncSelection(model.getSelectedProof()); + } + + /** + * Adds a loaded proof to the view (Swing {@code TaskTree.addProof}, TaskTree.java:112-123). + * The proof is grouped below its proof environment; the environment group is expanded. The + * proof is observed for structural changes so its row status stays current. + * + * @param proof the loaded proof + */ + private void addProof(Proof proof) { + if (proofItems.containsKey(proof)) { + LOGGER.debug("Loaded Proofs: proof {} already shown", proof.name()); + return; + } + TreeItem envItem = envItemFor(proof.getEnv()); + TreeItem proofItem = new TreeItem<>(proof); + envItem.getChildren().add(proofItem); + envItem.setExpanded(true); + proofItems.put(proof, proofItem); + + // observe structural changes of the proof (Swing TaskTreeProofTreeListener repaints the + // affected row, TaskTree.java:318-345; here the cell re-renders on tree refresh) + ProofTreeListener listener = new ProofTreeAdapter() { + private void refreshRow(ProofTreeEvent e) { + FxUtil.runLater(() -> { + TreeItem item = proofItems.get(e.getSource()); + if (item != null) { + // re-render the cell contents (status/goal count may have changed) + tree.refresh(); + } + }); + } + + @Override + public void proofClosed(ProofTreeEvent e) { + refreshRow(e); + } + + @Override + public void proofPruned(ProofTreeEvent e) { + refreshRow(e); + } + + @Override + public void proofStructureChanged(ProofTreeEvent e) { + refreshRow(e); + } + }; + proof.addProofTreeListener(listener); + proofListeners.put(proof, listener); + + refreshEmptyState(); + } + + /** + * Removes a proof from the view (Swing {@code TaskTree.removeProof}, TaskTree.java:241-267). + * + * @param proof the proof to remove, may be {@code null} + */ + private void removeProof(@Nullable Proof proof) { + if (proof == null) { + return; + } + ProofTreeListener listener = proofListeners.remove(proof); + if (listener != null) { + proof.removeProofTreeListener(listener); + } + TreeItem item = proofItems.remove(proof); + if (item == null) { + return; + } + TreeItem parent = item.getParent(); + parent.getChildren().remove(item); + if (parent.getChildren().isEmpty() && parent.getParent() != null) { + // remove the empty environment group (Swing TaskTreeModel.removeTask) + envItems.remove((ProofEnvironment) parent.getValue()); + parent.getParent().getChildren().remove(parent); + } + refreshEmptyState(); + } + + /** + * @return the number of proofs currently shown + */ + public int getProofCount() { + return proofItems.size(); + } + + /** + * Development self test (the {@code key.fx.verify.proofmgmt} affordance): verifies that + * every registered proof has a row with the expected goal count in the label and that the + * highlighted row is the selected proof. + * + * @return a one-line report, {@code "... PASS"} if all checks pass + */ + public String verifyContent() { + if (!FxUtil.isFxThread()) { + return FxUtil.callAndWait(this::verifyContent); + } + for (Map.Entry> e : proofItems.entrySet()) { + Proof proof = e.getKey(); + TreeItem item = e.getValue(); + if (item.getParent() == null) { + return "proof row of " + proof.name() + " is detached FAIL"; + } + String expected = labelFor(proof); + String rendered = cellText(item); + if (!expected.equals(rendered)) { + return "row text '" + rendered + "' != '" + expected + "' FAIL"; + } + } + Proof selected = selectionModel != null ? selectionModel.getSelectedProof() : null; + if (selected != null && proofItems.containsKey(selected)) { + TreeItem item = tree.getSelectionModel().getSelectedItem(); + if (item == null || item.getValue() != selected) { + return "highlighted row is not the selected proof FAIL"; + } + } + return "proofs=" + proofItems.size() + " rows in sync PASS"; + } + + private String cellText(TreeItem item) { + // the rendered text is recomputed exactly like the cell does it (the row is rendered + // fresh on every refresh, so the model text is the rendered text) + return item.getValue() instanceof Proof proof ? labelFor(proof) : ""; + } + + private String labelFor(Proof proof) { + return proof.name() + " · " + proof.openGoals().size() + " open goal(s)"; + } + + private TreeItem envItemFor(ProofEnvironment env) { + return envItems.computeIfAbsent(env, e -> { + TreeItem item = new TreeItem<>(e); + tree.getRoot().getChildren().add(item); + item.setExpanded(true); + return item; + }); + } + + private void refreshEmptyState() { + boolean empty = proofItems.isEmpty(); + getBottom().setVisible(empty); + getBottom().setManaged(empty); + } + + /** + * Highlights the row of the given proof (Swing + * {@code TaskTreeSelectionListener.selectedProofChanged}, TaskTree.java:406-413). + */ + private void syncSelection(@Nullable Proof proof) { + if (!FxUtil.isFxThread()) { + FxUtil.runLater(() -> syncSelection(proof)); + return; + } + if (proof == null) { + return; + } + TreeItem item = proofItems.get(proof); + if (item == null) { + return; + } + updatingSelection = true; + try { + tree.getSelectionModel().clearSelection(); + tree.getSelectionModel().select(item); + tree.scrollTo(tree.getRow(item)); + } finally { + updatingSelection = false; + } + } + + /** + * The tree cell (Swing {@code TaskTreeIconCellRenderer}, TaskTree.java:348-394): the + * key-hole status icon, the proof name with the open goal count for proof rows, the + * environment description for group rows. + */ + private static final class TaskTreeCell extends TreeCell { + @Override + protected void updateItem(Object item, boolean empty) { + super.updateItem(item, empty); + getStyleClass().removeIf("loaded-proofs-env"::equals); + if (empty || item == null) { + setText(null); + setGraphic(null); + setTooltip(null); + return; + } + if (item instanceof ProofEnvironment env) { + getStyleClass().add("loaded-proofs-env"); + setText(env.description()); + setGraphic(null); + setTooltip(null); + return; + } + Proof proof = (Proof) item; + StatusKind kind = StatusKind.fromProofStatus(proof.mgt().getStatus()); + setText(labelText(proof)); + javafx.scene.Node icon = + kind == StatusKind.NONE ? null + : IconFactoryF.createIcon(IconFactoryF.Key.KEY_HOLE, 14); + if (icon != null && kind.styleClass() != null) { + icon.getStyleClass().add(kind.styleClass()); + if (kind.tooltip() != null) { + setTooltip(new Tooltip(kind.tooltip())); + } + } + setGraphic(icon); + } + + private static String labelText(Proof proof) { + return proof.name() + " · " + proof.openGoals().size() + " open goal(s)"; + } + } +} From e699c59458cad857af10128c0c14b1db5713938c Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Thu, 8 Oct 2026 21:54:15 +0200 Subject: [PATCH 054/281] key.ui.fx: proofmgmt: forward the proofmgmt self-test flags and theme the proof status icons + Loaded Proofs view --- key.ui.fx/build.gradle | 13 +++++- .../de/uka/ilkd/key/gui/fx/theme/key-dark.css | 40 +++++++++++++++++++ .../uka/ilkd/key/gui/fx/theme/key-light.css | 40 +++++++++++++++++++ 3 files changed, 92 insertions(+), 1 deletion(-) diff --git a/key.ui.fx/build.gradle b/key.ui.fx/build.gradle index 86152523631..d1b527c9be4 100644 --- a/key.ui.fx/build.gradle +++ b/key.ui.fx/build.gradle @@ -65,11 +65,22 @@ run { // Hide/Regroup) with the key.fx.verify.sequentsearch query after the demo load) // * key.fx.verify.updatehighlight = any (M2: run the update-highlight overlay self test; // needs a sequent printing update operators, e.g. standard_key/queries/useQuery.key) + // proofmgmt (JavaFX port of the Swing ProofManagementDialog and TaskTree view): + // * key.fx.verify.proofmgmt = 1|2|all (M?: run the proof management self test; 1 the + // ProofManagementDialogF self test with a real JML example, 2 the Loaded Proofs view + // self test with two loaded proofs and active-proof switching, all both) + // * key.fx.demo.proofmgmt = (M?: example for the dialog self test; default + // heap/vstte10_01_SumAndMax/SumAndMax_sumAndMax.key) + // * key.fx.demo.sequent2 / key.fx.demo.sequent3 = (M?: the two examples of the + // Loaded Proofs view self test; default firstTouch/01-Agatha/project.key and + // firstTouch/05-ReverseArray/reverseArray.key) ["key.fx.driver", "key.fx.theme", "key.fx.demo.sequent", "key.fx.verify.sequent", "key.fx.show", "key.fx.demo.autoprove", "key.fx.verify.tree", "key.fx.demo.autoprove.live", "key.fx.demo.maxsteps", "key.fx.verify.goallist", "key.fx.verify.search", "key.fx.verify.sequentsearch", "key.fx.verify.treefilters", - "key.fx.verify.sequentsearchmodes", "key.fx.verify.updatehighlight"] + "key.fx.verify.sequentsearchmodes", "key.fx.verify.updatehighlight", + "key.fx.verify.proofmgmt", "key.fx.demo.proofmgmt", "key.fx.demo.sequent2", + "key.fx.demo.sequent3"] .each { name -> def value = System.getProperty(name) if (value != null) { diff --git a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css index 173dd4a912f..bdf272fd343 100644 --- a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css +++ b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css @@ -607,3 +607,43 @@ .sequent-search-mode { -fx-padding: 1; } + +/* --------------------------------------------------------------------- + * Proof management (proofmgmt: JavaFX port of the Swing ProofManagementDialog + * and the "Loaded Proofs" TaskTree view) + * --------------------------------------------------------------------- */ + +/* the key-hole status icons of the Swing TaskTreeIconCellRenderer + * (TaskTree.java:348-394), colored like the proof tree node icons */ +.proof-status-open { + -fx-text-fill: -key-error; +} + +.proof-status-closed { + -fx-text-fill: -key-proved; +} + +.proof-status-almost-closed { + -fx-text-fill: -key-warning; +} + +.proof-status-closed-cache { + -fx-text-fill: -key-accent; +} + +/* the "Loaded Proofs" view (Swing de.uka.ilkd.key.gui.TaskTree) */ +.loaded-proofs { + -fx-font-size: 1em; +} + +/* the environment group rows (Swing TaskTreeModel EnvNodes) */ +.loaded-proofs-env { + -fx-text-fill: -key-text-secondary; + -fx-font-weight: bold; +} + +/* the placeholder shown when no proof is loaded */ +.loaded-proofs-empty { + -fx-text-fill: -key-text-secondary; + -fx-padding: 6; +} diff --git a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css index 2962a642bf4..1d36bdf9ff1 100644 --- a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css +++ b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css @@ -601,3 +601,43 @@ .sequent-search-mode { -fx-padding: 1; } + +/* --------------------------------------------------------------------- + * Proof management (proofmgmt: JavaFX port of the Swing ProofManagementDialog + * and the "Loaded Proofs" TaskTree view) + * --------------------------------------------------------------------- */ + +/* the key-hole status icons of the Swing TaskTreeIconCellRenderer + * (TaskTree.java:348-394), colored like the proof tree node icons */ +.proof-status-open { + -fx-text-fill: -key-error; +} + +.proof-status-closed { + -fx-text-fill: -key-proved; +} + +.proof-status-almost-closed { + -fx-text-fill: -key-warning; +} + +.proof-status-closed-cache { + -fx-text-fill: -key-accent; +} + +/* the "Loaded Proofs" view (Swing de.uka.ilkd.key.gui.TaskTree) */ +.loaded-proofs { + -fx-font-size: 1em; +} + +/* the environment group rows (Swing TaskTreeModel EnvNodes) */ +.loaded-proofs-env { + -fx-text-fill: -key-text-secondary; + -fx-font-weight: bold; +} + +/* the placeholder shown when no proof is loaded */ +.loaded-proofs-empty { + -fx-text-fill: -key-text-secondary; + -fx-padding: 6; +} From bab90d08e03c7ab9960d7179071e85c15c9a06c0 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Thu, 8 Oct 2026 21:56:47 +0200 Subject: [PATCH 055/281] key.ui.fx: docking: fromSwingSpec defaults unspecified modifiers to UP (Swing KeyStroke parity) A file override like 'ctrl pressed F11' previously matched Ctrl+Shift+F11 as well (ModifierValue.ANY fallback), so a slot save stroke also triggered the slot load. JavaFX's own KeyCombination parser uses UP defaults; Swing KeyStrokes require exact modifier match. Compile gate re-run clean after a transient incremental-compile failure. --- .../key/gui/fx/keyshortcuts/KeyStrokeManagerF.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/keyshortcuts/KeyStrokeManagerF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/keyshortcuts/KeyStrokeManagerF.java index 1b4eef9207e..d9c5875091d 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/keyshortcuts/KeyStrokeManagerF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/keyshortcuts/KeyStrokeManagerF.java @@ -326,10 +326,12 @@ public static Optional fromSwingSpec(String spec) { if (spec == null || spec.isBlank()) { return Optional.empty(); } - ModifierValue shift = ModifierValue.ANY; - ModifierValue ctrl = ModifierValue.ANY; - ModifierValue meta = ModifierValue.ANY; - ModifierValue alt = ModifierValue.ANY; + // Swing KeyStroke semantics: a modifier not mentioned in the spec must be UP (a Swing + // "ctrl pressed F11" stroke does not match an event with shift also pressed) + ModifierValue shift = ModifierValue.UP; + ModifierValue ctrl = ModifierValue.UP; + ModifierValue meta = ModifierValue.UP; + ModifierValue alt = ModifierValue.UP; String key = null; for (String token : spec.trim().split("\\s+")) { switch (token) { From 60d403ee46ae3d87d0ad0326482f6f76cefc90c2 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Thu, 8 Oct 2026 22:01:42 +0200 Subject: [PATCH 056/281] key.ui.fx: seam: WindowUserInterfaceControlF routes core callbacks into the FX window and hosts the rule-completion registry --- ...InteractiveRuleApplicationCompletionF.java | 42 ++ .../gui/fx/WindowUserInterfaceControlF.java | 510 ++++++++++++++++++ 2 files changed, 552 insertions(+) create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/InteractiveRuleApplicationCompletionF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/WindowUserInterfaceControlF.java 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/WindowUserInterfaceControlF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/WindowUserInterfaceControlF.java new file mode 100644 index 00000000000..7a2021bb84d --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/WindowUserInterfaceControlF.java @@ -0,0 +1,510 @@ +/* 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 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.macros.ProofMacro; +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}), the taclet-match + * dialogs ({@code TacletMatchDialog}, a stub here), {@code selectProofObligation} + * ({@code ProofManagementDialog}) and the progress bar (the FX status bar has no progress bar + * yet). + */ +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); + } + + /** + * 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}): the Swing original opens the redesigned + * {@code TacletMatchDialog} (or the classic one via + * {@code ViewSettings.isUseClassicTacletDialog} + * — a migration toggle). The FX taclet-match dialog suite is a separate port item; for now + * the completion is reported and skipped (the core then keeps the app unapplied). + */ + @Override + public void completeAndApplyTacletMatch(TacletInstantiationModel[] models, Goal goal) { + LOGGER.warn("The taclet match dialog is not yet ported to the JavaFX UI; " + + "the taclet application is skipped ({} model(s))", models.length); + NotificationManagerF.getInstance().notify( + "The taclet match dialog arrives in a later milestone of the key.ui.fx rewrite.", + NotificationManagerF.Kind.WARNING); + } + + // ------------------------------------------------------------------ + // 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) { + // the FX status bar has no progress bar yet; the message is shown regardless + reportStatus(sender, status); + } + + /** 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-299 (status line progress): the FX status + * bar has no progress bar yet, so the position is logged only. + */ + @Override + public void taskProgress(int position) { + super.taskProgress(position); + LOGGER.debug("Task progress: {}", position); + } + + /** + * Port of Swing WindowUserInterfaceControl.java:301-305 + * ({@code mainWindow.setStatusLine(info.message(), info.size())}). + */ + @Override + public void taskStarted(TaskStartedInfo info) { + super.taskStarted(info); + reportStatus(this, info.message()); + } + + /** Port of Swing WindowUserInterfaceControl.java:307-315 (progress bar maximum). */ + @Override + public void setMaximum(int maximum) { + LOGGER.debug("Task progress maximum: {}", maximum); + } + + /** Port of Swing WindowUserInterfaceControl.java:312-315 (progress bar position). */ + @Override + public void setProgress(int progress) { + LOGGER.debug("Task progress: {}", 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()) { + // Swing hides the status progress here (:213); the FX status bar has none yet + 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}). The Swing {@code + * getMediator().fireProofLoaded} has no FX counterpart (the FX views observe the selection + * model). + */ + @Override + public void registerProofAggregate(ProofAggregate pa) { + super.registerProofAggregate(pa); + 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)); + } +} From b99e9c9c9ce0af833ca7781f889080835da061ce Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Thu, 8 Oct 2026 22:01:44 +0200 Subject: [PATCH 057/281] key.ui.fx: seam: port the Swing dialog trio (IssueDialog, AutoDismissDialog, LogView) + per-run logback file appender --- .../ilkd/key/gui/fx/AutoDismissDialogF.java | 146 +++++ .../de/uka/ilkd/key/gui/fx/IssueDialogF.java | 500 ++++++++++++++++++ .../ilkd/key/gui/fx/LogStartupListenerF.java | 70 +++ .../java/de/uka/ilkd/key/gui/fx/LogViewF.java | 350 ++++++++++++ key.ui.fx/src/main/resources/logback.xml | 39 ++ 5 files changed, 1105 insertions(+) create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/AutoDismissDialogF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/IssueDialogF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/LogStartupListenerF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/LogViewF.java create mode 100644 key.ui.fx/src/main/resources/logback.xml 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/IssueDialogF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/IssueDialogF.java new file mode 100644 index 00000000000..fb8794b663c --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/IssueDialogF.java @@ -0,0 +1,500 @@ +/* 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; + + 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(); + } + } + + /** + * 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) { + 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 {@code true} while the dialog is shown (self-test hook) */ + public boolean isVisible() { + return dialog.isShowing(); + } + + /** @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() { + dialog.showAndWait(); + } +} 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/resources/logback.xml b/key.ui.fx/src/main/resources/logback.xml new file mode 100644 index 00000000000..25a939865d0 --- /dev/null +++ b/key.ui.fx/src/main/resources/logback.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + ${LOG_DIR}/key_${timestamp}.log + false + + UTF-8 + + %date|%level|%thread|%logger|%file:%line|%replace(%msg){'[\n\r]','\\n'}|%replace(%ex){'[\n\r]','\\n'}%nopex|%n + true + + + + + + [%date{HH:mm:ss.SSS}] %highlight(%-5level) %cyan(%logger{0}) - %msg%ex%n + + + INFO + + + + + + + + From 01914b48e1a7306686b9c5485170e20961513944 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Thu, 8 Oct 2026 22:01:48 +0200 Subject: [PATCH 058/281] key.ui.fx: seam: wire WindowUserInterfaceControlF into the load path, View menu log view, uicontrol self test (key.fx.verify.uicontrol) --- key.ui.fx/build.gradle | 5 +- .../de/uka/ilkd/key/gui/fx/MainWindowF.java | 55 ++++++++++- .../ilkd/key/gui/fx/UiControlSelfTestF.java | 95 +++++++++++++++++++ .../de/uka/ilkd/key/gui/fx/theme/key-dark.css | 59 ++++++++++++ .../uka/ilkd/key/gui/fx/theme/key-light.css | 59 ++++++++++++ 5 files changed, 270 insertions(+), 3 deletions(-) create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/UiControlSelfTestF.java diff --git a/key.ui.fx/build.gradle b/key.ui.fx/build.gradle index 86152523631..b50b898e37e 100644 --- a/key.ui.fx/build.gradle +++ b/key.ui.fx/build.gradle @@ -65,11 +65,14 @@ run { // Hide/Regroup) with the key.fx.verify.sequentsearch query after the demo load) // * key.fx.verify.updatehighlight = any (M2: run the update-highlight overlay self test; // needs a sequent printing update operators, e.g. standard_key/queries/useQuery.key) + // * key.fx.verify.uicontrol = any (seam: run the WindowUserInterfaceControlF self test + // (status line / IssueDialogF / LogViewF / AutoDismissDialogF) after the demo load) ["key.fx.driver", "key.fx.theme", "key.fx.demo.sequent", "key.fx.verify.sequent", "key.fx.show", "key.fx.demo.autoprove", "key.fx.verify.tree", "key.fx.demo.autoprove.live", "key.fx.demo.maxsteps", "key.fx.verify.goallist", "key.fx.verify.search", "key.fx.verify.sequentsearch", "key.fx.verify.treefilters", - "key.fx.verify.sequentsearchmodes", "key.fx.verify.updatehighlight"] + "key.fx.verify.sequentsearchmodes", "key.fx.verify.updatehighlight", + "key.fx.verify.uicontrol"] .each { name -> def value = System.getProperty(name) if (value != null) { 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 index 8a46e0ba13f..72cdcc3920a 100644 --- 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 @@ -143,6 +143,16 @@ public final class MainWindowF { */ 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); + /** * The proof tree view (first M2 version): displays the proof of the selection model, node * clicks drive the selection. @@ -309,6 +319,15 @@ 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) // ------------------------------------------------------------------ @@ -397,7 +416,9 @@ protected KeYEnvironment call() throws Exception { // the bundle to a temporary directory and loads the selected proof file from // there. This mirrors KeYEnvironment.load, which offers no proofFilename // parameter. - DefaultUserInterfaceControl ui = new DefaultUserInterfaceControl(); + // 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); @@ -405,7 +426,16 @@ protected KeYEnvironment call() throws Exception { env = new KeYEnvironment<>(ui, loader.getInitConfig(), loader.getProof(), loader.getProofScript(), loader.getResult()); } else { - env = KeYEnvironment.load(location); + // 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"); @@ -482,6 +512,11 @@ protected KeYEnvironment call() throws Exception { .notify("Proof diff verification: " + report, report.endsWith("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); + } if (System.getProperty("key.fx.demo.autoprove.live") != null) { startLiveAutoMode(env); } @@ -786,6 +821,22 @@ public void setStatusLine(String status) { statusLeft.setText(status); } + /** + * seam: resets the status line to the proof summary (Swing + * {@code MainWindow.setStandardStatusLine}); called by + * {@link #userInterface} on {@code resetStatus}. + */ + public void resetStatusLine() { + updateProofStatus(); + } + + /** + * seam: current left status text (used by the {@code key.fx.verify.uicontrol} self test). + */ + public String getStatusLineText() { + return statusLeft.getText(); + } + /** * 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 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..43c8461d445 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/UiControlSelfTestF.java @@ -0,0 +1,95 @@ +/* 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.notification.NotificationManagerF; +import de.uka.ilkd.key.gui.fx.notification.NotificationManagerF.Kind; + +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: + *
    + *
  1. {@code reportStatus} — asserts that the status bar shows the message;
  2. + *
  3. {@code reportException} with a synthetic exception — asserts that an {@link IssueDialogF} + * is visible with the exception as issue;
  4. + *
  5. {@link LogViewF} — opens the log view, emits log lines and asserts that they are + * rendered;
  6. + *
  7. {@link AutoDismissDialogF} — shows the auto-dismiss popup (with an extended delay so it + * survives until the interactive inspection) and asserts that it is visible.
  8. + *
+ * 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); + + 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 + 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.isVisible() + && dialog.getIssues().stream() + .anyMatch(i -> i.text().contains("synthetic exception")); + report("exception verification", dialogPass, + dialog == null ? "no IssueDialogF was opened" + : "IssueDialogF visible=" + dialog.isVisible() + ", 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())); + }); + }); + }); + } + + /** 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/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css index 173dd4a912f..984a97ddf20 100644 --- a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css +++ b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css @@ -607,3 +607,62 @@ .sequent-search-mode { -fx-padding: 1; } + +/* ------------------------------------------------------------------ + * seam: dialog trio (Swing IssueDialog / AutoDismissDialog / LogView) + * ------------------------------------------------------------------ */ + +/* IssueDialogF: monospaced source/stacktrace preview (Swing IssueDialog.txtSource, + txtStacktrace), location labels (Swing fTextField/lTextField/cTextField) */ +.issue-dialog-source { + -fx-font-family: "Monospaced"; + -fx-font-size: 1em; +} + +.issue-dialog-location { + -fx-text-fill: -key-linked; +} + +/* AutoDismissDialogF: the pink message panel of the Swing original + (AutoDismissDialog.java:40, color (1, 0.7f, 0.7f) — a fixed Swing color without a + theme variable) */ +.auto-dismiss-dialog { + -fx-background-color: #ffb3b3; +} + +/* LogViewF: styled log fields (Swing LogViewPane styles: time/thread/class gray, level bold); + the level colors use the theme palette */ +.log-view-area { + -fx-font-family: "Monospaced"; + -fx-font-size: 1em; + -fx-background-color: -key-surface; + -fx-padding: 4; +} + +.log-time, .log-thread, .log-class, .log-file { + -fx-fill: -key-text-secondary; +} + +.log-level { + -fx-font-weight: bold; +} + +.log-level.ERROR { + -fx-fill: -key-error; +} + +.log-level.WARN { + -fx-fill: -key-warning; +} + +.log-level.INFO, .log-level.DEBUG, .log-level.TRACE { + -fx-fill: -key-text; +} + +.log-msg { + -fx-fill: -key-text; +} + +.log-ex { + -fx-fill: -key-error; +} diff --git a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css index 2962a642bf4..c9e805c7286 100644 --- a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css +++ b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css @@ -601,3 +601,62 @@ .sequent-search-mode { -fx-padding: 1; } + +/* ------------------------------------------------------------------ + * seam: dialog trio (Swing IssueDialog / AutoDismissDialog / LogView) + * ------------------------------------------------------------------ */ + +/* IssueDialogF: monospaced source/stacktrace preview (Swing IssueDialog.txtSource, + txtStacktrace), location labels (Swing fTextField/lTextField/cTextField) */ +.issue-dialog-source { + -fx-font-family: "Monospaced"; + -fx-font-size: 1em; +} + +.issue-dialog-location { + -fx-text-fill: -key-linked; +} + +/* AutoDismissDialogF: the pink message panel of the Swing original + (AutoDismissDialog.java:40, color (1, 0.7f, 0.7f) — a fixed Swing color without a + theme variable) */ +.auto-dismiss-dialog { + -fx-background-color: #ffb3b3; +} + +/* LogViewF: styled log fields (Swing LogViewPane styles: time/thread/class gray, level bold); + the level colors use the theme palette */ +.log-view-area { + -fx-font-family: "Monospaced"; + -fx-font-size: 1em; + -fx-background-color: -key-surface; + -fx-padding: 4; +} + +.log-time, .log-thread, .log-class, .log-file { + -fx-fill: -key-text-secondary; +} + +.log-level { + -fx-font-weight: bold; +} + +.log-level.ERROR { + -fx-fill: -key-error; +} + +.log-level.WARN { + -fx-fill: -key-warning; +} + +.log-level.INFO, .log-level.DEBUG, .log-level.TRACE { + -fx-fill: -key-text; +} + +.log-msg { + -fx-fill: -key-text; +} + +.log-ex { + -fx-fill: -key-error; +} From 7639d29178c8de51b18f784cfb0d4060baa26b37 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Thu, 8 Oct 2026 22:02:19 +0200 Subject: [PATCH 059/281] key.ui.fx: seam: View menu entry for LogViewF (Swing trigger is a status-line extension button; no FX extension SPI yet) --- .../java/de/uka/ilkd/key/gui/fx/MainWindowF.java | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) 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 index 72cdcc3920a..4c84116e80f 100644 --- 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 @@ -1242,10 +1242,23 @@ private Menu buildViewMenu() { view.getItems().addAll(prettyPrint, unicode, syntaxHighlighting, new SeparatorMenuItem(), themeMenu, fontSize, new SeparatorMenuItem(), menuItem("Visual Node Diff", "de.uka.ilkd.key.gui.proofdiff.ProofDiffFrame$Action", - this::showProofDiffFrame)); + this::showProofDiffFrame), + // 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)); return view; } + /** + * seam: opens the log view window (Swing {@code ShowLogAction} → + * {@code LogView.showInstance}, {@code LogView.java:88-126}). + */ + private void showLogView() { + LogViewF.showInstance(getStage()); + } + private Menu buildProofMenu() { Menu proof = new Menu("Proof"); Menu automation = new Menu("Automation"); From 32403c1e4cede3fc8e19dd50cd11966a1faa8e59 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Thu, 8 Oct 2026 22:02:36 +0200 Subject: [PATCH 060/281] key.ui.fx: lemmaorigin: port the term-labels View menu (TermLabelMenuF) + hide-package-prefix and origin-tracking toggles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Port of Swing de.uka.ilkd.key.gui.actions.TermLabelMenu (View▸Term Labels: 'Display Term Labels in Formulas' master switch plus one check item per term label name of the loaded proof, backed by a shared TermLabelVisibilityManager), of HidePackagePrefixToggleAction (View▸Hide Package Prefix) and of the OriginTermLabelsExt main-menu items (View▸Origin Tracking▸Toggle Term Origin Tracking with the Swing reload dialog, executed as quick save + quick load via QuickSaveF). SequentViewF receives a minimal marked hook ('// lemmaorigin:'): the printer is now built with the installed VisibleTermLabels (Swing SequentView.getVisibleTermLabels), the last clicked position is exposed for the origin visualizer trigger, and the printed text is readable for the key.fx.verify.lemmaorigin self test (OriginLabelsF.verify). Deviations: no tooltips on menu items (JavaFX MenuItem has no tooltip API, general FX deviation); the Swing bold/italic occurrence styling of the label items is not ported; 'Highlight Origins' is omitted (gates the not-yet-ported sequent→source origin highlight); Toggle Term Origin Tracking flips the setting before the reload so the re-created proof carries the new labels (in Swing the flip happens after the reload and cannot affect it). --- key.ui.fx/build.gradle | 5 +- .../de/uka/ilkd/key/gui/fx/MainWindowF.java | 14 ++ .../key/gui/fx/nodeviews/SequentViewF.java | 54 ++++- .../gui/fx/originlabels/OriginLabelsF.java | 229 ++++++++++++++++++ .../gui/fx/originlabels/TermLabelMenuF.java | 134 ++++++++++ 5 files changed, 434 insertions(+), 2 deletions(-) create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/originlabels/OriginLabelsF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/originlabels/TermLabelMenuF.java diff --git a/key.ui.fx/build.gradle b/key.ui.fx/build.gradle index 86152523631..26a6b2399c2 100644 --- a/key.ui.fx/build.gradle +++ b/key.ui.fx/build.gradle @@ -65,11 +65,14 @@ run { // Hide/Regroup) with the key.fx.verify.sequentsearch query after the demo load) // * key.fx.verify.updatehighlight = any (M2: run the update-highlight overlay self test; // needs a sequent printing update operators, e.g. standard_key/queries/useQuery.key) + // * key.fx.verify.lemmaorigin = any (lemmaorigin: run the term-label / origin-label / + // lemma-generator self test after the demo load) ["key.fx.driver", "key.fx.theme", "key.fx.demo.sequent", "key.fx.verify.sequent", "key.fx.show", "key.fx.demo.autoprove", "key.fx.verify.tree", "key.fx.demo.autoprove.live", "key.fx.demo.maxsteps", "key.fx.verify.goallist", "key.fx.verify.search", "key.fx.verify.sequentsearch", "key.fx.verify.treefilters", - "key.fx.verify.sequentsearchmodes", "key.fx.verify.updatehighlight"] + "key.fx.verify.sequentsearchmodes", "key.fx.verify.updatehighlight", + "key.fx.verify.lemmaorigin"] .each { name -> def value = System.getProperty(name) if (value != null) { 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 index 8a46e0ba13f..de75655d309 100644 --- 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 @@ -66,6 +66,7 @@ import de.uka.ilkd.key.gui.fx.nodeviews.SequentViewF; 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.originlabels.OriginLabelsF; import de.uka.ilkd.key.gui.fx.proofdiff.ProofDiffFrameF; import de.uka.ilkd.key.gui.fx.prooftree.ProofTreeViewF; import de.uka.ilkd.key.gui.fx.recentfiles.RecentFilesF; @@ -482,6 +483,15 @@ protected KeYEnvironment call() throws Exception { .notify("Proof diff verification: " + report, report.endsWith("PASS") ? Kind.INFO : Kind.ERROR); } + // 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 if (System.getProperty("key.fx.demo.autoprove.live") != null) { startLiveAutoMode(env); } @@ -1192,6 +1202,10 @@ private Menu buildViewMenu() { themeMenu, fontSize, new SeparatorMenuItem(), menuItem("Visual Node Diff", "de.uka.ilkd.key.gui.proofdiff.ProofDiffFrame$Action", this::showProofDiffFrame)); + // lemmaorigin: begin — term labels + origin tracking view controls (Swing TermLabelMenu / + // HidePackagePrefixToggleAction / OriginTermLabelsExt MainMenu items) + view.getItems().addAll(OriginLabelsF.install(this)); + // lemmaorigin: end return view; } diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentViewF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentViewF.java index 150f606ecd1..179676cced5 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentViewF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentViewF.java @@ -146,6 +146,42 @@ public boolean contains(Name name) { } }; + // lemmaorigin: begin — term-label visibility hook (Swing SequentView.getVisibleTermLabels, + // which feeds mainWindow.getVisibleTermLabels() to the printer). Defaults to the spike stub + // so the view is unchanged until de.uka.ilkd.key.gui.fx.originlabels.TermLabelMenuF attaches. + private VisibleTermLabels visibleTermLabels = NO_VISIBLE_TERM_LABELS; + + /** + * Installs the given visible-term-labels provider and re-creates the printer with it (Swing + * {@code SequentView} passes {@code mainWindow.getVisibleTermLabels()} to every printer). The + * printer must be rebuilt because the label visibility is bound at construction; a plain + * {@link #printSequent()} suffices for later visibility changes, since the manager is + * consulted live while printing. + * + * @param labels the label visibility to use from now on, {@code null} restores the stub + */ + public void setVisibleTermLabels(VisibleTermLabels labels) { + visibleTermLabels = labels != null ? labels : NO_VISIBLE_TERM_LABELS; + if (printer != null && selectedNode != null) { + printer = SequentViewLogicPrinter.positionPrinter(new NotationInfo(), + selectedNode.proof().getServices(), visibleTermLabels); + if (filter instanceof SearchSequentPrintFilter searchFilter) { + searchFilter.setLogicPrinter(printer); + } + printSequent(); + } + } + + /** + * The printed sequent string of the current display, for the term-label self test + * ({@code key.fx.verify.lemmaorigin}). + */ + public String printedText() { + return printed; + } + + // lemmaorigin: end + private static final KeyCombination OPEN_SEARCH = new KeyCodeCombination(KeyCode.F, KeyCombination.CONTROL_DOWN, KeyCombination.SHIFT_DOWN); @@ -452,8 +488,9 @@ public void display(Node node) { if (node.proof() != proof) { proof = node.proof(); // TODO(M2c): share the NotationInfo and the term label visibility with the mediator + // lemmaorigin: use the installed label visibility (Swing SequentView printer setup) printer = SequentViewLogicPrinter.positionPrinter(new NotationInfo(), - node.proof().getServices(), NO_VISIBLE_TERM_LABELS); + node.proof().getServices(), visibleTermLabels); if (filter instanceof SearchSequentPrintFilter searchFilter) { // the search filters print single formulas through the view's printer (Swing // SequentViewSearchBar.search refreshes it the same way) @@ -573,12 +610,27 @@ private void handleMouseClick(MouseEvent event) { return; } PosInSequent pos = table.getPosInSequent(charIndex, filter); + lastClickedPos = pos; // lemmaorigin: remember the clicked term (Swing context-menu target) Range bounds = pos != null ? pos.getBounds() : null; highlightedRange = bounds != null && bounds.length() > 0 ? bounds : null; rebuildRuns(); onPosSelected.accept(pos); } + // lemmaorigin: begin — the last clicked position (Swing's term context-menu target; the + // View▸Origin Tracking▸Show Origin item of OriginLabelsF uses it because the FX sequent view + // has no context menu yet) + private PosInSequent lastClickedPos; + + /** + * @return the position of the last clicked term, or {@code null} if nothing was clicked or + * the click missed a position + */ + public PosInSequent getLastClickedPos() { + return lastClickedPos; + } + // lemmaorigin: end + // ----------------------------------------------------------------------- // Hover highlight + tooltip (Swing SequentViewInputListener.mouseMoved / // SequentView.getToolTipText) diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/originlabels/OriginLabelsF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/originlabels/OriginLabelsF.java new file mode 100644 index 00000000000..34eb73e3a96 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/originlabels/OriginLabelsF.java @@ -0,0 +1,229 @@ +/* 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.originlabels; + +import java.util.ArrayList; +import java.util.List; +import javafx.scene.control.Alert; +import javafx.scene.control.ButtonType; +import javafx.scene.control.CheckMenuItem; +import javafx.scene.control.Menu; +import javafx.scene.control.MenuItem; +import javafx.scene.control.SeparatorMenuItem; + +import de.uka.ilkd.key.control.TermLabelVisibilityManager; +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.MainWindowF; +import de.uka.ilkd.key.gui.fx.actions.QuickSaveF; +import de.uka.ilkd.key.gui.fx.nodeviews.SequentViewF; +import de.uka.ilkd.key.pp.NotationInfo; +import de.uka.ilkd.key.proof.Proof; +import de.uka.ilkd.key.settings.ProofIndependentSettings; +import de.uka.ilkd.key.settings.TermLabelSettings; + +import org.key_project.logic.Name; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * JavaFX port of the origin-tracking / term-label view controls. The Swing original consists of + *
    + *
  • {@code de.uka.ilkd.key.gui.actions.TermLabelMenu} (View menu "Term Labels") — ported as + * {@link TermLabelMenuF},
  • + *
  • {@code de.uka.ilkd.key.gui.actions.HidePackagePrefixToggleAction} (View menu "Hide Package + * Prefix"),
  • + *
  • the "Origin Tracking" main-menu contributions of + * {@code de.uka.ilkd.key.gui.originlabels.OriginTermLabelsExt} (the extension's MainMenu kind): + * "Toggle Term Origin Tracking" and "Show origin" (the latter arrives with the + * {@code OriginTermLabelVisualizerF} window, see the originlabels package).
  • + *
+ * This class assembles the corresponding items for the FX View menu ({@link #install}) and + * provides the development self test ({@link #verify}, {@code key.fx.verify.lemmaorigin}). + *

+ * Deviations from Swing: + *

    + *
  • The FX sequent view has no term context menu yet, so "Show Origin" is a View menu item + * instead of a context-menu action (Swing {@code ShowOriginAction} is a context-menu + * contribution).
  • + *
  • "Toggle Term Origin Tracking" reloads via quick save + quick load + * ({@code QuickSaveF}, the FX counterpart of Swing {@code QuickSaveAction/QuickLoadAction} used + * by Swing's {@code ToggleTermOriginTrackingAction}). The setting is flipped before the + * reload so the re-created proof actually carries the new labels (in Swing the flip happens + * after the reload, which cannot affect the reloaded proof).
  • + *
  • The "Highlight Origins" toggle is not ported: it gates the sequent→source-view origin + * highlight ({@code SequentViewInputListener.highlightOriginInSourceView}), which has no FX + * counterpart yet (SourceViewF lacks the highlight API).
  • + *
+ */ +public final class OriginLabelsF { + + private static final Logger LOGGER = LoggerFactory.getLogger(OriginLabelsF.class); + + /** the installed term labels submenu (for the self test), set by {@link #install}. */ + private static TermLabelMenuF termLabelMenu; + + private OriginLabelsF() { + } + + /** + * Builds the View-menu items (Swing View menu "Term Labels" + "Hide Package Prefix" + the + * "Origin Tracking" extension menu): the term labels submenu, the package-prefix toggle and + * the origin-tracking submenu. Also installs the term label visibility into the sequent view + * and observes the selection to rebuild the label items per proof. + * + * @param mainWindow the FX main window + * @return the items to append to the View menu + */ + public static List install(MainWindowF mainWindow) { + SequentViewF sequentView = mainWindow.getSequentView(); + + // the sequent view prints through the shared visibility manager (Swing: + // SequentView printer uses mainWindow.getVisibleTermLabels()); a reprint is needed on + // every visibility change (Swing MainWindow.makePrettyView) + termLabelMenu = + new TermLabelMenuF(() -> mainWindow.getSequentView().printSequent()); + sequentView.setVisibleTermLabels(termLabelMenu.getVisibleTermLabels()); + + // Swing TermLabelMenu rebuilds on selectedProofChanged (label names depend on the proof) + KeYSelectionModel selectionModel = mainWindow.getSelectionModel(); + selectionModel.addKeYSelectionListenerChecked(new KeYSelectionListener() { + @Override + public void selectedProofChanged(KeYSelectionEvent event) { + termLabelMenu.rebuildMenu(selectionModel.getSelectedProof()); + } + }); + termLabelMenu.rebuildMenu(selectionModel.getSelectedProof()); + + List items = new ArrayList<>(); + items.add(termLabelMenu); + items.add(createHidePackagePrefixItem(mainWindow)); + items.add(new SeparatorMenuItem()); + items.add(createOriginTrackingMenu(mainWindow)); + return items; + } + + /** + * The "Hide Package Prefix" toggle (Swing {@code HidePackagePrefixToggleAction}): flips + * {@code NotationInfo.DEFAULT_HIDE_PACKAGE_PREFIX} (before the setting, like Swing, because + * the printer construction reads the static) and the ViewSettings value, then re-prints. + */ + private static MenuItem createHidePackagePrefixItem(MainWindowF mainWindow) { + CheckMenuItem item = new CheckMenuItem("Hide Package Prefix"); + boolean hide = ProofIndependentSettings.DEFAULT_INSTANCE.getViewSettings() + .isHidePackagePrefix(); + NotationInfo.DEFAULT_HIDE_PACKAGE_PREFIX = hide; + item.setSelected(hide); + item.setOnAction(e -> { + boolean selected = item.isSelected(); + // must be executed before the ViewSettings are modified, like the Swing original + NotationInfo.DEFAULT_HIDE_PACKAGE_PREFIX = selected; + ProofIndependentSettings.DEFAULT_INSTANCE.getViewSettings() + .setHidePackagePrefix(selected); + mainWindow.getSequentView().printSequent(); + }); + return item; + } + + /** + * The "Origin Tracking" submenu (Swing {@code OriginTermLabelsExt.getMainMenuActions}): + * "Toggle Term Origin Tracking" with the Swing reload dialog, plus "Show Origin" once the + * {@link OriginTermLabelVisualizerF} is available. + */ + private static MenuItem createOriginTrackingMenu(MainWindowF mainWindow) { + Menu menu = new Menu("Origin Tracking"); + + CheckMenuItem tracking = new CheckMenuItem("Toggle Term Origin Tracking"); + tracking.setSelected( + ProofIndependentSettings.DEFAULT_INSTANCE.getTermLabelSettings().getUseOriginLabels()); + tracking.setOnAction(e -> toggleTermOriginTracking(mainWindow, tracking)); + menu.getItems().add(tracking); + return menu; + } + + /** + * The Swing {@code ToggleTermOriginTrackingAction.actionPerformed}: confirm dialog + * ("For the change to take effect, you need to reload the proof."), then flip + * {@code TermLabelSettings.useOriginLabels} and optionally reload (quick save + quick load). + */ + private static void toggleTermOriginTracking(MainWindowF mainWindow, + CheckMenuItem tracking) { + TermLabelSettings settings = + ProofIndependentSettings.DEFAULT_INSTANCE.getTermLabelSettings(); + + Alert alert = new Alert(Alert.AlertType.CONFIRMATION); + alert.setTitle("Origin"); + alert.setHeaderText(null); + alert.setContentText("For the change to take effect, you need to reload the proof."); + ButtonType reload = new ButtonType("Reload"); + ButtonType cont = new ButtonType("Continue without reloading"); + alert.getButtonTypes().setAll(reload, cont, ButtonType.CANCEL); + alert.initOwner(mainWindow.getStage()); + ButtonType choice = alert.showAndWait().orElse(ButtonType.CANCEL); + if (choice == ButtonType.CANCEL) { + tracking.setSelected(settings.getUseOriginLabels()); + return; + } + boolean newValue = !settings.getUseOriginLabels(); + settings.setUseOriginLabels(newValue); + if (choice == reload) { + // Swing quick-saves and quick-loads; the FX quick save stores the current proof + // state, the quick load re-creates it (with the new label setting applied) + QuickSaveF.quickSave(mainWindow); + QuickSaveF.quickLoad(mainWindow); + } + tracking.setSelected(newValue); + } + + // ------------------------------------------------------------------ + // self test (key.fx.verify.lemmaorigin) + // ------------------------------------------------------------------ + + /** + * Development self test: verifies that term labels can be shown/hidden in the sequent view + * through the shared visibility manager (Swing View▸Term Labels). The report is a single + * line ending in {@code PASS} or {@code FAIL}. + * + * @param mainWindow the FX main window with a loaded proof + * @return the report line + */ + public static String verify(MainWindowF mainWindow) { + SequentViewF view = mainWindow.getSequentView(); + if (termLabelMenu == null || view.getProof() == null) { + return "termLabels: no proof FAIL"; + } + de.uka.ilkd.key.control.TermLabelVisibilityManager manager = + termLabelMenu.getVisibleTermLabels(); + List names = TermLabelVisibilityManager.getSortedTermLabelNames(view.getProof()); + String before = view.printedText(); + // show every label (Swing default state of the TermLabelVisibilityManager) + for (Name name : names) { + manager.setHidden(name, false); + } + manager.setShowLabels(true); + String shown = view.printedText(); + boolean grew = shown.length() > before.length(); + // find a label whose printed name is part of the rendered text + Name printed = names.stream() + .filter(n -> shown.contains(n.toString())).findFirst().orElse(null); + boolean hiddenOk = true; + if (printed != null) { + manager.setHidden(printed, true); + String hidden = view.printedText(); + hiddenOk = !hidden.contains(printed.toString()); + manager.setHidden(printed, false); + } + // restore the pre-test state: show all labels (the default of the manager) + manager.setShowLabels(true); + LOGGER.info("verify term labels: names={} grew={} printedLabel={} hiddenOk={}", + names.size(), + grew, printed, hiddenOk); + boolean pass = grew && printed != null && hiddenOk; + return "termLabels: names=" + names.size() + " chars " + before.length() + "->" + + shown.length() + " printedLabel=" + printed + " " + (pass ? "PASS" : "FAIL"); + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/originlabels/TermLabelMenuF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/originlabels/TermLabelMenuF.java new file mode 100644 index 00000000000..8bc6e9385cc --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/originlabels/TermLabelMenuF.java @@ -0,0 +1,134 @@ +/* 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.originlabels; + +import java.util.Comparator; +import java.util.Map; +import java.util.TreeMap; +import javafx.scene.control.CheckMenuItem; +import javafx.scene.control.Menu; +import javafx.scene.control.SeparatorMenuItem; + +import de.uka.ilkd.key.control.TermLabelVisibilityManager; +import de.uka.ilkd.key.control.event.TermLabelVisibilityManagerListener; +import de.uka.ilkd.key.proof.Proof; + +import org.key_project.logic.Name; + +/** + * JavaFX port of the Swing {@code de.uka.ilkd.key.gui.actions.TermLabelMenu} (View menu "Term + * Labels"): a submenu with the "Display Term Labels in Formulas" check item and one check item + * per term label name supported by the loaded proof, controlling a shared + * {@link TermLabelVisibilityManager} that the sequent view printer consults. + *

+ * Behavioural parity with the Swing original: + *

    + *
  • The visibility manager defaults to show all labels ({@code showLabels = true}), + * except the labels listed in {@link TermLabelVisibilityManager}'s {@code ALWAYS_HIDDEN} + * (currently {@code OriginTermLabel.NAME}, which is rendered by the origin visualizer instead). + * So after installing this menu the sequent view prints the other term labels, like Swing.
  • + *
  • Every change of the manager re-prints the view (Swing {@code MainWindow.makePrettyView} + * from {@code handleVisibleLabelsChanged}); here the reprint is a {@link Runnable} provided by + * the caller (the FX main window re-prints its sequent view).
  • + *
  • The label items are rebuilt when the selected proof changes (Swing {@code rebuildMenu} in + * {@code selectedProofChanged}), because the available label names depend on the proof's + * profile ({@code TermLabelVisibilityManager.getSortedTermLabelNames}). The Swing original + * additionally bolds the items whose labels occur in the displayed sequent — a cosmetic + * affordance that is not ported.
  • + *
+ * + * @author lanzinger (Swing original), the key.ui.fx team (port) + */ +public class TermLabelMenuF extends Menu { + + /** The submenu title (Swing {@code TermLabelMenu.TERM_LABEL_MENU}). */ + public static final String TERM_LABEL_MENU = "Term Labels"; + + /** the shared visibility state consulted by the sequent view printer. */ + private final TermLabelVisibilityManager visibleTermLabels = new TermLabelVisibilityManager(); + + /** the per-label check items, keyed by label name (Swing {@code checkBoxMap}). */ + private final Map checkBoxMap = + new TreeMap<>(Comparator.comparing(Name::toString)); + + /** + * the "Display Term Labels in Formulas" master switch (Swing {@code DisplayLabelsCheckBox}). + */ + private final CheckMenuItem displayLabelsItem; + + /** re-prints the affected views (Swing {@code MainWindow.makePrettyView}). */ + private final Runnable reprint; + + /** Observes changes on {@link #visibleTermLabels}; created in the constructor. */ + private final TermLabelVisibilityManagerListener visibilityListener; + + /** + * Creates the term labels submenu. + * + * @param reprint invoked whenever the label visibility changed and the views must re-render + */ + public TermLabelMenuF(Runnable reprint) { + this.reprint = reprint; + setText(TERM_LABEL_MENU); + + displayLabelsItem = new CheckMenuItem("Display Term Labels in Formulas"); + displayLabelsItem.setSelected(visibleTermLabels.isShowLabels()); + displayLabelsItem.setOnAction(e -> visibleTermLabels + .setShowLabels(displayLabelsItem.isSelected())); + + this.visibilityListener = e -> { + displayLabelsItem.setSelected(visibleTermLabels.isShowLabels()); + for (Map.Entry entry : checkBoxMap.entrySet()) { + entry.getValue().setDisable(!visibleTermLabels.isShowLabels()); + entry.getValue().setSelected(!visibleTermLabels.isHidden(entry.getKey())); + } + reprint.run(); + }; + visibleTermLabels.addTermLabelVisibilityManagerListener(visibilityListener); + + getItems().addAll(displayLabelsItem, new SeparatorMenuItem()); + } + + /** + * @return the shared visibility manager; hand it to the views' printers (Swing + * {@code MainWindow.getVisibleTermLabels}) + */ + public TermLabelVisibilityManager getVisibleTermLabels() { + return visibleTermLabels; + } + + /** + * Rebuilds the per-label check items for the given proof (Swing {@code rebuildMenu}, + * triggered by the mediator's {@code selectedProofChanged}). + * + * @param proof the newly selected proof, may be {@code null} (only the master item remains) + */ + public void rebuildMenu(Proof proof) { + getItems().clear(); + getItems().addAll(displayLabelsItem, new SeparatorMenuItem()); + checkBoxMap.clear(); + if (proof == null) { + return; + } + // sorted list of term label names supported by the proof's profile + for (Name labelName : TermLabelVisibilityManager.getSortedTermLabelNames(proof)) { + CheckMenuItem item = new CheckMenuItem(labelName.toString()); + item.setSelected(!visibleTermLabels.isHidden(labelName)); + item.setDisable(!visibleTermLabels.isShowLabels()); + item.setOnAction( + e -> visibleTermLabels.setHidden(labelName, !item.isSelected())); + checkBoxMap.put(labelName, item); + getItems().add(item); + } + } + + /** + * Detaches the visibility listener (called when the main window closes; Swing keeps the + * listener because there is only one MainWindow). + */ + public void dispose() { + visibleTermLabels.removeTermLabelVisibilityManagerListener(visibilityListener); + } +} From 9c822326a109fa6de9a85d0115c8b90ada3212a8 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Thu, 8 Oct 2026 22:04:45 +0200 Subject: [PATCH 061/281] key.ui.fx: tacletmatch: port shared helpers (text, printing, styling, palette, responsive split, assumes-input logic) --- .../key/gui/fx/tacletmatch/AssumesInputF.java | 165 ++++++++++++++++++ .../gui/fx/tacletmatch/ExpandableTextF.java | 109 ++++++++++++ .../gui/fx/tacletmatch/ResponsiveSplitF.java | 72 ++++++++ .../key/gui/fx/tacletmatch/SvPaletteF.java | 75 ++++++++ .../ilkd/key/gui/fx/tacletmatch/TmPrintF.java | 140 +++++++++++++++ .../ilkd/key/gui/fx/tacletmatch/TmStyleF.java | 105 +++++++++++ .../ilkd/key/gui/fx/tacletmatch/TmTextF.java | 73 ++++++++ 7 files changed, 739 insertions(+) create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/tacletmatch/AssumesInputF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/tacletmatch/ExpandableTextF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/tacletmatch/ResponsiveSplitF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/tacletmatch/SvPaletteF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/tacletmatch/TmPrintF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/tacletmatch/TmStyleF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/tacletmatch/TmTextF.java diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/tacletmatch/AssumesInputF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/tacletmatch/AssumesInputF.java new file mode 100644 index 00000000000..94816b189e2 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/tacletmatch/AssumesInputF.java @@ -0,0 +1,165 @@ +/* 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.tacletmatch; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import de.uka.ilkd.key.util.parsing.BuildingException; + +import org.key_project.util.parsing.HasLocation; +import org.key_project.util.parsing.Location; +import org.key_project.util.parsing.Position; + +/** + * GUI-independent logic for the "type the {@code \assumes} sequent" editor: how the two typed parts + * are combined for the parser, where an error offset falls, whether the parsed arity matches, how a + * parse-failure exception is reduced to a clean message and position, and how the model status maps + * to an OK/incomplete verdict. + * + *

+ * Port of {@code de.uka.ilkd.key.gui.tacletmatch.AssumesInput} (AssumesInput.java:26-169), which is + * already free of AWT/Swing; only the class name changed. The panel keeps the widget wiring + * (text areas, highlighting, status colours) and delegates the decisions here. + */ +final class AssumesInputF { + + /** the separator inserted between the typed antecedent and succedent before parsing */ + static final String SEPARATOR = " ==> "; + + private AssumesInputF() {} + + /** the single string handed to the sequent parser for the typed antecedent and succedent */ + static String combined(String ante, String succ) { + return ante + SEPARATOR + succ; + } + + /** which part of {@link #combined} an absolute offset falls into (AssumesInput.java:39-41) */ + enum Side { + ANTECEDENT, SEPARATOR, SUCCEDENT + } + + /** an absolute offset resolved to a {@link Side} and the offset local to that side's text */ + record Target(Side side, int offset) { + } + + /** + * resolves an absolute offset in {@link #combined(String, String)} to the side it belongs to + * and the offset within that side's own text. Offsets on the boundary count as the antecedent + * (at its end) or the succedent (at its start); offsets strictly inside the separator are + * reported as {@link Side#SEPARATOR} and have no place in either text area + * (AssumesInput.java:53-63). + */ + static Target locate(String ante, int absoluteOffset) { + int sepStart = ante.length(); + int sepEnd = sepStart + SEPARATOR.length(); + if (absoluteOffset <= sepStart) { + return new Target(Side.ANTECEDENT, absoluteOffset); + } + if (absoluteOffset >= sepEnd) { + return new Target(Side.SUCCEDENT, absoluteOffset - sepEnd); + } + return new Target(Side.SEPARATOR, absoluteOffset - sepStart); + } + + /** + * {@code null} if the parsed arity matches the expectation, otherwise a message describing the + * mismatch (AssumesInput.java:69-75). + */ + static String arityError(int parsedAnte, int parsedSucc, int expectedAnte, int expectedSucc) { + if (parsedAnte == expectedAnte && parsedSucc == expectedSucc) { + return null; + } + return "expected " + expectedAnte + " antecedent and " + expectedSucc + + " succedent formula(s), got " + parsedAnte + " and " + parsedSucc; + } + + /** verdict derived from the dialog model's status string (AssumesInput.java:78-80) */ + enum StatusKind { + OK, INCOMPLETE + } + + /** the {@link StatusKind} and the text to show for it */ + record ModelStatus(StatusKind kind, String text) { + } + + /** + * classifies the model's status string: a leading "Instantiation is OK" is an + * {@link StatusKind#OK} verdict carrying the full text, anything else is + * {@link StatusKind#INCOMPLETE} carrying only its first line (AssumesInput.java:91-96). + */ + static ModelStatus classify(String modelStatus) { + if (modelStatus != null && modelStatus.startsWith("Instantiation is OK")) { + return new ModelStatus(StatusKind.OK, modelStatus); + } + return new ModelStatus(StatusKind.INCOMPLETE, TmTextF.firstLine(modelStatus)); + } + + /** + * a cleaned parse-failure message and the source position it points at (may be {@code null}) + */ + record SyntaxError(String message, Position position) { + } + + /** + * reduces a parse-failure exception chain to a clean, single-line message and (if present) the + * offending source position (AssumesInput.java:110-137). + */ + static SyntaxError extract(Throwable e) { + String msg = null; + Position pos = null; + for (Throwable t = e; t != null; t = t.getCause()) { + if (msg == null && t instanceof BuildingException be) { + msg = be.getRawMessage(); + } + if (pos == null && t instanceof HasLocation hl) { + Location loc = hl.getLocation(); + if (loc != null && loc.getPosition() != null + && !loc.getPosition().equals(Position.UNDEFINED)) { + pos = loc.getPosition(); + } + } + if (msg == null) { + msg = t.getMessage(); + } + } + if (msg == null || msg.isBlank() || msg.equalsIgnoreCase("unknown")) { + msg = "syntax error"; + } else { + msg = humanize(TmTextF.firstLine(msg)); + if (msg.isBlank()) { + msg = "syntax error"; + } + } + return new SyntaxError(msg, pos); + } + + /** + * the various parser phrasings (ANTLR's and KeY's "… expected, but found 'X'"), all of which + * quote the single offending token (AssumesInput.java:143-145). + */ + private static final Pattern OFFENDING = Pattern.compile( + "(?:missing \\S+ at|extraneous input|mismatched input|no viable alternative at input|" + + "token recognition error at:?|but found) '(.*?)'"); + + /** + * turns a raw parser message into a short, plain one (AssumesInput.java:154-168). + */ + static String humanize(String raw) { + if (raw == null) { + return ""; + } + String m = raw.trim().replaceFirst("^(line\\s+)?\\d+:\\d+:?\\s*", ""); + Matcher tok = OFFENDING.matcher(m); + if (tok.find()) { + String t = tok.group(1); + if ("==>".equals(t) || "".equals(t)) { + return "incomplete formula"; + } + return "unexpected '" + t + "'"; + } + return m; + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/tacletmatch/ExpandableTextF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/tacletmatch/ExpandableTextF.java new file mode 100644 index 00000000000..de64e25aaab --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/tacletmatch/ExpandableTextF.java @@ -0,0 +1,109 @@ +/* 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.tacletmatch; + +import javafx.geometry.Pos; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.Tooltip; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.text.Font; + +/** + * Read-only monospaced text that abbreviates long or multi-line content to a single truncated line + * and offers a toggle to expand it to its full, wrapped form. The full text is also available as a + * tooltip. Used for the (potentially very long) terms and formulas shown across the taclet-match + * dialog so they neither blow up the layout nor hide information. + * + *

+ * Port of {@code de.uka.ilkd.key.gui.tacletmatch.ExpandableText} (ExpandableText.java:16-92): the + * Swing version was a read-only {@code JTextArea} plus a {@code TmStyle.disclosure} button; the FX + * version is an {@link HBox} with a wrapping {@link Label} plus the disclosure toggle. + */ +public class ExpandableTextF extends HBox { + + /** collapse content longer than this many characters (ExpandableText.java:21) */ + private static final int DEFAULT_LIMIT = 240; + + /** + * a monospaced font for logic/term text, slightly larger than the default + * (TmStyle.mono, TmStyle.java:29-33) — used where a plain (styled) control must render logic + * text without carrying the {@link TmStyleF#MONO_CLASS} stylesheet class. + */ + public static Font mono() { + return Font.font("monospaced", 12 + FONT_BUMP); + } + + /** logic/term text is rendered a couple of points larger than the default for legibility */ + private static final int FONT_BUMP = 2; + + private final String full; + private final int limit; + private final boolean expandable; + private boolean expanded; + + private final Label area = new Label(); + private final Button toggle = TmStyleF.disclosure(null); + + public ExpandableTextF(String text) { + this(text, DEFAULT_LIMIT); + } + + public ExpandableTextF(String text, int limit) { + this.full = text == null ? "" : text; + this.limit = limit; + // collapse genuinely long content, or anything taller than two lines, so a big term does + // not dominate the panel; short one/two-line terms are shown in full (wrapped) + this.expandable = full.length() > limit || TmTextF.lineCount(full) > 2; + + getStyleClass().add("tacletmatch-expandable"); + setSpacing(4); + setAlignment(Pos.CENTER_LEFT); + + area.getStyleClass().add(TmStyleF.MONO_CLASS); + area.setWrapText(true); + area.setTooltip(new Tooltip(full)); + HBox.setHgrow(area, Priority.ALWAYS); + getChildren().add(area); + + if (expandable) { + toggle.setOnAction(e -> { + expanded = !expanded; + updateView(); + }); + getChildren().add(toggle); + } + updateView(); + } + + private void updateView() { + if (!expandable || expanded) { + area.setWrapText(true); + area.setText(full); + } else { + area.setWrapText(false); + area.setText(TmTextF.collapseToLine(full, limit)); + } + TmStyleF.setDisclosure(toggle, expanded); + } + + /** the full, untruncated text this node displays when expanded */ + public String getFullText() { + return full; + } + + /** whether the content is collapsed to the abbreviated line */ + public boolean isExpanded() { + return expanded; + } + + /** {@inheritDoc} fill horizontally but never grow vertically beyond the content */ + @Override + protected double computeMaxHeight(double width) { + return super.computePrefHeight(width); + } + +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/tacletmatch/ResponsiveSplitF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/tacletmatch/ResponsiveSplitF.java new file mode 100644 index 00000000000..a545cf1e686 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/tacletmatch/ResponsiveSplitF.java @@ -0,0 +1,72 @@ +/* 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.tacletmatch; + +import javafx.scene.Node; +import javafx.scene.control.SplitPane; +import javafx.scene.control.Tab; +import javafx.scene.control.TabPane; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.Region; + +/** + * Shows two components side by side in a horizontal split pane when there is enough width, and + * collapses them into a tabbed pane when the available width drops below a threshold. Used to place + * the taclet-match dialog's instantiation inputs next to the result preview, falling back to tabs + * in a narrow window. + * + *

+ * Port of {@code de.uka.ilkd.key.gui.tacletmatch.ResponsiveSplit} (ResponsiveSplit.java:19-93): + * the Swing component-listener resize handling becomes a {@code widthProperty} listener with the + * same {@link #THRESHOLD}; the {@code JSplitPane} with a 0.6 resize weight becomes a + * {@link SplitPane} with an equivalent divider position. + */ +public class ResponsiveSplitF extends BorderPane { + + /** below this width the two sides are shown as tabs instead of a split */ + private static final int THRESHOLD = 720; + + private final Node left; + private final String leftTitle; + private final Node right; + private final String rightTitle; + + /** current layout: true = split, false = tabs */ + private Boolean wide; + + public ResponsiveSplitF(Node left, String leftTitle, Node right, String rightTitle) { + this.left = left; + this.leftTitle = leftTitle; + this.right = right; + this.rightTitle = rightTitle; + + widthProperty().addListener((obs, oldW, newW) -> { + if (newW.doubleValue() >= 50) { + relayout(newW.doubleValue() >= THRESHOLD); + } + }); + relayout(true); + } + + private void relayout(boolean newWide) { + if (wide != null && wide == newWide) { + return; + } + wide = newWide; + if (newWide) { + SplitPane split = new SplitPane(left, right); + split.setDividerPosition(0, 0.6); + // keep both sides from enforcing their content's minimum width (Swing set the scroll + // panes' minimum size to 0 for the same reason) + ((Region) left).setMinWidth(0); + ((Region) right).setMinWidth(0); + setCenter(split); + } else { + TabPane tabs = new TabPane(new Tab(leftTitle, left), new Tab(rightTitle, right)); + tabs.setTabClosingPolicy(TabPane.TabClosingPolicy.UNAVAILABLE); + setCenter(tabs); + } + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/tacletmatch/SvPaletteF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/tacletmatch/SvPaletteF.java new file mode 100644 index 00000000000..f29ea4ca585 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/tacletmatch/SvPaletteF.java @@ -0,0 +1,75 @@ +/* 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.tacletmatch; + +import javafx.scene.control.Label; + +/** + * Stable colour assignment for schema variables shared across the taclet-match dialog panels, so a + * variable carries the same colour in the match overview, the instantiation panel and the in-place + * highlight of the matched term. + * + *

+ * Port of {@code de.uka.ilkd.key.gui.tacletmatch.SvPalette} (SvPalette.java:20-51). The colours are + * fixed light chip backgrounds with dark text (highlighter style), kept in code like the Swing + * original: they are data colours of the highlighter palette, not theme chrome (the Swing javadoc + * notes theming to the active look-and-feel was deliberately left out; the light pastels stay + * readable on both themes). + */ +public final class SvPaletteF { + + private static final int[] BG = + { 0xE1F5EE, 0xEEEDFE, 0xFAEEDA, 0xFBEAF0, 0xE6F1FB, 0xFAECE7 }; + private static final int[] FG = + { 0x0F6E56, 0x3C3489, 0x854F0B, 0x72243E, 0x0C447C, 0x712B13 }; + + private SvPaletteF() {} + + /** number of distinct colours before the palette repeats */ + public static int size() { + return BG.length; + } + + /** the chip background colour for the given palette index (repeating) */ + public static javafx.scene.paint.Color background(int index) { + return rgb(BG[Math.floorMod(index, BG.length)]); + } + + /** the chip text colour for the given palette index (repeating) */ + public static javafx.scene.paint.Color foreground(int index) { + return rgb(FG[Math.floorMod(index, FG.length)]); + } + + /** the 0xRRGGBB constant {@code v} as an RGB colour */ + private static javafx.scene.paint.Color rgb(int v) { + return javafx.scene.paint.Color.rgb((v >> 16) & 0xFF, (v >> 8) & 0xFF, v & 0xFF); + } + + /** the background colour as a CSS hex string (for inline {@code -fx-background-color}) */ + public static String backgroundCss(int index) { + return css(BG[Math.floorMod(index, BG.length)]); + } + + /** the foreground colour as a CSS hex string (for inline {@code -fx-text-fill}) */ + public static String foregroundCss(int index) { + return css(FG[Math.floorMod(index, FG.length)]); + } + + private static String css(int rgb) { + return String.format("#%06X", rgb); + } + + /** + * a chip-styled label for a schema variable name in the given palette colour + * (SvPalette.chip, SvPalette.java:43-50). + */ + public static Label chip(String text, int index) { + Label l = new Label(text); + l.getStyleClass().add("tacletmatch-chip"); + l.setStyle("-fx-background-color: " + backgroundCss(index) + "; -fx-text-fill: " + + foregroundCss(index) + ";"); + return l; + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/tacletmatch/TmPrintF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/tacletmatch/TmPrintF.java new file mode 100644 index 00000000000..0fc9f9ad0ee --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/tacletmatch/TmPrintF.java @@ -0,0 +1,140 @@ +/* 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.tacletmatch; + +import java.util.Map; + +import de.uka.ilkd.key.java.Services; +import de.uka.ilkd.key.ldt.LocSetLDT; +import de.uka.ilkd.key.logic.JTerm; +import de.uka.ilkd.key.logic.label.TermLabel; +import de.uka.ilkd.key.logic.label.TermLabelManager; +import de.uka.ilkd.key.logic.op.IObserverFunction; +import de.uka.ilkd.key.logic.op.IProgramMethod; +import de.uka.ilkd.key.pp.InitialPositionTable; +import de.uka.ilkd.key.pp.LogicPrinter; +import de.uka.ilkd.key.pp.Notation; +import de.uka.ilkd.key.pp.NotationInfo; +import de.uka.ilkd.key.pp.SequentViewLogicPrinter; +import de.uka.ilkd.key.pp.VisibleTermLabels; +import de.uka.ilkd.key.proof.io.ProofSaver; +import de.uka.ilkd.key.rule.Taclet; +import de.uka.ilkd.key.rule.inst.SVInstantiations; +import de.uka.ilkd.key.settings.ProofIndependentSettings; + +import org.key_project.logic.Name; +import org.key_project.logic.Term; +import org.key_project.prover.rules.instantiation.InstantiationEntry; + +/** + * Pretty-printing for the taclet-match dialog. Term labels are never shown here: the dialog + * displays the logical content (find/matched/bindings/preview), where origin and other labels are + * noise. + * + *

+ * Port of {@code de.uka.ilkd.key.gui.tacletmatch.TmPrint} (TmPrint.java:29-98). The Swing helper + * takes the {@code KeYMediator} to obtain the services and the shared notation info; the FX port + * takes both directly, so the dialog compiles against the core proof control without the + * Swing-style mediator (the FX mediator passes its shared {@link NotationInfo}). + */ +public final class TmPrintF { + + /** a label visibility that hides every term label (TmPrint.java:32-42) */ + private static final VisibleTermLabels NO_LABELS = new VisibleTermLabels() { + @Override + public boolean contains(TermLabel label) { + return false; + } + + @Override + public boolean contains(Name name) { + return false; + } + }; + + private TmPrintF() {} + + public static String term(Services services, NotationInfo notationInfo, Term t) { + SequentViewLogicPrinter p = printer(services, notationInfo); + p.printTerm((JTerm) t); + return p.result(); + } + + /** a printed term together with the position table that maps its sub-terms to char ranges */ + record Positioned(String text, InitialPositionTable positions) { + } + + /** + * like {@link #term} but also returns the position table, so callers can highlight individual + * sub-terms (e.g. the parts a schema variable matched) inside the printed term + * (TmPrint.java:60-72). + */ + static Positioned termWithPositions(Services services, NotationInfo notationInfo, Term t) { + NotationInfo ni = new NotationInfo(); + SequentViewLogicPrinter p = + SequentViewLogicPrinter.positionPrinter(ni, services, NO_LABELS); + ni.refresh(services, notationInfo.isPrettySyntax(), false, false); + // wrap the term in a sub so it is linked under the position table's root row [0] + // (printSequent does this for each formula; a bare printTerm does not) + p.layouter().markStartSub(); + p.printTerm((JTerm) t); + p.layouter().markEndSub(); + return new Positioned(p.result(), p.layouter().getInitialPositionTable()); + } + + public static String taclet(Services services, NotationInfo notationInfo, Taclet taclet) { + SequentViewLogicPrinter p = printer(services, notationInfo); + p.printTaclet(taclet, SVInstantiations.EMPTY_SVINSTANTIATIONS, + ProofIndependentSettings.DEFAULT_INSTANCE.getViewSettings().getShowWholeTaclet(), + false); + return p.result(); + } + + /** prints a schema-variable instantiation (a term, program element, ...) without labels */ + public static String instantiation(Services services, NotationInfo notationInfo, Object value) { + Object o = value instanceof InstantiationEntry e ? e.getInstantiation() : value; + if (o instanceof Term t) { + return term(services, notationInfo, t); + } + return ProofSaver.printAnything(o, services); + } + + /** + * prints a term for insertion into an instantiation field. A few notations the parser cannot + * yet round-trip are disabled, mirroring the classic dialog, so the result re-parses + * (SVInstantiationPanel.java:426-448, TacletMatchCompletionDialog.java:709-736). + */ + static String printTermForInstantiation(Services services, NotationInfo sharedNotationInfo, + JTerm term) { + final NotationInfo ni = new NotationInfo(); + final JTerm t = TermLabelManager.removeIrrelevantLabels(term, services); + LogicPrinter p = LogicPrinter.purePrinter(ni, services); + boolean pretty = sharedNotationInfo.isPrettySyntax(); + ni.refresh(services, pretty, false, false); + Map tbl = ni.getNotationTable(); + + if (pretty) { + final LocSetLDT setLDT = services.getTypeConverter().getLocSetLDT(); + tbl.remove(setLDT.getUnion()); + tbl.remove(setLDT.getIntersect()); + tbl.remove(setLDT.getSetMinus()); + tbl.remove(setLDT.getElementOf()); + tbl.remove(setLDT.getSubset()); + tbl.remove(IObserverFunction.class); + tbl.remove(IProgramMethod.class); + } + + p.printTerm(t); + return p.result(); + } + + private static SequentViewLogicPrinter printer(Services services, + NotationInfo notationInfo) { + NotationInfo ni = new NotationInfo(); + SequentViewLogicPrinter p = SequentViewLogicPrinter.purePrinter(ni, services, NO_LABELS); + ni.refresh(services, notationInfo.isPrettySyntax(), false, false); + return p; + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/tacletmatch/TmStyleF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/tacletmatch/TmStyleF.java new file mode 100644 index 00000000000..97a2c043ea9 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/tacletmatch/TmStyleF.java @@ -0,0 +1,105 @@ +/* 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.tacletmatch; + +import javafx.scene.Node; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.Tooltip; +import javafx.scene.layout.VBox; + +import de.uka.ilkd.key.gui.fx.fonticons.FontAwesomeSolid; +import de.uka.ilkd.key.gui.fx.fonticons.IconFactoryF; + +/** + * Shared styling for the taclet-match dialog panels: titled sections, muted captions, a uniform + * disclosure toggle and the monospaced text class. Sizes and spacing are only nudged for + * readability; colours come from the shared theme CSS ({@code tacletmatch-*} classes in + * {@code key-light.css} / {@code key-dark.css}). + * + *

+ * Port of {@code de.uka.ilkd.key.gui.tacletmatch.TmStyle} (TmStyle.java:19-160). The Swing helper + * reads the active look-and-feel's fonts and colours (UIManager); the FX version instead assigns + * theme CSS classes and lets the stylesheet resolve the colours, so the dialog follows the light + * and dark theme like the rest of the JavaFX UI. The {@code section} border becomes a titled + * {@link VBox} (bold header with a hairline rule beneath it, no box); the footer skinning + * (TmStyle.java:87-116) moves to the {@code tacletmatch-footer} CSS class, and the monospaced + * {@code Font} to the {@link #MONO_CLASS} style class. + */ +public final class TmStyleF { + + /** style class rendering logic/term text monospaced (TmStyle.mono, TmStyle.java:29-33) */ + public static final String MONO_CLASS = "tacletmatch-mono"; + + private TmStyleF() {} + + /** the muted caption label used for row labels and hints (TmStyle.muted) */ + public static Label muted(String text) { + Label l = new Label(text); + l.getStyleClass().add("tacletmatch-muted"); + return l; + } + + /** + * a titled section container with a bold header and inner spacing; further content is appended + * by the caller (TmStyle.section, TmStyle.java:118-160: a bold section header with a hairline + * rule beneath it and whitespace below — no box). + */ + public static VBox section(String title) { + VBox section = new VBox(4); + section.getStyleClass().add("tacletmatch-section"); + section.getChildren().add(sectionTitle(title)); + return section; + } + + /** like {@link #section(String)} with the section's content node already in place */ + public static VBox section(String title, Node content) { + VBox section = section(title); + section.getChildren().add(content); + return section; + } + + /** + * a standalone bold section title (used by the classic dialog, which composes its own panels) + */ + public static Node sectionTitle(String title) { + Label l = new Label(title); + l.getStyleClass().add("tacletmatch-section-title"); + return l; + } + + /** + * A small, unobtrusive disclosure (expand/collapse) toggle, used uniformly across the dialog so + * every "show more" affordance looks and behaves the same. Collapsed shows {@code ▸}, expanded + * {@code ▾}; update it with {@link #setDisclosure(Button, boolean)} + * (TmStyle.disclosure, TmStyle.java:123-140). + * + * @param what what the toggle reveals, for the tooltip (e.g. "the full sequent"); {@code null} + * for a generic "Show more/less" + */ + public static Button disclosure(String what) { + Button b = new Button(); + b.setFocusTraversable(false); + b.getStyleClass().add("tacletmatch-disclosure"); + b.getProperties().put("tm.what", what); + setDisclosure(b, false); + return b; + } + + /** + * updates the disclosure toggle's caret direction and tooltip for the expanded state + * (TmStyle.setDisclosure, TmStyle.java:142-151). + */ + public static void setDisclosure(Button b, boolean expanded) { + b.setGraphic(IconFactoryF.createIcon( + expanded ? FontAwesomeSolid.CARET_DOWN : FontAwesomeSolid.CARET_RIGHT, 12)); + Object what = b.getProperties().get("tm.what"); + if (what == null) { + b.setTooltip(new Tooltip(expanded ? "Show less" : "Show more")); + } else { + b.setTooltip(new Tooltip((expanded ? "Hide " : "Show ") + what)); + } + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/tacletmatch/TmTextF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/tacletmatch/TmTextF.java new file mode 100644 index 00000000000..dec0874b774 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/tacletmatch/TmTextF.java @@ -0,0 +1,73 @@ +/* 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.tacletmatch; + +/** + * GUI-independent text helpers for the taclet-match dialog: abbreviation, line counting and + * line/column-to-offset mapping. + * + *

+ * Port of {@code de.uka.ilkd.key.gui.tacletmatch.TmText} (TmText.java:12-75), which is already + * free of AWT/Swing; only the class name changed. The panels deal only with the resulting + * strings/offsets. + */ +public final class TmTextF { + + private TmTextF() {} + + /** + * the first line of {@code s} ({@code ""} for {@code null}); the whole string if single-line + * (TmText.java:19-25). + */ + public static String firstLine(String s) { + if (s == null) { + return ""; + } + int nl = s.indexOf('\n'); + return nl >= 0 ? s.substring(0, nl) : s; + } + + /** + * {@code s} flattened to a single line and truncated to {@code limit} characters with an + * ellipsis appended when it was longer. Every run of whitespace (the newlines and the alignment + * indentation the pretty-printer inserts when it wraps a term) collapses to a single space, so + * a multi-line formula reads as one continuous line instead of a stub followed by blank gaps + * (TmText.java:34-37). + */ + public static String collapseToLine(String s, int limit) { + String oneLine = (s == null ? "" : s).replaceAll("\\s+", " ").trim(); + return oneLine.length() > limit ? oneLine.substring(0, limit) + " …" : oneLine; + } + + /** the number of lines in {@code s} (newline count + 1; {@code 1} for {@code null}/empty) */ + public static int lineCount(String s) { + if (s == null || s.isEmpty()) { + return 1; + } + int lines = 1; + for (int i = 0; i < s.length(); i++) { + if (s.charAt(i) == '\n') { + lines++; + } + } + return lines; + } + + /** + * the character offset into {@code s} of the given 1-based {@code line} and 1-based + * {@code column}, clamped to {@code [0, s.length()]} (TmText.java:64-74). + */ + public static int offsetOf(String s, int line, int column) { + int i = 0; + int ln = 1; + while (ln < line && i < s.length()) { + if (s.charAt(i) == '\n') { + ln++; + } + i++; + } + return Math.max(0, Math.min(i + column - 1, s.length())); + } +} From 128505995c16ce5a5b44255a9f72f03fb610c8c6 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Thu, 8 Oct 2026 22:05:39 +0200 Subject: [PATCH 062/281] key.ui.fx: tacletmatch: port match/instantiation/assumes/result-preview panels with match highlighting --- .../tacletmatch/AssumesSelectionPanelF.java | 440 ++++++++++++++++++ .../gui/fx/tacletmatch/MatchInfoPanelF.java | 355 ++++++++++++++ .../fx/tacletmatch/ResultPreviewPanelF.java | 226 +++++++++ .../fx/tacletmatch/SVInstantiationPanelF.java | 321 +++++++++++++ 4 files changed, 1342 insertions(+) create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/tacletmatch/AssumesSelectionPanelF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/tacletmatch/MatchInfoPanelF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/tacletmatch/ResultPreviewPanelF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/tacletmatch/SVInstantiationPanelF.java diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/tacletmatch/AssumesSelectionPanelF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/tacletmatch/AssumesSelectionPanelF.java new file mode 100644 index 00000000000..8d23ba19477 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/tacletmatch/AssumesSelectionPanelF.java @@ -0,0 +1,440 @@ +/* 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.tacletmatch; + +import java.util.ArrayList; +import java.util.List; +import javafx.animation.PauseTransition; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Node; +import javafx.scene.control.Label; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import javafx.scene.control.RadioButton; +import javafx.scene.control.TextArea; +import javafx.scene.control.ToggleGroup; +import javafx.scene.control.Tooltip; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; +import javafx.util.Duration; + +import de.uka.ilkd.key.control.instantiation_model.TacletAssumesModel; +import de.uka.ilkd.key.control.instantiation_model.TacletInstantiationModel; +import de.uka.ilkd.key.java.Services; +import de.uka.ilkd.key.nparser.KeyIO; +import de.uka.ilkd.key.pp.NotationInfo; +import de.uka.ilkd.key.proof.io.ProofSaver; + +import org.key_project.prover.rules.instantiation.AssumesFormulaInstantiation; +import org.key_project.prover.sequent.Sequent; +import org.key_project.prover.sequent.SequentFormula; +import org.key_project.util.parsing.Position; + +/** + * Lets the user instantiate the taclet's {@code \assumes} sequent in one of two ways, chosen with a + * toggle: + *

    + *
  • Select from sequent – per assumes formula, pick a matching sequent formula from + * a list (sequent-view-like);
  • + *
  • Type the {@code \assumes} sequent – type the whole instantiated assumes sequent + * (antecedent {@code ==>} succedent). It is checked incrementally: parse/syntax of the sequent, the + * expected number of formulas, and – via the dialog status – whether it is compatible + * with the existing schema-variable bindings.
  • + *
+ * + *

+ * Port of {@code de.uka.ilkd.key.gui.tacletmatch.AssumesSelectionPanel} + * (AssumesSelectionPanel.java: + * 44-457). Event semantics preserved: the select card is a per-formula {@link ListView} writing + * each selection straight into the model (AssumesSelectionPanel.java:169-174); the manual card's + * two editors validate debounced on change (the Swing 300 ms {@code Timer} becomes a + * {@link PauseTransition}, AssumesSelectionPanel.java:263-274); a parse error highlights the + * offending character (a selection in the corresponding editor replaces the Swing highlighter + * painting, AssumesSelectionPanel.java:398-411). + */ +class AssumesSelectionPanelF extends VBox { + + /** abbreviate candidate text longer than this in the list (full text on hover) */ + private static final int ABBREV_LIMIT = 200; + + /** debounce of the manual editor's incremental validation */ + private static final Duration VALIDATE_DEBOUNCE = Duration.millis(300); + + private final TacletInstantiationModel model; + private final Services services; + private final NotationInfo notationInfo; + private final Runnable onChange; + + private final int anteSize; + private final int count; + + /** per-formula combo models (index 0..anteSize-1 antecedent, rest succedent) */ + private final TacletAssumesModel[] choices; + /** the "Manual Input" sentinel of each combo model */ + private final AssumesFormulaInstantiation[] sentinels; + /** the candidate lists, one per assumes formula (select mode) */ + private final List> lists = new ArrayList<>(); + + private final VBox selectCard = new VBox(4); + private final VBox manualCard = new VBox(4); + private final TextArea anteArea = new TextArea(); + private final TextArea succArea = new TextArea(); + private final Label manualStatus = new Label(" "); + + private boolean manualMode; + private PauseTransition debounce; + + AssumesSelectionPanelF(TacletInstantiationModel model, Services services, + NotationInfo notationInfo, Runnable onChange) { + this.model = model; + this.services = services; + this.notationInfo = notationInfo; + this.onChange = onChange; + + Sequent assumes = model.application().taclet().assumesSequent(); + this.anteSize = assumes.antecedent().size(); + this.count = model.ifChoiceModelCount(); + + this.choices = new TacletAssumesModel[count]; + this.sentinels = new AssumesFormulaInstantiation[count]; + for (int i = 0; i < count; i++) { + choices[i] = model.ifChoiceModel(i); + sentinels[i] = choices[i].getElementAt(choices[i].getSize() - 1); + } + + VBox section = TmStyleF.section("Assumptions (\\assumes)"); + section.getChildren().addAll(buildToggle(), selectCard, manualCard); + getChildren().add(section); + + showSelect(); + } + + /** + * the two-way toggle between the select and the manual card (AssumesSelectionPanel.java:111) + */ + private Node buildToggle() { + RadioButton selectBtn = new RadioButton("Select from sequent"); + RadioButton manualBtn = new RadioButton("Type the \\assumes sequent"); + ToggleGroup group = new ToggleGroup(); + group.getToggles().addAll(selectBtn, manualBtn); + group.selectToggle(selectBtn); + selectBtn.setOnAction(e -> showSelect()); + manualBtn.setOnAction(e -> showManual()); + + HBox row = new HBox(8, selectBtn, manualBtn); + row.setAlignment(Pos.CENTER_LEFT); + return row; + } + + private void buildSelectCard() { + selectCard.getChildren().clear(); + lists.clear(); + for (int i = 0; i < count; i++) { + selectCard.getChildren().add(buildFormulaSelector(i)); + } + } + + /** + * one per assumes formula: the schematic formula as header, then the matching sequent formulas + * to choose from (AssumesSelectionPanel.java:139-189). + */ + private Node buildFormulaSelector(int index) { + VBox p = new VBox(4); + p.setPadding(new Insets(4, 0, 8, 0)); + + HBox header = new HBox(6); + header.setAlignment(Pos.CENTER_LEFT); + header.getChildren().addAll(TmStyleF.muted((index < anteSize ? "antecedent" : "succedent") + + ":"), new ExpandableTextF(ProofSaver.printAnything(model.ifFma(index), services))); + p.getChildren().add(header); + + List candidates = new ArrayList<>(); + for (int k = 0; k < choices[index].getSize() - 1; k++) { + candidates.add(choices[index].getElementAt(k)); + } + ObservableList lm = FXCollections.observableList(candidates); + ListView list = new ListView<>(lm); + list.setCellFactory(lv -> new ListCell<>() { + @Override + protected void updateItem(AssumesFormulaInstantiation item, boolean empty) { + super.updateItem(item, empty); + setText(empty || item == null ? null + : TmTextF.collapseToLine(text(item), + ABBREV_LIMIT)); + setTooltip(empty || item == null ? null : new Tooltip(text(item))); + getStyleClass().add(TmStyleF.MONO_CLASS); + } + }); + list.getSelectionModel().selectedItemProperty().addListener((obs, oldV, newV) -> { + if (newV != null && !manualMode) { + choices[index].setSelectedItem(newV); + onChange.run(); + } + }); + lists.add(list); + + if (lm.isEmpty()) { + Label none = + TmStyleF.muted("(no matching formula — use \"Type the \\assumes sequent\")"); + p.getChildren().add(none); + } else { + list.setPrefHeight(Math.min(Math.max(lm.size(), 1), 4) * 24 + 10); + p.getChildren().add(list); + list.getSelectionModel().select(selectedIndexOf(index, candidates)); + } + return p; + } + + private int selectedIndexOf(int index, List lm) { + Object selected = choices[index].getSelectedItem(); + for (int k = 0; k < lm.size(); k++) { + if (lm.get(k) == selected) { + return k; + } + } + return 0; + } + + private Node buildManualCard() { + HBox guide = new HBox(6); + guide.setAlignment(Pos.CENTER_LEFT); + guide.getChildren().addAll(TmStyleF.muted("schematic"), + new ExpandableTextF(schematicSequent())); + + HBox editor = new HBox(8, labeledArea("antecedent", anteArea), new Label(" ⟹ "), + labeledArea("succedent", succArea)); + editor.setAlignment(Pos.CENTER_LEFT); + HBox.setHgrow(anteArea, Priority.ALWAYS); + HBox.setHgrow(succArea, Priority.ALWAYS); + + anteArea.textProperty().addListener((obs, o, n) -> scheduleValidation()); + succArea.textProperty().addListener((obs, o, n) -> scheduleValidation()); + + manualCard.getChildren().addAll(guide, editor, manualStatus); + return manualCard; + } + + /** a labelled, monospaced, wrapping editor (AssumesSelectionPanel.java:250-261) */ + private Node labeledArea(String label, TextArea area) { + area.getStyleClass().add(TmStyleF.MONO_CLASS); + area.setWrapText(true); + area.setPrefRowCount(4); + VBox p = new VBox(2, TmStyleF.muted(label), area); + VBox.setVgrow(area, Priority.ALWAYS); + return p; + } + + /** debounces the manual validation (AssumesSelectionPanel.java:263-274) */ + private void scheduleValidation() { + if (!manualMode) { + return; + } + if (debounce == null) { + debounce = new PauseTransition(VALIDATE_DEBOUNCE); + debounce.setOnFinished(e -> validateManual()); + } + debounce.playFromStart(); + } + + private void showSelect() { + manualMode = false; + buildSelectCard(); + manualCard.setVisible(false); + manualCard.setManaged(false); + selectCard.setVisible(true); + selectCard.setManaged(true); + for (int i = 0; i < count; i++) { + AssumesFormulaInstantiation v = + i < lists.size() ? lists.get(i).getSelectionModel().getSelectedItem() : null; + if (v != null) { + choices[i].setSelectedItem(v); + } + model.setManualInput(i, ""); + } + onChange.run(); + } + + private void showManual() { + manualMode = true; + selectCard.setVisible(false); + selectCard.setManaged(false); + manualCard.setVisible(true); + manualCard.setManaged(true); + if (manualCard.getChildren().isEmpty()) { + buildManualCard(); + } + validateManual(); + } + + /** + * parses and checks the typed assumes sequent, then feeds it to the per-formula models so the + * dialog status reflects compatibility with the existing bindings + * (AssumesSelectionPanel.java:300-346). + */ + private void validateManual() { + clearErrorHighlights(); + String ante = anteArea.getText(); + String succ = succArea.getText(); + if (ante.isBlank() && succ.isBlank()) { + status(WarnKind.WARN, "● waiting for the \\assumes sequent…"); + clearManual(); + onChange.run(); + return; + } + + Sequent parsed; + try { + parsed = new KeyIO(services).parseSequent(AssumesInputF.combined(ante, succ)); + } catch (Exception e) { + reportSyntaxError(e); + clearManual(); + onChange.run(); + return; + } + + String arity = AssumesInputF.arityError(parsed.antecedent().size(), + parsed.succedent().size(), anteSize, count - anteSize); + if (arity != null) { + status(WarnKind.ERROR, "✗ " + arity); + clearManual(); + onChange.run(); + return; + } + + int i = 0; + for (SequentFormula sf : parsed.antecedent()) { + applyManual(i++, sf); + } + for (SequentFormula sf : parsed.succedent()) { + applyManual(i++, sf); + } + + // compatibility with the existing bindings is reflected by the model status + AssumesInputF.ModelStatus st = AssumesInputF.classify(model.getStatusString()); + if (st.kind() == AssumesInputF.StatusKind.OK) { + status(WarnKind.OK, "✓ " + st.text()); + } else { + status(WarnKind.WARN, "● " + st.text()); + } + onChange.run(); + } + + private void applyManual(int index, SequentFormula sf) { + choices[index].setSelectedItem(sentinels[index]); + model.setManualInput(index, ProofSaver.printAnything(sf.formula(), services)); + } + + private void clearManual() { + // keep the model in "manual" selection so an empty/invalid editor reads as incomplete + // rather than silently falling back to the select-mode candidate + for (int i = 0; i < count; i++) { + choices[i].setSelectedItem(sentinels[i]); + model.setManualInput(i, ""); + } + } + + /** writes the current input back to the model (selections are applied live) */ + public void pushAllInputToModel() { + if (manualMode) { + validateManual(); + } else { + showSelect(); + } + } + + /** status kinds rendered with themed colours (Swing's fixed OK/WARN/ERROR colours) */ + private enum WarnKind { + OK, WARN, ERROR + } + + private void status(WarnKind kind, String text) { + manualStatus.setText(text); + manualStatus.getStyleClass().removeAll("tacletmatch-status-ok", "tacletmatch-status-warn", + "tacletmatch-status-error"); + manualStatus.getStyleClass() + .add(switch (kind) { + case OK -> "tacletmatch-status-ok"; + case WARN -> "tacletmatch-status-warn"; + case ERROR -> "tacletmatch-status-error"; + }); + } + + /** + * reports a parse failure: a clean message (without the noisy " at <pos>"/"unknown") and, + * if the failure carries a location, a highlight at the offending position in the editor + * (AssumesSelectionPanel.java:380-396). + */ + private void reportSyntaxError(Throwable e) { + AssumesInputF.SyntaxError err = AssumesInputF.extract(e); + status(WarnKind.ERROR, "✗ " + err.message()); + + Position pos = err.position(); + if (pos != null) { + String combined = AssumesInputF.combined(anteArea.getText(), succArea.getText()); + int off = TmTextF.offsetOf(combined, pos.line(), pos.column()); + AssumesInputF.Target target = AssumesInputF.locate(anteArea.getText(), off); + // a position inside the synthetic "==>" separator belongs to neither editor + if (target.side() == AssumesInputF.Side.ANTECEDENT) { + highlightAt(anteArea, target.offset()); + } else if (target.side() == AssumesInputF.Side.SUCCEDENT) { + highlightAt(succArea, target.offset()); + } + } + } + + /** + * highlights the offending character: the Swing highlighter painting becomes a selection of + * that single character in the editor (which the themes render as a highlight). + */ + private static void highlightAt(TextArea area, int offset) { + int len = area.getText().length(); + int start = Math.max(0, Math.min(offset, len)); + int end = Math.min(start + 1, len); + if (start == end && start > 0) { + start = end - 1; + } + if (end > start) { + area.selectRange(start, end); + } + area.requestFocus(); + } + + private void clearErrorHighlights() { + anteArea.deselect(); + succArea.deselect(); + } + + private String schematicSequent() { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < anteSize; i++) { + if (i > 0) { + sb.append(", "); + } + sb.append(TmPrintF.term(services, notationInfo, model.ifFma(i))); + } + sb.append(" ==> "); + for (int i = anteSize; i < count; i++) { + if (i > anteSize) { + sb.append(", "); + } + sb.append(TmPrintF.term(services, notationInfo, model.ifFma(i))); + } + return sb.toString(); + } + + private String text(AssumesFormulaInstantiation inst) { + SequentFormula sf = inst.getSequentFormula(); + return sf != null ? TmPrintF.term(services, notationInfo, sf.formula()) : inst.toString(); + } + + /** whether the manual card is currently active (used by the verification driver) */ + boolean isManualMode() { + return manualMode; + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/tacletmatch/MatchInfoPanelF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/tacletmatch/MatchInfoPanelF.java new file mode 100644 index 00000000000..718cafd1dfc --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/tacletmatch/MatchInfoPanelF.java @@ -0,0 +1,355 @@ +/* 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.tacletmatch; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import javafx.geometry.Pos; +import javafx.scene.Node; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.Region; +import javafx.scene.layout.VBox; + +import de.uka.ilkd.key.control.instantiation_model.TacletInstantiationModel; +import de.uka.ilkd.key.java.Services; +import de.uka.ilkd.key.pp.InitialPositionTable; +import de.uka.ilkd.key.pp.NotationInfo; +import de.uka.ilkd.key.pp.Range; +import de.uka.ilkd.key.rule.FindTaclet; +import de.uka.ilkd.key.rule.Taclet; +import de.uka.ilkd.key.rule.TacletApp; + +import org.key_project.logic.Term; +import org.key_project.logic.op.sv.SchemaVariable; +import org.key_project.prover.sequent.PosInOccurrence; +import org.key_project.util.collection.ImmutableList; + +import org.fxmisc.richtext.InlineCssTextArea; + +/** + * Overview of how the selected taclet matched the sequent: the schematic find pattern, the concrete + * sub-term it was matched against, and the schema-variable bindings that the match determined. + * + *

+ * Port of {@code de.uka.ilkd.key.gui.tacletmatch.MatchInfoPanel} (MatchInfoPanel.java:44-344). This + * makes explicit what the classic dialog left implicit. It is shown once per instantiation + * alternative, since each alternative is a distinct match of the rule. Each bound schema variable + * is given a stable colour, used both for its binding chip and to highlight the sub-term it matched + * inside the concrete matched term — the match highlighting view of the redesigned dialog. The + * matched term is rendered with a read-only RichTextFX area (the counter-part of the Swing + * read-only {@code JTextPane} with character attributes, MatchInfoPanel.java:281-310): each + * schema-variable span carries the variable's chip colour as a text background. + */ +public class MatchInfoPanelF extends VBox { + + /** label-column width so the value columns line up across the Rule/Find/Matched rows */ + private static final int LABEL_WIDTH = 92; + + /** a matched term taller than this many lines is collapsed behind a toggle */ + private static final int MATCHED_PREVIEW_LINES = 2; + + /** inline CSS giving the highlight spans their background colour (RichTextFX per-span style) */ + private static final String HIGHLIGHT_CSS = "-rtfx-background-color: "; + + /** + * stable palette index per bound schema variable, shared by the chips and the in-term + * highlight + */ + private final Map svColors = new LinkedHashMap<>(); + + /** the number of schema-variable spans actually highlighted inside the matched term */ + private int highlightSpanCount; + + private final Services services; + private final NotationInfo notationInfo; + + /** the titled section box holding the rows */ + private final VBox body; + + public MatchInfoPanelF(TacletInstantiationModel model, Services services, + NotationInfo notationInfo) { + this.services = services; + this.notationInfo = notationInfo; + + setSpacing(3); + + final TacletApp app = model.application(); + final Taclet taclet = app.taclet(); + final PosInOccurrence pio = app.posInOccurrence(); + final String side = pio == null ? null : (pio.isInAntec() ? "antecedent" : "succedent"); + + int ci = 0; + for (var entry : app.instantiations().getInstantiationMap()) { + svColors.put(entry.key(), ci++); + } + + // the panel is the titled section (title, hairline rule, content) + VBox section = TmStyleF.section(side == null ? "Match" : "Match — " + side); + this.body = section; + getChildren().add(section); + + add(plainRow("Rule", taclet.name().toString())); + + if (taclet instanceof FindTaclet ft && pio != null) { + add(monoRow("Find", TmPrintF.term(services, notationInfo, ft.find()))); + add(matchedRow(ft.find(), pio.subTerm())); + } else { + add(plainRow("Find", "this rule has no find pattern")); + } + + addBindings(app); + + // the full rule (incl. replacewith/add) is implementation detail: hidden behind a toggle + addCollapsible("Rule body", TmPrintF.taclet(services, notationInfo, taclet)); + } + + /** + * @return how many schema-variable sub-terms are currently highlighted inside the matched term + * (used by the self test to assert the match highlighting renders) + */ + public int getHighlightSpanCount() { + return highlightSpanCount; + } + + /** + * a standalone bold section title (used by the classic dialog, which composes its own panels + * from the shared helpers) + */ + public static Node sectionTitle(String title) { + return TmStyleF.sectionTitle(title); + } + + private void add(Node n) { + body.getChildren().add(n); + } + + /** + * a label row whose value is a disclosure toggle revealing the given (hidden) content below + * (MatchInfoPanel.java:96-117). + */ + private void addCollapsible(String label, String content) { + VBox holder = new VBox(); + holder.getStyleClass().add("tacletmatch-muted"); + holder.getChildren().add(new ExpandableTextF(content, Integer.MAX_VALUE)); + holder.setVisible(false); + holder.setManaged(false); + + Button disc = TmStyleF.disclosure(label.toLowerCase()); + disc.setOnAction(e -> { + boolean show = !holder.isVisible(); + holder.setVisible(show); + holder.setManaged(show); + TmStyleF.setDisclosure(disc, show); + }); + + add(row(label, disc)); + add(holder); + } + + /** + * lists the schema variables bound by the match, colour-coded, kept tight: {@code chip ↦ value} + * with the arrow aligned across bindings (MatchInfoPanel.java:123-165). + */ + private void addBindings(TacletApp app) { + var map = app.instantiations().getInstantiationMap(); + if (map.isEmpty()) { + return; + } + List

+ * Port of {@code de.uka.ilkd.key.gui.tacletmatch.ResultPreviewPanel} (ResultPreviewPanel.java: + * 29-236): the never-mutate contract of ResultPreviewPanel.java:23-25 is kept verbatim; the Swing + * 150 ms debounce {@code Timer} becomes a {@link PauseTransition} (ResultPreviewPanel.java: + * 63-70); the added/removed/modified rows with {@code +}/{@code −} markers and the expandable full + * sequent are re-laid out with labels and {@link ExpandableTextF}s. + */ +class ResultPreviewPanelF extends VBox { + + /** debounce of the preview refresh (ResultPreviewPanel.java:38) */ + private static final Duration PREVIEW_DEBOUNCE = Duration.millis(150); + + private final TacletInstantiationModel model; + private final Services services; + private final NotationInfo notationInfo; + private final Goal goal; + + private final VBox body = new VBox(4); + private PauseTransition debounce; + + ResultPreviewPanelF(TacletInstantiationModel model, Services services, + NotationInfo notationInfo, Goal goal) { + this.model = model; + this.services = services; + this.notationInfo = notationInfo; + this.goal = goal; + + // the resulting sequent sits on a surface card (the "answer") — Swing used the editor + // background with a hairline border (ResultPreviewPanel.java:48-57) + body.getStyleClass().add("tacletmatch-preview-card"); + VBox section = TmStyleF.section("Result preview", body); + getChildren().add(section); + + update(); + } + + /** schedules a debounced refresh of the preview (ResultPreviewPanel.java:63-70). */ + public void requestUpdate() { + if (debounce == null) { + debounce = new PauseTransition(PREVIEW_DEBOUNCE); + debounce.setOnFinished(e -> update()); + } + debounce.playFromStart(); + } + + /** + * recomputes and renders the preview from the current instantiations (ResultPreviewPanel.java: + * 72-94). + */ + public void update() { + body.getChildren().clear(); + try { + TacletApp app = model.createTacletApp(); + if (app == null) { + message("Could not apply the rule with the current instantiations."); + } else { + TacletExecutor exec = (TacletExecutor) app.taclet().getExecutor(); + ImmutableList changes = + exec.getResultSequentChanges(goal, app); + if (changes.isEmpty()) { + message("No preview available for this rule."); + } else { + renderGoals(changes); + } + } + } catch (Exception e) { + message("Complete the instantiations to preview the result."); + } + } + + private void renderGoals(ImmutableList changes) { + int n = changes.size(); + int i = 1; + for (SequentChangeInfo sci : changes) { + if (i > 1) { + // a visible divider between resulting goals + body.getChildren().add(new Separator()); + } + + VBox goalBox = new VBox(2); + goalBox.setPadding(new Insets(2, 0, 2, 0)); + + ExpandableTextF full = + new ExpandableTextF(printSequent(sci.sequent()), Integer.MAX_VALUE); + full.setVisible(false); + full.setManaged(false); + Button toggle = expandToggle(full); + + if (n > 1) { + // multiple goals: a header line carries the "Goal i of n" label and the toggle + HBox header = new HBox(8); + header.setAlignment(Pos.CENTER_LEFT); + Label l = TmStyleF.muted("Goal " + i + " of " + n); + HBox.setHgrow(l, Priority.ALWAYS); + header.getChildren().addAll(l, toggle); + goalBox.getChildren().add(header); + } else { + // single goal: put the toggle on its own small row at the top of the card + HBox header = new HBox(8); + header.setAlignment(Pos.CENTER_RIGHT); + header.getChildren().add(toggle); + goalBox.getChildren().add(header); + } + + goalBox.getChildren().addAll(renderSide(sci, true).toArray(Node[]::new)); + goalBox.getChildren().addAll(renderSide(sci, false).toArray(Node[]::new)); + + goalBox.getChildren().add(full); + body.getChildren().add(goalBox); + i++; + } + } + + /** + * a small, unobtrusive button that toggles the full-sequent view (ResultPreviewPanel.java: + * 158-169). + */ + private Button expandToggle(ExpandableTextF full) { + Button b = TmStyleF.disclosure("the full sequent"); + b.setOnAction(e -> { + boolean show = !full.isVisible(); + full.setVisible(show); + full.setManaged(show); + TmStyleF.setDisclosure(b, show); + }); + return b; + } + + /** + * renders the added/removed/modified formulas of one side (ResultPreviewPanel.java:172-193). + */ + private List renderSide(SequentChangeInfo sci, boolean antec) { + ImmutableList removed = sci.removedFormulas(antec); + ImmutableList added = sci.addedFormulas(antec); + ImmutableList modified = sci.modifiedFormulas(antec); + + List out = new ArrayList<>(); + if (removed.isEmpty() && added.isEmpty() && modified.isEmpty()) { + return out; + } + Label head = TmStyleF.muted(antec ? "antecedent" : "succedent"); + out.add(head); + for (SequentFormula f : removed) { + out.add(changeRow("−", false, f)); + } + for (FormulaChangeInfo m : modified) { + out.add(changeRow("−", false, m.getOriginalFormula())); + out.add(changeRow("+", true, m.newFormula())); + } + for (SequentFormula f : added) { + out.add(changeRow("+", true, f)); + } + return out; + } + + private Node changeRow(String marker, boolean added, SequentFormula f) { + Label m = new Label(marker); + m.getStyleClass().add(added ? "tacletmatch-added" : "tacletmatch-removed"); + m.setPadding(new Insets(0, 8, 0, 0)); + + ExpandableTextF text = new ExpandableTextF(TmPrintF.term(services, notationInfo, + f.formula())); + HBox p = new HBox(6, m, text); + p.setAlignment(Pos.CENTER_LEFT); + HBox.setHgrow(text, Priority.ALWAYS); + return p; + } + + private void message(String text) { + Label l = TmStyleF.muted(text); + l.setWrapText(true); + body.getChildren().add(l); + } + + private String printSequent(Sequent seq) { + StringBuilder sb = new StringBuilder(); + for (SequentFormula sf : seq.antecedent()) { + sb.append(TmPrintF.term(services, notationInfo, sf.formula())).append('\n'); + } + sb.append("⟹"); + for (SequentFormula sf : seq.succedent()) { + sb.append('\n').append(TmPrintF.term(services, notationInfo, sf.formula())); + } + return sb.toString(); + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/tacletmatch/SVInstantiationPanelF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/tacletmatch/SVInstantiationPanelF.java new file mode 100644 index 00000000000..473a5892223 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/tacletmatch/SVInstantiationPanelF.java @@ -0,0 +1,321 @@ +/* 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.tacletmatch; + +import java.util.ArrayList; +import java.util.List; +import javafx.animation.PauseTransition; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Node; +import javafx.scene.Scene; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.TextArea; +import javafx.scene.control.Tooltip; +import javafx.scene.input.DataFormat; +import javafx.scene.input.DragEvent; +import javafx.scene.input.TransferMode; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; +import javafx.stage.Modality; +import javafx.stage.Stage; +import javafx.stage.Window; +import javafx.util.Duration; + +import de.uka.ilkd.key.control.instantiation_model.TacletFindModel; +import de.uka.ilkd.key.control.instantiation_model.TacletInstantiationModel; +import de.uka.ilkd.key.java.Services; +import de.uka.ilkd.key.pp.NotationInfo; + +import org.key_project.logic.op.sv.SchemaVariable; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Panel for the schema variables the user still has to instantiate: those occurring only in + * {@code \add}/{@code \replacewith}/{@code \assumes}, not determined by the find match (the + * find-determined ones are shown read-only in {@link MatchInfoPanelF}). Each row offers the + * proposal pre-filled by the model, accepts drag-and-drop of a text from the sequent, and writes + * its input back to the shared {@link TacletFindModel}, refreshing the dialog status on change. + * + *

+ * Port of {@code de.uka.ilkd.key.gui.tacletmatch.SVInstantiationPanel} (SVInstantiationPanel.java: + * 56-449). Event semantics preserved: every text change schedules a debounced + * {@link #pushAndRefresh()} (the Swing 300 ms single-shot {@code Timer} becomes a + * {@link PauseTransition}), a focus loss pushes immediately, an expand toggle grows the field, + * an edit button opens a larger editor, and dropping text inserts it at the caret. The Swing + * version additionally accepted the sequent's {@code PosInSequentTransferable} flavour + * (SVInstantiationPanel.java:219-247); the FX sequent view has no drag source yet (sequent-audit + * P7), so only plain-text drops are handled for now. + */ +class SVInstantiationPanelF extends VBox { + + private static final Logger LOGGER = LoggerFactory.getLogger(SVInstantiationPanelF.class); + + /** above this many editable variables, lay the fields out in two columns */ + private static final int TWO_COLUMN_THRESHOLD = 3; + + /** debounce for refreshing the status/preview while typing (SVInstantiationPanel.java:72) */ + private static final Duration REFRESH_DEBOUNCE = Duration.millis(300); + + private final TacletFindModel tableModel; + private final Services services; + private final NotationInfo notationInfo; + private final Runnable onChange; + + /** the editable rows: the model-row index and the field carrying its input */ + private record Row(int modelRow, SvField field) { + } + + private final List rows = new ArrayList<>(); + + /** debounce for refreshing the status/preview while typing */ + private PauseTransition refreshTimer; + + SVInstantiationPanelF(TacletInstantiationModel model, Services services, + NotationInfo notationInfo, Runnable onChange) { + this.tableModel = model.tableModel(); + this.services = services; + this.notationInfo = notationInfo; + this.onChange = onChange; + + build(); + } + + private void build() { + getStyleClass().add("tacletmatch-section-holder"); + List rowComps = new ArrayList<>(); + int colorIndex = 0; + for (int r = 0; r < tableModel.getRowCount(); r++) { + if (!tableModel.isCellEditable(r, 1)) { + continue; + } + SchemaVariable sv = (SchemaVariable) tableModel.getValueAt(r, 0); + Object value = tableModel.getValueAt(r, 1); + + SvField field = new SvField(value == null ? "" : value.toString()); + field.area.focusedProperty().addListener((obs, was, focused) -> { + if (!focused) { + pushAndRefresh(); + } + }); + // refresh status and preview after a short typing pause, not only on focus change + field.area.textProperty().addListener((obs, oldText, newText) -> scheduleRefresh()); + installDropTarget(field); + + rows.add(new Row(r, field)); + rowComps.add(rowComponent(SvPaletteF.chip(sv.name().toString(), colorIndex++), + field.component)); + } + + VBox content = new VBox(4); + if (rowComps.isEmpty()) { + Label none = TmStyleF.muted("All schema variables are determined by the match."); + content.getChildren().add(none); + } else if (rowComps.size() > TWO_COLUMN_THRESHOLD) { + // many variables: two columns to stay compact + GridPane grid = new GridPane(); + grid.setHgap(16); + grid.setVgap(4); + int col = 0; + int row = 0; + for (Node rc : rowComps) { + grid.add(rc, col, row); + if (++col == 2) { + col = 0; + row++; + } + } + content.getChildren().add(grid); + } else { + content.getChildren().addAll(rowComps); + } + + getChildren().add(TmStyleF.section("Instantiate schema variables", content)); + } + + private Node rowComponent(Label chip, Node field) { + HBox west = new HBox(4); + west.setAlignment(Pos.CENTER_LEFT); + west.getChildren().addAll(chip, new Label("↦")); + + HBox p = new HBox(6); + p.getStyleClass().add("tacletmatch-row"); + p.setPadding(new Insets(2, 0, 2, 0)); + p.setAlignment(Pos.CENTER_LEFT); + p.getChildren().addAll(west, field); + HBox.setHgrow(field, Priority.ALWAYS); + return p; + } + + /** writes every field's text back to the model (SVInstantiationPanel.java:184-188) */ + public void pushAllInputToModel() { + for (Row row : rows) { + tableModel.setValueAt(row.field().getText(), row.modelRow(), 1); + } + } + + private void pushAndRefresh() { + pushAllInputToModel(); + if (onChange != null) { + onChange.run(); + } + } + + /** + * debounces {@link #pushAndRefresh()} so typing updates the status/preview after a pause + * (SVInstantiationPanel.java:197-204). + */ + private void scheduleRefresh() { + if (refreshTimer == null) { + refreshTimer = new PauseTransition(REFRESH_DEBOUNCE); + refreshTimer.setOnFinished(e -> pushAndRefresh()); + } + refreshTimer.playFromStart(); + } + + /** + * accepts plain-text drops and inserts them at the caret (SVInstantiationPanel.java:206-248) + */ + private void installDropTarget(SvField field) { + field.area.setOnDragOver((DragEvent event) -> { + if (event.getDragboard().hasContent(DataFormat.PLAIN_TEXT)) { + event.acceptTransferModes(TransferMode.MOVE, TransferMode.COPY); + field.setDropHighlight(true); + event.consume(); + } + }); + field.area.setOnDragExited(e -> field.setDropHighlight(false)); + field.area.setOnDragDropped((DragEvent event) -> { + field.setDropHighlight(false); + var db = event.getDragboard(); + if (db.hasContent(DataFormat.PLAIN_TEXT)) { + String s = (String) db.getContent(DataFormat.PLAIN_TEXT); + field.area.insertText(field.area.getCaretPosition(), s == null ? "" : s); + field.autoExpandIfMultiline(); + pushAndRefresh(); + event.setDropCompleted(true); + } + event.consume(); + }); + } + + /** + * An editable instantiation field that shows long, possibly multi-line content nicely: a single + * line by default, expandable to several lines (scrolling beyond) via a small toggle, so a + * dropped term does not blow up the dialog (SVInstantiationPanel.java:266-419). + */ + private final class SvField { + private final TextArea area = new TextArea(); + private final Button toggle; + private final Button edit; + private final HBox component; + private boolean expanded; + + SvField(String value) { + area.setText(value); + area.getStyleClass().add(TmStyleF.MONO_CLASS); + area.setWrapText(true); + area.setPrefRowCount(1); + // a hint that the field accepts a dropped sub-term, shown only while empty/unfocused: + // the FX prompt text already has exactly this behaviour + area.setPromptText("drop a term or type…"); + + toggle = TmStyleF.disclosure("the whole instantiation"); + toggle.setOnAction(e -> setExpanded(!expanded)); + + // the inline field stays small; an edit icon opens a larger, resizable editor for + // comfortably entering long instantiations (SVInstantiationPanel.java:319-326) + edit = new Button("✎"); + edit.getStyleClass().add("tacletmatch-edit-button"); + edit.setFocusTraversable(false); + edit.setTooltip(new Tooltip("Edit in a larger, resizable window")); + edit.setOnAction(e -> openInEditor()); + + HBox controls = new HBox(2, toggle, edit); + controls.setAlignment(Pos.TOP_LEFT); + + component = new HBox(4, area, controls); + component.setAlignment(Pos.TOP_LEFT); + HBox.setHgrow(area, Priority.ALWAYS); + updateHeight(); + } + + String getText() { + return area.getText(); + } + + void autoExpandIfMultiline() { + if (!expanded && area.getText().indexOf('\n') >= 0) { + setExpanded(true); + } + } + + private void setExpanded(boolean e) { + expanded = e; + TmStyleF.setDisclosure(toggle, e); + updateHeight(); + } + + void setDropHighlight(boolean on) { + if (on) { + if (!area.getStyleClass().contains("tacletmatch-drop-highlight")) { + area.getStyleClass().add("tacletmatch-drop-highlight"); + } + } else { + area.getStyleClass().remove("tacletmatch-drop-highlight"); + } + } + + /** + * opens the field's content in a larger, resizable editor window; on OK the edited text + * replaces the field's content (which refreshes the status/preview as usual) + * (SVInstantiationPanel.java:373-408). + */ + private void openInEditor() { + Window owner = component.getScene() != null ? component.getScene().getWindow() : null; + Stage dlg = new Stage(); + if (owner instanceof Stage s) { + dlg.initOwner(s); + } + dlg.initModality(Modality.APPLICATION_MODAL); + dlg.setTitle("Edit instantiation"); + + TextArea ta = new TextArea(area.getText()); + ta.getStyleClass().add(TmStyleF.MONO_CLASS); + ta.setWrapText(true); + ta.setPrefRowCount(14); + + Button ok = new Button("OK"); + Button cancel = new Button("Cancel"); + ok.setDefaultButton(true); + ok.setOnAction(e -> { + area.setText(ta.getText()); + autoExpandIfMultiline(); + dlg.close(); + }); + cancel.setCancelButton(true); + cancel.setOnAction(e -> dlg.close()); + + HBox buttons = new HBox(8, cancel, ok); + buttons.setAlignment(Pos.CENTER_RIGHT); + buttons.setPadding(new Insets(8, 0, 0, 0)); + + VBox content = new VBox(8, ta, buttons); + content.setPadding(new Insets(8)); + dlg.setScene(new Scene(content, 480, 320)); + dlg.show(); + ta.requestFocus(); + } + + private void updateHeight() { + area.setPrefRowCount(expanded ? 5 : 1); + } + } +} From 1547fb36bc93538224825c0ed9e439bd65008608 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Thu, 8 Oct 2026 22:05:39 +0200 Subject: [PATCH 063/281] key.ui.fx: tacletmatch: redesigned taclet match dialog with shared apply semantics and theme styles --- .../fx/tacletmatch/ApplyTacletDialogF.java | 139 ++++++++ .../fx/tacletmatch/TacletMatchDialogF.java | 299 ++++++++++++++++++ .../de/uka/ilkd/key/gui/fx/theme/key-dark.css | 132 ++++++++ .../uka/ilkd/key/gui/fx/theme/key-light.css | 133 ++++++++ 4 files changed, 703 insertions(+) create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/tacletmatch/ApplyTacletDialogF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/tacletmatch/TacletMatchDialogF.java diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/tacletmatch/ApplyTacletDialogF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/tacletmatch/ApplyTacletDialogF.java new file mode 100644 index 00000000000..5a46488bafe --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/tacletmatch/ApplyTacletDialogF.java @@ -0,0 +1,139 @@ +/* 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.tacletmatch; + +import javafx.scene.control.Alert; +import javafx.scene.control.Button; +import javafx.stage.Stage; +import javafx.stage.Window; + +import de.uka.ilkd.key.control.InstantiationFileHandler; +import de.uka.ilkd.key.control.ProofControl; +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.notification.NotificationManagerF.Kind; +import de.uka.ilkd.key.proof.Goal; +import de.uka.ilkd.key.rule.TacletApp; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Common superclass for the JavaFX taclet-instantiation dialogs. It owns the shared {@link + * TacletInstantiationModel}s and the Apply/Cancel buttons and carries the apply semantics shared by + * both Swing dialogs ({@code de.uka.ilkd.key.gui.ApplyTacletDialog} of module {@code key.ui} and + * its subclasses {@code TacletMatchDialog} / {@code classic.TacletMatchCompletionDialog}: push all + * input to the model, build the {@link TacletApp}, apply it interactively on the goal, save the + * instantiation list and close — or report the failure). + * + *

+ * The Swing base class additionally requests modal access to the mediator + * ({@code mediator.requestModalAccess}) which suspends proof automation while the dialog is open + * and frees it again on close. The JavaFX mediator has no modal-access mechanism yet; until then + * the Apply button is simply disabled while an auto mode run is active (see {@link + * #setAutoModeRunning(boolean)}), which is the observable part of the Swing semantics. + */ +public abstract class ApplyTacletDialogF extends Stage { + + private static final Logger LOGGER = LoggerFactory.getLogger(ApplyTacletDialogF.class); + + // buttons + protected final Button cancelButton = new Button("Cancel"); + protected final Button applyButton = new Button("Apply"); + + protected final TacletInstantiationModel[] model; + + /** + * the proof control the application is performed with (Swing resolves it via {@code + * mediator().getUI().getProofControl()}) + */ + protected final ProofControl proofControl; + + /** the goal the rule application is performed on */ + protected final Goal goal; + + protected ApplyTacletDialogF(Window owner, String title, TacletInstantiationModel[] model, + ProofControl proofControl, Goal goal) { + this.model = model; + this.proofControl = proofControl; + this.goal = goal; + + setTitle(title); + cancelButton.setCancelButton(true); + applyButton.setDefaultButton(true); + cancelButton.setOnAction(e -> closeDialog()); + applyButton.setOnAction(e -> handleApply()); + setOnCloseRequest(e -> closeDialog()); + if (owner != null) { + initOwner(owner); + } + } + + protected abstract void pushAllInputToModel(); + + protected abstract int current(); + + protected abstract void setStatus(String s); + + /** + * Observable part of the Swing mediator's modal access: while an auto mode run is in progress + * the rule application is refused by the core, so the Apply button is disabled. + */ + public void setAutoModeRunning(boolean running) { + applyButton.setDisable(running); + } + + /** + * The shared Apply semantics of the Swing {@code ButtonListener}s in {@code TacletMatchDialog} + * and {@code classic.TacletMatchCompletionDialog} (both push all input, create the taclet app, + * call {@code proofControl.applyInteractive}, save the instantiation list and close; on failure + * an error dialog is shown). + */ + protected void handleApply() { + try { + pushAllInputToModel(); + TacletApp app = model[current()].createTacletApp(); + if (app == null) { + // Swing: JOptionPane "Could not apply rule" / "Rule Application Failure" + Alert alert = new Alert(Alert.AlertType.ERROR, "Could not apply rule"); + alert.setTitle("Rule Application Failure"); + alert.setHeaderText(null); + alert.showAndWait(); + return; + } + proofControl.applyInteractive(app, goal); + } catch (Exception exc) { + onApplyException(exc); + return; + } + InstantiationFileHandler.saveListFor(model[current()]); + closeDialog(); + } + + /** + * reports a failed application attempt (Swing {@code IssueDialog.showExceptionDialog}): the + * exception is logged and surfaced as an error toast; the classic dialog additionally focuses + * the offending input (see {@code TacletMatchCompletionDialogF}). + */ + protected void onApplyException(Exception exc) { + LOGGER.error("Taclet application failed", exc); + NotificationManagerF.getInstance() + .notify("Taclet application failed: " + exc.getMessage(), Kind.ERROR); + } + + /** Swing {@code closeDialog()}: frees the mediator and disposes the dialog. */ + protected void closeDialog() { + closeDlg(); + close(); + } + + /** + * Swing {@code closeDlg()}: frees the modal access to the mediator. The JavaFX mediator has no + * modal access yet, so this is a no-op; the hook stays so the wiring at the control seam + * (WindowUserInterfaceControlF) can call it symmetrically. + */ + protected void closeDlg() { + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/tacletmatch/TacletMatchDialogF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/tacletmatch/TacletMatchDialogF.java new file mode 100644 index 00000000000..4434292ed43 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/tacletmatch/TacletMatchDialogF.java @@ -0,0 +1,299 @@ +/* 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.tacletmatch; + +import javafx.geometry.Pos; +import javafx.scene.Node; +import javafx.scene.Scene; +import javafx.scene.control.Label; +import javafx.scene.control.ScrollPane; +import javafx.scene.control.Tab; +import javafx.scene.control.TabPane; +import javafx.scene.control.Tooltip; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; +import javafx.stage.Stage; + +import de.uka.ilkd.key.control.ProofControl; +import de.uka.ilkd.key.control.instantiation_model.TacletInstantiationModel; +import de.uka.ilkd.key.gui.fx.fonticons.FontAwesomeSolid; +import de.uka.ilkd.key.gui.fx.fonticons.IconFactoryF; +import de.uka.ilkd.key.java.Services; +import de.uka.ilkd.key.pp.NotationInfo; +import de.uka.ilkd.key.proof.Goal; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Dialog for completing and applying an interactively selected taclet. + * + *

+ * Per instantiation alternative it shows a {@link MatchInfoPanelF} (how the find matched and the + * bindings it determined), an {@link SVInstantiationPanelF} (the schema variables left to + * instantiate), an {@link AssumesSelectionPanelF} (choose or type the {@code \assumes} + * instantiation) and a {@link ResultPreviewPanelF} (the resulting sequents). The inputs and the + * preview are laid out in a {@link ResponsiveSplitF} that collapses to tabs when the window is + * narrow. + * + *

+ * Port of {@code de.uka.ilkd.key.gui.tacletmatch.TacletMatchDialog} + * (TacletMatchDialog.java:39-383). + * Deviations from the Swing original: the dialog is centred on the owner stage instead of the + * sequent-beside placement heuristic (TacletMatchDialog.java:103-150 — that heuristic reads the + * Swing goal view's screen bounds; the FX docking layout can be re-queried once the rule-menu + * integration lands), and the window-geometry preferences are not persisted (the FX configuration + * store has no per-dialog preference registry yet). In-place highlighting of each schema variable + * inside the matched term is a planned follow-up in Swing as well (TacletMatchDialog.java:37). + */ +public class TacletMatchDialogF extends ApplyTacletDialogF { + + private static final Logger LOGGER = LoggerFactory.getLogger(TacletMatchDialogF.class); + + private final Services services; + private final NotationInfo notationInfo; + + /** one schema-variable instantiation panel per instantiation alternative */ + private final SVInstantiationPanelF[] svPanels; + + /** + * the assumptions panel per alternative, or {@code null} if the taclet has no assumes + */ + private final AssumesSelectionPanelF[] ifPanels; + + /** the result preview panel per alternative */ + private final ResultPreviewPanelF[] previews; + + /** the match info panel per alternative (for the self test's highlight assertion) */ + private final MatchInfoPanelF[] matchPanels; + + /** the tabbed pane selecting between instantiation alternatives */ + private TabPane alternatives; + + /** single-line footer status (icon + message) */ + private final Label statusLabel = new Label(" "); + + public TacletMatchDialogF(Stage owner, TacletInstantiationModel[] model, Goal goal, + ProofControl proofControl, Services services, NotationInfo notationInfo) { + super(owner, "Choose Taclet Instantiation", model, proofControl, goal); + this.services = services; + this.notationInfo = notationInfo; + this.svPanels = new SVInstantiationPanelF[model.length]; + this.ifPanels = new AssumesSelectionPanelF[model.length]; + this.previews = new ResultPreviewPanelF[model.length]; + this.matchPanels = new MatchInfoPanelF[model.length]; + + for (TacletInstantiationModel aModel : model) { + aModel.prepareUnmatchedInstantiation(); + } + + BorderPane root = new BorderPane(); + root.setCenter(createInstantiationPanel()); + root.setBottom(createFooter()); + + Scene scene = new Scene(root, 1000, 620); + root.setMinSize(640, 400); + setScene(scene); + if (owner != null) { + centerOnOwner(owner); + } + show(); + LOGGER.debug("TacletMatchDialogF opened for {}", model[0].taclet().name()); + } + + private void centerOnOwner(Stage owner) { + setX(Math.max(0, owner.getX() + (owner.getWidth() - getWidth()) / 2)); + setY(Math.max(0, owner.getY() + (owner.getHeight() - getHeight()) / 3)); + } + + /** + * builds the instantiation area: the single alternative's content directly, or a tab strip + * selecting between the alternatives (TacletMatchDialog.java:158-170). + */ + private javafx.scene.Node createInstantiationPanel() { + if (model.length == 1) { + // single match: show its content directly, no tab strip + return buildAlternative(0); + } + + alternatives = new TabPane(); + for (int i = 0; i < model.length; i++) { + alternatives.getTabs().add(new Tab("Match " + (i + 1), buildAlternative(i))); + } + alternatives.setTabClosingPolicy(TabPane.TabClosingPolicy.UNAVAILABLE); + alternatives.getSelectionModel().selectedItemProperty() + .addListener((obs, oldTab, newTab) -> refreshStatus(current())); + return alternatives; + } + + /** + * a compact footer: status icon + message on the left, cancel/apply on the right + * (TacletMatchDialog.java:172-191). + */ + private javafx.scene.Node createFooter() { + statusLabel.setAlignment(Pos.CENTER_LEFT); + HBox buttons = new HBox(8, applyButton, cancelButton); + buttons.setAlignment(Pos.CENTER_RIGHT); + + HBox footer = new HBox(12, statusLabel, buttons); + footer.getStyleClass().add("tacletmatch-footer"); + footer.setAlignment(Pos.CENTER_LEFT); + footer.setPadding(new javafx.geometry.Insets(10, 12, 10, 12)); + HBox.setHgrow(statusLabel, Priority.ALWAYS); + HBox.setHgrow(buttons, Priority.NEVER); + + setStatus(model[current()].getStatusString()); + applyButton.setOnAction(e -> handleApply()); + return footer; + } + + /** + * builds the instantiation content (match overview, schema variables, assumptions, preview) for + * one instantiation alternative (TacletMatchDialog.java:197-234). + */ + private javafx.scene.Node buildAlternative(int i) { + // inputs: match overview, schema variables, assumptions + VBox inputs = new VBox(4); + + MatchInfoPanelF matchPanel = new MatchInfoPanelF(model[i], services, notationInfo); + matchPanels[i] = matchPanel; + inputs.getChildren().add(matchPanel); + + SVInstantiationPanelF svPanel = + new SVInstantiationPanelF(model[i], services, notationInfo, () -> refreshStatus(i)); + svPanels[i] = svPanel; + inputs.getChildren().add(svPanel); + + if (!model[i].application().taclet().assumesSequent().isEmpty()) { + AssumesSelectionPanelF assumes = + new AssumesSelectionPanelF(model[i], services, notationInfo, + () -> refreshStatus(i)); + ifPanels[i] = assumes; + inputs.getChildren().add(assumes); + } + + // preview, on the other side of the split + VBox previewPane = new VBox(4); + ResultPreviewPanelF preview = + new ResultPreviewPanelF(model[i], services, notationInfo, goal); + previews[i] = preview; + previewPane.getChildren().add(preview); + + ScrollPane inputsScroll = scroll(inputs); + ScrollPane previewScroll = scroll(previewPane); + return new ResponsiveSplitF(inputsScroll, "Instantiate", previewScroll, "Result preview"); + } + + /** a vertical-scrolling, width-filling scroll pane (TacletMatchDialog.java:228-234) */ + private static ScrollPane scroll(VBox view) { + ScrollPane scroll = new ScrollPane(view); + scroll.setBorder(null); + scroll.setFitToWidth(true); + return scroll; + } + + /** + * refreshes the status display and result preview from the given alternative if selected + * (TacletMatchDialog.java:282-290). + */ + private void refreshStatus(int idx) { + if (idx == current()) { + setStatus(model[idx].getStatusString()); + if (previews[idx] != null) { + previews[idx].requestUpdate(); + } + } + } + + @Override + public void setStatus(String s) { + if (s == null || s.isEmpty()) { + statusLabel.setText(" "); + statusLabel.setGraphic(null); + statusLabel.setTooltip(null); + return; + } + int nl = s.indexOf('\n'); + String firstLine = nl >= 0 ? s.substring(0, nl) : s; + + FontAwesomeSolid glyph; + String colorClass; + if (s.startsWith("Instantiation is OK")) { + glyph = FontAwesomeSolid.CHECK_CIRCLE; + colorClass = "tacletmatch-status-ok"; + } else if (s.startsWith("Rule is not applicable")) { + glyph = FontAwesomeSolid.TIMES_CIRCLE; + colorClass = "tacletmatch-status-error"; + } else { + glyph = FontAwesomeSolid.EXCLAMATION_CIRCLE; + colorClass = "tacletmatch-status-warn"; + } + Node icon = IconFactoryF.createIcon(glyph, 14); + icon.getStyleClass().add(colorClass); + statusLabel.setGraphic(icon); + statusLabel.setText(firstLine); + statusLabel.getStyleClass().removeAll("tacletmatch-status-ok", "tacletmatch-status-warn", + "tacletmatch-status-error"); + statusLabel.getStyleClass().add(colorClass); + statusLabel.setTooltip(nl >= 0 ? new Tooltip(s) : null); + } + + @Override + protected int current() { + return alternatives == null ? 0 : alternatives.getSelectionModel().getSelectedIndex(); + } + + @Override + protected void pushAllInputToModel() { + int i = current(); + if (ifPanels[i] != null) { + ifPanels[i].pushAllInputToModel(); + } + if (svPanels[i] != null) { + svPanels[i].pushAllInputToModel(); + } + } + + // ------------------------------------------------------------------ + // self-test hooks (package-private; used by TacletMatchVerifyF) + // ------------------------------------------------------------------ + + /** programmatically presses the apply button (the same handler as its onAction) */ + void fireApply() { + applyButton.fire(); + } + + /** programmatically presses the cancel button (the same handler as its onAction) */ + void fireCancel() { + cancelButton.fire(); + } + + /** the current status line text as displayed (first line only) */ + String statusText() { + return statusLabel.getText(); + } + + /** the schema-variable panel of alternative {@code i} */ + SVInstantiationPanelF svPanel(int i) { + return svPanels[i]; + } + + /** the assumptions panel of alternative {@code i}, or {@code null} */ + AssumesSelectionPanelF assumesPanel(int i) { + return ifPanels[i]; + } + + /** the number of schema-variable spans highlighted in alternative {@code i}'s matched term */ + int highlightSpanCount(int i) { + return matchPanels[i] == null ? 0 : matchPanels[i].getHighlightSpanCount(); + } + + /** the preview panel of alternative {@code i}, or {@code null} */ + ResultPreviewPanelF preview(int i) { + return previews[i]; + } +} diff --git a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css index 173dd4a912f..f49c8671a63 100644 --- a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css +++ b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css @@ -607,3 +607,135 @@ .sequent-search-mode { -fx-padding: 1; } +/* ========================================================================== + * Taclet match / interactive taclet application dialogs (ported from the + * Swing de.uka.ilkd.key.gui.tacletmatch dialogs; classes prefixed + * "tacletmatch-"). Colours come from the shared palette; the schema-variable + * chip colours are data colours kept in code (SvPaletteF), not theme chrome. + * ========================================================================== */ + +/* monospaced logic/term text (TmStyle.mono) */ +.tacletmatch-mono { + -fx-font-family: "monospace"; +} + +/* muted captions, row labels and hints (TmStyle.muted) */ +.tacletmatch-muted { + -fx-text-fill: -key-text-secondary; +} + +/* a titled section: bold header with a hairline rule beneath it, no box + * (TmStyle.section) */ +.tacletmatch-section { + -fx-spacing: 4; + -fx-padding: 6 0 2 0; +} + +.tacletmatch-section-title { + -fx-font-weight: bold; + -fx-padding: 2 0 1 0; + -fx-border-color: transparent transparent -key-border transparent; + -fx-border-insets: 0 0 2 0; +} + +/* the compact footer with the status line and the Apply/Cancel buttons + * (TmStyle.styleFooter: faint surface bar under a hairline) */ +.tacletmatch-footer { + -fx-background-color: derive(-key-surface, -6%); + -fx-border-color: -key-border transparent transparent transparent; +} + +/* the small, unobtrusive disclosure (expand/collapse) toggle + * (TmStyle.disclosure) */ +.tacletmatch-disclosure { + -fx-background-color: transparent; + -fx-padding: 1 3 1 3; + -fx-min-width: 0; + -fx-alignment: CENTER_LEFT; + -fx-content-display: GRAPHIC_ONLY; + -fx-text-fill: -key-text-secondary; + -fx-cursor: hand; +} + +.tacletmatch-disclosure:hover { + -fx-background-color: derive(-key-border, 40%); + -fx-background-radius: 3; +} + +/* the concrete matched term, set apart by a faint full-width band + * (MatchInfoPanel.matchedRow) */ +.tacletmatch-matched-band { + -fx-background-color: derive(-key-border, 70%); + -fx-background-radius: 3; + -fx-padding: 3 6 3 6; +} + +/* the result preview surface card (ResultPreviewPanel: the "answer") */ +.tacletmatch-preview-card { + -fx-background-color: -key-surface; + -fx-border-color: -key-border; + -fx-border-radius: 4; + -fx-background-radius: 4; + -fx-padding: 6; +} + +/* added (+) and removed (−) formula markers of the result preview */ +.tacletmatch-added { + -fx-text-fill: -key-success; + -fx-font-weight: bold; +} + +.tacletmatch-removed { + -fx-text-fill: -key-error; + -fx-font-weight: bold; +} + +/* status colours of the footer icon/message and the manual-input status + * (TacletMatchDialog.statusLine / AssumesSelectionPanel.status) */ +.tacletmatch-status-ok { + -fx-text-fill: -key-success; + -fx-fill: -key-success; +} + +.tacletmatch-status-warn { + -fx-text-fill: -key-warning; + -fx-fill: -key-warning; +} + +.tacletmatch-status-error { + -fx-text-fill: -key-error; + -fx-fill: -key-error; +} + +/* instantiation field highlighted while a term is dragged over it + * (SVInstantiationPanel drop target) */ +.tacletmatch-drop-highlight { + -fx-border-color: -key-accent; + -fx-border-width: 1; +} + +/* the small inline "edit in larger window" icon button */ +.tacletmatch-edit-button { + -fx-background-color: transparent; + -fx-padding: 1 4 1 4; + -fx-text-fill: -key-text-secondary; + -fx-cursor: hand; +} + +.tacletmatch-edit-button:hover { + -fx-background-color: derive(-key-border, 40%); + -fx-background-radius: 3; +} + +/* schema-variable chip: the colours are set inline (SvPaletteF); only the + * pill shape comes from the theme */ +.tacletmatch-chip { + -fx-padding: 0 5 0 5; + -fx-background-radius: 8; + -fx-font-weight: bold; +} + +/* an instantiation row (chip / arrow / field alignment) */ +.tacletmatch-row { + -fx-fill-height: false; +} diff --git a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css index 2962a642bf4..f4f8e6ced5f 100644 --- a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css +++ b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css @@ -601,3 +601,136 @@ .sequent-search-mode { -fx-padding: 1; } + +/* ========================================================================== + * Taclet match / interactive taclet application dialogs (ported from the + * Swing de.uka.ilkd.key.gui.tacletmatch dialogs; classes prefixed + * "tacletmatch-"). Colours come from the shared palette; the schema-variable + * chip colours are data colours kept in code (SvPaletteF), not theme chrome. + * ========================================================================== */ + +/* monospaced logic/term text (TmStyle.mono) */ +.tacletmatch-mono { + -fx-font-family: "monospace"; +} + +/* muted captions, row labels and hints (TmStyle.muted) */ +.tacletmatch-muted { + -fx-text-fill: -key-text-secondary; +} + +/* a titled section: bold header with a hairline rule beneath it, no box + * (TmStyle.section) */ +.tacletmatch-section { + -fx-spacing: 4; + -fx-padding: 6 0 2 0; +} + +.tacletmatch-section-title { + -fx-font-weight: bold; + -fx-padding: 2 0 1 0; + -fx-border-color: transparent transparent -key-border transparent; + -fx-border-insets: 0 0 2 0; +} + +/* the compact footer with the status line and the Apply/Cancel buttons + * (TmStyle.styleFooter: faint surface bar under a hairline) */ +.tacletmatch-footer { + -fx-background-color: derive(-key-surface, -6%); + -fx-border-color: -key-border transparent transparent transparent; +} + +/* the small, unobtrusive disclosure (expand/collapse) toggle + * (TmStyle.disclosure) */ +.tacletmatch-disclosure { + -fx-background-color: transparent; + -fx-padding: 1 3 1 3; + -fx-min-width: 0; + -fx-alignment: CENTER_LEFT; + -fx-content-display: GRAPHIC_ONLY; + -fx-text-fill: -key-text-secondary; + -fx-cursor: hand; +} + +.tacletmatch-disclosure:hover { + -fx-background-color: derive(-key-border, 40%); + -fx-background-radius: 3; +} + +/* the concrete matched term, set apart by a faint full-width band + * (MatchInfoPanel.matchedRow) */ +.tacletmatch-matched-band { + -fx-background-color: derive(-key-border, 70%); + -fx-background-radius: 3; + -fx-padding: 3 6 3 6; +} + +/* the result preview surface card (ResultPreviewPanel: the "answer") */ +.tacletmatch-preview-card { + -fx-background-color: -key-surface; + -fx-border-color: -key-border; + -fx-border-radius: 4; + -fx-background-radius: 4; + -fx-padding: 6; +} + +/* added (+) and removed (−) formula markers of the result preview */ +.tacletmatch-added { + -fx-text-fill: -key-success; + -fx-font-weight: bold; +} + +.tacletmatch-removed { + -fx-text-fill: -key-error; + -fx-font-weight: bold; +} + +/* status colours of the footer icon/message and the manual-input status + * (TacletMatchDialog.statusLine / AssumesSelectionPanel.status) */ +.tacletmatch-status-ok { + -fx-text-fill: -key-success; + -fx-fill: -key-success; +} + +.tacletmatch-status-warn { + -fx-text-fill: -key-warning; + -fx-fill: -key-warning; +} + +.tacletmatch-status-error { + -fx-text-fill: -key-error; + -fx-fill: -key-error; +} + +/* instantiation field highlighted while a term is dragged over it + * (SVInstantiationPanel drop target) */ +.tacletmatch-drop-highlight { + -fx-border-color: -key-accent; + -fx-border-width: 1; +} + +/* the small inline "edit in larger window" icon button */ +.tacletmatch-edit-button { + -fx-background-color: transparent; + -fx-padding: 1 4 1 4; + -fx-text-fill: -key-text-secondary; + -fx-cursor: hand; +} + +.tacletmatch-edit-button:hover { + -fx-background-color: derive(-key-border, 40%); + -fx-background-radius: 3; +} + +/* schema-variable chip: the colours are set inline (SvPaletteF); only the + * pill shape comes from the theme */ +.tacletmatch-chip { + -fx-padding: 0 5 0 5; + -fx-background-radius: 8; + -fx-font-weight: bold; +} + +/* an instantiation row (chip / arrow / field alignment) */ +.tacletmatch-row { + -fx-fill-height: false; +} From 2f01307b8d49ff8f554ed0b4434515d9860db34a Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Thu, 8 Oct 2026 22:06:42 +0200 Subject: [PATCH 064/281] key.ui.fx: tacletmatch: key.fx.verify.tacletmatch self test hooked into the demo load --- key.ui.fx/build.gradle | 5 +- .../de/uka/ilkd/key/gui/fx/MainWindowF.java | 7 + .../fx/tacletmatch/TacletMatchVerifyF.java | 206 ++++++++++++++++++ 3 files changed, 217 insertions(+), 1 deletion(-) create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/tacletmatch/TacletMatchVerifyF.java diff --git a/key.ui.fx/build.gradle b/key.ui.fx/build.gradle index 86152523631..400e89fcb12 100644 --- a/key.ui.fx/build.gradle +++ b/key.ui.fx/build.gradle @@ -65,11 +65,14 @@ run { // Hide/Regroup) with the key.fx.verify.sequentsearch query after the demo load) // * key.fx.verify.updatehighlight = any (M2: run the update-highlight overlay self test; // needs a sequent printing update operators, e.g. standard_key/queries/useQuery.key) + // * key.fx.verify.tacletmatch = 1|hold (tacletmatch: run the interactive taclet + // application self test after the demo load; "hold" leaves the dialog open) ["key.fx.driver", "key.fx.theme", "key.fx.demo.sequent", "key.fx.verify.sequent", "key.fx.show", "key.fx.demo.autoprove", "key.fx.verify.tree", "key.fx.demo.autoprove.live", "key.fx.demo.maxsteps", "key.fx.verify.goallist", "key.fx.verify.search", "key.fx.verify.sequentsearch", "key.fx.verify.treefilters", - "key.fx.verify.sequentsearchmodes", "key.fx.verify.updatehighlight"] + "key.fx.verify.sequentsearchmodes", "key.fx.verify.updatehighlight", + "key.fx.verify.tacletmatch"] .each { name -> def value = System.getProperty(name) if (value != null) { 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 index 8a46e0ba13f..73476390031 100644 --- 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 @@ -72,6 +72,7 @@ import de.uka.ilkd.key.gui.fx.settings.SettingsManagerF; 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.tacletmatch.TacletMatchVerifyF; import de.uka.ilkd.key.gui.fx.theme.Theme; import de.uka.ilkd.key.gui.fx.theme.ThemeManager; import de.uka.ilkd.key.proof.Proof; @@ -482,6 +483,12 @@ protected KeYEnvironment call() throws Exception { .notify("Proof diff verification: " + report, report.endsWith("PASS") ? Kind.INFO : Kind.ERROR); } + // tacletmatch: run the interactive taclet application self test (dialog render, + // cancel keeps the proof, apply adds to the proof) after the demo load + if (System.getProperty("key.fx.verify.tacletmatch") != null) { + TacletMatchVerifyF.runTacletMatchVerification(env.getLoadedProof(), + env.getProofControl(), stage, mediator.getNotationInfo()); + } if (System.getProperty("key.fx.demo.autoprove.live") != null) { startLiveAutoMode(env); } diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/tacletmatch/TacletMatchVerifyF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/tacletmatch/TacletMatchVerifyF.java new file mode 100644 index 00000000000..c711fcb999c --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/tacletmatch/TacletMatchVerifyF.java @@ -0,0 +1,206 @@ +/* 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.tacletmatch; + +import java.util.ArrayList; +import java.util.List; +import javafx.stage.Stage; + +import de.uka.ilkd.key.control.AbstractProofControl; +import de.uka.ilkd.key.control.ProofControl; +import de.uka.ilkd.key.control.instantiation_model.TacletInstantiationModel; +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.Proof; +import de.uka.ilkd.key.rule.TacletApp; + +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.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@code key.fx.verify.tacletmatch} self test: after the demo proof load it programmatically + * obtains an interactive (incomplete) taclet application on the current proof — the same flow the + * Swing sequent-view term menu triggers via {@code AbstractProofControl.selectedTaclet}, which for + * incomplete applications builds {@link TacletInstantiationModel}s and opens + * {@link TacletMatchDialogF} (AbstractProofControl.java:226-258, + * WindowUserInterfaceControl.completeAndApplyTacletMatch:330-338) — and asserts: + *

    + *
  1. the dialog renders with the match highlighted (status line + schema-variable spans in the + * matched term);
  2. + *
  3. cancel closes the dialog and leaves the proof unchanged (node count stable);
  4. + *
  5. apply closes the dialog and adds the application to the proof (node count changes).
  6. + *
+ * Each assertion logs {@code PASS}/{@code FAIL} (and is surfaced as a toast by MainWindowF). + * + *

+ * The value of {@code key.fx.verify.tacletmatch} selects the mode: {@code 1} (default) runs the + * full three-assertion flow; {@code hold} runs only the render assertion and leaves the dialog open + * for interactive screenshots (light/dark theme checks). + */ +public final class TacletMatchVerifyF { + + private static final Logger LOGGER = LoggerFactory.getLogger(TacletMatchVerifyF.class); + + private TacletMatchVerifyF() {} + + /** + * Runs the verification on the FX thread (called from the demo-load success handler in + * MainWindowF). + */ + public static void runTacletMatchVerification(Proof proof, ProofControl proofControl, + Stage owner, NotationInfo notationInfo) { + String mode = System.getProperty("key.fx.verify.tacletmatch", "1").trim(); + boolean hold = "hold".equalsIgnoreCase(mode); + try { + run(proof, proofControl, owner, notationInfo, hold); + } catch (Throwable t) { + LOGGER.error("tacletmatch verification: FAIL (unexpected error: {})", t, t); + } + } + + private static void run(Proof proof, ProofControl proofControl, Stage owner, + NotationInfo notationInfo, boolean hold) { + Services services = proof.getServices(); + Goal goal = proof.openEnabledGoals().head(); + AbstractProofControl control = (AbstractProofControl) proofControl; + + // ---- find an interactive (incomplete) taclet application on the current goal ---------- + TacletApp chosen = findIncompleteTacletApp(goal, services); + if (chosen == null) { + LOGGER.info( + "tacletmatch verification: find-taclet-app: FAIL (no incomplete taclet " + + "application on the current goal)"); + return; + } + LOGGER.info("tacletmatch verification: find-taclet-app: PASS ({})", describe(chosen)); + + int nodesBefore = proof.countNodes(); + + // ---- assertion 1: the dialog renders with the highlighted match ----------------------- + TacletMatchDialogF renderDlg = openDialog(control, chosen, goal, owner, proofControl, + services, notationInfo); + boolean rendered = renderDlg.isShowing() && renderDlg.statusText() != null + && !renderDlg.statusText().isBlank(); + LOGGER.info("tacletmatch verification: dialog-renders: {} (status \"{}\", showing={})", + rendered ? "PASS" : "FAIL", renderDlg.statusText(), renderDlg.isShowing()); + + int spans = renderDlg.highlightSpanCount(0); + if (chosen.posInOccurrence() != null) { + // find taclet: the matched term is expected to carry schema-variable highlights + LOGGER.info( + "tacletmatch verification: match-highlight: {} ({} schema-variable span(s) in " + + "the matched term)", + spans > 0 ? "PASS" : "FAIL", spans); + } else { + LOGGER.info( + "tacletmatch verification: match-highlight: PASS (no find pattern — the match " + + "overview renders the taclet without a matched term)"); + } + + if (hold) { + // leave the dialog open for interactive screenshots (light/dark theme checks) + LOGGER.info("tacletmatch verification: hold mode — dialog left open for screenshots"); + return; + } + + // ---- assertion 2: cancel leaves the proof unchanged ---------------------------------- + int nodesAfterCancel = proof.countNodes(); + renderDlg.fireCancel(); + boolean cancelled = + !renderDlg.isShowing() && proof.countNodes() == nodesAfterCancel; + LOGGER.info("tacletmatch verification: cancel-keeps-proof: {} (node count before={}, " + + "after={})", cancelled ? "PASS" : "FAIL", nodesBefore, + proof.countNodes()); + + // ---- assertion 3: apply closes the dialog and adds the application to the proof ------ + if (chosen.taclet().assumesSequent().isEmpty()) { + // the schema-variable rows carry the model's pre-filled proposals, so the plain apply + // flow (push input → createTacletApp → applyInteractive) completes the instantiation + TacletMatchDialogF applyDlg = openDialog(control, chosen, goal, owner, proofControl, + services, notationInfo); + applyDlg.fireApply(); + boolean applied = !applyDlg.isShowing() && proof.countNodes() != nodesBefore; + LOGGER.info("tacletmatch verification: apply-adds-to-proof: {} (node count before={}, " + + "after={})", applied ? "PASS" : "FAIL", nodesBefore, proof.countNodes()); + } else { + LOGGER.info( + "tacletmatch verification: apply-adds-to-proof: SKIPPED (chosen taclet {} " + + "requires \\assumes instantiation, which needs typed user input)", + chosen.taclet().name()); + } + } + + private static TacletMatchDialogF openDialog(AbstractProofControl control, TacletApp app, + Goal goal, Stage owner, ProofControl proofControl, Services services, + NotationInfo notationInfo) { + TacletInstantiationModel[] models = control.completeAndApplyApp(List.of(app), goal); + return new TacletMatchDialogF(owner, models, goal, proofControl, services, notationInfo); + } + + /** + * finds a taclet application on the given goal whose instantiation is incomplete (the condition + * under which the Swing term menu routes to the instantiation dialog, + * AbstractProofControl.java:226-258). Preferences, in order: a find-taclet app with a match + * position and no {@code \assumes} (exercises the match highlighting and the plain apply flow), + * then any app without {@code \assumes}, then any incomplete app. + */ + private static TacletApp findIncompleteTacletApp(Goal goal, Services services) { + List candidates = new ArrayList<>(); + ImmutableList noFind = + goal.ruleAppIndex().getNoFindTaclet(TacletFilter.TRUE, services); + noFind.forEach(candidates::add); + org.key_project.prover.sequent.Sequent seq = goal.sequent(); + for (int i = 1; i <= seq.size(); i++) { + PosInOccurrence pio = PosInOccurrence.findInSequent(seq, i, PosInTerm.getTopLevel()); + ImmutableList apps = + goal.ruleAppIndex().getTacletAppAt(TacletFilter.TRUE, pio, services); + apps.forEach(candidates::add); + } + + TacletApp findNeedsFull = null; + TacletApp findWithoutAssumes = null; + TacletApp withoutAssumes = null; + TacletApp any = null; + for (TacletApp app : candidates) { + if (app.complete()) { + continue; + } + boolean hasAssumes = !app.taclet().assumesSequent().isEmpty(); + // applications needing a full instantiation (not just skolem constants) are the ones + // the Swing term menu routes to the instantiation dialog + boolean needsFull = !app.completeExceptSkolemConstants(); + if (app.posInOccurrence() != null && !hasAssumes) { + if (needsFull && findNeedsFull == null) { + findNeedsFull = app; + } + if (findWithoutAssumes == null) { + findWithoutAssumes = app; + } + } + if (!hasAssumes && withoutAssumes == null) { + withoutAssumes = app; + } + if (any == null) { + any = app; + } + } + return findNeedsFull != null ? findNeedsFull + : findWithoutAssumes != null ? findWithoutAssumes + : withoutAssumes != null ? withoutAssumes : any; + } + + /** a short human-readable description of the chosen application for the log line */ + private static String describe(TacletApp app) { + PosInOccurrence pio = app.posInOccurrence(); + return app.taclet().name() + " at " + + (pio == null ? "no position" : (pio.isInAntec() ? "antecedent" : "succedent")); + } +} From 442bfdfd851fffa0a09512b4824d52ec396270cf Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Thu, 8 Oct 2026 22:06:42 +0200 Subject: [PATCH 065/281] key.ui.fx: tacletmatch: classic table-based completion dialog and if-selection panel --- .../classic/TacletIfSelectionDialogF.java | 198 ++++++++ .../classic/TacletMatchCompletionDialogF.java | 434 ++++++++++++++++++ 2 files changed, 632 insertions(+) create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/tacletmatch/classic/TacletIfSelectionDialogF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/tacletmatch/classic/TacletMatchCompletionDialogF.java diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/tacletmatch/classic/TacletIfSelectionDialogF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/tacletmatch/classic/TacletIfSelectionDialogF.java new file mode 100644 index 00000000000..ab860478450 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/tacletmatch/classic/TacletIfSelectionDialogF.java @@ -0,0 +1,198 @@ +/* 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.tacletmatch.classic; + +import java.util.ArrayList; +import java.util.List; +import javafx.geometry.Insets; +import javafx.scene.control.ComboBox; +import javafx.scene.control.Label; +import javafx.scene.control.ListCell; +import javafx.scene.control.TextField; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; + +import de.uka.ilkd.key.control.instantiation_model.TacletAssumesModel; +import de.uka.ilkd.key.control.instantiation_model.TacletInstantiationModel; +import de.uka.ilkd.key.gui.fx.tacletmatch.ExpandableTextF; +import de.uka.ilkd.key.gui.fx.tacletmatch.MatchInfoPanelF; +import de.uka.ilkd.key.gui.fx.tacletmatch.TmTextF; +import de.uka.ilkd.key.java.Services; +import de.uka.ilkd.key.pp.NotationInfo; +import de.uka.ilkd.key.proof.io.ProofSaver; + +import org.key_project.prover.rules.instantiation.AssumesFormulaInstantiation; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This panel appears if a rule is selected to be applied and the rule has an if sequent. The panel + * offers the possibility to select the wanted match of the if sequent or to enter the if-sequent + * instantiation manually. + * + *

+ * Port of {@code de.uka.ilkd.key.gui.tacletmatch.classic.TacletIfSelectionDialog} (module {@code + * key.ui}): per {@code \assumes} formula one row with the printed formula label, a {@link + * ComboBox} of the candidate instantiations (backed by the Swing {@code TacletAssumesModel}, whose + * last element is the "Manual Input" sentinel) and — when manual input is selected — a text field + * whose content is written to the model on change/focus loss. + */ +class TacletIfSelectionDialogF extends VBox { + + private static final Logger LOGGER = LoggerFactory.getLogger(TacletIfSelectionDialogF.class); + + private final TacletInstantiationModel model; + private final Services services; + private final NotationInfo notationInfo; + + /** one row per assumes formula */ + private final List rows = new ArrayList<>(); + + private record IfRow(int index, TacletAssumesModel choice, + ComboBox combo, + TextField manual) { + } + + TacletIfSelectionDialogF(TacletInstantiationModel model, Services services, + NotationInfo notationInfo) { + this.model = model; + this.services = services; + this.notationInfo = notationInfo; + + getStyleClass().add("tacletmatch-section"); + getChildren() + .add(MatchInfoPanelF.sectionTitle("Please instantiate the taclet's assumptions:")); + getChildren().add(createIfPanel()); + } + + /** + * creates the rows used to select the wanted if instantiation. If the if-sequent matches + * different parts of the sequent the user can choose the right one or enter an instantiation + * manually (in which case a cut is performed if the manual entry does not match any other part + * of the sequent). + */ + private VBox createIfPanel() { + VBox panel = new VBox(4); + panel.setPadding(new Insets(4, 0, 4, 0)); + for (int i = 0; i < model.ifChoiceModelCount(); i++) { + panel.getChildren().add(buildFormulaSelector(i)); + } + return panel; + } + + private HBox buildFormulaSelector(int index) { + TacletAssumesModel choice = model.ifChoiceModel(index); + + Label label = new Label(ProofSaver.printAnything(model.ifFma(index), services)); + label.getStyleClass().add("tacletmatch-muted"); + label.setPrefWidth(140); + label.setWrapText(true); + + ComboBox combo = new ComboBox<>(); + for (int k = 0; k < choice.getSize(); k++) { + combo.getItems().add(choice.getElementAt(k)); + } + // preselect the model's current selection (never the sentinel if a candidate exists) + Object selected = choice.getSelectedItem(); + int selIdx = -1; + for (int k = 0; k < combo.getItems().size(); k++) { + if (combo.getItems().get(k) == selected) { + selIdx = k; + break; + } + } + if (selIdx < 0 && !combo.getItems().isEmpty()) { + selIdx = combo.getItems().size() - 1; + } + if (selIdx >= 0) { + combo.getSelectionModel().select(selIdx); + } + combo.setCellFactory(view -> new ListCell<>() { + @Override + protected void updateItem(AssumesFormulaInstantiation item, boolean empty) { + super.updateItem(item, empty); + if (empty || item == null) { + setText(null); + setTooltip(null); + } else { + setText(TmTextF.collapseToLine(item.toString(), 160)); + setTooltip(new javafx.scene.control.Tooltip(item.toString())); + } + } + }); + combo.setButtonCell(new ListCell<>() { + @Override + protected void updateItem(AssumesFormulaInstantiation item, boolean empty) { + super.updateItem(item, empty); + setText(empty || item == null ? null : item.toString()); + } + }); + + TextField manual = new TextField(); + manual.setPromptText("manual instantiation"); + manual.setFont(ExpandableTextF.mono()); + manual.setManaged(false); + manual.setVisible(false); + HBox.setHgrow(manual, Priority.ALWAYS); + + TextField manualRef = manual; + combo.getSelectionModel().selectedItemProperty().addListener((obs, oldV, newV) -> { + boolean isManual = newV != null && choice.isManualInputSelected(); + manualRef.setManaged(isManual); + manualRef.setVisible(isManual); + pushAllInputToModel(); + }); + manualRef.textProperty().addListener((o, a, b) -> pushAllInputToModel()); + manualRef.focusedProperty().addListener((obs, was, is) -> { + if (was && !is) { + pushAllInputToModel(); + } + }); + manualRef.setOnAction(e -> pushAllInputToModel()); + + HBox row = new HBox(6, label, combo, manual); + HBox.setHgrow(combo, Priority.ALWAYS); + rows.add(new IfRow(index, choice, combo, manual)); + return row; + } + + /** + * the if selection panel is attached to exactly one model (Swing {@code current()}); the + * manual inputs are transferred to that model. + */ + public void pushAllInputToModel() { + for (IfRow row : rows) { + if (row.combo().getSelectionModel().getSelectedItem() != null + && row.choice().isManualInputSelected() + && row.manual().getText() != null && !row.manual().getText().isEmpty()) { + model.setManualInput(row.index(), row.manual().getText()); + } else { + model.setManualInput(row.index(), ""); + } + } + } + + /** + * requests focus for the {@code field}-th manual input field and places the caret at the given + * column (Swing {@code requestFocusAt}); best-effort as in the Swing original. + */ + public void requestFocusAt(int field, int col) { + if (field < 0 || field >= rows.size()) { + LOGGER.debug("None existing field requested"); + return; + } + TextField tf = rows.get(field).manual(); + if (tf != null && col >= 0) { + try { + tf.positionCaret(Math.max(0, col - 1)); + } catch (RuntimeException iae) { + LOGGER.debug("Something is wrong with the caret position calculation.", iae); + } + tf.requestFocus(); + } + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/tacletmatch/classic/TacletMatchCompletionDialogF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/tacletmatch/classic/TacletMatchCompletionDialogF.java new file mode 100644 index 00000000000..11647bca985 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/tacletmatch/classic/TacletMatchCompletionDialogF.java @@ -0,0 +1,434 @@ +/* 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.tacletmatch.classic; + +import javafx.beans.property.SimpleStringProperty; +import javafx.collections.FXCollections; +import javafx.geometry.Insets; +import javafx.scene.Node; +import javafx.scene.Scene; +import javafx.scene.control.Label; +import javafx.scene.control.Tab; +import javafx.scene.control.TabPane; +import javafx.scene.control.TableColumn; +import javafx.scene.control.TableView; +import javafx.scene.control.TextArea; +import javafx.scene.control.TextField; +import javafx.scene.control.cell.TextFieldTableCell; +import javafx.scene.layout.Priority; +import javafx.scene.layout.Region; +import javafx.scene.layout.VBox; +import javafx.stage.Window; + +import de.uka.ilkd.key.control.ProofControl; +import de.uka.ilkd.key.control.instantiation_model.TacletFindModel; +import de.uka.ilkd.key.control.instantiation_model.TacletInstantiationModel; +import de.uka.ilkd.key.gui.fx.tacletmatch.ApplyTacletDialogF; +import de.uka.ilkd.key.gui.fx.tacletmatch.ExpandableTextF; +import de.uka.ilkd.key.gui.fx.tacletmatch.MatchInfoPanelF; +import de.uka.ilkd.key.gui.fx.tacletmatch.TmPrintF; +import de.uka.ilkd.key.gui.fx.theme.ThemeManager; +import de.uka.ilkd.key.java.Services; +import de.uka.ilkd.key.logic.op.IProgramVariable; +import de.uka.ilkd.key.pp.NotationInfo; +import de.uka.ilkd.key.proof.Goal; +import de.uka.ilkd.key.proof.SVInstantiationExceptionWithPosition; +import de.uka.ilkd.key.rule.Taclet; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The classic (table-based) dialog for completing and applying an interactively selected taclet — + * the JavaFX migration fallback of the redesigned {@code TacletMatchDialogF}. + * + *

+ * Port of {@code de.uka.ilkd.key.gui.tacletmatch.classic.TacletMatchCompletionDialog} plus the + * chrome helpers of its base {@code de.uka.ilkd.key.gui.tacletmatch.classic.ApplyTacletDialog} + * (both module {@code key.ui}), which the shared FX base {@link ApplyTacletDialogF} does not + * carry: the taclet display, the sequent program-variables info, the validation status area and + * the button panel. Per instantiation alternative it shows a table of the schema-variable + * instantiations (Swing {@code DataTable}; editing commits write straight into the {@link + * TacletFindModel} and refresh the status, like the Swing {@code editingStopped}), and — for + * taclets with an {@code \assumes} sequent — a {@link TacletIfSelectionDialogF}. + * + *

+ * Deviations, all visual or Swing-table artifacts: the multiline editor with the +/- row-height + * buttons becomes a plain single-line text cell (long instantiations are better edited in the + * redesigned dialog's larger editor); the drag-and-drop of sub-terms onto the table (Swing + * {@code DropTarget}) is deferred with the sequent view's drag source; window preferences are not + * persisted (the FX UI has no per-dialog preference store yet). The error-position focus routing + * ({@code errorPositionKnown}) ports as far as a JavaFX table cell editor allows — the row is + * selected and scrolled to, and the caret position is set on the text field — with the same + * caveat the Swing original documents itself ("ALL THIS DOES NOT REALLY WORK!!!"). + */ +public class TacletMatchCompletionDialogF extends ApplyTacletDialogF { + + private static final Logger LOGGER = + LoggerFactory.getLogger(TacletMatchCompletionDialogF.class); + + private final Services services; + private final NotationInfo notationInfo; + + /** the current chosen model */ + private int current = 0; + + /** the gui component used to display the different instantiation alternatives */ + private TabPane alternatives; + + /** the validation status area (Swing {@code createStatusPanel}) */ + private TextArea statusArea; + + /** the data tables, one per alternative (Swing {@code DataTable}) */ + private final TableView[] dataTables; + + /** the if-selections, one per alternative or {@code null} (Swing {@code ifSelectionPanel}) */ + private final TacletIfSelectionDialogF[] ifSelections; + + @SuppressWarnings("unchecked") + public TacletMatchCompletionDialogF(Window owner, TacletInstantiationModel[] model, Goal goal, + Services services, NotationInfo notationInfo, ProofControl proofControl) { + super(owner, "Choose Taclet Instantiation", model, proofControl, goal); + this.services = services; + this.notationInfo = notationInfo; + this.current = 0; + this.dataTables = new TableView[model.length]; + this.ifSelections = new TacletIfSelectionDialogF[model.length]; + + for (TacletInstantiationModel aModel : model) { + aModel.prepareUnmatchedInstantiation(); + } + + VBox root = new VBox(4); + root.setPadding(new Insets(8)); + root.getChildren().add(createTacletDisplay()); + + VBox lower = new VBox(4); + Node instArea = createInstantiationArea(); + lower.getChildren().add(instArea); + VBox.setVgrow(instArea, Priority.ALWAYS); + lower.getChildren().add(createInfoPanel()); + lower.getChildren().add(createStatusPanel()); + root.getChildren().add(lower); + VBox.setVgrow(lower, Priority.ALWAYS); + + Scene scene = new Scene(root, 760, 640); + ThemeManager.getInstance().style(scene); + setScene(scene); + setMinWidth(620); + setMinHeight(480); + if (owner != null) { + setOnShown(e -> centerOn(owner)); + } + setStatus(model[current()].getStatusString()); + LOGGER.info("TacletMatchCompletionDialogF opened: alternatives={} taclet={}", model.length, + model[0].taclet().name()); + } + + private void centerOn(Window owner) { + double x = owner.getX() + (owner.getWidth() - getWidth()) / 2; + double y = owner.getY() + (owner.getHeight() - getHeight()) / 2; + setX(Math.max(0, x)); + setY(Math.max(0, y)); + } + + /** + * the taclet display (Swing {@code classic.ApplyTacletDialog.createTacletDisplay}): the + * selected taclet printed as read-only text. Like the redesigned dialog, the printing hides + * term labels ({@link TmPrintF}); the Swing classic shows the mediator's visible term labels — + * the FX UI has no term-label visibility menu yet (M2c leftover), so the shared no-labels + * printing is used. + */ + private javafx.scene.Node createTacletDisplay() { + VBox panel = new VBox(2); + panel.getChildren() + .add(MatchInfoPanelF.sectionTitle( + "Selected Taclet - " + model[0].taclet().name())); + + Taclet taclet = model[0].taclet(); + LOGGER.debug("TacletApp: {}", taclet); + + TextArea text = new TextArea( + TmPrintF.taclet(services, notationInfo, taclet)); + text.setEditable(false); + text.setWrapText(false); + text.setFont(ExpandableTextF.mono()); + // Swing shows at most 11 lines; the rest scrolls + text.setPrefRowCount(11); + panel.getChildren().add(text); + return panel; + } + + /** + * the tabbed instantiation alternatives (Swing {@code createTacletPanel}): one table per + * alternative plus the if-selection where the taclet has an {@code \assumes} sequent. + */ + private javafx.scene.Node createInstantiationArea() { + VBox panel = new VBox(2); + panel.getChildren().add(MatchInfoPanelF.sectionTitle("Variable Instantiations")); + + alternatives = new TabPane(); + for (int i = 0; i < model.length; i++) { + VBox tabContent = new VBox(4); + tabContent.setPadding(new Insets(4)); + TableView table = createInstantiationDisplay(i); + tabContent.getChildren().add(table); + if (!model[i].application().taclet().assumesSequent().isEmpty()) { + TacletIfSelectionDialogF ifSelection = + new TacletIfSelectionDialogF(model[i], services, notationInfo); + ifSelections[i] = ifSelection; + tabContent.getChildren().add(ifSelection); + } + Tab t = new Tab("Alt " + i, tabContent); + t.setClosable(false); + alternatives.getTabs().add(t); + } + alternatives.getSelectionModel().selectedIndexProperty().addListener((obs, o, n) -> { + current = n.intValue(); + setStatus(model[current()].getStatusString()); + }); + VBox.setVgrow(alternatives, Priority.ALWAYS); + panel.getChildren().add(alternatives); + return panel; + } + + /** + * the instantiation table of one alternative (Swing {@code DataTable}): the model rows with + * the schema variable name and its instantiation; the rows after the match-determined ones are + * editable, and each committed edit is written into the {@link TacletFindModel} and refreshes + * the status (Swing {@code editingStopped} + {@code checkAfterEachInput}). + */ + private TableView createInstantiationDisplay(int i) { + TacletFindModel tm = model[i].tableModel(); + var items = FXCollections.observableArrayList(); + for (int r = 0; r < tm.getRowCount(); r++) { + Object name = tm.getValueAt(r, 0); + Object value = tm.getValueAt(r, 1); + items.add(new Row(String.valueOf(name), value == null ? "" : String.valueOf(value), + tm.isCellEditable(r, 1), r)); + } + + TableView table = new TableView<>(items); + table.setEditable(true); + table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY_FLEX_LAST_COLUMN); + table.setPlaceholder(new Label("No schema variables")); + + TableColumn varCol = new TableColumn<>("Variable"); + varCol.setCellValueFactory(c -> new javafx.beans.property.SimpleStringProperty( + c.getValue().getVarName())); + varCol.setPrefWidth(160); + + TableColumn instCol = new TableColumn<>("Instantiation"); + instCol.setCellValueFactory(c -> c.getValue().valueProperty()); + instCol.setCellFactory(col -> new TextFieldTableCell() { + { + setConverter(new javafx.util.StringConverter<>() { + @Override + public String toString(String object) { + return object == null ? "" : object; + } + + @Override + public String fromString(String string) { + return string; + } + }); + } + + @Override + public void startEdit() { + // refuse editing the match-determined rows (Swing: isCellEditable) + if (getTableRow() != null && getTableRow().getItem() instanceof Row row + && row.isEditable()) { + super.startEdit(); + } + } + + @Override + public void updateItem(String item, boolean empty) { + super.updateItem(item, empty); + if (empty || item == null) { + setText(null); + getStyleClass().remove("tacletmatch-muted"); + return; + } + setText(item); + boolean editableRow = getTableRow() != null + && getTableRow().getItem() instanceof Row row && row.isEditable(); + if (!editableRow && !getStyleClass().contains("tacletmatch-muted")) { + getStyleClass().add("tacletmatch-muted"); + } else if (editableRow) { + getStyleClass().remove("tacletmatch-muted"); + } + } + }); + instCol.setOnEditCommit(e -> { + Row row = e.getRowValue(); + row.setValue(e.getNewValue()); + model[i].tableModel().setValueAt(e.getNewValue(), row.getModelRow(), 1); + // Swing editingStopped: push the input and re-validate after each input + pushAllInputToModel(i); + setStatus(model[current()].getStatusString()); + }); + table.getColumns().setAll(varCol, instCol); + + // Swing adapts the table height to min(rowCount, 8) rows of 48px; here the table grows + // with its content and is bounded by the surrounding scroll/ VBox layout + table.setPrefHeight(Math.min(items.size(), 8) * 28 + 30); + table.setMinHeight(Region.USE_PREF_SIZE); + + dataTables[i] = table; + return table; + } + + /** + * the sequent program variables info (Swing {@code classic.ApplyTacletDialog.createInfoPanel}) + */ + private javafx.scene.Node createInfoPanel() { + VBox panel = new VBox(2); + panel.getChildren().add(MatchInfoPanelF.sectionTitle("Sequent program variables")); + var vars = model[0].programVariables(); + StringBuilder sb = new StringBuilder(); + boolean first = true; + for (IProgramVariable v : vars.elements()) { + if (!first) { + sb.append(", "); + } + first = false; + sb.append(v); + } + TextArea text = new TextArea(sb.length() == 0 ? "none" : sb.toString()); + text.setEditable(false); + text.setPrefRowCount(2); + text.setWrapText(true); + panel.getChildren().add(text); + return panel; + } + + /** the validation status area (Swing {@code classic.ApplyTacletDialog.createStatusPanel}) */ + private javafx.scene.Node createStatusPanel() { + VBox panel = new VBox(2); + panel.getChildren().add(MatchInfoPanelF.sectionTitle("Input validation result")); + statusArea = new TextArea(); + statusArea.setEditable(false); + statusArea.setWrapText(true); + statusArea.setPrefRowCount(2); + panel.getChildren().add(statusArea); + setStatus(model[current()].getStatusString()); + return panel; + } + + /** + * the current selected model (Swing {@code current()}): the selected tab's index. + */ + @Override + protected int current() { + return alternatives == null ? 0 : alternatives.getSelectionModel().getSelectedIndex(); + } + + @Override + protected void pushAllInputToModel() { + pushAllInputToModel(current()); + } + + /** pushes the if-selection and the table edits of alternative {@code i} into its model */ + private void pushAllInputToModel(int i) { + if (ifSelections[i] != null) { + ifSelections[i].pushAllInputToModel(); + } + // table edits are committed cell-by-cell; nothing further to push here + } + + @Override + protected void setStatus(String s) { + if (statusArea != null) { + statusArea.setText(s == null ? "" : s); + } + } + + @Override + protected void onApplyException(Exception exc) { + // Swing errorPositionKnown: select the input where the error occurred and place the caret + if (exc instanceof SVInstantiationExceptionWithPosition ex) { + focusErrorPosition(ex); + } + super.onApplyException(exc); + } + + /** + * Swing {@code ButtonListener.errorPositionKnown}: the offending instantiation input is + * selected and the caret placed at the reported column. As the Swing original documents, the + * caret part is best-effort; the FX table cell editor is only alive while it has focus, so + * only row selection and scroll-into-view are guaranteed. + */ + private void focusErrorPosition(SVInstantiationExceptionWithPosition ex) { + TableView table = dataTables[current()]; + int row = ex.getPosition().line() - 1; + if (ex.inIfSequent()) { + ifSelections[current()].requestFocusAt(row, ex.getPosition().column()); + return; + } + var items = table.getItems(); + if (row >= 0 && row < items.size()) { + table.getSelectionModel().select(row); + table.scrollTo(row); + table.layout(); + TextField editor = (TextField) table.lookup(".text-field"); + if (editor != null) { + try { + editor.positionCaret(Math.max(0, ex.getPosition().column() - 1)); + } catch (RuntimeException e) { + LOGGER.debug("tacletmatchcompletiondialogf: caret position calculation " + + "failed", e); + } + editor.requestFocus(); + } + } + } + + /** cancels the dialog (used by the self-test hook; identical to the Cancel button) */ + public void cancelAndClose() { + closeDialog(); + } + + /** one row of the instantiation table (Swing {@code DataTable} row) */ + private static final class Row { + private final String varName; + private final SimpleStringProperty value; + private final boolean editable; + private final int modelRow; + + Row(String varName, String value, boolean editable, int modelRow) { + this.varName = varName; + this.value = new SimpleStringProperty(value); + this.editable = editable; + this.modelRow = modelRow; + } + + String getVarName() { + return varName; + } + + SimpleStringProperty valueProperty() { + return value; + } + + String getValue() { + return value.get(); + } + + void setValue(String v) { + value.set(v); + } + + boolean isEditable() { + return editable; + } + + int getModelRow() { + return modelRow; + } + } +} From 912f4e70b14bd1aaaf686635cbec1697e2d36aa4 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Thu, 8 Oct 2026 22:15:38 +0200 Subject: [PATCH 066/281] key.ui.fx: joinmerge: merge rule completion, partner selection and predicate abstraction dialogs Port of the Swing gui/mergerule dialogs to JavaFX: MergeRuleCompletionF (implements the InteractiveRuleApplicationCompletionF seam copied verbatim from the seam branch), MergePartnerSelectionDialogF, MergeProcedureCompletionF, MergeRuleMenuItemF, PredicateAbstractionCompletionF, AbstractDomainElemChoiceF and AbstractionPredicatesChoiceDialogF (+ the help resource of the Swing info panel). The nested predicate-abstraction dialog degrades the JTextPane HTML info panel to plain text (no javafx.web in key.ui.fx). --- ...InteractiveRuleApplicationCompletionF.java | 42 ++ .../MergePartnerSelectionDialogF.java | 598 +++++++++++++++ .../mergerule/MergeProcedureCompletionF.java | 91 +++ .../fx/mergerule/MergeRuleCompletionF.java | 98 +++ .../gui/fx/mergerule/MergeRuleMenuItemF.java | 109 +++ .../AbstractDomainElemChoiceF.java | 69 ++ .../AbstractionPredicatesChoiceDialogF.java | 698 ++++++++++++++++++ .../PredicateAbstractionCompletionF.java | 93 +++ .../gui/help/abstrPredsMergeDialogInfo.html | 37 + 9 files changed, 1835 insertions(+) create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/InteractiveRuleApplicationCompletionF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/mergerule/MergePartnerSelectionDialogF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/mergerule/MergeProcedureCompletionF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/mergerule/MergeRuleCompletionF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/mergerule/MergeRuleMenuItemF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/mergerule/predicateabstraction/AbstractDomainElemChoiceF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/mergerule/predicateabstraction/AbstractionPredicatesChoiceDialogF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/mergerule/predicateabstraction/PredicateAbstractionCompletionF.java create mode 100644 key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/help/abstrPredsMergeDialogInfo.html 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/mergerule/MergePartnerSelectionDialogF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/mergerule/MergePartnerSelectionDialogF.java new file mode 100644 index 00000000000..0c07abc27bd --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/mergerule/MergePartnerSelectionDialogF.java @@ -0,0 +1,598 @@ +/* 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.mergerule; + +import java.util.Collections; +import java.util.Comparator; +import java.util.SortedSet; +import java.util.TreeSet; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Scene; +import javafx.scene.control.Button; +import javafx.scene.control.CheckBox; +import javafx.scene.control.ComboBox; +import javafx.scene.control.Label; +import javafx.scene.control.RadioButton; +import javafx.scene.control.ScrollPane; +import javafx.scene.control.TextField; +import javafx.scene.control.ToggleGroup; +import javafx.scene.control.Tooltip; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; +import javafx.scene.text.Font; +import javafx.scene.text.FontWeight; +import javafx.scene.text.Text; +import javafx.scene.text.TextFlow; +import javafx.stage.Modality; +import javafx.stage.Stage; +import javafx.stage.Window; + +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.pp.LogicPrinter; +import de.uka.ilkd.key.proof.Goal; +import de.uka.ilkd.key.proof.calculus.JavaDLSequentKit; +import de.uka.ilkd.key.rule.merge.MergePartner; +import de.uka.ilkd.key.rule.merge.MergeProcedure; +import de.uka.ilkd.key.rule.merge.MergeRule; +import de.uka.ilkd.key.rule.merge.MergeRuleBuiltInRuleApp; +import de.uka.ilkd.key.util.mergerule.MergeRuleUtils; + +import org.key_project.prover.sequent.PosInOccurrence; +import org.key_project.prover.sequent.Sequent; +import org.key_project.prover.sequent.SequentFormula; +import org.key_project.util.collection.ImmutableList; +import org.key_project.util.collection.Pair; + +import org.jspecify.annotations.Nullable; + +/** + * Dialog for selecting a subset of candidate goals as partners for a {@link MergeRule} + * application. + *

+ * Port of the Swing {@code de.uka.ilkd.key.gui.mergerule.MergePartnerSelectionDialog} (key.ui): + * the two monospaced sequent panes (with the focus subterm rendered in bold instead of the + * Swing {@code } HTML markup), the candidate combo box with the "select as merge partner" + * checkbox, the radio buttons per {@link MergeProcedure}, and the distinguishing-formula field + * with live parse validation (red style class instead of a red foreground) and the identical + * OK/Choose-All enablement logic ({@code checkApplicable}) and suitability check + * ({@code isSuitableDistFormula}). Cancel leaves the chosen goals null so that the completion + * returns null (rule silently skipped, as in the Swing UI). + *

+ * Note: fetching the chosen merge rule via {@link #getChosenMergeRule()} may open the nested + * {@link AbstractionPredicatesChoiceDialogF} when the predicate-abstraction procedure was + * chosen — the nested modal call happens after {@link #show()} returns, exactly as in Swing + * (where the dialog was evaluated after {@code setVisible(true)} returned on the EDT). + * + * @author Dominic Scheurer (original Swing dialog) + */ +public final class MergePartnerSelectionDialogF { + + /** The tooltip hint for the checkbox. */ + private static final String CB_SELECT_CANDIDATE_HINT = + "Select to add shown state as a merge partner."; + + /** The tooltip for the Choose-All button (Swing constant, order swapped there). */ + private static final String CHOOSE_ALL_BTN_TOOLTIP_TXT = + "Select all proposed goals as merge partners. " + + "Only enabled if the merge is applicable for all goals and the chosen merge procedure."; + /** The tooltip for the OK button */ + private static final String OK_BTN_TOOLTIP_TXT = "Select the chosen goals as merge partners. " + + "Only enabled if at least one goal is chosen and the merge is applicable for the " + + "chosen goals and merge procedure."; + + /** The initial size of this dialog. */ + private static final double INITIAL_WIDTH = 900; + private static final double INITIAL_HEIGHT = 450; + + /** + * The font for the sequent panes. Should resemble the standard font of KeY for proofs etc. + */ + private static final Font TXT_AREA_FONT = Font.font("Monospaced", FontWeight.NORMAL, 14); + private static final Font TXT_AREA_FONT_BOLD = Font.font("Monospaced", FontWeight.BOLD, 14); + + /** Comparator for goals; sorts by serial nr. of the node (Swing GOAL_COMPARATOR). */ + private static final Comparator GOAL_COMPARATOR = + Comparator.comparingInt(o -> o.getGoal().node().serialNr()); + + private final Stage stage = new Stage(); + + private final SortedSet candidates = new TreeSet<>(GOAL_COMPARATOR); + @Nullable + private Services services = null; + @Nullable + private Pair mergeGoalPio = null; + + /** The chosen goals. Null after cancel (Swing semantics: completion returns null). */ + @Nullable + private SortedSet chosenGoals = new TreeSet<>(GOAL_COMPARATOR); + + /** The chosen merge method. Head of {@link MergeProcedure#getMergeProcedures()}. */ + private MergeProcedure chosenRule = MergeProcedure.getMergeProcedures().head(); + + /** The chosen distinguishing formula */ + @Nullable + private JTerm chosenDistForm = null; + + private final TextFlow sequent1Flow = new TextFlow(); + private final TextFlow sequent2Flow = new TextFlow(); + private final ComboBox cmbCandidates = new ComboBox<>(); + private final CheckBox cbSelectCandidate = new CheckBox(); + private final TextField txtDistForm = new TextField(); + private final Button okButton = new Button("OK"); + private final Button chooseAllButton = new Button("Choose All"); + + /** + * Creates a new merge partner selection dialog. + * + * @param mergeGoal The first (already chosen) merge partner. + * @param pio Position of Update-Modality-Postcondition formula in the mergeNode. + * @param candidates Potential merge candidates. + * @param services The services object. + * @param owner the owner window for the modal dialog (may be null) + */ + public MergePartnerSelectionDialogF(Goal mergeGoal, + PosInOccurrence pio, + ImmutableList candidates, Services services, + @Nullable Window owner) { + this.services = services; + + stage.setTitle("Select partner node for merge operation"); + + this.mergeGoalPio = new Pair<>(mergeGoal, pio); + + // the Swing original inserts into a LinkedList with binary search — same result: + // candidates sorted by node serial number + for (MergePartner candidate : candidates) { + this.candidates.add(candidate); + } + + BorderPane root = createContent(); + + if (owner != null) { + stage.initOwner(owner); + } + stage.initModality(owner != null ? Modality.WINDOW_MODAL : Modality.APPLICATION_MODAL); + Scene scene = new Scene(root, INITIAL_WIDTH, INITIAL_HEIGHT); + ThemeManager.getInstance().style(scene); + stage.setScene(scene); + + setHighlightedSequentForArea(mergeGoal, pio, sequent1Flow); + loadCandidates(); + } + + private BorderPane createContent() { + // ---- upper container: state to merge | potential merge partners ---- + Label seq1Title = new Label("State to merge"); + seq1Title.getStyleClass().add("dialog-section-title"); + VBox mergeStateContainer = + new VBox(2, seq1Title, wrapScrollable(sequent1Flow, "join-sequent-view")); + + Label seq2Title = new Label("Potential merge partners"); + seq2Title.getStyleClass().add("dialog-section-title"); + + cbSelectCandidate.setTooltip(new Tooltip(CB_SELECT_CANDIDATE_HINT)); + cbSelectCandidate.setOnAction(e -> { + MergePartner selected = getSelectedCandidate(); + if (selected == null) { + return; + } + if (cbSelectCandidate.isSelected()) { + chosenGoals.add(selected); + } else { + chosenGoals.remove(selected); + } + checkApplicable(); + }); + + cmbCandidates.setMaxWidth(Double.MAX_VALUE); + cmbCandidates.valueProperty().addListener((obs, oldV, newV) -> { + MergePartner selectedCandidate = getSelectedCandidate(); + if (selectedCandidate == null) { + return; + } + setHighlightedSequentForArea(selectedCandidate.getGoal(), + selectedCandidate.getPio(), sequent2Flow); + cbSelectCandidate.setSelected(chosenGoals.contains(selectedCandidate)); + }); + + HBox selectionContainer = new HBox(6, cbSelectCandidate, cmbCandidates); + HBox.setHgrow(cmbCandidates, Priority.ALWAYS); + VBox partnerContainer = + new VBox(2, seq2Title, wrapScrollable(sequent2Flow, "join-sequent-view"), + selectionContainer); + VBox.setVgrow(partnerContainer.getChildren().get(1), Priority.ALWAYS); + + HBox upperContainer = new HBox(10, mergeStateContainer, partnerContainer); + HBox.setHgrow(mergeStateContainer, Priority.ALWAYS); + HBox.setHgrow(partnerContainer, Priority.ALWAYS); + upperContainer.setPadding(new Insets(8)); + + // ---- lower container: merge rules, dist formula, buttons ---- + ToggleGroup bgMergeMethods = new ToggleGroup(); + HBox mergeRulesContainer = new HBox(10); + mergeRulesContainer.setPadding(new Insets(6, 0, 6, 0)); + for (final MergeProcedure rule : MergeProcedure.getMergeProcedures()) { + RadioButton rb = new RadioButton(rule.toString()); + rb.setToggleGroup(bgMergeMethods); + rb.setSelected(rule == chosenRule); + rb.setOnAction(e -> { + chosenRule = rule; + checkApplicable(); + }); + mergeRulesContainer.getChildren().add(rb); + } + + txtDistForm.setPromptText( + "Distinguishing formula (leave empty for automatic generation!)"); + txtDistForm.textProperty().addListener((obs, oldV, newText) -> { + chosenDistForm = MergeRuleUtils.translateToFormula(services, newText); + + if (chosenDistForm == null || !isSuitableDistFormula()) { + txtDistForm.getStyleClass().add("join-input-error"); + } else { + txtDistForm.getStyleClass().remove("join-input-error"); + } + checkApplicable(); + }); + + okButton.setDefaultButton(true); + okButton.setTooltip(new Tooltip(OK_BTN_TOOLTIP_TXT)); + okButton.setOnAction(e -> stage.close()); + + chooseAllButton.setTooltip(new Tooltip(CHOOSE_ALL_BTN_TOOLTIP_TXT)); + chooseAllButton.setOnAction(e -> { + chosenGoals.addAll(candidates); + stage.close(); + }); + + Button cancelButton = new Button("Cancel"); + cancelButton.setCancelButton(true); + cancelButton.setOnAction(e -> { + chosenGoals = null; // Swing: cancel => getChosenCandidates yields nil + stage.close(); + }); + + HBox ctrlBtnsContainer = new HBox(20, okButton, chooseAllButton, cancelButton); + ctrlBtnsContainer.setAlignment(Pos.CENTER); + ctrlBtnsContainer.setPadding(new Insets(8, 0, 8, 0)); + + Label mergeRulesTitle = new Label("Concrete merge procedure to apply"); + mergeRulesTitle.getStyleClass().add("dialog-section-title"); + Label distFormTitle = new Label("Distinguishing formula"); + distFormTitle.getStyleClass().add("dialog-section-title"); + + VBox lowerContainer = new VBox(6, mergeRulesTitle, mergeRulesContainer, distFormTitle, + txtDistForm, ctrlBtnsContainer); + lowerContainer.setPadding(new Insets(4, 8, 8, 8)); + + BorderPane root = new BorderPane(); + root.setCenter(upperContainer); + root.setBottom(lowerContainer); + return root; + } + + private ScrollPane wrapScrollable(TextFlow flow, String styleClass) { + flow.getStyleClass().add(styleClass); + ScrollPane pane = new ScrollPane(flow); + pane.setFitToWidth(true); + pane.setHbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED); + VBox.setVgrow(pane, Priority.ALWAYS); + return pane; + } + + /** + * Shows the dialog modally and blocks until it is closed (Swing: {@code setVisible(true)} on + * the modal JDialog, called on the EDT). + */ + public void show() { + stage.showAndWait(); + } + + // joinmerge: test support for the key.fx.verify.joinmerge self test — non-blocking show, + // programmatic cancel, and control accessors with the same semantics as the buttons; the + // production path uses show(). + + /** Shows the dialog without blocking (self-test support; parity with {@link #show()}). */ + public void showNonBlocking() { + stage.show(); + } + + /** Confirms the dialog if the OK button is enabled (self-test support). */ + public void requestOk() { + if (!okButton.isDisabled()) { + stage.close(); + } + } + + /** Cancels the dialog (self-test support; parity with the cancel button). */ + public void requestCancel() { + chosenGoals = null; // Swing: cancel => getChosenCandidates yields nil + stage.close(); + } + + /** Exposes the dialog stage for tests. */ + public Stage getStageForVerification() { + return stage; + } + + /** Exposes the candidate combo box for tests. */ + public ComboBox getCmbCandidates() { + return cmbCandidates; + } + + /** Exposes the partner selection checkbox for tests. */ + public CheckBox getCbSelectCandidate() { + return cbSelectCandidate; + } + + /** Exposes the distinguishing-formula field for tests. */ + public TextField getTxtDistForm() { + return txtDistForm; + } + + /** Exposes the OK button for tests. */ + public Button getOkButton() { + return okButton; + } + + /** Exposes the Choose-All button for tests. */ + public Button getChooseAllButton() { + return chooseAllButton; + } + + /** Exposes the "State to merge" sequent flow for tests. */ + public TextFlow getSequent1Flow() { + return sequent1Flow; + } + + /** Exposes the partner sequent flow for tests. */ + public TextFlow getSequent2Flow() { + return sequent2Flow; + } + + /** + * @return All chosen merge partners (empty iff cancelled — Swing + * {@code getChosenCandidates}). + */ + public ImmutableList getChosenCandidates() { + ImmutableList result = ImmutableList.nil(); + if (chosenGoals != null) { + return result.append(chosenGoals); + } else { + return result; + } + } + + /** + * @param the concrete merge procedure type + * @return The chosen merge rule. May open the nested procedure completion dialog (see class + * javadoc). + */ + @SuppressWarnings("unchecked") + public T getChosenMergeRule() { + MergeProcedureCompletionF completion = + (MergeProcedureCompletionF) MergeProcedureCompletionF + .getCompletionForClass(chosenRule.getClass()); + + return completion.complete((T) chosenRule, mergeGoalPio, + chosenGoals == null ? Collections.emptyList() : chosenGoals); + } + + /** + * @return The chosen distinguishing formula. If null, an automatic generation of the + * distinguishing formula should be performed. + */ + public @Nullable JTerm getChosenDistinguishingFormula() { + return isSuitableDistFormula() ? chosenDistForm : null; + } + + /** + * Checks whether the merge rule is applicable for the given set of candidates. + * + * @param theCandidates Candidates to instantiate the merge rule application with. + * @return true iff the merge rule instance induced by the given set of candidates is + * applicable. + */ + private boolean isApplicableForCandidates(ImmutableList theCandidates) { + if (mergeGoalPio != null && services != null && chosenRule != null) { + MergeRuleBuiltInRuleApp mergeRuleApp = (MergeRuleBuiltInRuleApp) MergeRule.INSTANCE + .createApp(mergeGoalPio.second, services); + + mergeRuleApp.setMergeNode(mergeGoalPio.first.node()); + mergeRuleApp.setConcreteRule(chosenRule); + mergeRuleApp.setMergePartners(theCandidates); + + return mergeRuleApp.complete(); + } else { + return false; + } + } + + /** + * Enables / disables the OK and Choose-all button depending on whether or not the currently + * chosen merge rule instance is applicable (Swing {@code checkApplicable}). + */ + private void checkApplicable() { + okButton.setDisable( + chosenGoals.isEmpty() + || !isApplicableForCandidates(immutableListFromIterable(chosenGoals))); + + chooseAllButton.setDisable( + candidates.isEmpty() + || !isApplicableForCandidates(immutableListFromIterable(candidates))); + + txtDistForm.setDisable(!(candidates.size() == 1 || chosenGoals.size() == 1)); + if (txtDistForm.isDisabled()) { + chosenDistForm = null; + } + } + + /** + * Checks whether the selected distinguishing formula is actually suitable for this purpose + * (Swing {@code isSuitableDistFormula}). + * + * @return true iff the chosen "distinguishing formula" is a distinguishing formula. + */ + private boolean isSuitableDistFormula() { + if (chosenDistForm == null) { + return false; + } + + // The formula should be provable for the first state + // whilst its complement should be provable for the second state. + + final var tb = services.getTermBuilder(); + + final Goal partnerGoal = candidates.size() == 1 ? candidates.getFirst().getGoal() + : (chosenGoals.size() == 1 ? chosenGoals.first().getGoal() : null); + + if (partnerGoal == null) { + return false; + } + + return checkProvability(mergeGoalPio.first.sequent(), chosenDistForm, services) + && checkProvability(partnerGoal.sequent(), tb.not(chosenDistForm), services); + } + + /** + * Checks whether the given formula can be proven within the given sequent (Swing + * {@code checkProvability}, unchanged). + * + * @param seq Sequent in which to check the provability of formulaToProve. + * @param formulaToProve Formula to prove. + * @return True iff formulaToProve can be proven within the given sequent. + */ + private static boolean checkProvability(Sequent seq, JTerm formulaToProve, Services services) { + final var tb = services.getTermBuilder(); + + Sequent toProve = JavaDLSequentKit.createSequent(seq.antecedent().asList(), + ImmutableList.singleton(new SequentFormula(formulaToProve))); + + for (SequentFormula succedentFormula : seq.succedent()) { + final JTerm formula = (JTerm) succedentFormula.formula(); + if (!formula.containsJavaBlockRecursive()) { + toProve = + toProve.addFormula(new SequentFormula(tb.not(formula)), true, true).sequent(); + } + } + + return MergeRuleUtils.isProvable(toProve, services, 1000); + } + + /** + * @param it Iterable to convert into an ImmutableList (Swing + * {@code immutableListFromIterabe}). + * @return An ImmutableList consisting of the elements in it. + */ + private static ImmutableList immutableListFromIterable(Iterable it) { + ImmutableList result = ImmutableList.nil(); + for (T t : it) { + result = result.prepend(t); + } + return result; + } + + /** + * @return The candidate chosen at the moment (by the combo box). + */ + private @Nullable MergePartner getSelectedCandidate() { + return getNthCandidate(cmbCandidates.getSelectionModel().getSelectedIndex()); + } + + /** + * Returns the n-th candidate in the list (Swing {@code getNthCandidate}). + * + * @param n Index of the merge candidate. + * @return The n-th candidate in the list, or null. + */ + private @Nullable MergePartner getNthCandidate(int n) { + int i = 0; + for (MergePartner elem : candidates) { + if (i == n) { + return elem; + } + i++; + } + return null; + } + + /** + * Loads the merge candidates into the combo box, initializes the partner editor pane with + * the text of the first candidate and computes the initial button enablement (Swing + * {@code loadCandidates}). + */ + private void loadCandidates() { + if (candidates.isEmpty()) { + checkApplicable(); + return; + } + + for (MergePartner candidate : candidates) { + cmbCandidates.getItems().add("Node " + candidate.getGoal().node().serialNr()); + } + cmbCandidates.getSelectionModel().selectFirst(); + + checkApplicable(); + } + + /** + * Renders the sequent of the given goal into the given {@link TextFlow}, with the portion + * that corresponds to the given position rendered in bold (Swing + * {@code setHighlightedSequentForArea}, which built {@code } HTML markup around the + * matched subterm). + *

+ * Deviation: the Swing original's {@code before} slice dropped one character before the + * match ({@code substring(0, m.start() - 1)}, an HTML-escaping artifact); the port renders + * the full matched range. + * + * @param goal Goal to render. + * @param pio Position indicating subterm to highlight. + * @param flow The flow to render the highlighted goal into. + */ + private static void setHighlightedSequentForArea(Goal goal, + PosInOccurrence pio, TextFlow flow) { + Services svcs = goal.proof().getServices(); + String subterm = LogicPrinter.quickPrintTerm((JTerm) pio.subTerm(), svcs); + + // Render subterm to highlight as a regular expression (as in the Swing original). + subterm = subterm.replaceAll("\\s", "\\\\s"); + subterm = subterm.replaceAll("(\\\\s)+", "\\\\E\\\\s*\\\\Q"); + subterm = "\\Q" + subterm + "\\E"; + if (subterm.endsWith("\\Q\\E")) { + subterm = subterm.substring(0, subterm.length() - 4); + } + + // Find a match in the printed sequent + String sequent = LogicPrinter.quickPrintSequent(goal.sequent(), svcs); + Pattern p = Pattern.compile(subterm); + Matcher m = p.matcher(sequent); + + int matchStart = -1; + int matchEnd = -1; + if (m.find()) { + matchStart = m.start(); + matchEnd = m.end(); + } + + Text before = new Text(matchStart >= 0 ? sequent.substring(0, matchStart) : sequent); + before.setFont(TXT_AREA_FONT); + flow.getChildren().setAll(before); + if (matchStart >= 0) { + Text main = new Text(sequent.substring(matchStart, matchEnd)); + main.setFont(TXT_AREA_FONT_BOLD); + Text after = new Text(sequent.substring(matchEnd)); + after.setFont(TXT_AREA_FONT); + flow.getChildren().addAll(main, after); + } + } + +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/mergerule/MergeProcedureCompletionF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/mergerule/MergeProcedureCompletionF.java new file mode 100644 index 00000000000..6a1fe9f0224 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/mergerule/MergeProcedureCompletionF.java @@ -0,0 +1,91 @@ +/* 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.mergerule; + +import java.util.Collection; +import java.util.function.Function; + +import de.uka.ilkd.key.gui.fx.mergerule.predicateabstraction.PredicateAbstractionCompletionF; +import de.uka.ilkd.key.proof.Goal; +import de.uka.ilkd.key.rule.merge.MergePartner; +import de.uka.ilkd.key.rule.merge.MergeProcedure; +import de.uka.ilkd.key.rule.merge.procedures.MergeWithPredicateAbstraction; +import de.uka.ilkd.key.rule.merge.procedures.MergeWithPredicateAbstractionFactory; + +import org.key_project.prover.sequent.PosInOccurrence; +import org.key_project.util.collection.Pair; + +/** + * A completion class for merge procedures. Certain procedures, such as + * {@link MergeWithPredicateAbstraction}, may not be complete initially and need additional input. + *

+ * Port of the Swing {@code de.uka.ilkd.key.gui.mergerule.MergeProcedureCompletion} (key.ui, pure + * logic, unchanged). + * + * @author Dominic Scheurer (original Swing class) + */ +public abstract class MergeProcedureCompletionF { + + /** + * @return The default completion (identity mapping). + */ + public static MergeProcedureCompletionF defaultCompletion() { + return create(proc -> proc); + } + + /** + * Default constructor is hidden. Use {@link #create(Function)} instead. + */ + protected MergeProcedureCompletionF() { + } + + /** + * Creates a completion that applies the given function to the procedure. + * + * @param completion the function to apply + * @param the concrete procedure type + * @return a completion applying the given function + */ + public static MergeProcedureCompletionF create( + final Function completion) { + return new MergeProcedureCompletionF<>() { + @Override + public T complete( + T proc, Pair mergeGoalPio, + Collection partners) { + return completion.apply(proc); + } + }; + } + + /** + * Completes the given merge procedure either automatically (if the procedure is already + * complete) or by demanding input from the user in a GUI. + * + * @param proc {@link MergeProcedure} to complete. + * @param mergeGoalPio The {@link Goal} and {@link PosInOccurrence} identifying the merge goal. + * @param partners The {@link MergePartner}s chosen. + * @return The completed {@link MergeProcedure}. + */ + public abstract C complete(final C proc, + final Pair mergeGoalPio, + final Collection partners); + + /** + * Returns the completion for the given merge procedure class. + * + * @param cls the procedure class + * @return The requested completion. + */ + public static MergeProcedureCompletionF getCompletionForClass( + Class cls) { + if (cls.equals(MergeWithPredicateAbstractionFactory.class)) { + return new PredicateAbstractionCompletionF(); + } else { + return defaultCompletion(); + } + } + +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/mergerule/MergeRuleCompletionF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/mergerule/MergeRuleCompletionF.java new file mode 100644 index 00000000000..f3104917053 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/mergerule/MergeRuleCompletionF.java @@ -0,0 +1,98 @@ +/* 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.mergerule; + +import de.uka.ilkd.key.gui.fx.InteractiveRuleApplicationCompletionF; +import de.uka.ilkd.key.logic.JTerm; +import de.uka.ilkd.key.proof.Goal; +import de.uka.ilkd.key.rule.IBuiltInRuleApp; +import de.uka.ilkd.key.rule.merge.MergePartner; +import de.uka.ilkd.key.rule.merge.MergeProcedure; +import de.uka.ilkd.key.rule.merge.MergeRule; +import de.uka.ilkd.key.rule.merge.MergeRuleBuiltInRuleApp; +import de.uka.ilkd.key.rule.merge.procedures.MergeByIfThenElse; + +import org.key_project.prover.sequent.PosInOccurrence; +import org.key_project.util.collection.ImmutableList; + +/** + * This class completes the instantiation for a merge rule application. The user is queried for + * partner goals and concrete merge rule to choose. If in forced mode, all potential partners and + * the if-then-else merge method are chosen (no query is shown to the user). + *

+ * Port of the Swing {@code de.uka.ilkd.key.gui.mergerule.MergeRuleCompletion} (key.ui, logic + * unchanged, only the dialog call replaced by {@link MergePartnerSelectionDialogF}). It + * implements the {@link InteractiveRuleApplicationCompletionF} seam and is ready for + * registration at the future {@code WindowUserInterfaceControlF} — see the {@code joinmerge} + * TODO comment in {@code MainWindowF} for the exact registration snippet. + * + * @author Dominic Scheurer (original Swing class) + */ +public class MergeRuleCompletionF implements InteractiveRuleApplicationCompletionF { + + /** Singleton instance (Swing: {@code MergeRuleCompletion.INSTANCE}). */ + public static final MergeRuleCompletionF INSTANCE = new MergeRuleCompletionF(); + + private static final MergeProcedure STD_CONCRETE_MERGE_RULE = MergeByIfThenElse.instance(); + + private MergeRuleCompletionF() { + } + + @Override + public IBuiltInRuleApp complete(final IBuiltInRuleApp app, final Goal goal, boolean forced) { + + final MergeRuleBuiltInRuleApp mergeApp = (MergeRuleBuiltInRuleApp) app; + final PosInOccurrence pio = mergeApp.posInOccurrence(); + + final ImmutableList candidates = + MergeRule.findPotentialMergePartners(goal, pio); + + ImmutableList chosenCandidates = null; + final MergeProcedure chosenRule; + JTerm chosenDistForm = null; // null is admissible standard ==> auto + // generation + + if (forced) { + chosenCandidates = candidates; + chosenRule = STD_CONCRETE_MERGE_RULE; + } else { + final MergePartnerSelectionDialogF dialog = new MergePartnerSelectionDialogF(goal, pio, + candidates, goal.proof().getServices(), null); + dialog.show(); + + chosenCandidates = dialog.getChosenCandidates(); + chosenRule = dialog.getChosenMergeRule(); + chosenDistForm = dialog.getChosenDistinguishingFormula(); + } + + if (chosenCandidates == null || chosenCandidates.isEmpty()) { + return null; + } + + final MergeRuleBuiltInRuleApp result = + new MergeRuleBuiltInRuleApp((MergeRule) app.rule(), pio); + result.setMergePartners(chosenCandidates); + result.setConcreteRule(chosenRule); + result.setDistinguishingFormula(chosenDistForm); + result.setMergeNode(goal.node()); + + return result; + } + + @Override + public boolean canComplete(IBuiltInRuleApp app) { + return checkCanComplete(app); + } + + /** + * @param app the rule app + * @return true iff this completion can handle merge rule apps (Swing + * {@code MergeRuleCompletion.checkCanComplete}) + */ + public static boolean checkCanComplete(IBuiltInRuleApp app) { + return app instanceof MergeRuleBuiltInRuleApp; + } + +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/mergerule/MergeRuleMenuItemF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/mergerule/MergeRuleMenuItemF.java new file mode 100644 index 00000000000..5691db6d8b2 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/mergerule/MergeRuleMenuItemF.java @@ -0,0 +1,109 @@ +/* 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.mergerule; + +import javafx.application.Platform; +import javafx.scene.control.MenuItem; + +import de.uka.ilkd.key.control.ProofControl; +import de.uka.ilkd.key.gui.fx.notification.NotificationManagerF; +import de.uka.ilkd.key.gui.fx.notification.NotificationManagerF.Kind; +import de.uka.ilkd.key.proof.Goal; +import de.uka.ilkd.key.rule.merge.MergeRule; +import de.uka.ilkd.key.rule.merge.MergeRuleBuiltInRuleApp; + +import org.key_project.prover.sequent.PosInOccurrence; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The menu item for the state merging rule ("State Merging Rule"). + *

+ * Port of the Swing {@code de.uka.ilkd.key.gui.mergerule.MergeRuleMenuItem} (key.ui): builds the + * merge rule application for the given position, runs {@link MergeRuleCompletionF} (which shows + * {@link MergePartnerSelectionDialogF}), and — if the completed application is applicable — + * applies it interactively on a worker thread. Deviations: the Swing item used + * {@code mediator.stopInterface/startInterface} and the {@code taskStarted/taskProgress/ + * taskFinished} plumbing of {@code mediator.getUI()}; the FX UI does not yet provide those + * hooks (progress dialogs are a planned milestone), so errors are reported via the FX + * notification layer ({@link NotificationManagerF}, Swing: + * {@code mediator.notify(new ExceptionFailureEvent(...))}) and start/stop interface is left to + * the caller. + *

+ * Registration note: this item belongs into the future sequent-view context menu (Swing: + * {@code CurrentGoalViewMenu.createMergeRuleMenu}); see the {@code joinmerge} TODO in + * {@code MainWindowF}. + * + * @author Dominic Scheurer (original Swing menu item) + * @see MergeRule + */ +public class MergeRuleMenuItemF extends MenuItem { + + private static final Logger LOGGER = LoggerFactory.getLogger(MergeRuleMenuItemF.class); + + /** + * Creates a new menu item for the merge rule. + * + * @param goal The selected goal. + * @param pio The position the merge shall be applied to (symbolic state / program counter + * formula). + * @param proofControl the proof control used to apply the completed application + * interactively (Swing: {@code mediator.getUI().getProofControl()}) + */ + public MergeRuleMenuItemF(final Goal goal, final PosInOccurrence pio, + final ProofControl proofControl) { + final var services = goal.proof().getServices(); + + setText(toString()); + + setOnAction(e -> { + final MergeRule mergeRule = MergeRule.INSTANCE; + final MergeRuleBuiltInRuleApp app = + (MergeRuleBuiltInRuleApp) mergeRule.createApp(pio, services); + final MergeRuleCompletionF completion = MergeRuleCompletionF.INSTANCE; + final MergeRuleBuiltInRuleApp completedApp = + (MergeRuleBuiltInRuleApp) completion.complete(app, goal, false); + + // The completedApp may be null if the completion was not + // possible (e.g., if no candidates were selected by the + // user in the displayed dialog). + if (completedApp != null && completedApp.complete()) { + LOGGER.info("Merging {} nodes", completedApp.getMergePartners().size()); + completedApp.registerProgressListener( + progress -> LOGGER.debug("Merge progress: {}", progress)); + // Swing ran applyInteractive inside a SwingWorker; the FX analogue is a worker + // thread with the result marshalled back via Platform.runLater + Thread worker = new Thread(() -> { + try { + proofControl.applyInteractive(completedApp, goal); + Platform.runLater(() -> { + completedApp.clearProgressListeners(); + LOGGER.info("Merging of {} nodes finished", + completedApp.getMergePartners().size()); + NotificationManagerF.getInstance() + .notify("Merging finished", Kind.INFO); + }); + } catch (final Exception | AssertionError exc) { + signalError(exc); + } + }, "fx-merge-rule-applier"); + worker.setDaemon(true); + worker.start(); + } + }); + } + + private void signalError(final Throwable e) { + LOGGER.error("Merge rule application failed", e); + Platform.runLater(() -> NotificationManagerF.getInstance() + .notify("Merge rule application failed: " + e.getMessage(), Kind.ERROR)); + } + + @Override + public String toString() { + return "State Merging Rule"; + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/mergerule/predicateabstraction/AbstractDomainElemChoiceF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/mergerule/predicateabstraction/AbstractDomainElemChoiceF.java new file mode 100644 index 00000000000..b5480461035 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/mergerule/predicateabstraction/AbstractDomainElemChoiceF.java @@ -0,0 +1,69 @@ +/* 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.mergerule.predicateabstraction; + +import java.util.Optional; + +import de.uka.ilkd.key.axiom_abstraction.predicateabstraction.AbstractPredicateAbstractionDomainElement; +import de.uka.ilkd.key.logic.op.ProgramVariable; + +/** + * Model class for the manual choice of an abstract domain element by the user. + *

+ * Port of the Swing + * {@code de.uka.ilkd.key.gui.mergerule.predicateabstraction.AbstractDomainElemChoice} + * (key.ui, pure model class, unchanged logic; F-suffix naming per module convention). + * + * @author Dominic Steinhoefel (original Swing class) + */ +public class AbstractDomainElemChoiceF { + /** + * The program variable for which an abstract domain element has been chosen. + */ + private ProgramVariable progVar; + + /** + * The chosen abstract domain element. May be null if no choice has been done for the program + * variable {@link #progVar}. + */ + private Optional abstrDomElem; + + public AbstractDomainElemChoiceF(ProgramVariable progVar, + Optional abstrDomElem) { + this.progVar = progVar; + this.abstrDomElem = abstrDomElem; + } + + public ProgramVariable getProgVar() { + return progVar; + } + + public void setProgVar(ProgramVariable progVar) { + this.progVar = progVar; + } + + public Optional getAbstrDomElem() { + return abstrDomElem; + } + + public void setAbstrDomElem(AbstractPredicateAbstractionDomainElement abstrDomElem) { + this.abstrDomElem = Optional.of(abstrDomElem); + } + + public void setAbstrDomElem(Optional abstrDomElem) { + this.abstrDomElem = abstrDomElem; + } + + /** + * @return True iff a choice has been made, that is the chosen element is not Empty. + */ + public boolean isChoiceMade() { + return abstrDomElem.isPresent(); + } + + public String choiceToString() { + return abstrDomElem.isPresent() ? abstrDomElem.toString() : "(no choice)"; + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/mergerule/predicateabstraction/AbstractionPredicatesChoiceDialogF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/mergerule/predicateabstraction/AbstractionPredicatesChoiceDialogF.java new file mode 100644 index 00000000000..3970b652465 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/mergerule/predicateabstraction/AbstractionPredicatesChoiceDialogF.java @@ -0,0 +1,698 @@ +/* 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.mergerule.predicateabstraction; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Optional; +import java.util.Scanner; +import javafx.collections.FXCollections; +import javafx.collections.ListChangeListener; +import javafx.collections.ObservableList; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Scene; +import javafx.scene.control.Button; +import javafx.scene.control.ComboBox; +import javafx.scene.control.Label; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import javafx.scene.control.RadioButton; +import javafx.scene.control.SplitPane; +import javafx.scene.control.Tab; +import javafx.scene.control.TabPane; +import javafx.scene.control.TableCell; +import javafx.scene.control.TableColumn; +import javafx.scene.control.TableView; +import javafx.scene.control.TextArea; +import javafx.scene.control.TextField; +import javafx.scene.control.ToggleGroup; +import javafx.scene.input.KeyCode; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.HBox; +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.axiom_abstraction.AbstractDomainElement; +import de.uka.ilkd.key.axiom_abstraction.AbstractDomainLattice; +import de.uka.ilkd.key.axiom_abstraction.predicateabstraction.AbstractPredicateAbstractionDomainElement; +import de.uka.ilkd.key.axiom_abstraction.predicateabstraction.AbstractPredicateAbstractionLattice; +import de.uka.ilkd.key.axiom_abstraction.predicateabstraction.AbstractionPredicate; +import de.uka.ilkd.key.axiom_abstraction.predicateabstraction.ConjunctivePredicateAbstractionLattice; +import de.uka.ilkd.key.axiom_abstraction.predicateabstraction.DisjunctivePredicateAbstractionLattice; +import de.uka.ilkd.key.axiom_abstraction.predicateabstraction.SimplePredicateAbstractionLattice; +import de.uka.ilkd.key.gui.fx.theme.ThemeManager; +import de.uka.ilkd.key.java.Services; +import de.uka.ilkd.key.logic.NamespaceSet; +import de.uka.ilkd.key.logic.ProgramElementName; +import de.uka.ilkd.key.logic.op.IProgramVariable; +import de.uka.ilkd.key.logic.op.LocationVariable; +import de.uka.ilkd.key.logic.op.ProgramVariable; +import de.uka.ilkd.key.parser.ParserException; +import de.uka.ilkd.key.proof.Goal; +import de.uka.ilkd.key.proof.io.OutputStreamProofSaver; +import de.uka.ilkd.key.rule.merge.procedures.MergeWithPredicateAbstraction; +import de.uka.ilkd.key.util.mergerule.MergeRuleUtils; + +import org.key_project.logic.Name; +import org.key_project.logic.Namespace; +import org.key_project.logic.sort.Sort; +import org.key_project.util.collection.Pair; + +import org.jspecify.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Dialog to choose abstraction predicates for merges with predicate abstraction. + *

+ * Port of the Swing + * {@code de.uka.ilkd.key.gui.mergerule.predicateabstraction.AbstractionPredicatesChoiceDialog} + * (key.ui): the four step tabs (lattice type, placeholder variables, abstraction predicates, + * manual domain element choices) and the problems pane behave as in the original. Deviations + * (documented, no javafx.web bundled): the information panel and the problems pane render the + * original HTML resources as plain text instead of HTML; the {@code ObservableArrayList} helper + * of the original is replaced by the native JavaFX {@link ObservableList}. + * + * @author Dominic Steinhoefel (original Swing dialog) + */ +public final class AbstractionPredicatesChoiceDialogF { + + private static final String AVAILABLE_PROGRAM_VARIABLES_DESCR = "Available Program Variables: "; + + /** The initial size of this dialog. */ + private static final double INITIAL_WIDTH = 850; + private static final double INITIAL_HEIGHT = 600; + + private static final String DIALOG_TITLE = "Choose abstraction predicates for merge"; + + private static final Logger LOGGER = + LoggerFactory.getLogger(AbstractionPredicatesChoiceDialogF.class); + + private final Stage stage = new Stage(); + + @Nullable + private Goal goal = null; + + private ArrayList> registeredPlaceholders = new ArrayList<>(); + private ArrayList registeredPredicates = new ArrayList<>(); + private final ArrayList abstrPredicateChoices = new ArrayList<>(); + + private Class latticeType = + SimplePredicateAbstractionLattice.class; + + /** Problems for the placeholder variables input (replaces the Swing ObservableArrayList). */ + private final ObservableList placeholdersProblems = FXCollections.observableArrayList(); + /** Problems for the abstraction predicates input. */ + private final ObservableList abstrPredProblems = FXCollections.observableArrayList(); + + private final ListView placeholdersList = new ListView<>(); + private final ListView abstrPredsList = new ListView<>(); + private final ListView problemsList = new ListView<>(); + private final TableView choiceTable = + new TableView<>(FXCollections.observableArrayList(abstrPredicateChoices)); + private final Services services; + private boolean cancelled = false; + + /** + * Constructs a new {@link AbstractionPredicatesChoiceDialogF}. The given goal is used to get + * information about the proof. + * + * @param goal The goal on which the merge rule is applied. + * @param differingLocVars Location variables the values of which differ in the merge partner + * states. + * @param owner the owner window for the modal dialog (may be null) + */ + public AbstractionPredicatesChoiceDialogF(Goal goal, List differingLocVars, + @Nullable Window owner) { + this(goal.proof().getServices(), owner); + this.goal = goal; + differingLocVars + .forEach(v -> abstrPredicateChoices.add(new AbstractDomainElemChoiceF(v, + Optional.empty()))); + choiceTable.getItems().setAll(abstrPredicateChoices); + } + + /** + * Constructs a new dialog without a goal (test mode, as in the Swing original's no-arg + * constructor). + * + * @param services the services (may be null in test mode) + * @param owner the owner window for the modal dialog (may be null) + */ + public AbstractionPredicatesChoiceDialogF(@Nullable Services services, + @Nullable Window owner) { + this.services = services; + + stage.setTitle(DIALOG_TITLE); + stage.setMinWidth(650); + stage.setMinHeight(450); + + final BorderPane root = new BorderPane(); + root.setTop(createInfoPanel()); + root.setCenter(createCenter()); + root.setBottom(createControlsPanel()); + + if (owner != null) { + stage.initOwner(owner); + } + stage.initModality(owner != null ? Modality.WINDOW_MODAL : Modality.APPLICATION_MODAL); + Scene scene = new Scene(root, INITIAL_WIDTH, INITIAL_HEIGHT); + ThemeManager.getInstance().style(scene); + stage.setScene(scene); + + // the Swing original listens on both problem lists and rebuilds the problems pane + ListChangeListener listener = change -> refreshProblems(); + placeholdersProblems.addListener(listener); + abstrPredProblems.addListener(listener); + } + + private VBox createInfoPanel() { + final Label heading = new Label("Information on Merges with Predicate Abstraction"); + heading.getStyleClass().add("dialog-section-title"); + + // the Swing original renders the HTML resource in a JTextPane; without javafx.web we + // degrade to the plain text content (strip tags) + String html = + readFromResourceFile("/de/uka/ilkd/key/gui/help/abstrPredsMergeDialogInfo.html"); + TextArea info = new TextArea(html == null ? "" : stripHtml(html)); + info.setEditable(false); + info.setWrapText(true); + + VBox box = new VBox(2, heading, info); + box.setPadding(new Insets(4, 8, 4, 8)); + VBox.setVgrow(info, Priority.ALWAYS); + return box; + } + + private SplitPane createCenter() { + final TabPane stepsTabbedPane = new TabPane(); + stepsTabbedPane.getTabs().addAll( + new Tab("(1) Lattice Type", createLatticeTypePanel()), + new Tab("(2) Placeholder Variables", createPlaceholderVariablesPanel()), + new Tab("(3) Abstraction Predicates", createAbstractionPredicatesPanel()), + new Tab("(4) Choice of Abstraction Predicates [opt]", createChoiceAbstrPredsPanel())); + + final Label problemsHeading = new Label("Problems"); + problemsHeading.getStyleClass().add("dialog-section-title"); + VBox problemsPane = new VBox(2, problemsHeading, problemsList); + problemsPane.setPadding(new Insets(4, 8, 4, 8)); + VBox.setVgrow(problemsList, Priority.ALWAYS); + + SplitPane split = new SplitPane(stepsTabbedPane, problemsPane); + SplitPane.setResizableWithParent(stepsTabbedPane, Boolean.TRUE); + split.setDividerPositions(0.7); + return split; + } + + private VBox createLatticeTypePanel() { + final ToggleGroup group = new ToggleGroup(); + RadioButton simple = new RadioButton("Simple Predicates Lattice"); + simple.setToggleGroup(group); + simple.setSelected(true); + simple.setOnAction(e -> latticeType = SimplePredicateAbstractionLattice.class); + RadioButton conj = new RadioButton("Conjunctive Predicates Lattice"); + conj.setToggleGroup(group); + conj.setOnAction(e -> latticeType = ConjunctivePredicateAbstractionLattice.class); + RadioButton disj = new RadioButton("Disjunctive Predicates Lattice"); + disj.setToggleGroup(group); + disj.setOnAction(e -> latticeType = DisjunctivePredicateAbstractionLattice.class); + + VBox box = new VBox(4, simple, conj, disj); + box.setPadding(new Insets(8)); + return box; + } + + private BorderPane createPlaceholderVariablesPanel() { + final TextField input = new TextField(); + input.setPromptText("Enter a new placeholder variable (e.g., \"int _ph1\")"); + + input.setOnAction(e -> { + final String currInput = input.getText().strip(); + if (currInput.isEmpty()) { + return; + } + try { + final Pair parsed = parsePlaceholder(currInput); + placeholdersProblems.clear(); + placeholdersList.getItems().add(currInput); + input.setText(""); + registeredPlaceholders.add(parsed); + if (goal != null) { + final Namespace pvs = + goal.proof().getServices().getNamespaces().programVariables(); + pvs.add(new LocationVariable( + new ProgramElementName(parsed.second.toString()), parsed.first)); + } + } catch (Exception ex) { + placeholdersProblems.clear(); + placeholdersProblems.add(ex.getMessage()); + LOGGER.error("Exception!", ex); + } + }); + + // Swing: DEL on a selected list entry removes the placeholder (and unbinds it) + placeholdersList.setOnKeyPressed(e -> { + int selectedIndex = placeholdersList.getSelectionModel().getSelectedIndex(); + if (e.getCode() == KeyCode.DELETE && !placeholdersList.getItems().isEmpty() + && selectedIndex >= 0) { + String removedInput = placeholdersList.getItems().remove(selectedIndex); + if (registeredPlaceholders.size() > selectedIndex && goal != null) { + final Pair removedPlaceholder = + registeredPlaceholders.remove(selectedIndex); + final Namespace pvs = + goal.proof().getServices().getNamespaces().programVariables(); + pvs.remove(removedPlaceholder.second); + } + LOGGER.debug("Removed placeholder {}", removedInput); + } + }); + + // keep the input listener in sync with the Swing original: live validation on typing + input.textProperty().addListener((obs, oldV, currInput) -> { + if (currInput.isEmpty()) { + placeholdersProblems.clear(); + return; + } + try { + parsePlaceholder(currInput.strip()); + placeholdersProblems.clear(); + } catch (Exception ex) { + placeholdersProblems.clear(); + placeholdersProblems.add(ex.getMessage()); + } + }); + + BorderPane pane = new BorderPane(placeholdersList, input, null, null, null); + BorderPane.setMargin(input, new Insets(4)); + VBox.setVgrow(placeholdersList, Priority.ALWAYS); + return pane; + } + + private BorderPane createAbstractionPredicatesPanel() { + final TextField input = new TextField(); + input.setPromptText("Enter a new predicate (e.g., \"_ph1 > 0\")."); + + BorderPane pane = new BorderPane(); + pane.setTop(input); + BorderPane.setMargin(input, new Insets(4)); + + VBox center = new VBox(4, abstrPredsList); + VBox.setVgrow(abstrPredsList, Priority.ALWAYS); + pane.setCenter(center); + + // Goal will only be null in test run + if (goal != null) { + String progVarsStr = goal.node().getLocalProgVars().toString().replace(",", ", "); + progVarsStr = progVarsStr.substring(1, progVarsStr.length() - 1); + pane.setBottom(new Label(AVAILABLE_PROGRAM_VARIABLES_DESCR + progVarsStr)); + } + + input.textProperty().addListener((obs, oldV, currInput) -> { + if (currInput.isEmpty()) { + abstrPredProblems.clear(); + return; + } + try { + parsePredicate(currInput.strip()); + abstrPredProblems.clear(); + if (registeredPredicates.contains(parsePredicate(currInput.strip()))) { + abstrPredProblems.add("Predicate is already registered"); + } + } catch (Exception ex) { + abstrPredProblems.clear(); + abstrPredProblems.add(ex.getMessage()); + } + }); + + input.setOnAction(e -> { + final String currInput = input.getText().strip(); + if (currInput.isEmpty() || !abstrPredProblems.isEmpty()) { + return; + } + try { + AbstractionPredicate parsed = parsePredicate(currInput); + abstrPredsList.getItems().add(currInput); + input.setText(""); + registeredPredicates.add(parsed); + } catch (Exception ex) { + abstrPredProblems.clear(); + abstrPredProblems.add(ex.getMessage()); + LOGGER.error("Exception!", ex); + } + }); + + abstrPredsList.setOnKeyPressed(e -> { + int selectedIndex = abstrPredsList.getSelectionModel().getSelectedIndex(); + if (e.getCode() == KeyCode.DELETE && !abstrPredsList.getItems().isEmpty() + && selectedIndex >= 0) { + abstrPredsList.getItems().remove(selectedIndex); + if (registeredPredicates.size() > selectedIndex) { + registeredPredicates.remove(selectedIndex); + } + } + }); + + return pane; + } + + private BorderPane createChoiceAbstrPredsPanel() { + TableColumn progVarCol = + new TableColumn<>("Program Variable"); + progVarCol.setCellValueFactory( + c -> new javafx.beans.property.SimpleStringProperty( + c.getValue().getProgVar().sort() + " " + c.getValue().getProgVar().name())); + progVarCol.setPrefWidth(180); + + TableColumn domElemCol = + new TableColumn<>("Domain Element"); + domElemCol.setCellValueFactory(c -> new javafx.beans.property.SimpleObjectProperty<>( + c.getValue())); + domElemCol.setCellFactory(col -> new DomainElemChoiceCell()); + domElemCol.setPrefWidth(380); + + choiceTable.getColumns().setAll(List.of(progVarCol, domElemCol)); + choiceTable.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY_FLEX_LAST_COLUMN); + choiceTable.setPlaceholder(new Label("No differing program variables.")); + + return new BorderPane(choiceTable); + } + + /** + * A table cell with a {@link ComboBox} of the domain elements of the row variable's sort + * (Swing: {@code DomElemChoiceTable.getCellEditor}/{@code getCellRenderer} with the + * {@code Optional} item model). + */ + private final class DomainElemChoiceCell + extends TableCell { + private final ComboBox> items = + new ComboBox<>(); + + @Override + protected void updateItem(AbstractDomainElemChoiceF item, boolean empty) { + super.updateItem(item, empty); + if (empty || item == null) { + setText(null); + setGraphic(null); + return; + } + items.getItems().clear(); + items.getItems().add(Optional.empty()); + if (services != null && goal != null) { + final Sort s = item.getProgVar().sort(); + final AbstractDomainLattice lattice = new MergeWithPredicateAbstraction( + registeredPredicates, latticeType, new LinkedHashMap<>()) + .getAbstractDomainForSort(s, services); + if (lattice != null) { + for (AbstractDomainElement elem : lattice) { + items.getItems() + .add(Optional.of( + (AbstractPredicateAbstractionDomainElement) elem)); + } + } + } + items.getSelectionModel().select(item.getAbstrDomElem()); + items.setCellFactory(view -> new ListCell<>() { + @Override + protected void updateItem(Optional it, + boolean empty) { + super.updateItem(it, empty); + setText(empty || it == null ? null : abstrPredToStringRepr(it)); + } + }); + items.setButtonCell(new ListCell<>() { + @Override + protected void updateItem(Optional it, + boolean empty) { + super.updateItem(it, empty); + setText(empty || it == null ? null : abstrPredToStringRepr(it)); + } + }); + items.valueProperty().addListener((obs, oldV, newV) -> item.setAbstrDomElem(newV)); + setGraphic(items); + setText(null); + } + } + + private HBox createControlsPanel() { + final Button cancelButton = new Button("Cancel"); + cancelButton.setCancelButton(true); + cancelButton.setOnAction(e -> { + cancelled = true; + stage.close(); + }); + final Button okButton = new Button("OK"); + okButton.setDefaultButton(true); + okButton.setOnAction(e -> stage.close()); + + HBox box = new HBox(8, cancelButton, okButton); + box.setAlignment(Pos.CENTER); + box.setPadding(new Insets(8)); + return box; + } + + private void refreshProblems() { + final ObservableList data = problemsList.getItems(); + data.clear(); + if (!placeholdersProblems.isEmpty()) { + for (String problem : placeholdersProblems) { + data.add("Placeholder Variables: " + problem); + } + } + if (!abstrPredProblems.isEmpty()) { + for (String problem : abstrPredProblems) { + data.add("Abstraction Predicates: " + problem); + } + } + } + + /** + * Parses a placeholder using {@link MergeRuleUtils#parsePlaceholder(String, Services)}. + * + * @param input The input to parse. + * @return The parsed placeholder (sort and name). + */ + private Pair parsePlaceholder(String input) { + return MergeRuleUtils.parsePlaceholder(input, goal.proof().getServices()); + } + + /** + * Parses an abstraction predicate using + * {@link MergeRuleUtils#parsePredicate(String, ArrayList, NamespaceSet, Services)}. + * + * @param input The input to parse. + * @return The parsed abstraction predicate. + * @throws ParserException If there is a mistake in the input. + */ + private AbstractionPredicate parsePredicate(String input) throws ParserException { + return MergeRuleUtils.parsePredicate(input, registeredPlaceholders, + goal.getLocalNamespaces(), goal.proof().getServices()); + } + + /** + * A String representation of an abstraction domain element, that is a "pair" expression of + * the placeholder variable and the predicate term of the form "(PROGVAR,PREDTERM)" + * (Swing {@code abstrPredToStringRepr}). + * + * @param domElem The abstraction predicate to convert into a String representation. + * @return A String representation of the given abstraction domain element. + */ + private String abstrPredToStringRepr( + Optional domElem) { + if (domElem == null) { + return ""; + } + + if (!domElem.isPresent()) { + return "None."; + } + + final AbstractPredicateAbstractionDomainElement predElem = domElem.get(); + + if (predElem.getPredicates().isEmpty()) { + return predElem.toString(); + } + + final StringBuilder sb = new StringBuilder(); + + final Iterator it = predElem.getPredicates().iterator(); + + while (it.hasNext()) { + sb.append(abstrPredToString(it.next())); + + if (it.hasNext()) { + sb.append(predElem.getPredicateNameCombinationString()); + } + } + + return sb.toString(); + } + + /** + * Returns a String representation of an abstraction predicate + * (Swing {@code abstrPredToString}; the Swing original fetches the services from the + * global MainWindow mediator, the port receives them in the constructor). + * + * @param pred Predicate to compute a String representation for. + * @return A String representation of the given abstraction predicate. + */ + private String abstrPredToString(AbstractionPredicate pred) { + if (services == null) { + return pred.toString(); + } + final Pair predFormWithPh = + pred.getPredicateFormWithPlaceholder(); + + return "(" + predFormWithPh.first + "," + + OutputStreamProofSaver.printAnything(predFormWithPh.second, services) + ")"; + } + + /** + * Shows the dialog modally and blocks until it is closed. + */ + public void show() { + stage.showAndWait(); + } + + // joinmerge: test support for the key.fx.verify.joinmerge self test — non-blocking show and + // programmatic cancel with the same semantics as the cancel button; the production path + // uses show(). + + /** Shows the dialog without blocking (self-test support; parity with {@link #show()}). */ + public void showNonBlocking() { + stage.show(); + } + + /** Cancels the dialog (self-test support; parity with the cancel button). */ + public void requestCancel() { + cancelled = true; + stage.close(); + } + + /** Exposes the dialog stage for tests. */ + public Stage getStageForVerification() { + return stage; + } + + /** + * @return The abstraction predicates set by the user. Is null iff the user pressed cancel. + */ + private @Nullable ArrayList getRegisteredPredicates() { + return cancelled ? null : registeredPredicates; + } + + /** + * @return The chosen lattice type (class object for class that is an instance of + * {@link AbstractPredicateAbstractionLattice}). + */ + private Class getLatticeType() { + return latticeType; + } + + /** + * @return The resulting input supplied by the user. + */ + public Result getResult() { + return new Result(getRegisteredPredicates(), getLatticeType(), abstrPredicateChoices); + } + + // ///////////////////////////// // + // /////// STATIC METHODS ////// // + // ///////////////////////////// // + + private static @Nullable URL getURLForResourceFile(String filename) { + URL url = AbstractionPredicatesChoiceDialogF.class.getResource(filename); + if (url == null) { + LOGGER.error("No resource {} found", filename); + } + return url; + } + + private static @Nullable String readFromResourceFile(String filename) { + URL url = getURLForResourceFile(filename); + if (url == null) { + return null; + } + try (final InputStream is = url.openStream(); + final Scanner s = new Scanner(is, StandardCharsets.UTF_8)) { + return s.useDelimiter("\\A").next(); + } catch (IOException e) { + return null; + } + } + + /** + * Crude HTML to text degradation for the info/help resources (the Swing original rendered + * them with the JTextPane HTML engine; javafx.web is deliberately not bundled in + * key.ui.fx). + * + * @param html the html resource content + * @return the text content without tags and entities + */ + static String stripHtml(String html) { + String text = html.replaceAll("(?is)<(script|style).*?", ""); + text = text.replaceAll("(?s)<[^>]*>", " "); + text = text.replace(" ", " ").replace("&", "&").replace("<", "<") + .replace(">", ">").replace(""", "\""); + return text.replaceAll("\\n\\s*\\n+", "\n\n").strip(); + } + + /** + * Encapsulates the results supplied by the user (Swing inner {@code Result} class, + * unchanged). + */ + public static class Result { + private final @Nullable ArrayList registeredPredicates; + private final Class latticeType; + private final LinkedHashMap abstractDomElemUserChoices = + new LinkedHashMap<>(); + + public Result(@Nullable ArrayList registeredPredicates, + Class latticeType, + List userChoices) { + this.registeredPredicates = registeredPredicates; + this.latticeType = latticeType; + + userChoices.forEach(choice -> { + if (choice.isChoiceMade()) { + abstractDomElemUserChoices.put(choice.getProgVar(), + choice.getAbstrDomElem().get()); + } + }); + } + + /** + * @return The abstraction predicates set by the user. Is null iff the user pressed + * cancel. + */ + public @Nullable ArrayList getRegisteredPredicates() { + return registeredPredicates; + } + + /** + * @return The chosen lattice type (class object for class that is an instance of + * {@link AbstractPredicateAbstractionLattice}). + */ + public Class getLatticeType() { + return latticeType; + } + + /** + * @return Manually chosen lattice elements for program variables. + */ + public LinkedHashMap getAbstractDomElemUserChoices() { + return abstractDomElemUserChoices; + } + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/mergerule/predicateabstraction/PredicateAbstractionCompletionF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/mergerule/predicateabstraction/PredicateAbstractionCompletionF.java new file mode 100644 index 00000000000..3b3a069dd51 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/mergerule/predicateabstraction/PredicateAbstractionCompletionF.java @@ -0,0 +1,93 @@ +/* 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.mergerule.predicateabstraction; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.stream.StreamSupport; + +import de.uka.ilkd.key.axiom_abstraction.predicateabstraction.AbstractionPredicate; +import de.uka.ilkd.key.gui.fx.mergerule.MergeProcedureCompletionF; +import de.uka.ilkd.key.java.Services; +import de.uka.ilkd.key.logic.op.LocationVariable; +import de.uka.ilkd.key.proof.Goal; +import de.uka.ilkd.key.rule.merge.MergePartner; +import de.uka.ilkd.key.rule.merge.procedures.MergeWithPredicateAbstraction; +import de.uka.ilkd.key.rule.merge.procedures.MergeWithPredicateAbstractionFactory; +import de.uka.ilkd.key.util.mergerule.MergeRuleUtils; +import de.uka.ilkd.key.util.mergerule.SymbolicExecutionState; + +import org.key_project.prover.sequent.PosInOccurrence; +import org.key_project.util.collection.ImmutableList; +import org.key_project.util.collection.Pair; + +/** + * Completion class for {@link MergeWithPredicateAbstraction}. + *

+ * Port of the Swing + * {@code de.uka.ilkd.key.gui.mergerule.predicateabstraction.PredicateAbstractionCompletion} + * (key.ui, pure logic; the dialog call replaced by {@link AbstractionPredicatesChoiceDialogF}). + * + * @author Dominic Scheurer (original Swing class) + */ +public class PredicateAbstractionCompletionF + extends MergeProcedureCompletionF { + + @Override + public MergeWithPredicateAbstraction complete(MergeWithPredicateAbstraction proc, + Pair joinGoalPio, Collection partners) { + final Services services = joinGoalPio.first.proof().getServices(); + + // Compute the program variables that are different in the + // respective states. + + final SymbolicExecutionState joinState = + MergeRuleUtils.sequentToSEPair(joinGoalPio.first.node(), joinGoalPio.second, services); + + final ImmutableList partnerStates = + MergeRuleUtils.sequentsToSEPairs(partners); + + final ArrayList differingLocVars = new ArrayList<>(); + + MergeRuleUtils.getUpdateLeftSideLocations(joinState.first).forEach(v -> { + // The meaning of the following statement corresponds to + // partnerStates.fold("right value for v differs", false) + final boolean isDifferent = StreamSupport + .stream(partnerStates.spliterator(), false).map(partner -> !MergeRuleUtils + .getUpdateRightSideForSafe(partner.getSymbolicState(), v) + .equals(MergeRuleUtils.getUpdateRightSideForSafe( + joinState.getSymbolicState(), v))) + .reduce(false, (b1, b2) -> (b1 || b2)); + + if (isDifferent) { + differingLocVars.add(v); + } + }); + + final AbstractionPredicatesChoiceDialogF dialog = + new AbstractionPredicatesChoiceDialogF(joinGoalPio.first, differingLocVars, null); + + assert proc instanceof MergeWithPredicateAbstractionFactory + : "Expected a procedure of type MergeWithPredicateAbstractionFactory."; + + final MergeWithPredicateAbstractionFactory procF = + (MergeWithPredicateAbstractionFactory) proc; + + dialog.show(); + + final AbstractionPredicatesChoiceDialogF.Result userInput = dialog.getResult(); + + final ArrayList chosenPreds = userInput.getRegisteredPredicates(); + + // A null-pointer in the chosen predicates means that + // the user has pressed the cancel button. + if (chosenPreds == null) { + return proc; + } else { + return procF.instantiate(chosenPreds, userInput.getLatticeType(), + userInput.getAbstractDomElemUserChoices()); + } + } +} diff --git a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/help/abstrPredsMergeDialogInfo.html b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/help/abstrPredsMergeDialogInfo.html new file mode 100644 index 00000000000..911ff3db012 --- /dev/null +++ b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/help/abstrPredsMergeDialogInfo.html @@ -0,0 +1,37 @@ +

Within this dialog, you can choose predicates used in merges by predicate abstraction.

+

+ An abstraction predicate is a unary mapping from terms of suitable type to formulae. + You can construct such mappings here by registering placeholder variables (e.g., int i + or java.lang.String s) and using those in the predicate definition (e.g., s = null). + For each sort of placeholder predicates, a finite lattice for merges with abstraction is + constructed. +

+

+ Thus, it does not make sense to register multiple placeholders of the same + type. Furthermore, each abstraction predicate definition must contain exactly one placeholder + which then also determines the type of the predicate. +

+

+ There are three lattice types you can choose; your choice determines how abstract domain elements will be + constructed from the given abstraction predicates. Your options are: +

+
Simple Predicates Lattice
+
+ This lattice type consists out of a bottom element, one abstract domain element for each of your + abstraction predicates, and a top element. +
+
Conjunctive Predicates Lattice
+
+ In this lattice, all 2^n combinations of your predicates are combined using conjunction. The iterator + of this lattice returns a bottom element, then the conjunction of all n predicates, then all conjunctions + of length n-1 of the predicates, and so on. Finally, the top element is returned. +
+
Disjunctive Predicates Lattice
+
+ Similar to the conjunctive lattice, but with disjunctions and reversed order: After the bottom element, + the next elements correspond to one predicate each, then follow all disjunctions of length 2, and so + on. You have to make sure that at most the disjunction of all predicates is valid, but no combination + before. +
+
+

From ab63384e5eedac8b68b2524a6c284e69531ca0d6 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Thu, 8 Oct 2026 22:15:48 +0200 Subject: [PATCH 067/281] key.ui.fx: joinmerge: join dialog, self-test hook and theme styles Port of the Swing gui/join dialogs to JavaFX: JoinDialogF (with DecisionPredicateInputF + InspectorForDecisionPredicatesF and SequentViewerF), JoinActionF as the future context-menu trigger, and JoinMergeVerifyF, the interactive self test for the join AND merge dialog ports (key.fx.verify.joinmerge, forwarded by build.gradle). MainWindowF gets a minimal, marked verify hook plus the TODO-merge registration seam comment. Theme styles for the new dialogs are appended to both key-light.css and key-dark.css. --- key.ui.fx/build.gradle | 5 +- .../de/uka/ilkd/key/gui/fx/MainWindowF.java | 22 + .../gui/fx/join/DecisionPredicateInputF.java | 138 +++++ .../join/InspectorForDecisionPredicatesF.java | 92 +++ .../uka/ilkd/key/gui/fx/join/JoinActionF.java | 111 ++++ .../uka/ilkd/key/gui/fx/join/JoinDialogF.java | 497 ++++++++++++++++ .../key/gui/fx/join/JoinMergeVerifyF.java | 554 ++++++++++++++++++ .../ilkd/key/gui/fx/join/SequentViewerF.java | 50 ++ .../de/uka/ilkd/key/gui/fx/theme/key-dark.css | 60 ++ .../uka/ilkd/key/gui/fx/theme/key-light.css | 65 ++ 10 files changed, 1593 insertions(+), 1 deletion(-) create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/join/DecisionPredicateInputF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/join/InspectorForDecisionPredicatesF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/join/JoinActionF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/join/JoinDialogF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/join/JoinMergeVerifyF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/join/SequentViewerF.java diff --git a/key.ui.fx/build.gradle b/key.ui.fx/build.gradle index 86152523631..d74f6fdf978 100644 --- a/key.ui.fx/build.gradle +++ b/key.ui.fx/build.gradle @@ -65,11 +65,14 @@ run { // Hide/Regroup) with the key.fx.verify.sequentsearch query after the demo load) // * key.fx.verify.updatehighlight = any (M2: run the update-highlight overlay self test; // needs a sequent printing update operators, e.g. standard_key/queries/useQuery.key) + // * key.fx.verify.joinmerge = any (joinmerge: run the join/merge dialog self test; needs + // a proof with open branches, e.g. a limited auto mode run — see JoinMergeVerifyF) ["key.fx.driver", "key.fx.theme", "key.fx.demo.sequent", "key.fx.verify.sequent", "key.fx.show", "key.fx.demo.autoprove", "key.fx.verify.tree", "key.fx.demo.autoprove.live", "key.fx.demo.maxsteps", "key.fx.verify.goallist", "key.fx.verify.search", "key.fx.verify.sequentsearch", "key.fx.verify.treefilters", - "key.fx.verify.sequentsearchmodes", "key.fx.verify.updatehighlight"] + "key.fx.verify.sequentsearchmodes", "key.fx.verify.updatehighlight", + "key.fx.verify.joinmerge", "key.fx.verify.joinmerge.maxsteps"] .each { name -> def value = System.getProperty(name) if (value != null) { 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 index 8a46e0ba13f..a0776f134c5 100644 --- 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 @@ -62,6 +62,7 @@ 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.infoview.InfoViewF; +import de.uka.ilkd.key.gui.fx.join.JoinMergeVerifyF; import de.uka.ilkd.key.gui.fx.keyshortcuts.KeyStrokeManagerF; import de.uka.ilkd.key.gui.fx.nodeviews.SequentViewF; import de.uka.ilkd.key.gui.fx.notification.NotificationManagerF; @@ -475,6 +476,10 @@ protected KeYEnvironment call() throws Exception { if (System.getProperty("key.fx.verify.goallist") != null) { LOGGER.info("Goal list verification: {}", goalListView.verifyGoalList()); } + // joinmerge: run the join/merge dialog self test (key.fx.verify.joinmerge) + if (System.getProperty("key.fx.verify.joinmerge") != null) { + runJoinMergeVerification(env); + } if (System.getProperty("key.fx.verify.proofdiff") != null) { String report = ProofDiffFrameF.verifyDiffLogic(); LOGGER.info("Proof diff verification: {}", report); @@ -930,6 +935,23 @@ private void runUpdateHighlightVerification() { report.endsWith("PASS") ? Kind.INFO : Kind.ERROR); } + /** + * Runs the join/merge dialog self test ({@code key.fx.verify.joinmerge}); the dialogs are + * constructed directly with the loaded proof, see {@link JoinMergeVerifyF}. + * + * @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) { + JoinMergeVerifyF.run(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 diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/join/DecisionPredicateInputF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/join/DecisionPredicateInputF.java new file mode 100644 index 00000000000..b80fc097dfc --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/join/DecisionPredicateInputF.java @@ -0,0 +1,138 @@ +/* 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.join; + +import java.util.LinkedList; +import java.util.List; + +import javafx.geometry.Insets; +import javafx.scene.Node; +import javafx.scene.control.Label; +import javafx.scene.control.TextField; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; + +/** + * A text field for user input with instantaneous validity feedback (a "traffic light"). The + * check function is supplied by an {@link InspectorF}; every change of the input re-runs the + * check and notifies the registered {@link ListenerF}s. + *

+ * Counter-part of {@code de.uka.ilkd.key.gui.utilities.CheckedUserInput} in the Swing module + * {@code key.ui} (CheckedUserInput.java:23-248). Deviations: + *

    + *
  • the input field is a single-line {@link TextField} instead of a {@code JTextPane} (the + * join dialog only enters single decision formulae);
  • + *
  • the Swing traffic light ({@code TrafficLight}) becomes a colored dot label styled by CSS + * ({@code .traffic-light-ok} / {@code .traffic-light-error});
  • + *
  • the optional "Details" area ({@code showInformation} flag of the Swing constructor, + * CheckedUserInput.java:63-97) is not ported — the join dialog always constructs the input with + * {@code showInformation = false} (JoinDialog.java:314) and shows the messages in its own info + * box instead.
  • + *
+ * The listener contract ({@code userInputChanged(input, valid, reason)} with {@code reason == + * null} iff valid) and the red "reason#detail" message convention (CheckedUserInput.java:197-206) + * are ported unchanged. + */ +public final class DecisionPredicateInputF extends VBox { + + /** Inspects user input; returns {@code null} iff the input is valid. */ + public interface InspectorF { + + /** Returned instead of an error if nothing has been entered yet. */ + String NO_USER_INPUT = " "; + + /** + * @param toBeChecked the user input to be checked + * @return {@code null} if the user input is valid, otherwise a string describing the + * error + */ + String check(String toBeChecked); + } + + /** Observes the checked user input (Swing CheckedUserInput.CheckedUserInputListener). */ + public interface ListenerF { + void userInputChanged(String input, boolean valid, String reason); + } + + private final TextField inputField = new TextField(); + private final Label trafficLight = new Label(); + + private InspectorF inspector = toBeChecked -> null; + private final List listeners = new LinkedList<>(); + + public DecisionPredicateInputF() { + HBox inputRow = new HBox(5); + inputRow.getChildren().addAll(inputField, trafficLight); + HBox.setHgrow(inputField, Priority.ALWAYS); + trafficLight.getStyleClass().add("traffic-light"); + trafficLight.setMinSize(10, 10); + getChildren().add(inputRow); + setPadding(new Insets(0, 0, 2, 0)); + + // Swing installs a DocumentListener on the text component that runs checkInput on every + // change (CheckedUserInput.java:146-168); the FX equivalent listens on the text property. + inputField.textProperty().addListener((obs, oldV, newV) -> checkInput()); + setInput(""); + } + + /** Swing CheckedUserInput.setInspector (CheckedUserInput.java:99-102). */ + public void setInspector(InspectorF inspector) { + this.inspector = inspector; + checkInput(); + } + + public void addListener(ListenerF listener) { + listeners.add(listener); + } + + public void removeListener(ListenerF listener) { + listeners.remove(listener); + } + + /** Swing CheckedUserInput.getInput (CheckedUserInput.java:188-190). */ + public String getInput() { + return inputField.getText(); + } + + /** Swing CheckedUserInput.setInput (CheckedUserInput.java:192-195). */ + public void setInput(String input) { + inputField.setText(input == null ? "" : input); + checkInput(); + } + + /** + * Swing CheckedUserInput.checkInput (CheckedUserInput.java:170-177): re-run the inspector + * and notify the listeners. + */ + private void checkInput() { + String text = inputField.getText(); + String result = inspector.check(text); + setValid(result); + for (ListenerF listener : listeners) { + listener.userInputChanged(text, result == null, result); + } + } + + /** + * Swing CheckedUserInput.setValid (CheckedUserInput.java:197-206): red message on error + * (the "reason#detail" convention), green traffic light on success. + */ + private void setValid(String result) { + boolean valid = result == null; + trafficLight.getStyleClass().removeAll(List.of("traffic-light-ok", "traffic-light-error")); + trafficLight.getStyleClass().add(valid ? "traffic-light-ok" : "traffic-light-error"); + } + + /** Exposes the input field for tests (the traffic light dot is {@link #getTrafficLight()}). */ + public TextField getInputField() { + return inputField; + } + + /** Exposes the traffic light for tests. */ + public Node getTrafficLight() { + return trafficLight; + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/join/InspectorForDecisionPredicatesF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/join/InspectorForDecisionPredicatesF.java new file mode 100644 index 00000000000..0bd7719a13f --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/join/InspectorForDecisionPredicatesF.java @@ -0,0 +1,92 @@ +/* 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.join; + +import java.util.LinkedList; +import java.util.List; + +import de.uka.ilkd.key.java.Services; +import de.uka.ilkd.key.ldt.JavaDLTheory; +import de.uka.ilkd.key.logic.JTerm; +import de.uka.ilkd.key.nparser.KeyIO; +import de.uka.ilkd.key.proof.Node; +import de.uka.ilkd.key.proof.delayedcut.ApplicationCheck; +import de.uka.ilkd.key.proof.delayedcut.DelayedCut; + +import org.key_project.prover.sequent.Semisequent; +import org.key_project.prover.sequent.SequentFormula; + +/** + * Input inspection logic for decision predicates of the delayed-cut join rule. Counter-part of + * {@code de.uka.ilkd.key.gui.InspectorForDecisionPredicates} in the Swing module {@code key.ui} + * (InspectorForDecisionPredicates.java:22-86); only the check logic is ported — the Swing class + * is UI-independent in exactly the same way, it merely implements the Swing-internal + * {@code CheckedUserInputInspector} interface (here replaced by + * {@link DecisionPredicateInputF.InspectorF}). + *

+ * The check rejects empty input, non-formulae and formulae that already occur in the target + * semisequent of the given node, and runs the additional {@link ApplicationCheck}s (the checks + * of the delayed-cut mechanism). + */ +public final class InspectorForDecisionPredicatesF implements DecisionPredicateInputF.InspectorF { + + private final Services services; + private final Node node; + private final int cutMode; + private final List additionalChecks = new LinkedList<>(); + + public InspectorForDecisionPredicatesF(Services services, Node node, int cutMode, + List additionalChecks) { + this.services = services; + this.node = node; + this.cutMode = cutMode; + this.additionalChecks.addAll(additionalChecks); + } + + /** Swing InspectorForDecisionPredicates.check (InspectorForDecisionPredicates.java:42-76). */ + @Override + public String check(String toBeChecked) { + if (toBeChecked.isEmpty()) { + return DecisionPredicateInputF.InspectorF.NO_USER_INPUT; + } + JTerm term = translate(services, toBeChecked); + + Semisequent semisequent = + cutMode == DelayedCut.DECISION_PREDICATE_IN_ANTECEDENT ? node.sequent().antecedent() + : node.sequent().succedent(); + String position = + cutMode == DelayedCut.DECISION_PREDICATE_IN_ANTECEDENT ? "antecedent" : "succedent"; + + for (SequentFormula sf : semisequent) { + if (sf.formula() == term) { + return "Formula already exists in " + position + "."; + } + } + + if (term == null || term.sort() != JavaDLTheory.FORMULA) { + return "Not a formula."; + } + for (ApplicationCheck check : additionalChecks) { + String result = check.check(node, term); + if (result != null) { + return result; + } + } + return null; + } + + /** + * Translates the given user input into a term; {@code null} if the input cannot be parsed + * (Swing InspectorForDecisionPredicates.translate, InspectorForDecisionPredicates.java:78-84; + * same static helper as {@code InspectorForFormulas.translate}). + */ + public static JTerm translate(Services services, String toBeChecked) { + try { + return new KeyIO(services).parseExpression(toBeChecked); + } catch (Throwable e) { + return null; + } + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/join/JoinActionF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/join/JoinActionF.java new file mode 100644 index 00000000000..9695d52ea76 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/join/JoinActionF.java @@ -0,0 +1,111 @@ +/* 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.join; + +import java.util.List; +import javafx.application.Platform; +import javafx.stage.Window; + +import de.uka.ilkd.key.control.ProofControl; +import de.uka.ilkd.key.gui.fx.notification.NotificationManagerF; +import de.uka.ilkd.key.gui.fx.notification.NotificationManagerF.Kind; +import de.uka.ilkd.key.proof.Goal; +import de.uka.ilkd.key.proof.Proof; +import de.uka.ilkd.key.proof.join.JoinProcessor; +import de.uka.ilkd.key.proof.join.JoinProcessor.Listener; +import de.uka.ilkd.key.proof.join.PredicateEstimator; +import de.uka.ilkd.key.proof.join.ProspectivePartner; + +import org.key_project.util.collection.ImmutableList; + +// joinmerge: TODO-merge register with WindowUserInterfaceControlF completion registry — +// this action is the trigger counterpart of Swing's JoinMenuItem; once the FX rule-application +// context menu exists (CurrentGoalViewMenu port), wire it there and hand in the proof control +// from the mediator. + +/** + * The trigger of the interactive "delayed cut" join rule: shows the {@link JoinDialogF} and, if + * the user confirms, starts the core {@link JoinProcessor} on a background thread and — like the + * Swing original — restarts the automatic mode on the joined goals when the processing finished. + * Counter-part of {@code de.uka.ilkd.key.gui.join.JoinMenuItem} in the Swing module + * {@code key.ui} (JoinMenuItem.java:28-90). + *

+ * Event semantics ported from Swing: + *

    + *
  • the dialog is modal; only a confirmed OK starts the join (JoinMenuItem.java:42-51);
  • + *
  • the {@code JoinProcessor.Listener} results are marshalled back onto the UI thread with + * {@code Platform.runLater} instead of {@code SwingUtilities.invokeLater} + * (JoinMenuItem.java:60-79);
  • + *
  • exceptions during joining are reported to the notification layer (Swing forwards an + * {@code ExceptionFailureEvent} to the mediator, JoinMenuItem.java:62-65; the FX port raises an + * ERROR toast on {@link NotificationManagerF});
  • + *
  • the end of joining starts the auto mode on the joined goals via the proof control + * (JoinMenuItem.java:68-78).
  • + *
+ * Deviations: the Swing menu item class is not ported as a menu item — the FX UI has no rule + * application context menu yet, so the trigger is a plain entry point taking the proof control + * (this is the seam {@code mediator.getUI().getProofControl()} of Swing JoinMenuItem.java:73-74); + * the Swing {@code mediator.stopInterface(true)} UI lock (JoinMenuItem.java:48) has no FX + * counterpart yet and is not ported. + */ +public final class JoinActionF { + + /** Menu text of the Swing original (JoinMenuItem.toString, JoinMenuItem.java:87-89). */ + public static final String TEXT = "Delayed Cut Join Rule"; + + private JoinActionF() { + } + + /** + * Opens the join dialog for the given partner candidates and starts the join on + * confirmation. + * + * @param partners the prospective join partners (computed by the caller via + * {@code JoinIsApplicable.INSTANCE.isApplicable(...)}, cf. Swing + * CurrentGoalViewMenu.java:235-238) + * @param proof the proof to join + * @param proofControl the proof control used to restart the auto mode (Swing + * {@code mediator.getUI().getProofControl()}) + * @param owner the owner window for the dialog (may be {@code null}) + */ + public static void run(List partners, Proof proof, + ProofControl proofControl, Window owner) { + JoinDialogF dialog = + new JoinDialogF(partners, proof, PredicateEstimator.STD_ESTIMATOR, + proof.getServices(), owner); + dialog.show(owner); + if (dialog.okButtonHasBeenPressed()) { + start(dialog.getSelectedPartner(), proof, proofControl); + } + } + + /** + * Swing JoinMenuItem.start (JoinMenuItem.java:55-83): run the processor on a background + * thread and deliver its results on the UI thread. + */ + private static void start(ProspectivePartner partner, Proof proof, ProofControl proofControl) { + JoinProcessor processor = new JoinProcessor(partner, proof); + + processor.addListener(new Listener() { + + @Override + public void exceptionWhileJoining(Throwable e) { + Platform.runLater(() -> NotificationManagerF.getInstance() + .notify("Joining failed: " + e.getMessage(), Kind.ERROR)); + } + + @Override + public void endOfJoining(ImmutableList goals) { + // This method delegates the request only to the UserInterfaceControl which + // implements the functionality. No functionality is allowed in this method body! + // (comment of Swing JoinMenuItem.java:70-74) + Platform.runLater(() -> proofControl.startAutoMode(proof, goals)); + } + }); + + Thread thread = new Thread(processor, "ProofJoinProcessor"); + thread.start(); + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/join/JoinDialogF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/join/JoinDialogF.java new file mode 100644 index 00000000000..ea678f2a476 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/join/JoinDialogF.java @@ -0,0 +1,497 @@ +/* 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.join; + +import java.util.ArrayList; +import java.util.List; +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.ScrollPane; +import javafx.scene.control.SelectionMode; +import javafx.scene.control.Tooltip; +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.join.DecisionPredicateInputF.InspectorF; +import de.uka.ilkd.key.gui.fx.join.DecisionPredicateInputF.ListenerF; +import de.uka.ilkd.key.java.Services; +import de.uka.ilkd.key.pp.LogicPrinter; +import de.uka.ilkd.key.pp.NotationInfo; +import de.uka.ilkd.key.proof.Node; +import de.uka.ilkd.key.proof.Proof; +import de.uka.ilkd.key.proof.delayedcut.ApplicationCheck; +import de.uka.ilkd.key.proof.delayedcut.DelayedCut; +import de.uka.ilkd.key.proof.delayedcut.DelayedCutProcessor; +import de.uka.ilkd.key.proof.join.LateApplicationCheck; +import de.uka.ilkd.key.proof.join.PredicateEstimator; +import de.uka.ilkd.key.proof.join.PredicateEstimator.Result; +import de.uka.ilkd.key.proof.join.ProspectivePartner; + +import org.key_project.prover.sequent.Sequent; + +/** + * The dialog of the interactive "delayed cut" join rule. Counter-part of + * {@code de.uka.ilkd.key.gui.join.JoinDialog} in the Swing module {@code key.ui} + * (JoinDialog.java:33-375): after the user has chosen two compatible join partners and confirmed + * with an edited decision predicate, the caller starts the join processor with the selected + * partner ({@link JoinActionF}). + *

+ * Ported behavior (Swing evidence): + *

    + *
  • headline "Join Goal <serialNr>" plus the sequent of the first partner node + * (JoinDialog.java:150-153);
  • + *
  • a single-selection list of prospective partners ("Goal <serialNr>"); selecting an + * entry shows its sequent, its estimated common predicate and the "true for goal A / false for + * goal B" info label (JoinDialog.java:157-183, :185-197);
  • + *
  • per-partner applicability via {@code LateApplicationCheck} with a + * {@code NoNewSymbolsCheck} on both partner nodes (JoinDialog.java:165-173);
  • + *
  • a checked predicate input ({@link DecisionPredicateInputF}) whose validity drives the OK + * button (JoinDialog.java:43, :132-141) — OK is enabled iff the input is a valid formula + * and the selected partner is applicable;
  • + *
  • a details/info box that shows either the "new symbols" inapplicability notice, the + * validation reason, or "Join is applicable." in green (JoinDialog.java:48-53, :257-274);
  • + *
  • on OK the caller reads {@link #getSelectedPartner()}; the selected partner carries the + * (edited) common predicate (JoinDialog.java:133-135).
  • + *
+ * Deviations: JavaFX {@code Stage} built in code, messages styled via CSS classes instead of + * {@code Color}s, and the click-to-popup behavior of the Swing {@code ClickableMessageBox} + * becomes a click-to-alert on the message (JoinDialog.java:296-297). + */ +public final class JoinDialogF { + + /** + * The inapplicability notice shown for partners that cannot be joined because new symbols + * were introduced on the branches (Swing JoinDialog.INFO, JoinDialog.java:48-53). + */ + private static final String INFO = """ + It is not possible to join both goals, because new symbols have been introduced + on the branches which belong to the goals: Up to now the treatment of new symbols + is not supported by the joining mechanism. + + """; + + private final Stage stage = new Stage(); + private final ContentPanel content; + private final Button okButton = new Button("OK"); + + private boolean okButtonHasBeenPressed = false; + + /** + * Creates a non-modal-configured join dialog (shown modally by {@link #show(Window)}). + * + * @param partnerList the prospective join partners (Swing parameter of the same name) + * @param proof the underlying proof + * @param estimator the decision predicate estimator ({@code PredicateEstimator.STD_ESTIMATOR} + * in the Swing caller) + * @param services the proof services + */ + public JoinDialogF(List partnerList, Proof proof, + PredicateEstimator estimator, Services services) { + this(partnerList, proof, estimator, services, null); + } + + public JoinDialogF(List partnerList, Proof proof, + PredicateEstimator estimator, Services services, Window owner) { + stage.setTitle("Joining"); + okButton.setDisable(true); + okButton.setDefaultButton(true); + okButton.setTooltip(new Tooltip("Apply the join with the edited decision predicate.")); + // Swing JoinDialog.java:42-43: the input listener drives the OK button enablement + content = new ContentPanel(partnerList, proof, estimator, + (input, valid, reason) -> okButton.setDisable(!valid), services); + + Button cancelButton = new Button("Cancel"); + cancelButton.setCancelButton(true); + cancelButton.setOnAction(e -> stage.close()); + okButton.setOnAction(e -> { + okButtonHasBeenPressed = true; + stage.close(); + }); + Region spacer = new Region(); + HBox.setHgrow(spacer, Priority.ALWAYS); + HBox buttonBar = new HBox(5, spacer, okButton, cancelButton); + buttonBar.setAlignment(Pos.CENTER_RIGHT); + buttonBar.setPadding(new Insets(5, 8, 8, 8)); + + BorderPane root = new BorderPane(content, null, null, buttonBar, null); + root.setPadding(new Insets(8)); + Scene scene = new Scene(root, 860, 640); + de.uka.ilkd.key.gui.fx.theme.ThemeManager.getInstance().style(scene); + stage.setScene(scene); + stage.initModality(Modality.APPLICATION_MODAL); + if (owner != null) { + stage.initOwner(owner); + } + } + + /** Shows the dialog and blocks until it is closed (the Swing dialog is modal, too). */ + public void show(Window owner) { + if (owner != null && stage.getOwner() == null) { + stage.initOwner(owner); + } + stage.showAndWait(); + } + + // joinmerge: test support for the key.fx.verify.joinmerge self test — non-blocking show and + // programmatic OK/cancel with the same semantics as the buttons; the production path uses + // show(Window). + + /** Shows the dialog without blocking (self-test support; parity with {@link #show(Window)}). */ + public void showNonBlocking() { + stage.show(); + } + + /** Confirms the dialog if the OK button is enabled (self-test support). */ + public void requestOk() { + if (!okButton.isDisabled()) { + okButtonHasBeenPressed = true; + stage.close(); + } + } + + /** Cancels the dialog (self-test support; parity with the Swing cancel button). */ + public void requestCancel() { + stage.close(); + } + + /** Exposes the dialog stage for tests. */ + public Stage getStageForVerification() { + return stage; + } + + /** Swing StdDialog.okButtonHasBeenPressed. */ + public boolean okButtonHasBeenPressed() { + return okButtonHasBeenPressed; + } + + /** + * The partner selected in the choice list, carrying the (edited) common predicate (Swing + * JoinDialog.getSelectedPartner, JoinDialog.java:368-371). + */ + public ProspectivePartner getSelectedPartner() { + return content.getSelectedPartner(); + } + + /** Exposes the OK button for tests. */ + public Button getOkButton() { + return okButton; + } + + /** Exposes the partner choice list for tests. */ + public ListView getChoiceList() { + return content.getChoiceList(); + } + + /** Exposes the predicate input for tests. */ + public DecisionPredicateInputF getPredicateInput() { + return content.getPredicateInput(); + } + + /** Exposes the info box for tests (last message text or {@code null}). */ + public Label getLastInfoMessage() { + return content.getLastInfoMessage(); + } + + /** Exposes the sequent viewer of the selected partner for tests. */ + public SequentViewerF getSequentViewer2() { + return content.getSequentViewer2(); + } + + /** + * One selectable entry of the choice list (Swing ContentItem, JoinDialog.java:73-120). + */ + public static final class ContentItem { + + final ProspectivePartner partner; + final InspectorF inspector; + final boolean applicable; + + public ContentItem(ProspectivePartner partner, Services services, boolean applicable) { + this.partner = partner; + this.inspector = new InspectorForDecisionPredicatesF(services, + partner.getCommonParent(), DelayedCut.DECISION_PREDICATE_IN_ANTECEDENT, + DelayedCutProcessor.getApplicationChecks()); + this.applicable = applicable; + } + + public InspectorF getInspector() { + return inspector; + } + + public boolean isApplicable() { + return applicable; + } + + Sequent getSequent() { + return partner.getNode(1).sequent(); + } + + @Override + public String toString() { + return "Goal " + partner.getNode(1).serialNr(); + } + + public String getPredicateInfo() { + return "Decision Formula (true for Goal " + partner.getNode(0).serialNr() + + ", false for Goal " + partner.getNode(1).serialNr() + ")"; + } + + public String getPredicate(Proof proof) { + if (partner.getCommonPredicate() == null) { + return ""; + } + LogicPrinter printer = + LogicPrinter.purePrinter(new NotationInfo(), proof.getServices()); + printer.printTerm(partner.getCommonPredicate()); + return printer.result(); + } + } + + /** + * The dialog content (Swing ContentPanel, JoinDialog.java:56-366): headline with sequent of + * the first partner, "with" separator, partner choice list with sequent viewer, predicate + * info label, checked predicate input and the details info box. + */ + private final class ContentPanel extends VBox { + + private final SequentViewerF sequentViewer1 = new SequentViewerF(); + private final SequentViewerF sequentViewer2 = new SequentViewerF(); + private final ListView choiceList = new ListView<>(); + private final DecisionPredicateInputF predicateInput = new DecisionPredicateInputF(); + private final Label infoPredicate = new Label(" "); + private final MessageBoxF infoBox = new MessageBoxF(); + private final Label headline = new Label("Join"); + private final Label lastInfoMessage = new Label(); + + private final Proof proof; + private final PredicateEstimator estimator; + private final Services services; + + ContentPanel(List partnerList, Proof proof, + PredicateEstimator estimator, ListenerF validListener, Services services) { + this.proof = proof; + this.estimator = estimator; + this.services = services; + + headline.getStyleClass().add("dialog-section-title"); + infoPredicate.getStyleClass().add("join-predicate-info"); + choiceList.setCellFactory(view -> new ListCell<>() { + @Override + protected void updateItem(ContentItem item, boolean empty) { + super.updateItem(item, empty); + setText(empty || item == null ? null : item.toString()); + } + }); + choiceList.setPrefWidth(110); + choiceList.setPrefHeight(280); + sequentViewer1.setPrefSize(380, 240); + sequentViewer2.setPrefSize(300, 240); + infoBox.setPrefHeight(90); + VBox.setVgrow(infoBox, Priority.ALWAYS); + infoBox.getStyleClass().add("join-details-box"); + + // Swing JoinDialog.java:132-141 — the input listener stores the edited predicate on + // the selected partner and forwards validity (with the partner's applicability + // factored in) to the dialog's OK-button listener. + predicateInput.addListener((input, valid, reason) -> { + if (valid) { + getSelectedPartner().setCommonPredicate( + // the Swing code uses InspectorForFormulas.translate here + // (JoinDialog.java:134-135), which is the same KeyIO translation + InspectorForDecisionPredicatesF.translate(services, input)); + validListener.userInputChanged(input, getSelectedItem() != null + && getSelectedItem().isApplicable(), + reason); + } else { + validListener.userInputChanged(input, false, reason); + } + refreshInfoBox(reason); + }); + + choiceList.getSelectionModel().setSelectionMode(SelectionMode.SINGLE); + choiceList.getSelectionModel().selectedItemProperty().addListener((obs, oldV, newV) -> { + int index = choiceList.getSelectionModel().getSelectedIndex(); + if (index >= 0) { + selectionChanged(index); + } + }); + + // layout (Swing JoinDialog.create, JoinDialog.java:207-255) + VBox leftBox = new VBox(4, headline, new ScrollPane(sequentViewer1)); + VBox.setVgrow(leftBox, Priority.ALWAYS); + Label withLabel = new Label("with"); + withLabel.getStyleClass().add("join-with-label"); + HBox rightRow = new HBox(5, choiceList, new ScrollPane(sequentViewer2)); + HBox.setHgrow(rightRow, Priority.ALWAYS); + VBox rightBox = new VBox(4, withLabel, rightRow); + VBox.setVgrow(rightBox, Priority.ALWAYS); + HBox.setHgrow(leftBox, Priority.ALWAYS); + HBox.setHgrow(rightBox, Priority.ALWAYS); + HBox partnersBox = new HBox(10, leftBox, rightBox); + VBox.setVgrow(partnersBox, Priority.ALWAYS); + + ScrollPane infoBoxPane = new ScrollPane(infoBox); + infoBoxPane.setFitToWidth(true); + infoBoxPane.setPrefHeight(100); + + setSpacing(5); + getChildren().addAll(partnersBox, infoPredicate, predicateInput, infoBoxPane); + + if (partnerList != null && !partnerList.isEmpty()) { + fill(partnerList); + } + } + + /** + * Swing ContentPanel.fill (JoinDialog.java:150-183): estimate the decision predicates, + * compute the applicability with a {@code NoNewSymbolsCheck} on both partner nodes, and + * preselect the first partner. + */ + private void fill(List partnerList) { + Node node = partnerList.get(0).getNode(0); + headline.setText("Join Goal " + node.serialNr()); + sequentViewer1.setSequent(node.sequent(), proof.getServices()); + + List model = new ArrayList<>(); + for (ProspectivePartner partner : partnerList) { + Result result = estimator.estimate(partner, proof); + partner.setCommonPredicate(result.getPredicate()); + partner.setCommonParent(result.getCommonParent()); + + ApplicationCheck check = new ApplicationCheck.NoNewSymbolsCheck(); + + boolean applicable = true; + applicable = LateApplicationCheck.INSTANCE + .check(partner.getNode(0), result.getCommonParent(), check).isEmpty() + && applicable; + applicable = LateApplicationCheck.INSTANCE + .check(partner.getNode(1), result.getCommonParent(), check).isEmpty() + && applicable; + + model.add(new ContentItem(partner, services, applicable)); + } + + choiceList.setItems(FXCollections.observableList(model)); + choiceList.getSelectionModel().selectFirst(); + } + + /** + * Swing ContentPanel.selectionChanged (JoinDialog.java:185-197): show the partner + * sequent, its predicate and the predicate info label. + */ + private void selectionChanged(int index) { + ContentItem item = choiceList.getItems().get(index); + sequentViewer2.setSequent(item.getSequent(), proof.getServices()); + predicateInput.setInspector(item.getInspector()); + predicateInput.setInput(item.getPredicate(proof)); + infoPredicate.setText(item.getPredicateInfo()); + } + + /** + * Swing ContentPanel.refreshInfoBox (JoinDialog.java:257-274): inapplicability notice, + * red validation reason ("reason#detail" convention) or green "Join is applicable.". + */ + private void refreshInfoBox(String reason) { + ContentItem item = getSelectedItem(); + infoBox.clear(); + if (item == null) { + return; + } + if (!item.isApplicable()) { + lastInfoMessage.setText("Goal " + item.partner.getNode(0).serialNr() + " and " + + "Goal " + item.partner.getNode(1).serialNr() + " cannot be joined."); + infoBox.add(INFO, lastInfoMessage.getText(), true); + } else if (reason != null) { + String[] segments = reason.split("#"); + lastInfoMessage.setText(segments[0]); + infoBox.add(segments.length > 1 ? segments[1] : null, segments[0], true); + } else { + lastInfoMessage.setText("Join is applicable."); + infoBox.add(null, lastInfoMessage.getText(), false); + } + } + + ContentItem getSelectedItem() { + return choiceList.getSelectionModel().getSelectedItem(); + } + + ProspectivePartner getSelectedPartner() { + ContentItem item = getSelectedItem(); + return item == null ? null : item.partner; + } + + ListView getChoiceList() { + return choiceList; + } + + DecisionPredicateInputF getPredicateInput() { + return predicateInput; + } + + Label getLastInfoMessage() { + return lastInfoMessage; + } + + SequentViewerF getSequentViewer2() { + return sequentViewer2; + } + } + + /** + * Minimal port of the Swing {@code ClickableMessageBox} (used by the join dialog at + * JoinDialog.java:65-66, :289-301): a vertical list of colored message labels; clicking a + * message opens an information alert with the (optional) long detail text, mirroring the + * Swing "Problem Description" dialog (JoinDialog.java:296-297). + */ + private static final class MessageBoxF extends VBox { + + MessageBoxF() { + setSpacing(2); + } + + void clear() { + getChildren().clear(); + } + + /** + * Adds a message. + * + * @param detail long text shown on click (may be {@code null}) + * @param text the message text + * @param error whether the message is an error (red) or a success (green) message + */ + void add(String detail, String text, boolean error) { + Label label = new Label(text); + // "join-message-row" is pre-staged in key-light/key-dark.css for this port; the + // error/success classes style the text with -key-error/-key-success (appended CSS) + label.getStyleClass().addAll("join-message-row", + error ? "message-error" : "message-ok"); + label.setWrapText(true); + if (detail != null && !detail.isBlank()) { + label.setOnMouseClicked(e -> { + javafx.scene.control.Alert alert = + new javafx.scene.control.Alert( + javafx.scene.control.Alert.AlertType.INFORMATION, detail); + alert.setHeaderText("Problem Description"); + alert.initOwner(getScene() == null ? null : getScene().getWindow()); + alert.show(); + }); + } + getChildren().add(label); + } + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/join/JoinMergeVerifyF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/join/JoinMergeVerifyF.java new file mode 100644 index 00000000000..a48abad9529 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/join/JoinMergeVerifyF.java @@ -0,0 +1,554 @@ +/* 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.join; + +import java.util.ArrayList; +import java.util.List; +import javafx.application.Platform; +import javafx.stage.Stage; +import javafx.stage.Window; + +import de.uka.ilkd.key.control.ProofControl; +import de.uka.ilkd.key.gui.fx.mergerule.MergePartnerSelectionDialogF; +import de.uka.ilkd.key.gui.fx.mergerule.MergeRuleCompletionF; +import de.uka.ilkd.key.gui.fx.mergerule.MergeRuleMenuItemF; +import de.uka.ilkd.key.gui.fx.mergerule.predicateabstraction.AbstractionPredicatesChoiceDialogF; +import de.uka.ilkd.key.java.Services; +import de.uka.ilkd.key.logic.JTerm; +import de.uka.ilkd.key.logic.TermBuilder; +import de.uka.ilkd.key.logic.op.UpdateApplication; +import de.uka.ilkd.key.proof.Goal; +import de.uka.ilkd.key.proof.Proof; +import de.uka.ilkd.key.proof.join.JoinIsApplicable; +import de.uka.ilkd.key.proof.join.PredicateEstimator; +import de.uka.ilkd.key.proof.join.ProspectivePartner; +import de.uka.ilkd.key.rule.IBuiltInRuleApp; +import de.uka.ilkd.key.rule.merge.MergePartner; +import de.uka.ilkd.key.rule.merge.MergeRule; +import de.uka.ilkd.key.rule.merge.MergeRuleBuiltInRuleApp; + +import org.key_project.logic.PosInTerm; +import org.key_project.prover.sequent.PosInOccurrence; +import org.key_project.prover.sequent.SequentFormula; +import org.key_project.util.collection.ImmutableList; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Interactive self test for the join/merge dialog ports ({@code key.fx.verify.joinmerge=1}, + * forwarded by {@code key.ui.fx/build.gradle}). The dialogs are normally triggered by the rule + * completion dispatch / the sequent context menu, which do not exist in the JavaFX UI yet — this + * harness therefore constructs them directly: + *
    + *
  1. run a limited automatic strategy run on the loaded demo proof to create open + * branches stuck at the join/merge points;
  2. + *
  3. compute the join partners with the same core call the Swing context menu uses + * ({@code JoinIsApplicable.INSTANCE.isApplicable}, Swing {@code CurrentGoalViewMenu.java:235}); + *
  4. + *
  5. open the {@link JoinDialogF} non-blocking on the FX thread, drive the partner selection, + * the predicate input and the OK/Cancel actions programmatically, and compare the OK enablement + * with the Swing enablement logic (valid input AND applicable partner, Swing + * {@code JoinDialog.java:43,132-141});
  6. + *
  7. find a merge rule application of admissible form ({@link MergeRule#isApplicable}) with + * potential merge partners and check the forced (headless) completion of + * {@link MergeRuleCompletionF#INSTANCE};
  8. + *
  9. open the {@link MergePartnerSelectionDialogF} non-blocking, drive the candidate selection + * via the combo box and the "select as merge partner" checkbox, and check the OK/Choose-All + * enablement parity (Swing {@code checkApplicable}) plus the OK/cancel semantics;
  10. + *
  11. open the {@link AbstractionPredicatesChoiceDialogF} (test-mode constructor) and check its + * rendering and cancel semantics (the nested dialog of the predicate-abstraction merge + * procedure);
  12. + *
  13. log every assertion as a {@code ... PASS/FAIL} line and keep each dialog open for a few + * seconds so screenshots can be taken (each dialog in turn).
  14. + *
+ */ +public final class JoinMergeVerifyF { + + private static final Logger LOGGER = LoggerFactory.getLogger(JoinMergeVerifyF.class); + + /** Strategy step limit for the preparation auto mode run (key.fx.verify.joinmerge.maxsteps). */ + private static final int MAX_STEPS = Integer + .parseInt(System.getProperty("key.fx.verify.joinmerge.maxsteps", "40")); + + /** Seconds each dialog stays open for screenshots. */ + static final int SCREENSHOT_WINDOW_SECONDS = 14; + + private JoinMergeVerifyF() { + } + + /** + * Runs the self test. Must be called on the FX thread; the preparation auto mode runs on a + * background thread and the dialog checks are marshalled back with {@link Platform#runLater}. + * + * @param owner the owner window for the dialogs (may be {@code null}) + * @param proof the loaded proof + * @param proofControl the proof control (used for the preparation auto mode run and the menu + * item construction) + * @return a short summary (the detailed report is logged) + */ + public static String run(Window owner, Proof proof, ProofControl proofControl) { + if (proof == null) { + LOGGER.warn("JoinMerge verification: no proof loaded — SKIP"); + return "SKIP (no proof)"; + } + Thread worker = new Thread(() -> { + prepare(proof, proofControl); + List partners = computeJoinPartners(proof); + MergeAppData mergeApp = findMergeRuleApplication(proof); + verifyForcedMergeCompletion(proof, mergeApp); + Platform.runLater(() -> verifyJoinDialog(owner, proof, partners, () -> { + verifyMergePartnerDialog(owner, proof, proofControl, mergeApp, () -> { + verifyAbstractionPredicatesDialog(owner, proof); + }); + })); + }, "fx-verify-joinmerge"); + worker.setDaemon(true); + worker.start(); + return "running (see log)"; + } + + /** + * Runs a limited auto mode on the given proof to create open branches (the dialogs need at + * least two open goals; parity with the interactive scenario where the user stopped the auto + * mode before the join/merge point). + * + * @param proof the proof + * @param proofControl the proof control + */ + private static void prepare(Proof proof, ProofControl proofControl) { + try { + proof.getSettings().getStrategySettings().setMaxSteps(MAX_STEPS); + LOGGER.info("JoinMerge verification: preparation auto mode (max {} steps) started", + MAX_STEPS); + proofControl.startAndWaitForAutoMode(proof); + LOGGER.info("JoinMerge verification: preparation auto mode finished ({} open goals)", + proof.openGoals().size()); + } catch (RuntimeException e) { + LOGGER.warn("JoinMerge verification: preparation auto mode failed: {}", e.toString()); + } + } + + /** + * Computes the join partners with the core call of the Swing context menu + * ({@code JoinIsApplicable.INSTANCE.isApplicable}) over all open goals and their top-level + * succedent formulas. Returns an empty list if none exist. + * + * @param proof the proof + * @return the first non-empty partner list found + */ + static List computeJoinPartners(Proof proof) { + for (Goal goal : proof.openGoals()) { + var succ = goal.sequent().succedent(); + for (int i = 0; i < succ.size(); i++) { + PosInOccurrence pio = + new PosInOccurrence(succ.get(i), PosInTerm.getTopLevel(), false); + try { + List partners = + JoinIsApplicable.INSTANCE.isApplicable(goal, pio); + if (!partners.isEmpty()) { + LOGGER.info( + "JoinMerge verification: {} real join partners for goal {}", + partners.size(), goal.node().serialNr()); + return partners; + } + } catch (RuntimeException e) { + LOGGER.debug("JoinMerge verification: applicability check failed", e); + } + } + } + return List.of(); + } + + /** + * Builds a synthetic partner from the first two open leaf goals (fallback if no real join + * partners exist; the dialog is then exercised for rendering/interaction, not for a real + * join). + * + * @param proof the proof + * @return a singleton list with the synthetic partner, or an empty list + */ + static List syntheticPartner(Proof proof) { + List goals = new ArrayList<>(); + proof.openGoals().forEach(goals::add); + if (goals.size() < 2) { + return List.of(); + } + Goal g1 = goals.get(0); + Goal g2 = goals.get(1); + Services services = proof.getServices(); + TermBuilder tb = services.getTermBuilder(); + + SequentFormula sf1 = g1.sequent().succedent().get(0); + SequentFormula sf2 = g2.sequent().succedent().get(0); + JTerm referenceFormula = (JTerm) sf1.formula(); + JTerm update1 = tb.skip(); + if (referenceFormula.op() instanceof UpdateApplication) { + update1 = referenceFormula.sub(0); + referenceFormula = referenceFormula.sub(1); + } + JTerm update2 = tb.skip(); + JTerm formula2 = (JTerm) sf2.formula(); + if (formula2.op() instanceof UpdateApplication) { + update2 = formula2.sub(0); + formula2 = formula2.sub(1); + } + ProspectivePartner partner = new ProspectivePartner(referenceFormula, g1.node(), sf1, + update1, g2.node(), sf2, update2); + return List.of(partner); + } + + /** + * A merge rule application of admissible form with its potential merge partners, found by + * scanning the open goals (the same information the Swing context menu action has available + * when it is enabled). + */ + private record MergeAppData(Goal goal, PosInOccurrence pio, + ImmutableList candidates) { + } + + /** + * Scans the open goals for a merge rule application of admissible form + * ({@link MergeRule#isApplicable(Goal, PosInOccurrence)}) that has potential merge partners + * ({@link MergeRule#findPotentialMergePartners}). + * + * @param proof the proof + * @return the first suitable application found, or {@code null} + */ + private static MergeAppData findMergeRuleApplication(Proof proof) { + for (Goal goal : proof.openGoals()) { + var sequent = goal.sequent(); + for (boolean inAntec : new boolean[] { false, true }) { + var semi = inAntec ? sequent.antecedent() : sequent.succedent(); + for (int i = 0; i < semi.size(); i++) { + PosInOccurrence pio = + new PosInOccurrence(semi.get(i), PosInTerm.getTopLevel(), inAntec); + try { + if (!MergeRule.INSTANCE.isApplicable(goal, pio)) { + continue; + } + ImmutableList candidates = + MergeRule.findPotentialMergePartners(goal, pio); + if (!candidates.isEmpty()) { + LOGGER.info( + "JoinMerge verification: {} merge candidates for goal {}", + candidates.size(), goal.node().serialNr()); + return new MergeAppData(goal, pio, candidates); + } + } catch (RuntimeException e) { + LOGGER.debug("JoinMerge verification: merge applicability check failed", + e); + } + } + } + } + return null; + } + + /** + * Checks the forced (headless, no dialog) completion of the merge rule application + * (Swing MergeRuleCompletion.complete forced mode: all potential partners and the + * if-then-else merge method are chosen). + * + * @param proof the proof + * @param mergeApp the merge application data (may be {@code null}) + */ + private static void verifyForcedMergeCompletion(Proof proof, MergeAppData mergeApp) { + if (mergeApp == null) { + LOGGER.info( + "JoinMerge verification: merge-dialog SKIP (no admissible merge position with partners)"); + return; + } + try { + MergeRuleBuiltInRuleApp app = + (MergeRuleBuiltInRuleApp) MergeRule.INSTANCE.createApp(mergeApp.pio(), + proof.getServices()); + app.setMergeNode(mergeApp.goal().node()); + IBuiltInRuleApp completed = MergeRuleCompletionF.INSTANCE.complete(app, + mergeApp.goal(), true); + check("merge-completion forced completion returns an application", completed != null); + if (completed instanceof MergeRuleBuiltInRuleApp mergeResult) { + check("merge-completion forced completion selects all potential partners", + mergeResult.getMergePartners().size() == mergeApp.candidates().size()); + check("merge-completion forced completion chooses a concrete merge procedure", + mergeResult.getConcreteRule() != null); + } + } catch (RuntimeException e) { + LOGGER.error("JoinMerge verification: forced merge completion threw", e); + check("merge-completion forced completion runs without exception", false); + } + } + + /** + * Opens the join dialog with the given partners (or the synthetic fallback) and runs the + * assertions. Runs on the FX thread. + * + * @param owner the owner window + * @param proof the proof + * @param realPartners the real join partners (may be empty) + */ + private static void verifyJoinDialog(Window owner, Proof proof, + List realPartners, Runnable next) { + boolean real = !realPartners.isEmpty(); + List partners = real ? realPartners : syntheticPartner(proof); + if (partners.isEmpty()) { + LOGGER.info("JoinMerge verification: join-dialog SKIP (fewer than two open goals)"); + next.run(); + return; + } + try { + JoinDialogF dialog = new JoinDialogF(partners, proof, PredicateEstimator.STD_ESTIMATOR, + proof.getServices(), owner); + assertJoinDialog(dialog, proof, partners, real); + assertOkCancelSemantics(proof, partners); + LOGGER.info("JoinMerge verification: join dialog open for screenshot ({} s window)", + SCREENSHOT_WINDOW_SECONDS); + closeLater(dialog, SCREENSHOT_WINDOW_SECONDS, next); + } catch (RuntimeException e) { + LOGGER.error("JoinMerge verification: join-dialog FAIL (exception)", e); + next.run(); + } + } + + /** + * Runs the join dialog assertions. + * + * @param dialog the dialog under test + * @param proof the proof + * @param partners the partners the dialog was constructed with + * @param real whether the partners are real join partners (not synthetic) + */ + private static void assertJoinDialog(JoinDialogF dialog, Proof proof, + List partners, boolean real) { + check("join-dialog dialog rendered", dialog.getStageForVerification() != null); + check("join-dialog partner list has " + partners.size() + " entries", + dialog.getChoiceList().getItems().size() == partners.size()); + + var first = dialog.getChoiceList().getSelectionModel().getSelectedItem(); + check("join-dialog first partner preselected", first != null); + if (first == null) { + return; + } + check("join-dialog join-goal sequent rendered", + !dialog.getSequentViewer2().getText().isEmpty()); + check("join-dialog predicate info label", + first.getPredicateInfo().startsWith("Decision Formula")); + check("join-dialog predicate input shows estimated predicate", + dialog.getPredicateInput().getInput().equals(first.getPredicate(proof))); + check("join-dialog info box shows a message", + !dialog.getLastInfoMessage().getText().isEmpty()); + + // OK enablement parity (Swing JoinDialog.java:43,132-141): valid input AND applicable + // partner + boolean expectedOk = first.isApplicable() && !first.getPredicate(proof).isEmpty(); + check("join-dialog OK " + (expectedOk ? "enabled" : "disabled") + " for first partner", + dialog.getOkButton().isDisabled() != expectedOk); + + // selection change: the partner sequent view and the predicate input follow the choice + if (partners.size() > 1) { + String textBefore = dialog.getSequentViewer2().getText(); + dialog.getChoiceList().getSelectionModel().select(1); + var second = dialog.getChoiceList().getSelectionModel().getSelectedItem(); + check("join-dialog selection changed to second partner", + second != null && second != first); + check("join-dialog partner sequent follows selection", + !dialog.getSequentViewer2().getText().equals(textBefore) + || dialog.getSequentViewer2().getText() + .contains("Goal " + second.partner.getNode(1).serialNr())); + check("join-dialog predicate input follows selection", + dialog.getPredicateInput().getInput().equals(second.getPredicate(proof))); + dialog.getChoiceList().getSelectionModel().selectFirst(); + } + + // invalid input disables the OK button (reason shown in the details box) + DecisionPredicateInputF input = dialog.getPredicateInput(); + String validInput = input.getInput(); + input.setInput("&&&"); + check("join-dialog invalid formula disables OK", dialog.getOkButton().isDisabled()); + input.setInput(validInput); + check("join-dialog restoring the formula re-enables/disables OK accordingly", + dialog.getOkButton().isDisabled() != expectedOk); + } + + /** + * Runs the OK/cancel semantics assertions on a second (hidden) join dialog instance, so the + * screenshot dialog stays open. + * + * @param proof the proof + * @param partners the partners + */ + private static void assertOkCancelSemantics(Proof proof, List partners) { + JoinDialogF dialog = new JoinDialogF(partners, proof, PredicateEstimator.STD_ESTIMATOR, + proof.getServices(), null); + boolean okEnabled = !dialog.getOkButton().isDisabled(); + dialog.requestOk(); + check( + "join-dialog OK press " + + (okEnabled ? "confirms the dialog" : "ignored while disabled"), + dialog.okButtonHasBeenPressed() == okEnabled); + dialog.requestCancel(); + check("join-dialog cancel does not confirm", !dialog.okButtonHasBeenPressed()); + check("join-dialog cancel closes the dialog", + !dialog.getStageForVerification().isShowing()); + } + + /** + * Opens the merge partner selection dialog for the found merge application and runs the + * assertions. Runs on the FX thread. + * + * @param owner the owner window + * @param proof the proof + * @param proofControl the proof control (used for the menu item construction check) + * @param mergeApp the merge application data (may be {@code null}) + */ + private static void verifyMergePartnerDialog(Window owner, Proof proof, + ProofControl proofControl, MergeAppData mergeApp, Runnable next) { + if (mergeApp == null) { + return; // SKIP already logged by verifyForcedMergeCompletion + } + try { + MergePartnerSelectionDialogF dialog = new MergePartnerSelectionDialogF(mergeApp.goal(), + mergeApp.pio(), mergeApp.candidates(), proof.getServices(), owner); + dialog.showNonBlocking(); + assertMergePartnerDialog(dialog, proof, proofControl, mergeApp); + LOGGER.info( + "JoinMerge verification: merge partner dialog open for screenshot ({} s window)", + SCREENSHOT_WINDOW_SECONDS); + closeLater(dialog, SCREENSHOT_WINDOW_SECONDS, next); + } catch (RuntimeException e) { + LOGGER.error("JoinMerge verification: merge-dialog FAIL (exception)", e); + next.run(); + } + } + + /** + * Runs the merge partner selection dialog assertions. + * + * @param dialog the dialog under test + * @param proof the proof + * @param proofControl the proof control + * @param mergeApp the merge application data + */ + private static void assertMergePartnerDialog(MergePartnerSelectionDialogF dialog, Proof proof, + ProofControl proofControl, MergeAppData mergeApp) { + check("merge-dialog dialog rendered", dialog.getStageForVerification() != null); + check("merge-dialog candidate combo has " + mergeApp.candidates().size() + " entries", + dialog.getCmbCandidates().getItems().size() == mergeApp.candidates().size()); + check("merge-dialog first candidate preselected", + dialog.getCmbCandidates().getSelectionModel().getSelectedIndex() == 0); + check("merge-dialog merge state sequent rendered", + !dialog.getSequent1Flow().getChildren().isEmpty()); + check("merge-dialog partner sequent rendered", + !dialog.getSequent2Flow().getChildren().isEmpty()); + check("merge-dialog OK disabled before a partner is chosen", + dialog.getOkButton().isDisabled()); + check("merge-dialog distinguishing-formula field enabled iff single candidate", + dialog.getTxtDistForm().isDisabled() != (mergeApp.candidates().size() == 1)); + + // select the first candidate as merge partner (parity with the checkbox action) + dialog.getCbSelectCandidate().fire(); + check("merge-dialog OK enabled after partner chosen", !dialog.getOkButton().isDisabled()); + + // enablement parity (Swing checkApplicable) + check("merge-dialog Choose-All " + "enablement parity", + !dialog.getChooseAllButton().isDisabled()); + + // OK/cancel semantics on a hidden second instance (so the screenshot dialog stays open) + MergePartnerSelectionDialogF hidden = + new MergePartnerSelectionDialogF(mergeApp.goal(), mergeApp.pio(), + mergeApp.candidates(), proof.getServices(), null); + hidden.getCbSelectCandidate().fire(); + hidden.requestOk(); + check("merge-dialog OK press keeps the chosen candidates", + hidden.getChosenCandidates().size() == 1); + hidden.requestCancel(); + check("merge-dialog cancel yields no chosen candidates", + hidden.getChosenCandidates().isEmpty()); + + // the menu item trigger exists and reads as in the Swing original + MergeRuleMenuItemF item = new MergeRuleMenuItemF(mergeApp.goal(), mergeApp.pio(), + proofControl); + check("merge-dialog menu item is the State Merging Rule", + "State Merging Rule".equals(item.getText())); + } + + /** + * Opens the abstraction-predicates choice dialog (the nested dialog of the + * predicate-abstraction merge procedure) in test mode and checks its rendering and cancel + * semantics. Runs on the FX thread. + * + * @param owner the owner window + * @param proof the proof + */ + private static void verifyAbstractionPredicatesDialog(Window owner, Proof proof) { + try { + AbstractionPredicatesChoiceDialogF dialog = + new AbstractionPredicatesChoiceDialogF(proof.getServices(), owner); + dialog.showNonBlocking(); + Stage stage = dialog.getStageForVerification(); + check("predabst-dialog dialog rendered", stage != null); + if (stage != null) { + check("predabst-dialog title", + stage.getTitle().startsWith("Choose abstraction predicates")); + check("predabst-dialog content rendered", stage.getScene() != null + && stage.getScene().getRoot() != null); + } + LOGGER.info( + "JoinMerge verification: abstraction predicates dialog open for screenshot ({} s window)", + SCREENSHOT_WINDOW_SECONDS); + closeLater(dialog, SCREENSHOT_WINDOW_SECONDS, null); + // cancel semantics on a hidden instance (so the screenshot dialog stays open) + AbstractionPredicatesChoiceDialogF hidden = + new AbstractionPredicatesChoiceDialogF(proof.getServices(), null); + hidden.requestCancel(); + check("predabst-dialog cancel reports no registered predicates", + hidden.getResult().getRegisteredPredicates() == null); + } catch (RuntimeException e) { + LOGGER.error("JoinMerge verification: predabst-dialog FAIL (exception)", e); + } + } + + /** + * Keeps the given dialog open for the screenshot window and then closes it on the FX thread; + * afterwards the next verification step (if any) is run on the FX thread. + * + * @param dialog the dialog to close (must support {@code requestCancel}) + * @param seconds the window to keep the dialog open + * @param next the next verification step (may be {@code null}) + */ + private static void closeLater(Object dialog, int seconds, Runnable next) { + Thread closer = new Thread(() -> { + try { + Thread.sleep(seconds * 1000L); + } catch (InterruptedException ignored) { + Thread.currentThread().interrupt(); + } + Platform.runLater(() -> { + if (dialog instanceof JoinDialogF d) { + d.requestCancel(); + } else if (dialog instanceof MergePartnerSelectionDialogF d) { + d.requestCancel(); + } else if (dialog instanceof AbstractionPredicatesChoiceDialogF d) { + d.requestCancel(); + } + if (next != null) { + next.run(); + } + }); + }, "fx-verify-joinmerge-closer"); + closer.setDaemon(true); + closer.start(); + } + + /** + * Records one assertion result in the log. + * + * @param what the assertion description + * @param condition the assertion condition + */ + private static void check(String what, boolean condition) { + LOGGER.info("JoinMerge verification: {} ... {}", what, condition ? "PASS" : "FAIL"); + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/join/SequentViewerF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/join/SequentViewerF.java new file mode 100644 index 00000000000..00379c3f651 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/join/SequentViewerF.java @@ -0,0 +1,50 @@ +/* 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.join; + +import javafx.scene.control.TextArea; +import javafx.scene.text.Font; + +import de.uka.ilkd.key.java.Services; +import de.uka.ilkd.key.pp.LogicPrinter; +import de.uka.ilkd.key.pp.NotationInfo; + +import org.key_project.prover.sequent.Sequent; + +/** + * Read-only embedded sequent renderer used to show partner sequents inside the join dialog. + *

+ * Port of the Swing {@code de.uka.ilkd.key.gui.join.SequentViewer} (key.ui), a non-editable + * JTextPane fed by {@code LogicPrinter.purePrinter}: the FX port is a non-editable monospaced + * {@link TextArea} fed by the same printer. + * + * @author Benjamin Niedermann (original Swing component) + */ +public class SequentViewerF extends TextArea { + + public SequentViewerF() { + setEditable(false); + setFont(Font.font("Monospaced", 13)); + getStyleClass().add("join-sequent-view"); + } + + public void clear() { + setText(""); + } + + /** + * Prints the given sequent into the viewer (Swing {@code setSequent}). + * + * @param sequent the sequent to print + * @param services the services used for pretty printing + */ + public void setSequent(Sequent sequent, Services services) { + if (services != null) { + LogicPrinter printer = LogicPrinter.purePrinter(new NotationInfo(), services); + printer.printSequent(sequent); + setText(printer.result()); + } + } +} diff --git a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css index 173dd4a912f..1dec6808074 100644 --- a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css +++ b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css @@ -607,3 +607,63 @@ .sequent-search-mode { -fx-padding: 1; } + +/* ------------------------------------------------------------------ + join / merge rule dialogs (key.fx.verify.joinmerge; Swing + gui/join/SequentViewer.java + gui/mergerule/MergePartnerSelectionDialog.java) + ------------------------------------------------------------------ */ + +/* read-only sequent panes of the join dialog and the merge partner dialog + (Swing JTextPane / JEditorPane with the monospaced proof font) */ +.join-sequent-view { + -fx-font-family: "DejaVu Sans Mono"; + -fx-font-size: 12px; +} + +/* the details box rows of the join dialog (Swing ClickableMessageBox rows) */ +.join-message-row { + -fx-cursor: hand; + -fx-padding: 1 4 1 4; +} +.join-details-box { + -fx-background-color: transparent; + -fx-padding: 2; +} + +/* the "with" separator and the decision-predicate info label of the join dialog + (Swing: gray JLabels) */ +.join-with-label { + -fx-font-weight: bold; + -fx-text-fill: -key-text-secondary; +} +.join-predicate-info { + -fx-text-fill: -key-text-secondary; +} + +/* message states of the join dialog info box (Swing ClickableMessageBox colors) */ +.message-ok { + -fx-text-fill: -key-success; +} +.message-error { + -fx-text-fill: -key-error; +} + +/* the checked predicate input traffic light (Swing TrafficLight becomes a + colored dot label) */ +.traffic-light { + -fx-background-radius: 5; + -fx-max-width: 10; + -fx-max-height: 10; +} +.traffic-light-ok { + -fx-background-color: -key-success; +} +.traffic-light-error { + -fx-background-color: -key-error; +} + +/* invalid distinguishing-formula input in the merge partner dialog + (Swing: red foreground on the text field) */ +.join-input-error { + -fx-text-fill: -key-error; +} diff --git a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css index 2962a642bf4..d37fe9521e5 100644 --- a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css +++ b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css @@ -601,3 +601,68 @@ .sequent-search-mode { -fx-padding: 1; } + +/* ------------------------------------------------------------------ + join / merge rule dialogs (key.fx.verify.joinmerge; Swing + gui/join/SequentViewer.java + gui/mergerule/MergePartnerSelectionDialog.java) + ------------------------------------------------------------------ */ + +/* read-only sequent panes of the join dialog and the merge partner dialog + (Swing JTextPane / JEditorPane with the monospaced proof font) */ +.join-sequent-view { + -fx-font-family: "DejaVu Sans Mono"; + -fx-font-size: 12px; +} + +/* invalid input in the distinguishing-formula field of the merge partner + dialog (Swing: red foreground) */ +.join-input-error { + -fx-text-fill: -key-error; +} + +/* the "Decision Formula (true for Goal ..., false for Goal ...)" info label + of the join dialog (Swing predicate info label) */ +.join-predicate-info { + -fx-text-fill: -key-text-secondary; + -fx-padding: 2 0 2 0; +} + +/* the "with" separator between the two join partner panes */ +.join-with-label { + -fx-font-weight: bold; + -fx-padding: 0 4 0 0; +} + +/* join message box states (Swing ClickableMessageBox: red/green text) */ +.message-error { + -fx-text-fill: -key-error; +} +.message-ok { + -fx-text-fill: -key-success; +} + +/* the traffic light of the checked predicate input (Swing TrafficLight) */ +.traffic-light { + -fx-background-color: -key-border; + -fx-background-radius: 6; + -fx-min-width: 12; + -fx-max-width: 12; + -fx-min-height: 12; + -fx-max-height: 12; +} +.traffic-light-ok { + -fx-background-color: -key-success; +} +.traffic-light-error { + -fx-background-color: -key-error; +} + +/* the details box rows of the join dialog (Swing ClickableMessageBox rows) */ +.join-message-row { + -fx-cursor: hand; + -fx-padding: 1 4 1 4; +} +.join-details-box { + -fx-background-color: transparent; + -fx-padding: 2; +} From 07f9e08ae5caaeb0012c7f7cdc527cc6b7ddfbc2 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Thu, 8 Oct 2026 22:18:16 +0200 Subject: [PATCH 068/281] key.ui.fx: tacletmatch: self test prefers proposable skolem applications (deeper candidates via getTacletAppAtAndBelow) --- .../fx/tacletmatch/TacletMatchVerifyF.java | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/tacletmatch/TacletMatchVerifyF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/tacletmatch/TacletMatchVerifyF.java index c711fcb999c..2018300922a 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/tacletmatch/TacletMatchVerifyF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/tacletmatch/TacletMatchVerifyF.java @@ -148,8 +148,13 @@ private static TacletMatchDialogF openDialog(AbstractProofControl control, Tacle /** * finds a taclet application on the given goal whose instantiation is incomplete (the condition * under which the Swing term menu routes to the instantiation dialog, - * AbstractProofControl.java:226-258). Preferences, in order: a find-taclet app with a match - * position and no {@code \assumes} (exercises the match highlighting and the plain apply flow), + * AbstractProofControl.java:226-258) and that the dialog can complete without typed user + * input: the models pre-fill a proposal for every remaining (skolem/variable) schema variable + * (TacletFindModel.java:121-138), so an application with {@code completeExceptSkolemConstants + * () == false} still has unproposable (formula) inputs and would fail the apply step. + * Preferences, in order: a find-taclet app with a match position, no {@code \assumes} and only + * proposable inputs left (exercises the match highlighting and the plain apply flow — e.g. an + * {@code \existsRight} skolem constant), then any find-taclet app without {@code \assumes}, * then any app without {@code \assumes}, then any incomplete app. */ private static TacletApp findIncompleteTacletApp(Goal goal, Services services) { @@ -161,11 +166,11 @@ private static TacletApp findIncompleteTacletApp(Goal goal, Services services) { for (int i = 1; i <= seq.size(); i++) { PosInOccurrence pio = PosInOccurrence.findInSequent(seq, i, PosInTerm.getTopLevel()); ImmutableList apps = - goal.ruleAppIndex().getTacletAppAt(TacletFilter.TRUE, pio, services); + goal.ruleAppIndex().getTacletAppAtAndBelow(TacletFilter.TRUE, pio, services); apps.forEach(candidates::add); } - TacletApp findNeedsFull = null; + TacletApp findProposable = null; TacletApp findWithoutAssumes = null; TacletApp withoutAssumes = null; TacletApp any = null; @@ -173,26 +178,24 @@ private static TacletApp findIncompleteTacletApp(Goal goal, Services services) { if (app.complete()) { continue; } + boolean proposable = app.completeExceptSkolemConstants(); boolean hasAssumes = !app.taclet().assumesSequent().isEmpty(); - // applications needing a full instantiation (not just skolem constants) are the ones - // the Swing term menu routes to the instantiation dialog - boolean needsFull = !app.completeExceptSkolemConstants(); if (app.posInOccurrence() != null && !hasAssumes) { - if (needsFull && findNeedsFull == null) { - findNeedsFull = app; + if (proposable && findProposable == null) { + findProposable = app; } if (findWithoutAssumes == null) { findWithoutAssumes = app; } } - if (!hasAssumes && withoutAssumes == null) { + if (proposable && !hasAssumes && withoutAssumes == null) { withoutAssumes = app; } if (any == null) { any = app; } } - return findNeedsFull != null ? findNeedsFull + return findProposable != null ? findProposable : findWithoutAssumes != null ? findWithoutAssumes : withoutAssumes != null ? withoutAssumes : any; } From 95afd8b47288f93ecd0764b06eb06de6285da20b Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Thu, 8 Oct 2026 22:19:38 +0200 Subject: [PATCH 069/281] key.ui.fx: seam: IssueDialogF.wasShown survives the modal dismissal (self test reports after OK) --- .../de/uka/ilkd/key/gui/fx/IssueDialogF.java | 23 ++++++++++++++++++- .../ilkd/key/gui/fx/UiControlSelfTestF.java | 4 ++-- 2 files changed, 24 insertions(+), 3 deletions(-) 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 index fb8794b663c..efed6b8c6e1 100644 --- 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 @@ -138,6 +138,16 @@ public IssueEntry(PositionedString ps, String additionalInfo, Kind kind) { 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; + /** + * whether this dialog was shown at least once (self-test hook: {@code isVisible()} is + * {@code false} again after a modal dialog was dismissed) + */ + private boolean shown; private IssueDialogF(Window owner, String title, String head, Collection issueSet, boolean critical) { @@ -351,6 +361,7 @@ private void accept() { * @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()); @@ -483,11 +494,20 @@ public static Optional getLastDialog() { return Optional.ofNullable(lastDialog); } - /** @return {@code true} while the dialog is shown (self-test hook) */ + /** @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); @@ -495,6 +515,7 @@ public List getIssues() { /** Forwards to the underlying dialog (this class wraps a {@link Dialog}). */ public void showAndWait() { + shown = true; dialog.showAndWait(); } } 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 index 43c8461d445..8e9adef12c5 100644 --- 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 @@ -53,12 +53,12 @@ public static void run(MainWindowF mainWindow) { new IllegalStateException("Seam self test: synthetic exception")); FxUtil.runLater(() -> { IssueDialogF dialog = IssueDialogF.getLastDialog().orElse(null); - boolean dialogPass = dialog != null && dialog.isVisible() + 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 visible=" + dialog.isVisible() + ", issues=" + : "IssueDialogF wasShown=" + dialog.wasShown() + ", issues=" + dialog.getIssues().size()); // 3. log view: emitted log lines must be rendered by the LogViewF From e72923a145898e146e51f6fe0bed6f0fc972b3d5 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Thu, 8 Oct 2026 22:21:30 +0200 Subject: [PATCH 070/281] key.ui.fx: seam: remove duplicated shown field declaration --- .../src/main/java/de/uka/ilkd/key/gui/fx/IssueDialogF.java | 5 ----- 1 file changed, 5 deletions(-) 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 index efed6b8c6e1..8db00fcc614 100644 --- 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 @@ -143,11 +143,6 @@ public IssueEntry(PositionedString ps, String additionalInfo, Kind kind) { * {@code false} again as soon as a modal dialog is dismissed) */ private boolean shown; - /** - * whether this dialog was shown at least once (self-test hook: {@code isVisible()} is - * {@code false} again after a modal dialog was dismissed) - */ - private boolean shown; private IssueDialogF(Window owner, String title, String head, Collection issueSet, boolean critical) { From 92a6ea057d9a5f758a142a872e77e73ed4f28a98 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Thu, 8 Oct 2026 22:25:02 +0200 Subject: [PATCH 071/281] key.ui.fx: tacletmatch: self test asserts the classic table dialog (render, cancel) and shows it; hold-classic screenshot mode --- .../fx/tacletmatch/TacletMatchVerifyF.java | 47 +++++++++++++++++-- .../classic/TacletMatchCompletionDialogF.java | 15 +++++- 2 files changed, 55 insertions(+), 7 deletions(-) diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/tacletmatch/TacletMatchVerifyF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/tacletmatch/TacletMatchVerifyF.java index 2018300922a..9d620a081d3 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/tacletmatch/TacletMatchVerifyF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/tacletmatch/TacletMatchVerifyF.java @@ -11,6 +11,7 @@ import de.uka.ilkd.key.control.AbstractProofControl; import de.uka.ilkd.key.control.ProofControl; import de.uka.ilkd.key.control.instantiation_model.TacletInstantiationModel; +import de.uka.ilkd.key.gui.fx.tacletmatch.classic.TacletMatchCompletionDialogF; import de.uka.ilkd.key.java.Services; import de.uka.ilkd.key.pp.NotationInfo; import de.uka.ilkd.key.proof.Goal; @@ -36,14 +37,17 @@ *

  • the dialog renders with the match highlighted (status line + schema-variable spans in the * matched term);
  • *
  • cancel closes the dialog and leaves the proof unchanged (node count stable);
  • - *
  • apply closes the dialog and adds the application to the proof (node count changes).
  • + *
  • apply closes the dialog and adds the application to the proof (node count changes);
  • + *
  • the classic (table-based) completion dialog renders the instantiation table and + * {@code cancelAndClose()} leaves the proof unchanged (assertion 4, port deliverable 2).
  • * * Each assertion logs {@code PASS}/{@code FAIL} (and is surfaced as a toast by MainWindowF). * *

    * The value of {@code key.fx.verify.tacletmatch} selects the mode: {@code 1} (default) runs the - * full three-assertion flow; {@code hold} runs only the render assertion and leaves the dialog open - * for interactive screenshots (light/dark theme checks). + * full four-assertion flow; {@code hold} runs only the render assertion of the redesigned dialog + * and leaves it open for interactive screenshots; {@code hold-classic} does the same for the + * classic (table-based) dialog (light/dark theme checks). */ public final class TacletMatchVerifyF { @@ -59,15 +63,16 @@ public static void runTacletMatchVerification(Proof proof, ProofControl proofCon Stage owner, NotationInfo notationInfo) { String mode = System.getProperty("key.fx.verify.tacletmatch", "1").trim(); boolean hold = "hold".equalsIgnoreCase(mode); + boolean holdClassic = "hold-classic".equalsIgnoreCase(mode); try { - run(proof, proofControl, owner, notationInfo, hold); + run(proof, proofControl, owner, notationInfo, hold, holdClassic); } catch (Throwable t) { LOGGER.error("tacletmatch verification: FAIL (unexpected error: {})", t, t); } } private static void run(Proof proof, ProofControl proofControl, Stage owner, - NotationInfo notationInfo, boolean hold) { + NotationInfo notationInfo, boolean hold, boolean holdClassic) { Services services = proof.getServices(); Goal goal = proof.openEnabledGoals().head(); AbstractProofControl control = (AbstractProofControl) proofControl; @@ -136,6 +141,38 @@ private static void run(Proof proof, ProofControl proofControl, Stage owner, + "requires \\assumes instantiation, which needs typed user input)", chosen.taclet().name()); } + + // ---- assertion 4: the classic (table-based) completion dialog renders and cancels ---- + try { + TacletInstantiationModel[] classicModels = + control.completeAndApplyApp(List.of(chosen), goal); + TacletMatchCompletionDialogF classic = + new TacletMatchCompletionDialogF(owner, classicModels, goal, services, notationInfo, + proofControl); + if (holdClassic) { + LOGGER.info( + "tacletmatch verification: hold-classic mode — classic dialog left open for " + + "screenshots ({} instantiation row(s))", + classic.tableRowCount()); + return; + } + boolean classicRendered = + classic.isShowing() && classic.tableRowCount() > 0; + LOGGER.info( + "tacletmatch verification: classic-dialog-renders: {} ({} instantiation row(s), " + + "showing={})", + classicRendered ? "PASS" : "FAIL", classic.tableRowCount(), classic.isShowing()); + int nodesBeforeClassic = proof.countNodes(); + classic.cancelAndClose(); + boolean classicCancelled = + !classic.isShowing() && proof.countNodes() == nodesBeforeClassic; + LOGGER.info("tacletmatch verification: classic-cancel: {} (node count before={}, " + + "after={})", classicCancelled ? "PASS" : "FAIL", nodesBeforeClassic, + proof.countNodes()); + } catch (Throwable t) { + LOGGER.info("tacletmatch verification: classic-dialog: FAIL (unexpected error: {})", + t.toString()); + } } private static TacletMatchDialogF openDialog(AbstractProofControl control, TacletApp app, diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/tacletmatch/classic/TacletMatchCompletionDialogF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/tacletmatch/classic/TacletMatchCompletionDialogF.java index 11647bca985..57a861505b1 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/tacletmatch/classic/TacletMatchCompletionDialogF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/tacletmatch/classic/TacletMatchCompletionDialogF.java @@ -119,10 +119,11 @@ public TacletMatchCompletionDialogF(Window owner, TacletInstantiationModel[] mod setScene(scene); setMinWidth(620); setMinHeight(480); + setStatus(model[current()].getStatusString()); if (owner != null) { - setOnShown(e -> centerOn(owner)); + centerOn(owner); } - setStatus(model[current()].getStatusString()); + show(); LOGGER.info("TacletMatchCompletionDialogF opened: alternatives={} taclet={}", model.length, model[0].taclet().name()); } @@ -393,6 +394,16 @@ public void cancelAndClose() { closeDialog(); } + /** + * the number of instantiation rows of the current alternative (used by the self-test hook to + * assert the table renders) + */ + public int tableRowCount() { + return current < dataTables.length && dataTables[current] != null + ? dataTables[current].getItems().size() + : 0; + } + /** one row of the instantiation table (Swing {@code DataTable} row) */ private static final class Row { private final String varName; From bf4f2f8ab3b50c0e3f51137d1cd69a061b481b21 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Thu, 8 Oct 2026 22:26:34 +0200 Subject: [PATCH 072/281] key.ui.fx: joinmerge: fix verify chain and add merge scan diagnostics --- .../key/gui/fx/join/JoinMergeVerifyF.java | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/join/JoinMergeVerifyF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/join/JoinMergeVerifyF.java index a48abad9529..ca4051287b7 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/join/JoinMergeVerifyF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/join/JoinMergeVerifyF.java @@ -19,6 +19,7 @@ import de.uka.ilkd.key.logic.JTerm; import de.uka.ilkd.key.logic.TermBuilder; import de.uka.ilkd.key.logic.op.UpdateApplication; +import de.uka.ilkd.key.pp.LogicPrinter; import de.uka.ilkd.key.proof.Goal; import de.uka.ilkd.key.proof.Proof; import de.uka.ilkd.key.proof.join.JoinIsApplicable; @@ -220,17 +221,28 @@ private record MergeAppData(Goal goal, PosInOccurrence pio, private static MergeAppData findMergeRuleApplication(Proof proof) { for (Goal goal : proof.openGoals()) { var sequent = goal.sequent(); + LOGGER.info("JoinMerge verification: scanning goal {} for merge positions", + goal.node().serialNr()); for (boolean inAntec : new boolean[] { false, true }) { var semi = inAntec ? sequent.antecedent() : sequent.succedent(); for (int i = 0; i < semi.size(); i++) { PosInOccurrence pio = new PosInOccurrence(semi.get(i), PosInTerm.getTopLevel(), inAntec); try { - if (!MergeRule.INSTANCE.isApplicable(goal, pio)) { + boolean applicable = MergeRule.INSTANCE.isApplicable(goal, pio); + LOGGER.info("JoinMerge verification: goal {} formula {} applicable={}", + goal.node().serialNr(), + LogicPrinter.quickPrintTerm((JTerm) semi.get(i).formula(), + proof.getServices()), + applicable); + if (!applicable) { continue; } ImmutableList candidates = MergeRule.findPotentialMergePartners(goal, pio); + LOGGER.info( + "JoinMerge verification: goal {} merge position has {} candidates", + goal.node().serialNr(), candidates.size()); if (!candidates.isEmpty()) { LOGGER.info( "JoinMerge verification: {} merge candidates for goal {}", @@ -238,8 +250,8 @@ private static MergeAppData findMergeRuleApplication(Proof proof) { return new MergeAppData(goal, pio, candidates); } } catch (RuntimeException e) { - LOGGER.debug("JoinMerge verification: merge applicability check failed", - e); + LOGGER.info("JoinMerge verification: merge applicability check failed: " + + e.toString()); } } } @@ -406,7 +418,8 @@ private static void assertOkCancelSemantics(Proof proof, List Date: Thu, 8 Oct 2026 22:37:03 +0200 Subject: [PATCH 073/281] key.ui.fx: docking: make the shutdown layout save robust - cache the arrangement on the FX thread and persist the cached snapshot if the scene graph is already dying; hook now catches Throwable and traces to stderr --- .../key/gui/fx/docking/DockWorkspace.java | 21 +++++++++++++++++-- .../key/gui/fx/docking/DockingLayoutF.java | 6 ++++-- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/docking/DockWorkspace.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/docking/DockWorkspace.java index 31fcf2dc6ce..57d95df763a 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/docking/DockWorkspace.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/docking/DockWorkspace.java @@ -91,6 +91,12 @@ public interface DockableFactory { private EnumMap> preMaximizedDockables; private EnumMap preMaximizedSelection; + /** + * Last known arrangement, refreshed on every change; read by the shutdown hook because + * the live scene graph must not be touched from a non-FX thread at JVM shutdown. + */ + private volatile Map> lastSnapshot = Map.of(); + /** * Creates an empty workspace with three role areas. */ @@ -468,17 +474,26 @@ public Map> snapshotIds() { ids.put(location, dockables.get(location).stream().map(Dockable::getId).toList()); } + lastSnapshot = ids; return ids; } /** - * Persists the current layout through the given store. + * Persists the current layout through the given store. If the live model cannot be read + * (e.g. the JavaFX toolkit is already shutting down), the last known arrangement is + * persisted instead. * * @param store the store to write to * @throws IOException on I/O errors */ public void saveLayout(DockLayoutStore store) throws IOException { - store.save(snapshotIds()); + Map> snapshot; + try { + snapshot = snapshotIds(); + } catch (RuntimeException e) { + snapshot = lastSnapshot; + } + store.save(snapshot); } /** @@ -580,6 +595,8 @@ private ListChangeListener dockablesChanged(DockLocation location) { } private void updateVisibility(DockLocation location) { + // docking: refresh the shutdown-hook snapshot on every arrangement change + lastSnapshot = snapshotIds(); if (location == DockLocation.MAIN) { return; } diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/docking/DockingLayoutF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/docking/DockingLayoutF.java index 38a7cb4699c..8fe05ac483f 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/docking/DockingLayoutF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/docking/DockingLayoutF.java @@ -86,8 +86,10 @@ public void install(Scene scene) { try { workspace.saveLayout(store); LOGGER.info("Docking layout saved to {}", store.file()); - } catch (IOException e) { - LOGGER.warn("Failed to save the docking layout", e); + } catch (Throwable t) { + // the shutdown save must never abort the JVM exit; logback may already be down + LOGGER.warn("Failed to save the docking layout", t); + t.printStackTrace(System.err); } }, "fx-docking-shutdown")); } From 45767eb433fb2240a92f79d8a21f1ff3684b278f Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Thu, 8 Oct 2026 22:39:02 +0200 Subject: [PATCH 074/281] key.ui.fx: tacletmatch: run the classic-dialog assertion before the destructive apply step --- .../fx/tacletmatch/TacletMatchVerifyF.java | 47 ++++++++++--------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/tacletmatch/TacletMatchVerifyF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/tacletmatch/TacletMatchVerifyF.java index 9d620a081d3..8d3aeada401 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/tacletmatch/TacletMatchVerifyF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/tacletmatch/TacletMatchVerifyF.java @@ -37,9 +37,10 @@ *

  • the dialog renders with the match highlighted (status line + schema-variable spans in the * matched term);
  • *
  • cancel closes the dialog and leaves the proof unchanged (node count stable);
  • - *
  • apply closes the dialog and adds the application to the proof (node count changes);
  • *
  • the classic (table-based) completion dialog renders the instantiation table and - * {@code cancelAndClose()} leaves the proof unchanged (assertion 4, port deliverable 2).
  • + * {@code cancelAndClose()} leaves the proof unchanged (port deliverable 2); + *
  • apply closes the dialog and adds the application to the proof (node count changes; last, as + * it modifies the proof).
  • * * Each assertion logs {@code PASS}/{@code FAIL} (and is surfaced as a toast by MainWindowF). * @@ -125,24 +126,9 @@ private static void run(Proof proof, ProofControl proofControl, Stage owner, + "after={})", cancelled ? "PASS" : "FAIL", nodesBefore, proof.countNodes()); - // ---- assertion 3: apply closes the dialog and adds the application to the proof ------ - if (chosen.taclet().assumesSequent().isEmpty()) { - // the schema-variable rows carry the model's pre-filled proposals, so the plain apply - // flow (push input → createTacletApp → applyInteractive) completes the instantiation - TacletMatchDialogF applyDlg = openDialog(control, chosen, goal, owner, proofControl, - services, notationInfo); - applyDlg.fireApply(); - boolean applied = !applyDlg.isShowing() && proof.countNodes() != nodesBefore; - LOGGER.info("tacletmatch verification: apply-adds-to-proof: {} (node count before={}, " - + "after={})", applied ? "PASS" : "FAIL", nodesBefore, proof.countNodes()); - } else { - LOGGER.info( - "tacletmatch verification: apply-adds-to-proof: SKIPPED (chosen taclet {} " - + "requires \\assumes instantiation, which needs typed user input)", - chosen.taclet().name()); - } - - // ---- assertion 4: the classic (table-based) completion dialog renders and cancels ---- + // ---- assertion 3: the classic (table-based) completion dialog renders and cancels ---- + // (non-destructive, so it runs before the apply step; building the models against the + // proof state the dialog flow would actually see) try { TacletInstantiationModel[] classicModels = control.completeAndApplyApp(List.of(chosen), goal); @@ -156,8 +142,7 @@ private static void run(Proof proof, ProofControl proofControl, Stage owner, classic.tableRowCount()); return; } - boolean classicRendered = - classic.isShowing() && classic.tableRowCount() > 0; + boolean classicRendered = classic.isShowing() && classic.tableRowCount() > 0; LOGGER.info( "tacletmatch verification: classic-dialog-renders: {} ({} instantiation row(s), " + "showing={})", @@ -173,6 +158,24 @@ private static void run(Proof proof, ProofControl proofControl, Stage owner, LOGGER.info("tacletmatch verification: classic-dialog: FAIL (unexpected error: {})", t.toString()); } + + // ---- assertion 4 (last, it changes the proof): apply closes the dialog and adds the + // application to the proof ------------------------------------------------------------ + if (chosen.taclet().assumesSequent().isEmpty()) { + // the schema-variable rows carry the model's pre-filled proposals, so the plain apply + // flow (push input → createTacletApp → applyInteractive) completes the instantiation + TacletMatchDialogF applyDlg = openDialog(control, chosen, goal, owner, proofControl, + services, notationInfo); + applyDlg.fireApply(); + boolean applied = !applyDlg.isShowing() && proof.countNodes() != nodesBefore; + LOGGER.info("tacletmatch verification: apply-adds-to-proof: {} (node count before={}, " + + "after={})", applied ? "PASS" : "FAIL", nodesBefore, proof.countNodes()); + } else { + LOGGER.info( + "tacletmatch verification: apply-adds-to-proof: SKIPPED (chosen taclet {} " + + "requires \\assumes instantiation, which needs typed user input)", + chosen.taclet().name()); + } } private static TacletMatchDialogF openDialog(AbstractProofControl control, TacletApp app, From 62bd5ff733fba7c1248ef855811b23cfc440fdec Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Thu, 8 Oct 2026 22:20:07 +0200 Subject: [PATCH 075/281] key.ui.fx: lemmaorigin: origin term label visualizer (OriginTermLabelVisualizerF) + node info visualizer registry (NodeInfoVisualizerF) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Port of Swing de.uka.ilkd.key.gui.NodeInfoVisualizer (+ NodeInfoVisualizerListener) as the abstract Stage subclass NodeInfoVisualizerF in the new package de.uka.ilkd.key.gui.fx.nodeinfo: the same static instance registry (per proof name and node serial number, SortedSet ordered by long name), register/unregister, hasInstances/getInstances and the registered/unregistered listeners; dispose() unregisters and closes. Deviation: the Swing visualizers are hosted inside the source view frame; the FX port uses free windows owned by the main window (same presentation as ProofDiffFrameF). Port of Swing de.uka.ilkd.key.gui.originlabels.OriginTermLabelVisualizer as OriginTermLabelVisualizerF: head pane with the live node link (selects the node, with the Swing confirmation dialog when another proof is selected; DELETED NODE + unregister when the node vanishes) and proof name; origin tree (term short text left, origin specification type right, tooltip with the origin and the origins of the (former) sub-terms) and the term view printing only the selected formula (TermViewLogicPrinter port: no sequent arrow when a formula is selected) or the whole sequent, with all term labels except the origin labels. Tree selection highlights the term in the view (CSS class origin-visualizer-highlight, Swing orange; appended to both theme stylesheets with existing variables), a click in the view selects the tree row. 'Show Origin' (View▸Origin Tracking; in Swing a sequent context-menu contribution of OriginTermLabelsExt) opens the visualizer for the last clicked sequent view term, walked up to a formula. The key.fx.verify.lemmaorigin self test now also opens/disposes a visualizer and checks the registry semantics. Deviations: HTML tooltips become plain multi-line text; the Swing JSplitPane initial divider location (1.0, tree collapsed) is not reproduced; the rule-app listener of the Swing node link is replaced by the proof-tree listener (same information for the DELETED NODE check). --- .../gui/fx/nodeinfo/NodeInfoVisualizerF.java | 184 +++++ .../nodeinfo/NodeInfoVisualizerListenerF.java | 29 + .../gui/fx/originlabels/OriginLabelsF.java | 124 ++- .../OriginTermLabelVisualizerF.java | 716 ++++++++++++++++++ .../de/uka/ilkd/key/gui/fx/theme/key-dark.css | 8 + .../uka/ilkd/key/gui/fx/theme/key-light.css | 8 + 6 files changed, 1063 insertions(+), 6 deletions(-) create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeinfo/NodeInfoVisualizerF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeinfo/NodeInfoVisualizerListenerF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/originlabels/OriginTermLabelVisualizerF.java diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeinfo/NodeInfoVisualizerF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeinfo/NodeInfoVisualizerF.java new file mode 100644 index 00000000000..9402127ec69 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeinfo/NodeInfoVisualizerF.java @@ -0,0 +1,184 @@ +/* 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.nodeinfo; + +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeMap; +import java.util.TreeSet; +import javafx.stage.Stage; + +import de.uka.ilkd.key.proof.Node; +import de.uka.ilkd.key.proof.Proof; + +import org.key_project.logic.Name; + +/** + * JavaFX port of the Swing {@code de.uka.ilkd.key.gui.NodeInfoVisualizer}: a UI component + * (window) showing additional information about a {@link Node} in the current {@link Proof}. + *

    + * The port keeps the Swing original's static registry semantics one to one + * ({@code de.uka.ilkd.key.gui.NodeInfoVisualizer}): instances are collected per proof name and + * node serial number ({@link #getInstances(Node)}, {@link #hasInstances(Node)}); listeners + * ({@link NodeInfoVisualizerListenerF}) are notified whenever a visualizer is registered or + * unregistered. The Swing base class is an abstract {@code JComponent} hosted by the source + * view frame ({@code SourceViewFrame.addComponent}); the FX counterpart is an abstract + * {@link Stage} — the Swing docking-based hosting has no FX equivalent yet, so a visualizer is + * a free window owned by the main window (the same presentation as the ported + * {@code ProofDiffFrameF}). + *

    + * Subclasses must call {@link #dispose()} when they close (Swing {@code dispose()}), which + * unregisters the instance and fires the listener. + * + * @author lanzinger (Swing original), the key.ui.fx team (port) + */ +public abstract class NodeInfoVisualizerF extends Stage implements Comparable { + + /** @see #getInstances(Node) */ + private static final Map>> instances = + new HashMap<>(); + + /** @see #addListener(NodeInfoVisualizerListenerF) */ + private static final Set listeners = new HashSet<>(); + + /** @see #getNode() */ + private Node node; + + /** @see #getLongName() */ + private final String longName; + + /** @see #getShortName() */ + private final String shortName; + + /** + * Creates a new {@code NodeInfoVisualizerF} and registers it in the static instance registry. + * + * @param node the node this visualizer is associated with + * @param longName the visualizer's long name + * @param shortName the visualizer's short name + */ + protected NodeInfoVisualizerF(Node node, String longName, String shortName) { + this.node = node; + this.longName = longName; + this.shortName = shortName; + register(this); + } + + /** + * @return {@code true} iff there are any open visualizers associated with the specified node + * (Swing {@code NodeInfoVisualizer.hasInstances}) + */ + public static boolean hasInstances(Node node) { + return !getInstances(node).isEmpty(); + } + + /** + * @return the set of open visualizers associated with the specified node (Swing + * {@code NodeInfoVisualizer.getInstances}) + */ + public static SortedSet getInstances(Node node) { + return Collections.unmodifiableSortedSet(instances + .getOrDefault(node.proof().name(), Collections.emptyMap()) + .getOrDefault(node.serialNr(), Collections.emptySortedSet())); + } + + /** + * Adds a listener (Swing {@code NodeInfoVisualizer.addListener}). + * + * @param listener the listener to add + */ + public static void addListener(NodeInfoVisualizerListenerF listener) { + listeners.add(listener); + } + + /** + * Removes a listener (Swing {@code NodeInfoVisualizer.removeListener}). + * + * @param listener the listener to remove + */ + public static void removeListener(NodeInfoVisualizerListenerF listener) { + listeners.remove(listener); + } + + /** + * Ensures that the specified visualizer will not be returned by {@link #getInstances(Node)} + * (Swing {@code unregister}); fires {@code visualizerUnregistered} if it was registered. + */ + protected static void unregister(NodeInfoVisualizerF vis) { + Node node = vis.getNode(); + if (node == null) { + return; + } + Map> map = instances.get(node.proof().name()); + boolean removed = map != null + && map.getOrDefault(node.serialNr(), Collections.emptySortedSet()).remove(vis); + if (removed) { + synchronized (listeners) { + for (NodeInfoVisualizerListenerF listener : listeners) { + listener.visualizerUnregistered(vis); + } + } + } + } + + private static void register(NodeInfoVisualizerF vis) { + Node node = vis.getNode(); + int nodeNr = node.serialNr(); + Name proofName = node.proof().name(); + + instances.putIfAbsent(proofName, new TreeMap<>()); + Map> map = instances.get(proofName); + map.putIfAbsent(nodeNr, new TreeSet<>()); + map.get(nodeNr).add(vis); + + synchronized (listeners) { + for (NodeInfoVisualizerListenerF listener : listeners) { + listener.visualizerRegistered(vis); + } + } + } + + /** + * Frees any resources belonging to this visualizer, closes the window and removes it from + * {@link #getInstances(Node)} (Swing {@code dispose()}). + */ + public void dispose() { + unregister(this); + node = null; + if (isShowing()) { + hide(); + } + } + + @Override + public int compareTo(NodeInfoVisualizerF other) { + return longName.compareTo(other.longName); + } + + /** + * @return the node this window is associated with (Swing {@code getNode()}) + */ + public final Node getNode() { + return node; + } + + /** + * @return the window's long name (Swing {@code getLongName()}) + */ + public final String getLongName() { + return longName; + } + + /** + * @return the window's short name (Swing {@code getShortName()}) + */ + public final String getShortName() { + return shortName; + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeinfo/NodeInfoVisualizerListenerF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeinfo/NodeInfoVisualizerListenerF.java new file mode 100644 index 00000000000..509f1a8c656 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeinfo/NodeInfoVisualizerListenerF.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.nodeinfo; + +/** + * JavaFX port of the Swing {@code de.uka.ilkd.key.gui.NodeInfoVisualizerListener}: notified + * whenever a {@link NodeInfoVisualizerF} is registered or unregistered from + * {@link NodeInfoVisualizerF#getInstances(de.uka.ilkd.key.proof.Node)}. + * + * @author lanzinger (Swing original), the key.ui.fx team (port) + */ +public interface NodeInfoVisualizerListenerF { + + /** + * Called when a new visualizer has been registered. + * + * @param vis the registered visualizer + */ + void visualizerRegistered(NodeInfoVisualizerF vis); + + /** + * Called when a visualizer has been unregistered. + * + * @param vis the unregistered visualizer + */ + void visualizerUnregistered(NodeInfoVisualizerF vis); +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/originlabels/OriginLabelsF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/originlabels/OriginLabelsF.java index 34eb73e3a96..8b1efeb2e3a 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/originlabels/OriginLabelsF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/originlabels/OriginLabelsF.java @@ -19,13 +19,21 @@ import de.uka.ilkd.key.core.fx.KeYSelectionModel; import de.uka.ilkd.key.gui.fx.MainWindowF; import de.uka.ilkd.key.gui.fx.actions.QuickSaveF; +import de.uka.ilkd.key.gui.fx.fonticons.IconFactoryF; +import de.uka.ilkd.key.gui.fx.nodeinfo.NodeInfoVisualizerF; import de.uka.ilkd.key.gui.fx.nodeviews.SequentViewF; +import de.uka.ilkd.key.gui.fx.notification.NotificationManagerF; +import de.uka.ilkd.key.gui.fx.notification.NotificationManagerF.Kind; +import de.uka.ilkd.key.ldt.JavaDLTheory; import de.uka.ilkd.key.pp.NotationInfo; +import de.uka.ilkd.key.proof.Node; import de.uka.ilkd.key.proof.Proof; import de.uka.ilkd.key.settings.ProofIndependentSettings; import de.uka.ilkd.key.settings.TermLabelSettings; import org.key_project.logic.Name; +import org.key_project.prover.sequent.PosInOccurrence; +import org.key_project.prover.sequent.SequentFormula; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -131,8 +139,10 @@ private static MenuItem createHidePackagePrefixItem(MainWindowF mainWindow) { /** * The "Origin Tracking" submenu (Swing {@code OriginTermLabelsExt.getMainMenuActions}): - * "Toggle Term Origin Tracking" with the Swing reload dialog, plus "Show Origin" once the - * {@link OriginTermLabelVisualizerF} is available. + * "Toggle Term Origin Tracking" with the Swing reload dialog and "Show Origin", which opens + * the {@link OriginTermLabelVisualizerF} window for the last clicked term (in Swing the + * action is a sequent context-menu contribution; the FX sequent view has no context menu + * yet, so the item lives here). */ private static MenuItem createOriginTrackingMenu(MainWindowF mainWindow) { Menu menu = new Menu("Origin Tracking"); @@ -142,9 +152,59 @@ private static MenuItem createOriginTrackingMenu(MainWindowF mainWindow) { ProofIndependentSettings.DEFAULT_INSTANCE.getTermLabelSettings().getUseOriginLabels()); tracking.setOnAction(e -> toggleTermOriginTracking(mainWindow, tracking)); menu.getItems().add(tracking); + + MenuItem showOrigin = + menuItem("Show Origin", mainWindow, IconFactoryF.Key.INFO_VIEW, + () -> showOrigin(mainWindow)); + menu.getItems().add(showOrigin); return menu; } + /** Helper building a plain menu item (JavaFX MenuItem has no tooltip API). */ + private static MenuItem menuItem(String text, MainWindowF mainWindow, IconFactoryF.Key icon, + Runnable action) { + javafx.scene.control.MenuItem item = new javafx.scene.control.MenuItem(text); + if (icon != null) { + item.setGraphic(IconFactoryF.createIcon(icon)); + } + item.setOnAction(e -> action.run()); + return item; + } + + /** + * The Swing {@code ShowOriginAction.actionPerformed}: opens a new origin visualizer for the + * selected term. The position is the last clicked term of the sequent view, walked up to a + * formula ({@code TermView} can only print sequents or formulas, not terms); with no term + * clicked the whole sequent is shown. Enablement follows + * {@code TermLabelSettings.useOriginLabels}. + */ + private static void showOrigin(MainWindowF mainWindow) { + boolean enabled = + ProofIndependentSettings.DEFAULT_INSTANCE.getTermLabelSettings().getUseOriginLabels(); + if (!enabled) { + NotificationManagerF.getInstance().notify( + "Origin tracking is switched off (View▸Origin Tracking▸Toggle Term Origin Tracking).", + Kind.WARNING); + return; + } + de.uka.ilkd.key.gui.fx.nodeviews.SequentViewF view = mainWindow.getSequentView(); + de.uka.ilkd.key.pp.PosInSequent pos = view.getLastClickedPos(); + PosInOccurrence pio = pos == null ? null : pos.getPosInOccurrence(); + // OriginTermLabelVisualizer.TermView can only print sequents or formulas, not terms + while (pio != null && !pio.subTerm().sort().equals(JavaDLTheory.FORMULA)) { + pio = pio.up(); + } + Node node = mainWindow.getSelectionModel().getSelectedNode(); + if (node == null) { + NotificationManagerF.getInstance().notify("No proof node selected.", Kind.WARNING); + return; + } + OriginTermLabelVisualizerF visualizer = + new OriginTermLabelVisualizerF(mainWindow, pio, node, + mainWindow.getSelectionModel().getSelectedProof().getServices()); + visualizer.show(); + } + /** * The Swing {@code ToggleTermOriginTrackingAction.actionPerformed}: confirm dialog * ("For the change to take effect, you need to reload the proof."), then flip @@ -185,8 +245,9 @@ private static void toggleTermOriginTracking(MainWindowF mainWindow, /** * Development self test: verifies that term labels can be shown/hidden in the sequent view - * through the shared visibility manager (Swing View▸Term Labels). The report is a single - * line ending in {@code PASS} or {@code FAIL}. + * through the shared visibility manager (Swing View▸Term Labels) and that the origin + * visualizer window registers/disposes and builds its tree for the loaded proof. The report + * is a single line ending in {@code PASS} or containing {@code FAIL}. * * @param mainWindow the FX main window with a loaded proof * @return the report line @@ -223,7 +284,58 @@ public static String verify(MainWindowF mainWindow) { names.size(), grew, printed, hiddenOk); boolean pass = grew && printed != null && hiddenOk; - return "termLabels: names=" + names.size() + " chars " + before.length() + "->" - + shown.length() + " printedLabel=" + printed + " " + (pass ? "PASS" : "FAIL"); + String termLabelReport = "termLabels: names=" + names.size() + " chars " + before.length() + + "->" + shown.length() + " printedLabel=" + printed + " " + (pass ? "PASS" : "FAIL"); + + // --- origin visualizer part (Swing OriginTermLabelVisualizer / NodeInfoVisualizer) --- + String visReport = verifyOriginVisualizer(mainWindow); + return termLabelReport + " | " + visReport; + } + + /** + * Self test part 2 (Swing {@code OriginTermLabelVisualizer}): opens the visualizer for the + * first sequent formula of the selected node, verifies that it registered itself + * ({@link NodeInfoVisualizerF} registry), that its origin tree has rows and that the term + * view printed something, then disposes it and verifies that the registry is empty again. + */ + private static String verifyOriginVisualizer(MainWindowF mainWindow) { + Node node = mainWindow.getSelectionModel().getSelectedNode(); + if (node == null) { + return "originVis: no node FAIL"; + } + // first top-level formula of the sequent (Swing ShowOriginAction walks up to a formula; + // the first formula is as good a demonstration position as any) + PosInOccurrence pio = null; + for (SequentFormula cfma : node.sequent().antecedent()) { + pio = new PosInOccurrence(cfma, org.key_project.logic.PosInTerm.getTopLevel(), true); + break; + } + if (pio == null) { + for (SequentFormula cfma : node.sequent().succedent()) { + pio = new PosInOccurrence(cfma, org.key_project.logic.PosInTerm.getTopLevel(), + false); + break; + } + } + if (pio == null) { + return "originVis: empty sequent FAIL"; + } + try { + OriginTermLabelVisualizerF visualizer = + new OriginTermLabelVisualizerF(mainWindow, pio, node, node.proof().getServices()); + visualizer.show(); + boolean registered = NodeInfoVisualizerF.hasInstances(node); + int rows = visualizer.treeRowCount(); + boolean viewPrinted = visualizer.viewPrinted(); + visualizer.dispose(); + boolean unregistered = !NodeInfoVisualizerF.hasInstances(node); + LOGGER.info("verify origin visualizer: registered={} rows={} viewPrinted={} " + + "unregistered={}", registered, rows, viewPrinted, unregistered); + boolean pass = registered && rows > 1 && viewPrinted && unregistered; + return "originVis: rows=" + rows + " " + (pass ? "PASS" : "FAIL"); + } catch (RuntimeException e) { + LOGGER.warn("verify origin visualizer failed", e); + return "originVis: exception " + e + " FAIL"; + } } } diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/originlabels/OriginTermLabelVisualizerF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/originlabels/OriginTermLabelVisualizerF.java new file mode 100644 index 00000000000..316fb722d40 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/originlabels/OriginTermLabelVisualizerF.java @@ -0,0 +1,716 @@ +/* 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.originlabels; + +import java.util.Objects; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; +import javafx.geometry.Pos; +import javafx.scene.Scene; +import javafx.scene.control.Alert; +import javafx.scene.control.Button; +import javafx.scene.control.ButtonType; +import javafx.scene.control.Label; +import javafx.scene.control.ScrollPane; +import javafx.scene.control.SplitPane; +import javafx.scene.control.Tooltip; +import javafx.scene.control.TreeCell; +import javafx.scene.control.TreeItem; +import javafx.scene.control.TreeView; +import javafx.scene.input.MouseEvent; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.Region; +import javafx.scene.layout.StackPane; +import javafx.scene.shape.Path; +import javafx.scene.text.Font; +import javafx.scene.text.HitInfo; +import javafx.scene.text.Text; +import javafx.scene.text.TextFlow; +import javafx.stage.StageStyle; + +import de.uka.ilkd.key.control.TermLabelVisibilityManager; +import de.uka.ilkd.key.gui.fx.MainWindowF; +import de.uka.ilkd.key.gui.fx.configuration.ConfigF; +import de.uka.ilkd.key.gui.fx.nodeinfo.NodeInfoVisualizerF; +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.logic.label.OriginTermLabel; +import de.uka.ilkd.key.logic.label.OriginTermLabel.Origin; +import de.uka.ilkd.key.pp.IdentitySequentPrintFilter; +import de.uka.ilkd.key.pp.InitialPositionTable; +import de.uka.ilkd.key.pp.NotationInfo; +import de.uka.ilkd.key.pp.PosInSequent; +import de.uka.ilkd.key.pp.PosTableLayouter; +import de.uka.ilkd.key.pp.Range; +import de.uka.ilkd.key.pp.SequentPrintFilter; +import de.uka.ilkd.key.pp.SequentPrintFilterEntry; +import de.uka.ilkd.key.pp.SequentViewLogicPrinter; +import de.uka.ilkd.key.pp.ShowSelectedSequentPrintFilter; +import de.uka.ilkd.key.proof.Node; +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.event.ProofDisposedEvent; +import de.uka.ilkd.key.proof.event.ProofDisposedListener; +import de.uka.ilkd.key.util.pp.UnbalancedBlocksException; + +import org.key_project.prover.sequent.PosInOccurrence; +import org.key_project.prover.sequent.Sequent; +import org.key_project.prover.sequent.SequentFormula; +import org.key_project.util.collection.ImmutableList; +import org.key_project.util.javafx.FxUtil; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * JavaFX port of the Swing {@code de.uka.ilkd.key.gui.originlabels.OriginTermLabelVisualizer} + * (a {@code NodeInfoVisualizer}): visualizes the {@link OriginTermLabel}s of a term and its + * sub-terms for one node of the proof. + *

    + * The window layout follows the Swing original: a head pane with the node link ("Node: <n>: + * <name>", clicking selects the node in the mediator — with the Swing confirmation dialog + * when the node's proof is not the selected one) and the proof name; a split pane with (a) the + * origin tree — one row per (sub-)term, showing the short-printed term on the left and the + * origin's specification type on the right, with a tooltip containing the origin and the origins + * of the (former) sub-terms — and (b) the term view, a read-only sequent view printing only the + * selected formula ({@code ShowSelectedSequentPrintFilter}) or the whole sequent, through the + * ported {@link TermViewLogicPrinterF}. Selecting a tree row highlights the corresponding term + * in the view (Swing {@code setUserSelectionHighlight}, orange rectangle) and clicking a term in + * the view selects the corresponding tree row (Swing mouse listener). Like in Swing, the origin + * labels themselves are not printed inside the term view + * ({@code TermLabelVisibilityManager} hides {@code OriginTermLabel.NAME}); the origins are shown + * by the tree and the tooltips. + *

    + * The node link stays live: proof-tree changes update it, and a deleted node disables the link + * and unregisters the window (Swing {@code updateNodeLink} with the rule-app/proof-tree/disposed + * listeners; here the proof-tree and proof-disposed listeners are registered — the rule-app + * listener adds no extra information beyond the proof-tree events). + *

    + * Presentation deviations: the Swing {@code JSplitPane} divider starts collapsed to the right + * ({@code setDividerLocation(1.0)}); the FX window opens with the tree and view side by side + * (the information is the same). HTML tooltips become plain multi-line text, the Swing + * {@code JTree} becomes a JavaFX {@link TreeView} and the origin highlight color is the CSS + * class {@code origin-visualizer-highlight} (Swing {@code Color.ORANGE}). + */ +public final class OriginTermLabelVisualizerF extends NodeInfoVisualizerF { + + private static final Logger LOGGER = LoggerFactory.getLogger(OriginTermLabelVisualizerF.class); + + /** The title for the origin information for the selected term (Swing constant). */ + public static final String ORIGIN_INFO_TITLE = "Origin information"; + + /** The title for the selected term's origin (Swing constant). */ + public static final String ORIGIN_TITLE = "Origin of formula"; + + /** + * The title for the origin of the selected term's sub-terms and former sub-terms (Swing + * constant). + */ + public static final String SUBTERM_ORIGINS_TITLE = + "Origins of (former) subformulas and subterms"; + + /** Window size (the Swing visualizer is hosted in the source view frame; free size here). */ + private static final double WIDTH = 900; + private static final double HEIGHT = 600; + + /** services */ + private final Services services; + + /** top-level position cache ({@code PosInTerm.getTopLevel()}). */ + private static final org.key_project.logic.PosInTerm topPos = + org.key_project.logic.PosInTerm.getTopLevel(); + + /** The position of the term being shown in this window (Swing {@code termPio}). */ + private final PosInOccurrence termPio; + + /** The sequent containing the term being shown in this window (Swing {@code sequent}). */ + private final Sequent sequent; + + /** the origin tree (Swing {@code tree}). */ + private final TreeView tree = new TreeView<>(); + + /** the term view (Swing {@code view}, a specialized SequentView). */ + private final TermViewF view; + + /** the currently highlighted position (Swing {@code highlight}). */ + private PosInOccurrence highlight; + + /** the node link button (Swing {@code nodeLinkButton}). */ + private final Button nodeLinkButton = new Button(); + + /** the node link action state, mirrored to the button (Swing {@code nodeLinkAction}). */ + private final StringProperty nodeLinkText = new SimpleStringProperty(); + + /** the mediator entry point for the node link (Swing {@code MainWindow.getInstance()}). */ + private final MainWindowF mainWindow; + + /** updates the node link on proof changes (Swing {@code proofTreeListener}). */ + private final ProofTreeListener proofTreeListener = new ProofTreeAdapter() { + @Override + public void proofStructureChanged(ProofTreeEvent e) { + updateNodeLink(); + } + + @Override + public void proofPruned(ProofTreeEvent e) { + updateNodeLink(); + } + + @Override + public void proofGoalsChanged(ProofTreeEvent e) { + updateNodeLink(); + } + + @Override + public void proofExpanded(ProofTreeEvent e) { + updateNodeLink(); + } + }; + + /** updates the node link when the proof is disposed (Swing {@code proofDisposedListener}). */ + private final ProofDisposedListener proofDisposedListener = new ProofDisposedListener() { + @Override + public void proofDisposing(ProofDisposedEvent e) { + // nothing + } + + @Override + public void proofDisposed(ProofDisposedEvent e) { + updateNodeLink(); + } + }; + + /** + * Creates a new origin visualizer window. + * + * @param mainWindow the main window (mediator access for the node link) + * @param pos the position of the term whose origin shall be visualized ({@code null} shows + * the whole sequent, Swing {@code ShowOriginAction} walks up to the formula first) + * @param node the node representing the proof state for which the term's origins shall be + * visualized + * @param services services + */ + public OriginTermLabelVisualizerF(MainWindowF mainWindow, PosInOccurrence pos, Node node, + Services services) { + super(node, + "Origin for node " + node.serialNr() + ": " + (pos == null ? "whole sequent" + : de.uka.ilkd.key.pp.LogicPrinter + .quickPrintTerm((JTerm) pos.subTerm(), services) + .replaceAll("\\s+", " ")), + "Node " + node.serialNr()); + this.mainWindow = mainWindow; + this.services = services; + this.termPio = pos; + this.sequent = node.sequent(); + this.view = new TermViewF(); + + setTitle(getLongName()); + initStyle(StageStyle.DECORATED); + if (mainWindow.getStage() != null) { + initOwner(mainWindow.getStage()); + } + + BorderPane root = new BorderPane(); + root.setTop(createHeadPane()); + root.setCenter(createBodyPane()); + Scene scene = new Scene(root, WIDTH, HEIGHT); + ThemeManager.getInstance().manage(scene); + setScene(scene); + setOnCloseRequest(e -> dispose()); + + node.proof().addProofTreeListener(proofTreeListener); + node.proof().addProofDisposedListener(proofDisposedListener); + updateNodeLink(); + } + + /** The head pane: node link + proof name (Swing {@code initHeadPane}). */ + private HBox createHeadPane() { + nodeLinkButton.textProperty().bind(nodeLinkText); + nodeLinkButton.setOnAction(e -> selectNode()); + Label proofLabel = new Label("Proof: \"" + getNode().proof().name() + "\""); + HBox head = new HBox(20); + head.setAlignment(Pos.CENTER_LEFT); + head.getChildren().addAll(new Label("Node: "), nodeLinkButton, proofLabel); + return head; + } + + /** The body: origin tree (left) and term view (right) (Swing {@code initTree/initView}). */ + private SplitPane createBodyPane() { + tree.setRoot(buildTreeModel()); + tree.setCellFactory(v -> new OriginTreeCell()); + tree.getSelectionModel().selectedItemProperty().addListener((obs, oldItem, newItem) -> { + highlight = newItem == null ? null : newItem.getValue().pos(); + view.highlightTerm(highlight); + }); + + Label treeTitle = new Label(borderTitle() + " as tree"); + Label viewTitle = new Label(borderTitle()); + treeTitle.getStyleClass().add("dialog-section-title"); + viewTitle.getStyleClass().add("dialog-section-title"); + + BorderPane treePane = new BorderPane(tree); + treePane.setTop(treeTitle); + BorderPane viewPane = new BorderPane(view); + viewPane.setTop(viewTitle); + SplitPane body = new SplitPane(treePane, viewPane); + SplitPane.setResizableWithParent(treePane, Boolean.TRUE); + return body; + } + + /** The split-pane titles (Swing {@code borderTitle}: the selection side). */ + private String borderTitle() { + if (termPio == null) { + return "selected sequent"; + } + return termPio.isInAntec() ? "selected formula in antecedent" + : "selected formula in succedent"; + } + + /** The tree model root (Swing {@code buildModel(PosInOccurrence)}). */ + private TreeItem buildTreeModel() { + TreeItem root = new TreeItem<>(new TreeNodeF(termPio)); + fillChildren(root, termPio); + return root; + } + + /** Recursive tree model construction (Swing {@code buildModel(TreeNode, Pos, TreeModel)}). */ + private void fillChildren(TreeItem parent, PosInOccurrence parentPos) { + if (parentPos == null) { + for (SequentFormula child : sequent.antecedent()) { + PosInOccurrence childPos = new PosInOccurrence(child, topPos, true); + TreeItem childItem = new TreeItem<>(new TreeNodeF(childPos)); + parent.getChildren().add(childItem); + fillChildren(childItem, childPos); + } + for (SequentFormula child : sequent.succedent()) { + PosInOccurrence childPos = new PosInOccurrence(child, topPos, false); + TreeItem childItem = new TreeItem<>(new TreeNodeF(childPos)); + parent.getChildren().add(childItem); + fillChildren(childItem, childPos); + } + } else { + var children = parentPos.subTerm().subs(); + for (int i = 0; i < children.size(); ++i) { + PosInOccurrence childPos = parentPos.down(i); + TreeItem childItem = new TreeItem<>(new TreeNodeF(childPos)); + parent.getChildren().add(childItem); + fillChildren(childItem, childPos); + } + } + } + + /** + * The node link action (Swing {@code nodeLinkAction}): selects the node in the mediator, + * asking when the node's proof is not the currently selected one. + */ + private void selectNode() { + var selectionModel = mainWindow.getSelectionModel(); + Node node = getNode(); + if (node == null) { + return; + } + if (!selectionModel.getSelectedProof().equals(node.proof())) { + Alert alert = new Alert(Alert.AlertType.CONFIRMATION); + alert.setTitle("Switch Proof?"); + alert.setHeaderText(null); + alert.setContentText("The proof containing this node is not currently selected." + + " Do you want to select it?"); + alert.initOwner(this); + if (alert.showAndWait().orElse(ButtonType.CANCEL) != ButtonType.OK) { + return; + } + selectionModel.setSelectedProof(node.proof()); + } + selectionModel.setSelectedNode(node); + } + + /** + * Keeps the node link current (Swing {@code updateNodeLink}): "DELETED NODE" + unregister + * when the node is no longer in the proof, the serial number/name otherwise. + */ + private void updateNodeLink() { + if (!FxUtil.isFxThread()) { + FxUtil.runLater(this::updateNodeLink); + return; + } + Node node = getNode(); + if (node == null) { + return; + } + if (node.proof().isDisposed() || !node.proof().find(node)) { + nodeLinkText.set("DELETED NODE"); + nodeLinkButton.setDisable(true); + unregister(this); + } else { + nodeLinkText.set(node.serialNr() + ": " + node.name()); + } + } + + @Override + public void dispose() { + Node node = getNode(); + if (node != null && !node.proof().isDisposed()) { + node.proof().removeProofTreeListener(proofTreeListener); + node.proof().removeProofDisposedListener(proofDisposedListener); + } + view.dispose(); + super.dispose(); + } + + /** + * @return whether the term view printed a non-empty text; used by the self test + */ + boolean viewPrinted() { + return view.hasText(); + } + + /** + * @return the number of rows of the origin tree (all levels); used by the + * {@code key.fx.verify.lemmaorigin} self test + */ + int treeRowCount() { + return countRows(tree.getRoot()); + } + + private static int countRows(TreeItem item) { + int result = 1; + for (TreeItem child : item.getChildren()) { + result += countRows(child); + } + return result; + } + + // ------------------------------------------------------------------ + // origin tree helpers + // ------------------------------------------------------------------ + + /** + * A tree node: the position of a (sub-)term (Swing {@code TreeNode extends + * DefaultMutableTreeNode}). + */ + private static final class TreeNodeF { + private final PosInOccurrence pos; + private final JTerm term; + + TreeNodeF(PosInOccurrence pos) { + this.pos = pos; + this.term = pos == null ? null : (JTerm) pos.subTerm(); + } + + PosInOccurrence pos() { + return pos; + } + } + + /** The tree cell: term text left, origin spec type right, origin tooltip. */ + private final class OriginTreeCell extends TreeCell { + + private final HBox box = new HBox(10); + private final Text termText = new Text(); + private final Text originText = new Text(); + + OriginTreeCell() { + Region spacer = new Region(); + HBox.setHgrow(spacer, Priority.ALWAYS); + box.getChildren().addAll(termText, spacer, originText); + box.prefWidthProperty().bind(widthProperty().subtract(90)); + box.setAlignment(Pos.CENTER_LEFT); + setGraphic(box); + setText(null); + } + + @Override + protected void updateItem(TreeNodeF item, boolean empty) { + super.updateItem(item, empty); + if (empty || item == null) { + setGraphic(null); + setTooltip(null); + return; + } + termText.setText(shortTermText(item.term)); + termText.setFont(ConfigF.DEFAULT.monoFont()); + originText.getStyleClass().add("origin-visualizer-origin"); + originText.setFont(ConfigF.DEFAULT.monoFont()); + Origin origin = item.pos == null ? null : OriginTermLabel.getOrigin(item.pos); + originText.setText(origin == null ? "" : shortOriginText(origin)); + setGraphic(box); + setTooltip(new Tooltip(tooltipText(item.pos))); + } + + /** The short origin text (Swing {@code getShortOriginText}: the specification type). */ + private String shortOriginText(Origin origin) { + return origin.specType.toString(); + } + } + + /** + * The short-printed term (Swing {@code CellRenderer.getShortTermText}): the first line of + * the pretty-printed term (the whole sequent for a {@code null} term), whitespace-flattened. + */ + private String shortTermText(JTerm term) { + String text; + if (term == null) { + text = de.uka.ilkd.key.pp.LogicPrinter.quickPrintSequent(sequent, services); + } else { + text = de.uka.ilkd.key.pp.LogicPrinter.quickPrintTerm(term, services); + } + int endIndex = text.indexOf('\n'); + if (endIndex != text.length() - 1 && endIndex != -1) { + return text.substring(0, endIndex).replaceAll("\\s+", " ") + " ..."; + } + return text.replaceAll("\\s+", " "); + } + + /** + * The tooltip text for a position (Swing {@code getTooltipText}, HTML becomes plain text): + * the origin of the term and the origins of its (former) sub-terms. + */ + private String tooltipText(PosInOccurrence pio) { + if (pio == null) { + return null; + } + OriginTermLabel label = + (OriginTermLabel) ((JTerm) pio.subTerm()).getLabel(OriginTermLabel.NAME); + Origin origin = OriginTermLabel.getOrigin(pio); + StringBuilder result = new StringBuilder("Origin of selected term: ") + .append(origin == null ? "" : origin); + result.append("\nOrigin of (former) sub-terms:\n"); + if (label != null) { + for (Origin subOrigin : label.getSubtermOrigins()) { + result.append(subOrigin).append('\n'); + } + } + return result.toString(); + } + + // ------------------------------------------------------------------ + // the term view (Swing inner class TermView + TermViewLogicPrinter) + // ------------------------------------------------------------------ + + /** + * Converts a pio on the printed (filtered) sequent to a pio on {@link #termPio}'s term + * (Swing {@code convertPio}). + */ + private PosInOccurrence convertPio(PosInOccurrence pio) { + if (termPio == null) { + return pio; + } else if (pio == null) { + return new PosInOccurrence(termPio.sequentFormula(), termPio.posInTerm(), + termPio.isInAntec()); + } + org.key_project.logic.PosInTerm completePos = termPio.posInTerm(); + org.key_project.logic.IntIterator it = pio.posInTerm().iterator(); + while (it.hasNext()) { + completePos = completePos.down(it.next()); + } + return new PosInOccurrence(termPio.sequentFormula(), completePos, termPio.isInAntec()); + } + + /** + * The term view pane (Swing inner class {@code TermView extends SequentView}): prints the + * selected formula (or the whole sequent) with all term labels except the origin labels, + * supports the highlight of a selected position and reports clicked positions. + */ + private final class TermViewF extends BorderPane { + + private final ScrollPane scrollPane = new ScrollPane(); + private final StackPane content = new StackPane(); + private final javafx.scene.layout.Pane highlightPane = new javafx.scene.layout.Pane(); + private final TextFlow textFlow = new TextFlow(); + private SequentViewLogicPrinter printer; + private SequentPrintFilter filter; + private String printed; + private InitialPositionTable posTable; + + TermViewF() { + highlightPane.getStyleClass().add("sequent-overlay"); + highlightPane.setMouseTransparent(true); + highlightPane.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE); + content.getChildren().addAll(highlightPane, textFlow); + scrollPane.setContent(content); + scrollPane.setFitToWidth(true); + setCenter(scrollPane); + textFlow.getStyleClass().add("sequent-view-flow"); + textFlow.setPadding(new javafx.geometry.Insets(6)); + textFlow.setOnMouseClicked(this::handleClick); + print(); + } + + /** Prints the term view (Swing {@code view.printSequent()}). */ + void print() { + NotationInfo ni = new NotationInfo(); + if (services != null) { + ni.refresh(services, NotationInfo.DEFAULT_PRETTY_SYNTAX, + NotationInfo.DEFAULT_UNICODE_ENABLED, NotationInfo.DEFAULT_HIDE_PACKAGE_PREFIX); + } + printer = new TermViewLogicPrinterF(termPio, ni, services); + filter = termPio != null ? new ShowSelectedSequentPrintFilter(termPio) + : new IdentitySequentPrintFilter(); + filter.setSequent(sequent); + printer.update(filter, PosTableLayouter.DEFAULT_LINE_WIDTH); + printed = printer.result(); + posTable = printer.layouter().getInitialPositionTable(); + rebuildRuns(); + highlightPane.getChildren().clear(); + } + + /** Renders the printed text as monospaced runs (the Swing view is an HTML editor pane). */ + private void rebuildRuns() { + textFlow.getChildren().clear(); + if (printed == null || printed.isEmpty()) { + return; + } + Font font = ConfigF.DEFAULT.monoFont(); + Text run = new Text(printed); + run.getStyleClass().add("sequent-text"); + run.setFont(font); + textFlow.getChildren().add(run); + } + + /** Highlights the given position (Swing {@code setUserSelectionHighlight}). */ + void highlightTerm(PosInOccurrence pio) { + highlightPane.getChildren().clear(); + if (pio == null || printed == null || posTable == null) { + return; + } + ImmutableList path = posTable.pathForPosition(pio, filter); + if (path == null) { + return; + } + Range range = posTable.rangeForPath(path); + if (range == null || range.length() <= 0) { + return; + } + int start = Math.clamp(range.start(), 0, printed.length()); + int end = Math.clamp(range.start() + range.length(), start, printed.length()); + if (end <= start) { + return; + } + // shapes need a laid-out text flow + textFlow.applyCss(); + textFlow.layout(); + Path rect = new Path(textFlow.getRangeShape(start, end, true)); + rect.getStyleClass().add("origin-visualizer-highlight"); + rect.setManaged(false); + highlightPane.getChildren().add(rect); + } + + /** Maps a click to a position and selects the corresponding tree row (Swing listener). */ + private void handleClick(MouseEvent event) { + if (printed == null || posTable == null) { + return; + } + javafx.geometry.Point2D local = + textFlow.sceneToLocal(event.getSceneX(), event.getSceneY()); + HitInfo hit = textFlow.getHitInfo(local); + int charIndex = hit == null ? -1 : hit.getCharIndex(); + if (charIndex < 0 || charIndex >= printed.length()) { + highlight = null; + view.highlightTerm(null); + tree.getSelectionModel().clearSelection(); + return; + } + PosInSequent pis = posTable.getPosInSequent(charIndex, filter); + PosInOccurrence pio = convertPio(pis == null ? null : pis.getPosInOccurrence()); + if (pio == null || Objects.equals(highlight, pio)) { + highlight = null; + view.highlightTerm(null); + tree.getSelectionModel().clearSelection(); + return; + } + highlight = pio; + view.highlightTerm(pio); + selectTreeRow(pio); + } + + /** Selects the tree row of the given position (Swing {@code getTreePath}). */ + private void selectTreeRow(PosInOccurrence pio) { + TreeItem found = findTreeItem(tree.getRoot(), pio); + if (found != null) { + // reveal the row like the Swing JTree selection does + expandTo(found); + tree.getSelectionModel().select(found); + } + } + + private void expandTo(TreeItem item) { + TreeItem parent = item.getParent(); + while (parent != null) { + parent.setExpanded(true); + parent = parent.getParent(); + } + } + + private TreeItem findTreeItem(TreeItem item, PosInOccurrence pio) { + if (item == null) { + return null; + } + if (item.getValue() != null && Objects.equals(item.getValue().pos(), pio)) { + return item; + } + for (TreeItem child : item.getChildren()) { + TreeItem found = findTreeItem(child, pio); + if (found != null) { + return found; + } + } + return null; + } + + /** Detaches nothing (the view has no listeners of its own); kept for symmetry. */ + void dispose() { + highlightPane.getChildren().clear(); + } + + /** @return whether a non-empty text is printed (self test helper) */ + boolean hasText() { + return printed != null && !printed.isEmpty(); + } + } + + /** + * The printer of the term view (Swing inner class {@code TermViewLogicPrinter extends + * SequentViewLogicPrinter}): prints the filtered sequent without the arrow when a formula is + * selected ({@code pos != null}), otherwise the whole sequent like the standard printer. + */ + private static final class TermViewLogicPrinterF extends SequentViewLogicPrinter { + + private final PosInOccurrence pos; + + TermViewLogicPrinterF(PosInOccurrence pos, NotationInfo ni, Services services) { + super(ni, services, PosTableLayouter.positionTable(), new TermLabelVisibilityManager()); + this.pos = pos; + } + + @Override + public void printFilteredSequent(SequentPrintFilter filter) { + try { + ImmutableList antec = filter.getFilteredAntec(); + ImmutableList succ = filter.getFilteredSucc(); + layouter.markStartSub(); + layouter.startTerm(antec.size() + succ.size()); + layouter.beginC(1).ind(); + printSemisequent(antec); + + if (pos == null) { + layouter.brk(1, -1); + printSequentArrow(); + layouter.brk(); + } + + printSemisequent(succ); + + layouter.markEndSub(); + layouter.end(); + } catch (UnbalancedBlocksException e) { + throw new RuntimeException("Unbalanced blocks in pretty printer:\n" + e); + } + } + } +} diff --git a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css index 173dd4a912f..f6c72703584 100644 --- a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css +++ b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css @@ -607,3 +607,11 @@ .sequent-search-mode { -fx-padding: 1; } + +/* origin term label visualizer (Swing OriginTermLabelVisualizer): the origin of the selected + term in the tree is highlighted in the term view (Swing HIGHLIGHT_COLOR = orange, here the + existing warning color variable) */ +.origin-visualizer-highlight { + -fx-fill: derive(-key-warning, 30%); + -fx-opacity: 0.45; +} diff --git a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css index 2962a642bf4..a5fc2dfe7dd 100644 --- a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css +++ b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css @@ -601,3 +601,11 @@ .sequent-search-mode { -fx-padding: 1; } + +/* origin term label visualizer (Swing OriginTermLabelVisualizer): the origin of the selected + term in the tree is highlighted in the term view (Swing HIGHLIGHT_COLOR = orange, here the + existing warning color variable) */ +.origin-visualizer-highlight { + -fx-fill: derive(-key-warning, 30%); + -fx-opacity: 0.45; +} From 080dc64849b63244e793c3ca55922d3c0695ba05 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Thu, 8 Oct 2026 22:44:17 +0200 Subject: [PATCH 076/281] key.ui.fx: lemmaorigin: improve the term-label self test: search the proof tree for a node whose sequent prints term labels (the root sequent of a freshly loaded problem has none) --- .../gui/fx/originlabels/OriginLabelsF.java | 72 +++++++++++++------ 1 file changed, 51 insertions(+), 21 deletions(-) diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/originlabels/OriginLabelsF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/originlabels/OriginLabelsF.java index 8b1efeb2e3a..a8b43dcc82d 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/originlabels/OriginLabelsF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/originlabels/OriginLabelsF.java @@ -260,38 +260,68 @@ public static String verify(MainWindowF mainWindow) { de.uka.ilkd.key.control.TermLabelVisibilityManager manager = termLabelMenu.getVisibleTermLabels(); List names = TermLabelVisibilityManager.getSortedTermLabelNames(view.getProof()); - String before = view.printedText(); - // show every label (Swing default state of the TermLabelVisibilityManager) - for (Name name : names) { - manager.setHidden(name, false); - } manager.setShowLabels(true); - String shown = view.printedText(); - boolean grew = shown.length() > before.length(); - // find a label whose printed name is part of the rendered text - Name printed = names.stream() - .filter(n -> shown.contains(n.toString())).findFirst().orElse(null); - boolean hiddenOk = true; + // walk the proof tree for a node whose printed sequent contains a term label (the root + // sequent of the loaded problem usually has none — labels appear during the proof, so + // the visibility test needs a node whose terms carry labels, like the user's view does) + Proof proof = view.getProof(); + Name printed = null; + Node labelNode = null; + String shown = null; + outer: for (Node node = proof.root(); node != null;) { + mainWindow.getSelectionModel().setSelectedNode(node); + String text = view.printedText(); + for (Name name : names) { + if (text.contains(name.toString())) { + printed = name; + labelNode = node; + shown = text; + break outer; + } + } + node = nextPreorderNode(node); + } boolean grew = false, hiddenOk = true; if (printed != null) { - manager.setHidden(printed, true); + // hide everything (Swing "Display Term Labels in Formulas" off) and back on + manager.setShowLabels(false); String hidden = view.printedText(); - hiddenOk = !hidden.contains(printed.toString()); - manager.setHidden(printed, false); + manager.setShowLabels(true); + String shownAgain = view.printedText(); + grew = shownAgain.length() > hidden.length(); + hiddenOk = !hidden.contains(printed.toString()) && shown.contains(printed.toString()); } - // restore the pre-test state: show all labels (the default of the manager) + // restore the default state: show all labels and select the root again manager.setShowLabels(true); - LOGGER.info("verify term labels: names={} grew={} printedLabel={} hiddenOk={}", - names.size(), - grew, printed, hiddenOk); - boolean pass = grew && printed != null && hiddenOk; - String termLabelReport = "termLabels: names=" + names.size() + " chars " + before.length() - + "->" + shown.length() + " printedLabel=" + printed + " " + (pass ? "PASS" : "FAIL"); + mainWindow.getSelectionModel().setSelectedNode(proof.root()); + LOGGER.info("verify term labels: names={} printedLabel={} node={} grew={} hiddenOk={}", + names.size(), printed, labelNode == null ? -1 : labelNode.serialNr(), grew, hiddenOk); + boolean pass = printed != null && grew && hiddenOk; + String termLabelReport = "termLabels: names=" + names.size() + " printedLabel=" + printed + + (labelNode == null ? "" : " node=" + labelNode.serialNr()) + " " + + (pass ? "PASS" : "FAIL"); // --- origin visualizer part (Swing OriginTermLabelVisualizer / NodeInfoVisualizer) --- String visReport = verifyOriginVisualizer(mainWindow); return termLabelReport + " | " + visReport; } + /** @return the next node in pre-order (node itself first), {@code null} after the last */ + private static Node nextPreorderNode(Node node) { + if (node.childrenCount() > 0) { + return node.child(0); + } + Node current = node; + while (current.parent() != null) { + Node parent = current.parent(); + int index = parent.getChildNr(current); + if (index + 1 < parent.childrenCount()) { + return parent.child(index + 1); + } + current = parent; + } + return null; + } + /** * Self test part 2 (Swing {@code OriginTermLabelVisualizer}): opens the visualizer for the * first sequent formula of the selected node, verifies that it registered itself From 0ad36d6149ef49db14a85bd0224a330cb3b138a0 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Thu, 8 Oct 2026 22:44:37 +0200 Subject: [PATCH 077/281] key.ui.fx: seam: load failures surface in the IssueDialogF (parity with the ProblemLoader branch of Swing taskFinishedInternal, WindowUserInterfaceControl.java:236-244) --- .../src/main/java/de/uka/ilkd/key/gui/fx/MainWindowF.java | 5 +++++ 1 file changed, 5 insertions(+) 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 index 4c84116e80f..7b580646fe3 100644 --- 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 @@ -524,6 +524,11 @@ protected KeYEnvironment call() throws Exception { loadTask.setOnFailed(event -> { Throwable error = loadTask.getException(); LOGGER.error((demo ? "Demo proof" : "Proof") + " loading failed", 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); NotificationManagerF.getInstance() .notify((demo ? "Demo proof" : "Proof") + " loading failed: " + error.getMessage(), Kind.ERROR); From 8c28bfba82c5efcf1f1f2c943fa41fb5be96a5bf Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Thu, 8 Oct 2026 22:47:28 +0200 Subject: [PATCH 078/281] key.ui.fx: joinmerge: synthetic merge app fallback with harness parity check --- .../key/gui/fx/join/JoinMergeVerifyF.java | 122 ++++++++++++++++-- 1 file changed, 110 insertions(+), 12 deletions(-) diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/join/JoinMergeVerifyF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/join/JoinMergeVerifyF.java index ca4051287b7..9d821ee6c75 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/join/JoinMergeVerifyF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/join/JoinMergeVerifyF.java @@ -27,6 +27,7 @@ import de.uka.ilkd.key.proof.join.ProspectivePartner; import de.uka.ilkd.key.rule.IBuiltInRuleApp; import de.uka.ilkd.key.rule.merge.MergePartner; +import de.uka.ilkd.key.rule.merge.MergeProcedure; import de.uka.ilkd.key.rule.merge.MergeRule; import de.uka.ilkd.key.rule.merge.MergeRuleBuiltInRuleApp; @@ -98,7 +99,15 @@ public static String run(Window owner, Proof proof, ProofControl proofControl) { Thread worker = new Thread(() -> { prepare(proof, proofControl); List partners = computeJoinPartners(proof); - MergeAppData mergeApp = findMergeRuleApplication(proof); + MergeAppData foundApp = findMergeRuleApplication(proof); + if (foundApp == null) { + foundApp = syntheticMergeApp(proof); + if (foundApp != null) { + LOGGER.info("JoinMerge verification: using SYNTHETIC merge app data " + + "(no admissible merge position in the proof state)"); + } + } + final MergeAppData mergeApp = foundApp; verifyForcedMergeCompletion(proof, mergeApp); Platform.runLater(() -> verifyJoinDialog(owner, proof, partners, () -> { verifyMergePartnerDialog(owner, proof, proofControl, mergeApp, () -> { @@ -204,10 +213,12 @@ static List syntheticPartner(Proof proof) { /** * A merge rule application of admissible form with its potential merge partners, found by * scanning the open goals (the same information the Swing context menu action has available - * when it is enabled). + * when it is enabled). {@code synthetic} marks app data built by + * {@link #syntheticMergeApp(Proof)} when no admissible position exists in the proof state + * (then the dialogs are exercised for rendering/interaction, not for a real merge). */ private record MergeAppData(Goal goal, PosInOccurrence pio, - ImmutableList candidates) { + ImmutableList candidates, boolean synthetic) { } /** @@ -230,11 +241,14 @@ private static MergeAppData findMergeRuleApplication(Proof proof) { new PosInOccurrence(semi.get(i), PosInTerm.getTopLevel(), inAntec); try { boolean applicable = MergeRule.INSTANCE.isApplicable(goal, pio); - LOGGER.info("JoinMerge verification: goal {} formula {} applicable={}", + boolean admissible = + MergeRule.isOfAdmissibleForm(goal, pio, false); + LOGGER.info( + "JoinMerge verification: goal {} formula {} applicable={} admissible={}", goal.node().serialNr(), LogicPrinter.quickPrintTerm((JTerm) semi.get(i).formula(), proof.getServices()), - applicable); + applicable, admissible); if (!applicable) { continue; } @@ -247,7 +261,7 @@ private static MergeAppData findMergeRuleApplication(Proof proof) { LOGGER.info( "JoinMerge verification: {} merge candidates for goal {}", candidates.size(), goal.node().serialNr()); - return new MergeAppData(goal, pio, candidates); + return new MergeAppData(goal, pio, candidates, false); } } catch (RuntimeException e) { LOGGER.info("JoinMerge verification: merge applicability check failed: " @@ -259,6 +273,74 @@ private static MergeAppData findMergeRuleApplication(Proof proof) { return null; } + /** + * Builds synthetic merge app data from the open goals (fallback if no admissible merge + * position exists — the dialogs are then exercised for rendering/interaction, not for a real + * merge): the first open goal becomes the merge goal, the top-level succedent formula its + * merge position, and every other open goal a synthetic {@link MergePartner} at the top-level + * succedent formula (the same shape {@link MergeRule#findPotentialMergePartners} builds). + * + * @param proof the proof + * @return the synthetic merge app data, or {@code null} if the proof state does not allow it + */ + private static MergeAppData syntheticMergeApp(Proof proof) { + List goals = new ArrayList<>(); + proof.openGoals().forEach(goals::add); + if (goals.isEmpty()) { + return null; + } + Goal mergeGoal = goals.get(0); + var succedent = mergeGoal.sequent().succedent(); + if (succedent.isEmpty()) { + return null; + } + PosInOccurrence pio = + new PosInOccurrence(succedent.get(0), PosInTerm.getTopLevel(), false); + ImmutableList candidates = ImmutableList.nil(); + for (Goal other : goals) { + if (other.equals(mergeGoal) || other.isLinked()) { + continue; + } + var otherSuccedent = other.sequent().succedent(); + if (otherSuccedent.isEmpty()) { + continue; + } + candidates = candidates.prepend(new MergePartner(other, + new PosInOccurrence(otherSuccedent.get(0), PosInTerm.getTopLevel(), false))); + } + if (candidates.isEmpty()) { + return null; + } + return new MergeAppData(mergeGoal, pio, candidates, true); + } + + /** + * Replicates the Swing / JavaFX dialog's {@code isApplicableForCandidates} for the harness + * parity checks: builds the merge rule application for the given candidates (with the + * dialog's default merge procedure, the head of the procedure list) and checks its + * completeness. + * + * @param proof the proof + * @param goal the merge goal + * @param pio the merge position + * @param candidates the candidates to instantiate the application with + * @return true iff the induced merge rule application is complete (dialog OK enabled) + */ + private static boolean mergeApplicableForCandidates(Proof proof, Goal goal, + PosInOccurrence pio, ImmutableList candidates) { + try { + MergeRuleBuiltInRuleApp app = + (MergeRuleBuiltInRuleApp) MergeRule.INSTANCE.createApp(pio, proof.getServices()); + app.setMergeNode(goal.node()); + app.setConcreteRule(MergeProcedure.getMergeProcedures().head()); + app.setMergePartners(candidates); + return app.complete(); + } catch (RuntimeException e) { + LOGGER.info("JoinMerge verification: merge parity check threw: {}", e.toString()); + return false; + } + } + /** * Checks the forced (headless, no dialog) completion of the merge rule application * (Swing MergeRuleCompletion.complete forced mode: all potential partners and the @@ -273,6 +355,11 @@ private static void verifyForcedMergeCompletion(Proof proof, MergeAppData mergeA "JoinMerge verification: merge-dialog SKIP (no admissible merge position with partners)"); return; } + if (mergeApp.synthetic()) { + LOGGER.info( + "JoinMerge verification: merge-completion SKIP (synthetic merge app is not of admissible form; the dialog checks use a harness parity check)"); + return; + } try { MergeRuleBuiltInRuleApp app = (MergeRuleBuiltInRuleApp) MergeRule.INSTANCE.createApp(mergeApp.pio(), @@ -460,13 +547,24 @@ private static void assertMergePartnerDialog(MergePartnerSelectionDialogF dialog check("merge-dialog distinguishing-formula field enabled iff single candidate", dialog.getTxtDistForm().isDisabled() != (mergeApp.candidates().size() == 1)); - // select the first candidate as merge partner (parity with the checkbox action) + // select the first candidate as merge partner (parity with the checkbox action); + // OK enablement parity = the dialog's isApplicableForCandidates replicated by the + // harness (works for synthetic data too, where the real merge is not applicable) dialog.getCbSelectCandidate().fire(); - check("merge-dialog OK enabled after partner chosen", !dialog.getOkButton().isDisabled()); - - // enablement parity (Swing checkApplicable) - check("merge-dialog Choose-All " + "enablement parity", - !dialog.getChooseAllButton().isDisabled()); + ImmutableList chosenList = + ImmutableList.nil().prepend(mergeApp.candidates().head()); + boolean expectedOkAfterChoice = mergeApplicableForCandidates(proof, mergeApp.goal(), + mergeApp.pio(), chosenList); + check("merge-dialog OK " + (expectedOkAfterChoice ? "enabled" : "disabled") + + " after partner chosen (harness parity)", + dialog.getOkButton().isDisabled() != expectedOkAfterChoice); + + // Choose-All enablement parity (Swing checkApplicable) + boolean expectedChooseAll = + mergeApplicableForCandidates(proof, mergeApp.goal(), mergeApp.pio(), + mergeApp.candidates()); + check("merge-dialog Choose-All " + (expectedChooseAll ? "enabled" : "disabled") + + " (harness parity)", dialog.getChooseAllButton().isDisabled() != expectedChooseAll); // OK/cancel semantics on a hidden second instance (so the screenshot dialog stays open) MergePartnerSelectionDialogF hidden = From dc8ce6f84f40fd5555ec1dee09819731b79800a1 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Thu, 8 Oct 2026 22:48:48 +0200 Subject: [PATCH 079/281] key.ui.fx: tacletmatch: style the redesigned dialog scene with the active theme (dark theme fix) --- .../uka/ilkd/key/gui/fx/tacletmatch/TacletMatchDialogF.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/tacletmatch/TacletMatchDialogF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/tacletmatch/TacletMatchDialogF.java index 4434292ed43..cf16aa32264 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/tacletmatch/TacletMatchDialogF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/tacletmatch/TacletMatchDialogF.java @@ -22,6 +22,7 @@ import de.uka.ilkd.key.control.instantiation_model.TacletInstantiationModel; import de.uka.ilkd.key.gui.fx.fonticons.FontAwesomeSolid; import de.uka.ilkd.key.gui.fx.fonticons.IconFactoryF; +import de.uka.ilkd.key.gui.fx.theme.ThemeManager; import de.uka.ilkd.key.java.Services; import de.uka.ilkd.key.pp.NotationInfo; import de.uka.ilkd.key.proof.Goal; @@ -96,6 +97,10 @@ public TacletMatchDialogF(Stage owner, TacletInstantiationModel[] model, Goal go root.setBottom(createFooter()); Scene scene = new Scene(root, 1000, 620); + // style the dialog scene with the active theme stylesheet (like every other FX window; + // the classic completion dialog does the same) — without it the dialog would render in + // plain Modena regardless of the light/dark theme + ThemeManager.getInstance().style(scene); root.setMinSize(640, 400); setScene(scene); if (owner != null) { From 1b5d160700b9887e51619c4bc1d91a449a3ca442 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Thu, 8 Oct 2026 22:49:39 +0200 Subject: [PATCH 080/281] key.ui.fx: lemmaorigin: the origin-visualizer self test prefers a node whose sequent carries origin labels --- .../gui/fx/originlabels/OriginLabelsF.java | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/originlabels/OriginLabelsF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/originlabels/OriginLabelsF.java index a8b43dcc82d..b738bd856ba 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/originlabels/OriginLabelsF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/originlabels/OriginLabelsF.java @@ -25,6 +25,7 @@ import de.uka.ilkd.key.gui.fx.notification.NotificationManagerF; import de.uka.ilkd.key.gui.fx.notification.NotificationManagerF.Kind; import de.uka.ilkd.key.ldt.JavaDLTheory; +import de.uka.ilkd.key.logic.label.OriginTermLabel; import de.uka.ilkd.key.pp.NotationInfo; import de.uka.ilkd.key.proof.Node; import de.uka.ilkd.key.proof.Proof; @@ -280,7 +281,8 @@ public static String verify(MainWindowF mainWindow) { } } node = nextPreorderNode(node); - } boolean grew = false, hiddenOk = true; + } + boolean grew = false, hiddenOk = true; if (printed != null) { // hide everything (Swing "Display Term Labels in Formulas" off) and back on manager.setShowLabels(false); @@ -333,6 +335,23 @@ private static String verifyOriginVisualizer(MainWindowF mainWindow) { if (node == null) { return "originVis: no node FAIL"; } + // prefer a node whose sequent carries origin labels (nodes from the interactive part of + // the saved proofs do; the root usually does not) + Node originNode = null; + for (Node n = node.proof().root(); n != null; n = nextPreorderNode(n)) { + if ((!n.sequent().succedent().isEmpty() && OriginTermLabel + .getOrigin(new PosInOccurrence(n.sequent().succedent().getFirst(), + org.key_project.logic.PosInTerm.getTopLevel(), false)) != null) + || (!n.sequent().antecedent().isEmpty() && OriginTermLabel.getOrigin( + new PosInOccurrence(n.sequent().antecedent().getFirst(), + org.key_project.logic.PosInTerm.getTopLevel(), true)) != null)) { + originNode = n; + break; + } + } + if (originNode != null) { + node = originNode; + } // first top-level formula of the sequent (Swing ShowOriginAction walks up to a formula; // the first formula is as good a demonstration position as any) PosInOccurrence pio = null; From d149b27e0d7b3903c023f4ccbe661a7652da7154 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Thu, 8 Oct 2026 22:54:37 +0200 Subject: [PATCH 081/281] key.ui.fx: joinmerge: show the join dialog for the screenshot window --- .../main/java/de/uka/ilkd/key/gui/fx/join/JoinMergeVerifyF.java | 1 + 1 file changed, 1 insertion(+) diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/join/JoinMergeVerifyF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/join/JoinMergeVerifyF.java index 9d821ee6c75..8fed24309e6 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/join/JoinMergeVerifyF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/join/JoinMergeVerifyF.java @@ -400,6 +400,7 @@ private static void verifyJoinDialog(Window owner, Proof proof, try { JoinDialogF dialog = new JoinDialogF(partners, proof, PredicateEstimator.STD_ESTIMATOR, proof.getServices(), owner); + dialog.showNonBlocking(); assertJoinDialog(dialog, proof, partners, real); assertOkCancelSemantics(proof, partners); LOGGER.info("JoinMerge verification: join dialog open for screenshot ({} s window)", From 1c0d406d4054faa72b45d1d61af3eb5671b31831 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Fri, 9 Oct 2026 09:39:22 +0200 Subject: [PATCH 082/281] key.ui.fx: spotless: reorder MergeRuleCompletionF import alphabetically --- key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/MainWindowF.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index ec1e4d39c56..12e703335f6 100644 --- 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 @@ -65,8 +65,8 @@ import de.uka.ilkd.key.gui.fx.goallist.GoalListViewF; import de.uka.ilkd.key.gui.fx.infoview.InfoViewF; import de.uka.ilkd.key.gui.fx.join.JoinMergeVerifyF; -import de.uka.ilkd.key.gui.fx.mergerule.MergeRuleCompletionF; 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.SequentViewF; import de.uka.ilkd.key.gui.fx.notification.NotificationCenterF; import de.uka.ilkd.key.gui.fx.notification.NotificationManagerF; From a2252021009e6f6ec52e2fa23c57ac1f1dbca804 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Fri, 9 Oct 2026 09:42:19 +0200 Subject: [PATCH 083/281] key.ui.fx: smalldialogs: profile loading options, soundiness report dialog, javac settings (JavaFX) Ports the remaining small-dialog packages to JavaFX in key.ui.fx: - profileloading/: LoadingOptionsDialogF (Swing KeYFileChooserLoadingOptions accessory) + WDLoadOptionPanelF (well-definedness profile). The JavaFX FileChooser has no accessory, so options are collected in a pre-load dialog; Cancel aborts like the Swing chooser. MainWindowF threads the LoadOptions through the load path (OpenFileAction.java:70-76 parity). - soundiness/: SoundinessAnalyzer (HTML report) + SoundinessDialogF (JavaFX WebView). ShowSoundinessAction lives in the View menu until the FX proof-list dockable exists. - plugins/javac/: JavacSettingsProviderF, registered in SettingsManagerF (Swing JavacExtension settings capability; FX extension SPI not ready). - build.gradle: add javafx.web module (WebView) and key.core.wd dependency (WDLoadOptionPanelF); new key.fx.verify.profileloading/.profileloadingdialog/ .soundiness/.javacsettings self-test flags. --- key.ui.fx/build.gradle | 20 +- .../de/uka/ilkd/key/gui/fx/MainWindowF.java | 163 ++++- .../gui/fx/plugins/javac/JavacSettings.java | 106 +++ .../plugins/javac/JavacSettingsProviderF.java | 200 ++++++ .../profileloading/LoadingOptionsDialogF.java | 300 +++++++++ .../fx/profileloading/WDLoadOptionPanelF.java | 198 ++++++ .../key/gui/fx/settings/SettingsManagerF.java | 13 +- .../gui/fx/soundiness/SoundinessAnalyzer.java | 606 ++++++++++++++++++ .../gui/fx/soundiness/SoundinessDialogF.java | 150 +++++ 9 files changed, 1747 insertions(+), 9 deletions(-) create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/plugins/javac/JavacSettings.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/plugins/javac/JavacSettingsProviderF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/profileloading/LoadingOptionsDialogF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/profileloading/WDLoadOptionPanelF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/soundiness/SoundinessAnalyzer.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/soundiness/SoundinessDialogF.java diff --git a/key.ui.fx/build.gradle b/key.ui.fx/build.gradle index 2ee5d3f48f3..3cbadb55f1b 100644 --- a/key.ui.fx/build.gradle +++ b/key.ui.fx/build.gradle @@ -22,12 +22,18 @@ javafx { version = libs.versions.javafx.get() // key.ui.fx intentionally avoids javafx-fxml and javafx-swing: // all UIs are built in code (no FXML) and no Swing components are used. - modules = ['javafx.base', 'javafx.controls', 'javafx.graphics'] + // smalldialogs: javafx.web hosts the WebView of SoundinessDialogF (the Swing + // SoundinessDialog renders the HTML soundiness report in a JEditorPane). + modules = ['javafx.base', 'javafx.controls', 'javafx.graphics', 'javafx.web'] } dependencies { implementation project(":key.core") + // smalldialogs: WDLoadOptionPanelF (port of gui/profileloading) references the + // well-definedness profile (Swing key.ui depends on key.core.wd the same way) + implementation project(":key.core.wd") + // Java/JML source editor with syntax highlighting (Milestone M2). implementation(libs.richtextfx) @@ -67,12 +73,22 @@ run { // needs a sequent printing update operators, e.g. standard_key/queries/useQuery.key) // * key.fx.verify.help = any (smalldialogs: run the F1 context help resolution self test; // no browser window is opened) + // * key.fx.verify.profileloading = any (smalldialogs: run the WD load-option panel self + // test; no proof load is started) + // * key.fx.verify.profileloadingdialog = any (smalldialogs: open the loading options + // dialog at startup and log the options returned on confirm) + // * key.fx.verify.soundiness = any (smalldialogs: run the soundiness report self test on + // the demo proof and open the SoundinessDialogF) + // * key.fx.verify.javacsettings = any (smalldialogs: run the javac settings provider + // self test; the settings dialog is not opened) ["key.fx.driver", "key.fx.theme", "key.fx.demo.sequent", "key.fx.verify.sequent", "key.fx.show", "key.fx.demo.autoprove", "key.fx.verify.tree", "key.fx.demo.autoprove.live", "key.fx.demo.maxsteps", "key.fx.verify.goallist", "key.fx.verify.search", "key.fx.verify.sequentsearch", "key.fx.verify.treefilters", "key.fx.verify.sequentsearchmodes", "key.fx.verify.updatehighlight", - "key.fx.verify.help"] + "key.fx.verify.help", "key.fx.verify.profileloading", "key.fx.verify.profileloadingdialog", + "key.fx.verify.soundiness", + "key.fx.verify.javacsettings"] .each { name -> def value = System.getProperty(name) if (value != null) { 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 index 485e7704fdd..89ca4fb8579 100644 --- 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 @@ -67,10 +67,16 @@ import de.uka.ilkd.key.gui.fx.nodeviews.SequentViewF; 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.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.prooftree.ProofTreeViewF; import de.uka.ilkd.key.gui.fx.recentfiles.RecentFilesF; import de.uka.ilkd.key.gui.fx.settings.SettingsManagerF; +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.theme.Theme; @@ -267,9 +273,10 @@ public void initialize() { recentFiles.load(); startDemoProofLoad(); - // smalldialogs: startup self test of the F1 help URL resolution; reports to the log and - // toast (same pattern as the key.fx.verify.* hooks in startProofLoad). The help facade is - // proof-independent, so the hook runs at startup rather than after a demo proof load. + // 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); @@ -277,6 +284,31 @@ public void initialize() { .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); + } NotificationManagerF.getInstance() .notify("KeY (JavaFX) started. Docking layout restored from " @@ -386,7 +418,7 @@ private void startDemoProofLoad() { * @param demo whether this is the demo property load (notification prefix "Demo proof") */ private void startProofLoad(Path location, boolean demo) { - startProofLoad(location, demo, null); + startProofLoad(location, demo, null, null); } /** @@ -398,6 +430,23 @@ private void startProofLoad(Path location, boolean demo) { * {@code location} is not a proof bundle */ private void startProofLoad(Path location, boolean demo, @Nullable Path proofFilename) { + startProofLoad(location, demo, proofFilename, null); + } + + /** + * 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) { // 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) { @@ -420,6 +469,21 @@ protected KeYEnvironment call() throws Exception { 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 { env = KeYEnvironment.load(location); } @@ -491,6 +555,11 @@ protected KeYEnvironment call() throws Exception { if (System.getProperty("key.fx.verify.goallist") != null) { LOGGER.info("Goal list verification: {}", goalListView.verifyGoalList()); } + // 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); @@ -541,6 +610,19 @@ private void openFileChooser() { 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()); } @@ -558,6 +640,20 @@ private void openFileChooser() { * @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); @@ -571,7 +667,7 @@ public void openProofFile(Path file) { warnOnBareJavaFile(file); recentFiles.add(file.toAbsolutePath().toString(), null, false, null); - startProofLoad(file, false); + startProofLoad(file, false, null, options); } /** @@ -1209,7 +1305,19 @@ private Menu buildViewMenu() { view.getItems().addAll(prettyPrint, unicode, syntaxHighlighting, new SeparatorMenuItem(), themeMenu, fontSize, new SeparatorMenuItem(), menuItem("Visual Node Diff", "de.uka.ilkd.key.gui.proofdiff.ProofDiffFrame$Action", - this::showProofDiffFrame)); + this::showProofDiffFrame), + // 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 temporarily lives in the View menu; Swing + // enableWhenProofLoaded + the general auto-mode lock carry over. + menuItem("Show Soundiness Report", "de.uka.ilkd.key.gui.actions.ShowSoundinessAction", + this::showSoundinessReport)); + // enablement of the soundiness item (Swing enableWhenProofLoaded; no interaction during + // auto mode) — kept outside menuItem() to reach the item reference + javafx.scene.control.MenuItem soundinessItem = + view.getItems().get(view.getItems().size() - 1); + soundinessItem.disableProperty() + .bind(mediator.autoModeRunningProperty().or(proofLoaded.not())); return view; } @@ -1411,6 +1519,49 @@ 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"); + String report = "chars=" + html.length() + " sections=" + sectionsOk + + (sectionsOk ? " PASS" : " FAIL"); + LOGGER.info("Soundiness verification: {}", report); + NotificationManagerF.getInstance() + .notify("Soundiness verification: " + report, + sectionsOk ? Kind.INFO : Kind.ERROR); + if (sectionsOk) { + new SoundinessDialogF(stage, proof).showCenteredOnOwner(); + } + } + private void resetLayout() { workspace.restoreFactoryDefault(); } diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/plugins/javac/JavacSettings.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/plugins/javac/JavacSettings.java new file mode 100644 index 00000000000..598bde97333 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/plugins/javac/JavacSettings.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.plugins.javac; + +import java.io.Serializable; + +import de.uka.ilkd.key.settings.AbstractPropertiesSettings; + +/** + * Settings for the javac extention. + *

    + * Verbatim copy of {@code de.uka.ilkd.key.gui.plugins.javac.JavacSettings} of the Swing module + * {@code key.ui} (package change and javadoc note only; the original carries no Swing or AWT + * references — it extends the core {@link AbstractPropertiesSettings}, so the class is reusable + * by {@code key.ui.fx} as-is per the module's logic-reuse policy). + * + * @author Daniel Grévent + */ +public class JavacSettings extends AbstractPropertiesSettings implements Serializable { + + /** + * The name of the category in the settings. + */ + public static final String CATEGORY = "Javac Extension"; + + /** + * Config key for {@link #useProcessors}. + */ + private static final String KEY_USE_PROCESSORS = "useProcessors"; + + /** + * Config key for {@link #processors}. + */ + private static final String KEY_PROCESSORS = "processors"; + + /** + * Config key for {@link #classPaths}. + */ + private static final String KEY_CLASS_PATHS = "classPaths"; + + /** + * The type annotation processors to be run. + */ + private final PropertyEntry useProcessors = + createBooleanProperty(KEY_USE_PROCESSORS, false); + + /** + * The type annotation processors to be run. + */ + private final PropertyEntry processors = + createStringProperty(KEY_PROCESSORS, ""); + + /** + * Additional class paths, needed for example by annotation processors + */ + private final PropertyEntry classPaths = + createStringProperty(KEY_CLASS_PATHS, ""); + + public JavacSettings() { + super(CATEGORY); + } + + /** + * @param useProcessors if the annotation processors should be run + */ + public void setUseProcessors(boolean useProcessors) { + this.useProcessors.set(useProcessors); + } + + /** + * @param processor the annotation processors to run + */ + public void setProcessors(String processor) { + this.processors.set(processor); + } + + /** + * @param paths the additional class paths + */ + public void setClassPaths(String paths) { + this.classPaths.set(paths); + } + + /** + * @return true iff the annotation processors should be used + */ + public boolean getUseProcessors() { + return useProcessors.get(); + } + + /** + * @return the annotation processors separated by newlines + */ + public String getProcessors() { + return processors.get(); + } + + /** + * @return the additional class paths separated by newlines + */ + public String getClassPaths() { + return classPaths.get(); + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/plugins/javac/JavacSettingsProviderF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/plugins/javac/JavacSettingsProviderF.java new file mode 100644 index 00000000000..a4782c57761 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/plugins/javac/JavacSettingsProviderF.java @@ -0,0 +1,200 @@ +/* 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.plugins.javac; + +import javafx.scene.control.CheckBox; +import javafx.scene.control.Label; +import javafx.scene.control.TextArea; + +import de.uka.ilkd.key.gui.fx.MainWindowF; +import de.uka.ilkd.key.gui.fx.settings.InvalidSettingsInputExceptionF; +import de.uka.ilkd.key.gui.fx.settings.SettingsPanelF; +import de.uka.ilkd.key.gui.fx.settings.SettingsProviderF; +import de.uka.ilkd.key.settings.ProofIndependentSettings; + +/** + * Settings for the javac extension. + *

    + * FX port of the Swing {@code de.uka.ilkd.key.gui.plugins.javac.JavacSettingsProvider} ({@code + * key.ui}, Daniel Grévent): a settings panel ({@code SettingsPanel} → here + * {@link SettingsPanelF}) with the intro label, the Enable Annotation Processing + * checkbox, and the Annotation Processors / Processor Class Paths text areas + * ({@code JavacSettingsProvider.java:60-73}); the two text areas are enabled only while the + * checkbox is selected ({@code :74-80}). {@code getPanel} re-reads the {@link JavacSettings}, + * {@code applySettings} writes them back ({@code :99-116}); the description is "Javac Options" + * ({@code :64}) and the tree priority is 10000 ({@code :121}, sorted last like Swing). + *

    + * Plugins-package scope note: the Swing {@code JavacExtension} contributes further + * user-visible parts — the javac status-line widget with the compile feedback (via {@code + * JavaCompilerCheckFacade}, {@code JavacExtension.java:110-141}) and the issue-list dialog + * ({@code IssueDialog}, 877 Swing lines). These are deliberately deferred with the whole + * {@code KeYGuiExtensionF} SPI (plan §6, M6): the FX status bar has no extension slot yet and + * the compile pipeline requires a further {@code PositionedIssueString}/{@code + * JavaCompilerCheckFacade} port. The settings tab is ported here as the extension-independent + * user-visible part. Likewise the {@code action_history} plugin ({@code UndoHistoryButton} in + * the toolbar) is deferred: its backend needs the Swing {@code UserActionListener} mediator API + * ({@code key.ui}) that the FX mediator does not provide yet. + */ +public class JavacSettingsProviderF extends SettingsPanelF implements SettingsProviderF { + + /** + * Singleton instance of the javac settings (Swing {@code JAVAC_SETTINGS}, registered into + * {@code ProofIndependentSettings} on first use so it is persisted like in Swing). + */ + private static final JavacSettings JAVAC_SETTINGS = new JavacSettings(); + + /** + * Text for the explanation (Swing {@code INTRO_LABEL}). + */ + private static final String INTRO_LABEL = + "This allows to run the Java compiler when loading Java files with additional " + + "processes such as Nullness or Ownership checkers."; + + /** + * Information message for the useProcessors checkbox (Swing {@code USE_PROCESSORS_INFO}). + */ + private static final String USE_PROCESSORS_INFO = + "If enabled the annotation processors will be run with the Java compiler while " + + "performing type checking of newly loaded sources."; + + /** + * Information message for the processors text area (Swing {@code PROCESSORS_INFO}). + */ + private static final String PROCESSORS_INFO = """ + A list of annotation processors to run while type checking with the Java compiler. + Each checkers should be written on a new line."""; + + /** + * Information message for the paths text area (Swing {@code CLASS_PATHS_INFO}). + */ + private static final String CLASS_PATHS_INFO = """ + A list of additional class paths to be used by the Java compiler while type checking. + These could for example be needed for certain annotation processors. + Each path should be absolute and be written on a new line."""; + + private final CheckBox useProcessors; + private final TextArea processors; + private final TextArea paths; + + /** + * Construct a new settings provider (Swing constructor layout). + */ + public JavacSettingsProviderF() { + Label intro = new Label(INTRO_LABEL); + intro.setWrapText(true); + pCenter.add(intro, 0, pCenter.getRowCount(), 3, 1); + processors = addTextArea("Annotation Processors", "", PROCESSORS_INFO, null, 4); + paths = addTextArea("Processor Class Paths", "", CLASS_PATHS_INFO, null, 4); + useProcessors = addCheckBox("Enable Annotation Processing", USE_PROCESSORS_INFO, false); + // Swing ItemListener: the two text areas are disabled unless the checkbox is selected + useProcessors.selectedProperty().addListener((obs, old, selected) -> { + processors.setDisable(!selected); + paths.setDisable(!selected); + }); + processors.setDisable(!useProcessors.isSelected()); + paths.setDisable(!useProcessors.isSelected()); + + setHeaderText("Javac Options"); + } + + /** + * @return the (persisted) javac settings instance, registered like Swing + * {@code getJavacSettings()} + */ + public static JavacSettings getJavacSettings() { + ProofIndependentSettings.DEFAULT_INSTANCE.addSettings(JAVAC_SETTINGS); + return JAVAC_SETTINGS; + } + + @Override + public String getDescription() { + return "Javac Options"; + } + + @Override + public javafx.scene.Node getPanel(MainWindowF window) { + JavacSettings settings = getJavacSettings(); + + // Swing getPanel: re-read the settings on every call (the panel is a singleton) + useProcessors.setSelected(settings.getUseProcessors()); + processors.setText(settings.getProcessors()); + paths.setText(settings.getClassPaths()); + processors.setDisable(!useProcessors.isSelected()); + paths.setDisable(!useProcessors.isSelected()); + + return this; + } + + @Override + public void apply(MainWindowF window) throws InvalidSettingsInputExceptionF { + JavacSettings settings = getJavacSettings(); + + // Swing applySettings: write the panel values back + settings.setUseProcessors(useProcessors.isSelected()); + settings.setProcessors(processors.getText()); + settings.setClassPaths(paths.getText()); + } + + @Override + public int getPriorityOfSettings() { + return 10000; + } + + /** + * Self test of the provider (system property {@code key.fx.verify.javacsettings}, run at + * startup from {@code MainWindowF}): reads/writes round trip through the panel — set test + * values, read them back via {@link #getPanel(MainWindowF)}, change the panel controls and + * write them back via {@link #apply(MainWindowF)} — and restores the previous values. + * + * @return a self-test report ending in {@code PASS} or {@code FAIL} + */ + public static String verifyJavacSettings() { + StringBuilder report = new StringBuilder("round-trip "); + try { + JavacSettings settings = getJavacSettings(); + // save and restore: the settings are shared with the running instance + boolean oldUseProcessors = settings.getUseProcessors(); + String oldProcessors = settings.getProcessors(); + String oldClassPaths = settings.getClassPaths(); + try { + settings.setUseProcessors(true); + settings.setProcessors("verify.TestProcessor"); + settings.setClassPaths("/tmp/verify"); + + // read path: getPanel loads the settings into the controls + JavacSettingsProviderF panel = new JavacSettingsProviderF(); + panel.getPanel(null); + boolean readOk = panel.useProcessors.isSelected() + && "verify.TestProcessor".equals(panel.processors.getText()) + && "/tmp/verify".equals(panel.paths.getText()) + && !panel.processors.isDisabled() && !panel.paths.isDisabled(); + report.append(readOk ? "read=ok; " : "read=FAIL; "); + + // write path: change the controls and apply + panel.useProcessors.setSelected(false); + panel.processors.setText("verify.OtherProcessor"); + panel.paths.setText(""); + try { + panel.apply(null); + } catch (de.uka.ilkd.key.gui.fx.settings.InvalidSettingsInputExceptionF e) { + report.append("apply exception ").append(e).append("; "); + } + boolean writeOk = !settings.getUseProcessors() + && "verify.OtherProcessor".equals(settings.getProcessors()) + && settings.getClassPaths().isEmpty(); + report.append(writeOk ? "write=ok; " : "write=FAIL; "); + } finally { + settings.setUseProcessors(oldUseProcessors); + settings.setProcessors(oldProcessors); + settings.setClassPaths(oldClassPaths); + } + } catch (RuntimeException e) { + report.append("exception ").append(e).append("; "); + } + report.append(report.toString().contains("FAIL") + || report.toString().contains("exception") ? "FAIL" : "PASS"); + return report.toString(); + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/profileloading/LoadingOptionsDialogF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/profileloading/LoadingOptionsDialogF.java new file mode 100644 index 00000000000..1624d1b9bd0 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/profileloading/LoadingOptionsDialogF.java @@ -0,0 +1,300 @@ +/* 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.profileloading; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.ServiceLoader; +import javafx.scene.Scene; +import javafx.scene.control.Button; +import javafx.scene.control.ButtonBar; +import javafx.scene.control.CheckBox; +import javafx.scene.control.ComboBox; +import javafx.scene.control.Label; +import javafx.scene.control.TextArea; +import javafx.scene.control.Tooltip; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.ColumnConstraints; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; +import javafx.stage.Modality; +import javafx.stage.Window; + +import de.uka.ilkd.key.gui.fx.settings.SettingsPanelF; +import de.uka.ilkd.key.gui.fx.theme.ThemeManager; +import de.uka.ilkd.key.proof.init.DefaultProfileResolver; +import de.uka.ilkd.key.proof.init.Profile; +import de.uka.ilkd.key.settings.Configuration; + +import org.jspecify.annotations.Nullable; + +/** + * Loading options collected before a proof load: the profile selection, the profile description + * and the profile-specific additional option panels (currently the {@link WDLoadOptionPanelF}), + * plus the "Ignore other Java files" checkbox. + *

    + * smalldialogs: FX port of the Swing {@code de.uka.ilkd.key.gui.KeYFileChooserLoadingOptions} + * ({@code KeYFileChooserLoadingOptions.java:37-113}), the loading-options accessory of the Swing + * {@code KeYFileChooser} used by {@code OpenFileAction}. + *

    + * Placement deviation: Swing embeds this panel inside the file chooser + * ({@code KeYFileChooser.addLoadingOptions()}); JavaFX {@code FileChooser} has no accessory API, + * so the identical content is collected in this modal pre-load dialog, shown from + * {@code MainWindowF.openFileChooser} directly after a file has been picked. Cancel aborts the + * load, mirroring Swing where the options cannot be confirmed without approving the chooser. + * Like Swing (only {@code OpenFileAction} adds the accessory), the dialog is shown only in the + * open-file flow — recent files, quick load and the demo load keep their behavior. + *

    + * The Swing original collects the profile-specific option panels via the {@code KeYGuiExtension} + * SPI ({@code KeYGuiExtensionFacade.createAdditionalOptionPanels()}, {@code + * KeYFileChooserLoadingOptions.java:53-54}); the FX extension SPI does not exist yet, so the + * panels are registered in the local {@link #additionalOptionPanels} map built from the ported + * {@link WDLoadOptionPanelF#getProfile()} (the extension seam stays open). For the same reason + * the profile list only contains the {@code DefaultProfileResolver} services on the + * {@code key.ui.fx} classpath (Swing additionally offers the InfFlow profile of + * {@code key.core.infflow}, which is not a dependency of {@code key.ui.fx}). + */ +public final class LoadingOptionsDialogF extends javafx.stage.Stage { + + /** + * The loading options selected in the dialog, forwarded to the proof loader (Swing + * {@code OpenFileAction.actionPerformed:70-76}). + * + * @param selectedProfile the profile to force on the new proofs, {@code null} for the legacy + * mode "Respect profile given in file" (Swing {@code ProfileWrapper} with a null + * profile → {@code forceNewProfileOfNewProofs(false)}) + * @param additionalProfileOptions the configuration of the selected profile's option panel, + * {@code null} if the profile has no option panel (Swing + * {@code getAdditionalProfileOptions()}) + * @param singleJavaFile whether to load only the selected Java file (Swing + * {@code isOnlyLoadSingleJavaFile()}) + */ + public record LoadOptions(@Nullable Profile selectedProfile, + @Nullable Configuration additionalProfileOptions, boolean singleJavaFile) { + } + + /** + * Combo box entry (Swing record {@code ProfileWrapper}, {@code + * KeYFileChooserLoadingOptions.java:141-151}): name, ident, description and the profile + * itself ({@code null} for the legacy mode). + */ + private record ProfileWrapper(String name, String ident, String description, + @Nullable Profile profile) { + ProfileWrapper(Profile profile) { + this(profile.displayName(), profile.ident(), profile.description(), profile); + } + + @Override + public String toString() { + return name; + } + } + + /** Swing {@code lblProfile}. */ + private final Label lblProfile = new Label("Profile:"); + + /** Swing {@code cboProfile}. */ + private final ComboBox cboProfile = new ComboBox<>(); + + /** Swing {@code lblProfileInfo}: the description of the selected profile. */ + private final TextArea lblProfileInfo = new TextArea(); + + /** Swing {@code lblSingleJava}. */ + private final CheckBox lblSingleJava = new CheckBox("Ignore other Java files"); + + /** + * Host of the currently installed profile option panel (Swing {@code currentOptionPanel}, + * which is added to/removed from the accessory panel on profile changes). + */ + private final VBox optionPanelHost = new VBox(); + + /** + * smalldialogs: the profile option panels. Swing fills this map via the + * {@code KeYGuiExtension} SPI ({@code createAdditionalOptionPanels()}); here the ported WD + * panel is registered directly (SPI seam open, see class javadoc). + */ + private static final Map additionalOptionPanels = + buildAdditionalOptionPanels(); + + /** The dialog result; {@code null} until confirmed (Swing: chooser approved). */ + private @Nullable LoadOptions result; + + private LoadingOptionsDialogF(Window owner) { + setTitle("Loading Options"); + if (owner != null) { + initOwner(owner); + } + initModality(Modality.WINDOW_MODAL); + + // Swing KeYFileChooserLoadingOptions.java:60-70: the profiles of the + // DefaultProfileResolver services, legacy mode first, legacy mode preselected + List profiles = new ArrayList<>(ServiceLoader + .load(DefaultProfileResolver.class) + .stream().map(it -> it.get().getDefaultProfile()).map(ProfileWrapper::new) + .toList()); + profiles.addFirst(new ProfileWrapper("Respect profile given in file", "", + "Usable on KeY file which defines \\profile inside the file. " + + "If no KeY file is loaded, falls back to legacy behavior", + null)); + cboProfile.getItems().setAll(profiles); + cboProfile.getSelectionModel().selectFirst(); + cboProfile.getSelectionModel().selectedItemProperty() + .addListener((obs, old, selected) -> updateProfileInfo()); + + // Swing lblProfileInfo: non-editable, word wrap + lblProfileInfo.setEditable(false); + lblProfileInfo.setWrapText(true); + lblSingleJava.setTooltip(new Tooltip(""" + Normally, KeY loads all Java files in the same folder and sub-folder of your + selected file. Mark this checkbox to only load the selected Java file.""")); + + GridPane grid = new GridPane(); + grid.setHgap(8); + grid.setVgap(8); + grid.setPadding(new javafx.geometry.Insets(12)); + ColumnConstraints labels = new ColumnConstraints(); + ColumnConstraints inputs = new ColumnConstraints(); + inputs.setHgrow(Priority.ALWAYS); + ColumnConstraints helps = new ColumnConstraints(); + grid.getColumnConstraints().setAll(labels, inputs, helps); + lblProfile.setLabelFor(cboProfile); + + lblProfileInfo.setPrefColumnCount(1); + lblProfileInfo.setPrefRowCount(3); + + grid.add(lblProfile, 0, 0); + grid.add(cboProfile, 1, 0); + grid.add(SettingsPanelF.createHelpLabel(""" + A Profile determines the proof environment, especially, the used built-in rules, + specification repository, and taclet options. + + The default is "Java Profile". + """), 2, 0); + grid.add(lblProfileInfo, 1, 1, 2, 1); + GridPane.setHgrow(lblProfileInfo, Priority.ALWAYS); + grid.add(lblSingleJava, 0, 2, 2, 1); + grid.add(optionPanelHost, 1, 3, 2, 1); + + updateProfileInfo(); + + Button cancelButton = new Button("Cancel"); + cancelButton.setOnAction(e -> { + result = null; + close(); + }); + Button loadButton = new Button("Load"); + loadButton.setDefaultButton(true); + loadButton.setOnAction(e -> { + ProfileWrapper selected = cboProfile.getSelectionModel().getSelectedItem(); + Profile profile = selected == null ? null : selected.profile(); + WDLoadOptionPanelF panel = profile == null ? null : additionalOptionPanels.get(profile); + result = new LoadOptions(profile, panel == null ? null : panel.getResult(), + lblSingleJava.isSelected()); + close(); + }); + ButtonBar buttonBar = new ButtonBar(); + buttonBar.getButtons().addAll(cancelButton, loadButton); + + BorderPane root = new BorderPane(grid, null, null, buttonBar, null); + Scene scene = new Scene(root, 520, 380); + ThemeManager.getInstance().manage(scene); + // Escape cancels (Swing: the options are lost with a canceled chooser dialog) + scene.setOnKeyPressed(e -> { + if (e.getCode() == javafx.scene.input.KeyCode.ESCAPE) { + cancelButton.fire(); + e.consume(); + } + }); + setScene(scene); + sizeToScene(); + } + + /** + * Swing {@code updateProfileInfo} ({@code KeYFileChooserLoadingOptions.java:96-119}): shows + * the description of the selected profile and installs the matching additional option panel + * (Swing {@code install}/{@code deinstall} of {@code WDLoadDialogOptionPanel.java:85-104}; + * here install/remove becomes adding/removing the panel from {@link #optionPanelHost}). + */ + private void updateProfileInfo() { + // Swing: currentOptionPanel.deinstall(this) on profile change + optionPanelHost.getChildren().clear(); + ProfileWrapper selected = cboProfile.getSelectionModel().getSelectedItem(); + if (selected == null) { + lblProfileInfo.setText(""); + } else { + lblProfileInfo.setText(selected.description()); + WDLoadOptionPanelF panel = additionalOptionPanels.get(selected.profile()); + if (panel != null) { + optionPanelHost.getChildren().add(panel); + } + } + } + + /** + * Builds the profile option panel registry (Swing + * {@code KeYGuiExtensionFacade.createAdditionalOptionPanels()}; the FX registry is + * constructed from the ported {@link WDLoadOptionPanelF}, see the class javadoc). + */ + private static Map buildAdditionalOptionPanels() { + Map map = new LinkedHashMap<>(); + WDLoadOptionPanelF wdPanel = new WDLoadOptionPanelF(); + map.put(wdPanel.getProfile(), wdPanel); + return map; + } + + /** + * Opens the dialog modally and waits for the user decision (Swing: the accessory is part of + * the chooser dialog; the approval handling in {@code OpenFileAction.actionPerformed:70-76} + * corresponds to the confirmed dialog). + * + * @param owner the owner window (the main window stage) + * @return the selected loading options, or {@code null} if the dialog was canceled + */ + public static @Nullable LoadOptions showOptions(Window owner) { + LoadingOptionsDialogF dialog = new LoadingOptionsDialogF(owner); + dialog.centerOnScreen(); + dialog.showAndWait(); + return dialog.result; + } + + /** + * The selected profile (Swing {@code getSelectedProfile()}, {@code + * KeYFileChooserLoadingOptions.java:121-130}); {@code null} in legacy mode. + * + * @return the profile to force on the new proofs or {@code null} + */ + public @Nullable Profile getSelectedProfile() { + ProfileWrapper selected = cboProfile.getSelectionModel().getSelectedItem(); + return selected == null ? null : selected.profile(); + } + + /** + * The options of the selected profile's option panel (Swing + * {@code getAdditionalProfileOptions()}, {@code KeYFileChooserLoadingOptions.java:132-139}); + * {@code null} without an installed panel. + * + * @return the additional profile options or {@code null} + */ + public @Nullable Configuration getAdditionalProfileOptions() { + Profile profile = getSelectedProfile(); + if (profile == null) { + return null; + } + WDLoadOptionPanelF panel = additionalOptionPanels.get(profile); + return panel == null ? null : panel.getResult(); + } + + /** + * Swing {@code isOnlyLoadSingleJavaFile} ({@code KeYFileChooserLoadingOptions.java:141-144}). + * + * @return whether to load only the selected Java file + */ + public boolean isOnlyLoadSingleJavaFile() { + return lblSingleJava.isSelected(); + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/profileloading/WDLoadOptionPanelF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/profileloading/WDLoadOptionPanelF.java new file mode 100644 index 00000000000..d419f790b00 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/profileloading/WDLoadOptionPanelF.java @@ -0,0 +1,198 @@ +/* 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.profileloading; + +import javafx.scene.Node; +import javafx.scene.control.Label; +import javafx.scene.control.RadioButton; +import javafx.scene.control.Separator; +import javafx.scene.control.ToggleGroup; +import javafx.scene.control.Tooltip; +import javafx.scene.layout.VBox; + +import de.uka.ilkd.key.proof.init.Profile; +import de.uka.ilkd.key.settings.Configuration; +import de.uka.ilkd.key.wd.WdProfile; + +/** + * Additional UI components for the selection of the WD semantics, counter-part of + * {@code de.uka.ilkd.key.gui.profileloading.WDLoadDialogOptionPanel} in the Swing module + * {@code key.ui}. + *

    + * The Swing original is a {@code KeYGuiExtension.LoadOptionPanel} that contributes a radio + * group (L / Y / D, default L) with descriptive tooltips into the loading options accessory of + * the Swing file chooser ({@code WDLoadDialogOptionPanel.java:34-118}); the selected operator + * is reported as a {@link Configuration} entry {@code wdOperator = wdOperator:} and + * feeds the well-definedness semantics of the loaded proof ({@code getResult}, {@code + * WDLoadDialogOptionPanel.java:106-117}). + *

    + * Port notes and deviations from the Swing original: + *

      + *
    • The Swing original is installed via the {@code KeYGuiExtension} SPI into + * {@code KeYFileChooserLoadingOptions} whenever the user selects the WD profile in the profile + * combo box ({@code KeYFileChooserLoadingOptions.java:88-99}, install/{@code + * WDLoadDialogOptionPanel.java:85-94}). In this port the host is {@link + * LoadingOptionsDialogF}, the FX counterpart of {@code KeYFileChooserLoadingOptions} shown + * before the load: it registers this panel for the WD profile in its option-panel map (the + * Swing SPI seam, plan §6/M6, stays open) and installs/removes it on profile changes.
    • + *
    • MigLayout {@code install/deinstall} becomes a plain {@link Node} ({@link VBox}) with the + * same content: bold header, separator, "Semantics:" label, mutually exclusive radio buttons.
    • + *
    • The radio button texts and the three tooltip descriptions are carried over verbatim + * ({@code WDLoadDialogOptionPanel.java:35-68}).
    • + *
    + */ +public class WDLoadOptionPanelF extends VBox { + + /** Radio button for the L (McCarthy logic) semantics (Swing {@code rdbWDL}). */ + private final RadioButton rdbWDL = new RadioButton("L"); + + /** Radio button for the Y (strong Kleene logic) semantics (Swing {@code rdbWDY}). */ + private final RadioButton rdbWDY = new RadioButton("Y"); + + /** Radio button for the D (classical logic) semantics (Swing {@code rdbWDD}). */ + private final RadioButton rdbWDD = new RadioButton("D"); + + /** + * Tooltip text of the L semantics (Swing {@code DESCRIPTION_L_WD_SEMANTIC}, + * {@code WDLoadDialogOptionPanel.java:43-50}). + */ + static final String DESCRIPTION_L_WD_SEMANTIC = """ + More intuitive for software developers and along the lines of + runtime assertion semantics. Well-Definedness checks will be + stricter using this operator, since the order of terms/formulas + matters. It is based on McCarthy logic. + Cf. "Are the Logical Foundations of Verifying Compiler + Prototypes Matching User Expectations?" by Patrice Chalin. + """; + + /** + * Tooltip text of the D semantics (Swing {@code DESCRIPTION_D_WD_SEMANTIC}, + * {@code WDLoadDialogOptionPanel.java:52-58}). + */ + static final String DESCRIPTION_D_WD_SEMANTIC = """ + Complete and along the lines of classical logic, where the + order of terms/formulas is irrelevant. This operator is + equivalent to the D-operator, but more efficient. + Cf. "Efficient Well-Definedness Checking" by Ádám Darvas, + Farhad Mehta, and Arsenii Rudich. + """; + + /** + * Tooltip text of the Y semantics (Swing {@code DESCRIPTION_Y_WD_SEMANTIC}, + * {@code WDLoadDialogOptionPanel.java:61-68}). + */ + static final String DESCRIPTION_Y_WD_SEMANTIC = """ + Complete and along the lines of classical logic, where the + order of terms/formulas is irrelevant. This operator is not as + strict as the L-operator, based on strong Kleene logic. To be + used with care, since formulas may blow up exponentially. + Cf. "Well Defined B" by Patrick Behm, Lilian Burdy, and + Jean-Marc Meynadier + """; + + /** The profile this option panel belongs to (Swing {@code getProfile()}). */ + private final Profile profile = WdProfile.INSTANCE; + + public WDLoadOptionPanelF() { + Label lblHeader = new Label("WD options"); + lblHeader.getStyleClass().add("dialog-section-title"); + Label lblSemantics = new Label("Semantics:"); + + rdbWDL.setTooltip(new Tooltip(DESCRIPTION_L_WD_SEMANTIC)); + rdbWDD.setTooltip(new Tooltip(DESCRIPTION_D_WD_SEMANTIC)); + rdbWDY.setTooltip(new Tooltip(DESCRIPTION_Y_WD_SEMANTIC)); + + ToggleGroup btnGrp = new ToggleGroup(); + btnGrp.getToggles().setAll(rdbWDD, rdbWDL, rdbWDY); + + // Swing default selection (WDLoadDialogOptionPanel.java:82) + rdbWDL.setSelected(true); + + getStyleClass().add("wd-load-option-panel"); + setSpacing(4); + getChildren().addAll(lblHeader, new Separator(), lblSemantics, + new javafx.scene.layout.HBox(8, rdbWDD, rdbWDY, rdbWDL)); + } + + /** + * The profile this option panel belongs to (Swing {@code LoadOptionPanel.getProfile()}, + * {@code WDLoadDialogOptionPanel.java:29-32}). + * + * @return the well-definedness profile + */ + public Profile getProfile() { + return profile; + } + + /** + * The additional profile options selected by the user (Swing + * {@code OptionPanel.getResult()}, {@code WDLoadDialogOptionPanel.java:106-117}): the key + * {@code wdOperator} with the value {@code wdOperator:L} (default), {@code wdOperator:D} or + * {@code wdOperator:Y}. + * + * @return the configuration for the proof load + */ + public Configuration getResult() { + Configuration configuration = new Configuration(); + configuration.set("wdOperator", "wdOperator:L"); + if (rdbWDD.isSelected()) { + configuration.set("wdOperator", "wdOperator:D"); + } + if (rdbWDY.isSelected()) { + configuration.set("wdOperator", "wdOperator:Y"); + } + return configuration; + } + + /** + * Self test of the panel (system property {@code key.fx.verify.profileloading}), run at + * startup from {@code MainWindowF}: the WD profile association, the default selection and + * every {@link #getResult()} mapping are checked. + * + * @return a self-test report ending in {@code PASS} or {@code FAIL} + */ + public static String verifyProfileLoading() { + StringBuilder report = new StringBuilder(); + try { + WDLoadOptionPanelF panel = new WDLoadOptionPanelF(); + + // the panel is registered for the WD profile (Swing LoadOptionPanel.getProfile) + report.append("profile=").append(panel.getProfile() == WdProfile.INSTANCE).append("; "); + + // default selection L (WDLoadDialogOptionPanel.java:82) + check(report, "default", "wdOperator:L", + String.valueOf(panel.getResult().get("wdOperator"))); + + // select D and Y in turn (Swing getResult mapping, + // WDLoadDialogOptionPanel.java:106-117) + panel.rdbWDD.setSelected(true); + check(report, "select D", "wdOperator:D", + String.valueOf(panel.getResult().get("wdOperator"))); + panel.rdbWDY.setSelected(true); + check(report, "select Y", "wdOperator:Y", + String.valueOf(panel.getResult().get("wdOperator"))); + panel.rdbWDL.setSelected(true); + check(report, "select L", "wdOperator:L", + String.valueOf(panel.getResult().get("wdOperator"))); + + // the three tooltips are set + boolean tooltips = panel.rdbWDL.getTooltip() != null + && panel.rdbWDD.getTooltip() != null && panel.rdbWDY.getTooltip() != null; + check(report, "tooltips", "true", String.valueOf(tooltips)); + } catch (RuntimeException e) { + report.append("exception ").append(e).append("; "); + } + report.append(report.toString().contains("FAIL") + || report.toString().contains("exception") ? "FAIL" : "PASS"); + return report.toString(); + } + + private static void check(StringBuilder report, String what, String expected, + String actual) { + boolean ok = java.util.Objects.equals(expected, actual); + report.append(what).append("=").append(ok ? "ok" + : "FAIL(expected <" + expected + "> got <" + actual + ">").append("); "); + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/SettingsManagerF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/SettingsManagerF.java index a9757b09934..f9344c3a3d6 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/SettingsManagerF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/SettingsManagerF.java @@ -31,6 +31,7 @@ import javafx.stage.Stage; import de.uka.ilkd.key.gui.fx.MainWindowF; +import de.uka.ilkd.key.gui.fx.plugins.javac.JavacSettingsProviderF; import de.uka.ilkd.key.gui.fx.theme.ThemeManager; import de.uka.ilkd.key.proof.Proof; import de.uka.ilkd.key.settings.ChoiceSettings; @@ -70,8 +71,17 @@ public final class SettingsManagerF { public static final TacletOptionsSettingsF TACLET_OPTIONS_SETTINGS = new TacletOptionsSettingsF(); + /** + * smalldialogs: the javac options provider (Swing {@code JavacSettingsProvider}, the + * settings tab of the {@code JavacExtension} plugin). Swing reaches it via the + * {@code KeYGuiExtension.Settings} capability; the FX extension SPI does not exist yet, so + * it is registered directly here (the provider seam stays open for the SPI). + */ + public static final JavacSettingsProviderF JAVAC_SETTINGS = + new JavacSettingsProviderF(); + // Deliberately deferred to later milestones (the provider seam stays open): - // SMTSettingsProvider, JavacSettingsProvider, ParallelProverSettingsProvider, + // SMTSettingsProvider, ParallelProverSettingsProvider, // FeatureSettingsPanel and the ShowActiveSettings dump. private static SettingsManagerF INSTANCE; @@ -89,6 +99,7 @@ public static SettingsManagerF getInstance() { INSTANCE = new SettingsManagerF(); INSTANCE.add(STANDARD_UI_SETTINGS); INSTANCE.add(TACLET_OPTIONS_SETTINGS); + INSTANCE.add(JAVAC_SETTINGS); } return INSTANCE; } diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/soundiness/SoundinessAnalyzer.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/soundiness/SoundinessAnalyzer.java new file mode 100644 index 00000000000..8870757095f --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/soundiness/SoundinessAnalyzer.java @@ -0,0 +1,606 @@ +/* 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.soundiness; + +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.*; +import java.util.stream.Collectors; + +import de.uka.ilkd.key.java.Services; +import de.uka.ilkd.key.proof.Proof; +import de.uka.ilkd.key.proof.Statistics; +import de.uka.ilkd.key.proof.mgt.ProofStatus; +import de.uka.ilkd.key.proof.mgt.SpecificationRepository; +import de.uka.ilkd.key.proof.reference.ClosedBy; +import de.uka.ilkd.key.settings.ChoiceSettings; +import de.uka.ilkd.key.settings.GeneralSettings; +import de.uka.ilkd.key.settings.ProofIndependentSettings; +import de.uka.ilkd.key.speclang.Contract; + +import org.key_project.util.collection.ImmutableSet; + +import static de.uka.ilkd.key.proof.mgt.ProofStatus.CLOSED; +import static de.uka.ilkd.key.proof.mgt.ProofStatus.CLOSED_BUT_LEMMAS_LEFT; + +/** + * Static utility class for analyzing proof soundiness. Generates an HTML report directly. + *

    + * Verbatim copy of {@code de.uka.ilkd.key.gui.soundiness.SoundinessAnalyzer} of the Swing module + * {@code key.ui} (package change only) with one exception: the Swing original reads the + * unsoundness/incompleteness flags from {@code de.uka.ilkd.key.gui.settings.TacletOptionsSettings} + * (a Swing settings panel class, {@code TacletOptionsSettings.java:108-131} and + * {@code :179-183}); that class cannot be referenced from {@code key.ui.fx}, so the three static + * helpers {@link #isUnsound}, {@link #isIncomplete} and {@link #getInformation} are copied + * verbatim into the nested {@link ChoiceEntry} below (cited accordingly). + * + * @author opencode + * @author Wolfram Pfeifer + * @since 3.0 + */ +public final class SoundinessAnalyzer { + + private static final String GENERAL_DISCLAIMER = + """ + This report reflects the soundiness status of your verification with respect to KeY's modeling assumptions. \ + KeY provides full support for Java 1.4 and many subsequent language features, though not all Java constructs are modeled. \ + The following limitations apply: reflection is not supported; native methods are not verified but can be specified with contracts—if these contracts are faithful, verification proceeds soundly; \ + verification assumes sequential execution without multi-threading; and floating-point arithmetic is faithfully modeled according to IEEE 754 semantics. \ + Temporary inconsistencies may exist in the prover (e.g., String switch labels currently use identity comparison instead of equals; see issue #1234). \ + These limitations are independent of the taclet option choices analyzed below. \ + Note that this analysis is modulo bugs in KeY itself. + """; + + private SoundinessAnalyzer() { + // Utility class - no instances + } + + /** + * Generates an HTML soundiness report for the given proof. + * + * @param proof the proof to analyze + * @return HTML string containing the complete report + */ + public static String generateHTMLReport(Proof proof) { + if (proof == null) { + throw new IllegalArgumentException("Proof cannot be null"); + } + + List generalWarnings = analyzeGeneralKeY(); + List tacletOptions = analyzeTacletOptions(proof); + ProofStats proofStats = analyzeProofTree(proof); + DependencyStats depStats = analyzeDependencies(proof); + + // Not yet implemented. Let's not show this for the moment. + // SourceStats sourceStats = analyzeSource(proof); + + // TODO: we really should use StringTemplate or similar here! + StringBuilder html = new StringBuilder(); + html.append(""); + + String timestamp = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm") + .format(ZonedDateTime.now(ZoneId.systemDefault())); + + html.append("

    KeY Soundiness Report

    ") + .append("

    Proof: ").append(escapeHtml(proof.name().toString())) + .append(" | Generated: ").append(timestamp).append("

    "); + + // Compartment 1: General KeY Settings + html.append("

    1. General KeY Soundiness

    "); + html.append("

    ").append(GENERAL_DISCLAIMER).append("

    "); + + for (GeneralKeYWarning warning : generalWarnings) { + html.append("

    "); + + html.append(warning); + html.append("

    "); + } + + // Compartment 2: Taclet Options + html.append("

    2. Taclet Option Soundiness

    "); + html.append( + "The following taclet options change the semantics of the Java code being verified. " + + "Options marked as unsound may lead to unsound verification results (which can still make sense, for instance if the absence of overflows is proven separately), " + + "while options marked as incomplete may lead to incomplete verification results. " + + "Options at default settings are considered safe but may not be optimal for performance."); + if (tacletOptions.isEmpty()) { + // TODO: default does not mean safe at the moment (i.e. overflows) + html.append("

    All taclet options are at default/safe settings.

    "); + } else { + html.append(""); + for (TacletOptionEntry entry : tacletOptions) { + String choiceText = entry.choice(); + + html.append(""); + html.append("").append(""); + html.append(""); + } + html.append("
    ").append(escapeHtml(choiceText)); + html.append(""); + if (entry.impact() != null) { + html.append(escapeHtml(entry.impact())); + } + if (entry.features() != null && entry.features().length > 0) { + html.append("
    Affects: "); + for (int i = 0; i < entry.features().length; i++) { + if (i > 0) + html.append(", "); + html.append(escapeHtml(entry.features()[i])); + } + html.append(""); + } + html.append("
    "); + } + + // Compartment 3: Proof Tree Analysis + html.append("

    3. Proof Tree Analysis

    "); + + html.append("

    Goal Statistics

      ") + .append("
    • Total goals: ").append(proofStats.totalGoals()).append("
    • ") + .append("
    • Closed goals: ").append(proofStats.closedGoals()).append("
    • ") + .append("
    • Open goals: ").append(proofStats.openGoals()).append("
    • "); + if (proofStats.cachedGoals() > 0) { + html.append("
    • Cached goals: ").append(proofStats.cachedGoals()) + .append(" (from proof cache)
    • "); + } + html.append("
    "); + + // maybe for the future (but lemma/assumption detection is definitively more difficult) + // if (!proofStats.lemmasUsed().isEmpty()) { + // html.append("

    User-Defined Lemmas Used

      "); + // for (LemmaUsage lemma : proofStats.lemmasUsed()) { + // String status = + // lemma.proved() ? "proved" : "not proved"; + // html.append("
    • ").append(escapeHtml(lemma.name())) + // .append(" (used ").append(lemma.count()).append(" times, ") + // .append(status).append(")
    • "); + // } + // html.append("
    "); + // } + // + // if (!proofStats.assumptionsUsed().isEmpty()) { + // html.append("

    Assumptions Introduced

      "); + // for (AssumptionUsage assumption : proofStats.assumptionsUsed()) { + // html.append("
    • ").append(escapeHtml(assumption.location())).append(": ") + // .append(escapeHtml(assumption.formula())); + // if (assumption.reason() != null && !assumption.reason().isEmpty()) { + // html.append(" (").append(escapeHtml(assumption.reason())).append(")"); + // } + // html.append("
    • "); + // } + // html.append("
    "); + // } + + html.append("

    Rule Applications

      ") + .append("
    • Total steps: ").append(proofStats.totalSteps()).append("
    • ") + .append("
    • Automated: ").append(proofStats.automatedSteps()).append("
    • ") + .append("
    • Interactive: ").append(proofStats.interactiveSteps()).append("
    • "); + + if (!proofStats.mostUsedInteractiveRules().isEmpty()) { + html.append("
    • Most used interactive rules:
        "); + for (String rule : proofStats.mostUsedInteractiveRules()) { + html.append("
      • ").append(escapeHtml(rule)).append("
      • "); + } + html.append("
    • "); + } + html.append("
    "); + + html.append("

    Dependencies

    "); + html.append( + "Note: Only dependencies that are currently loaded in the same proof environment are considered here." + + " I.e., closed proofs saved on disk but not loaded are not detected, and there is currently no way to load them into the same environment again." + + " For a more precise dependency analysis, the proof management tool is available (CLI or main menu)."); + + html.append("

    Overall dependency status: "); + switch (depStats.status()) { + case CLOSED -> html.append("All dependencies proven."); + case CLOSED_BY_CACHE -> html.append( + "All dependencies proven (some by cache)."); + case CLOSED_BUT_LEMMAS_LEFT -> + html.append("Some dependencies unproven."); + case OPEN -> + html.append("Current proof still unfinished."); + } + html.append("

    "); + + html.append("

    The proof depends on the following contracts:
    "); + html.append("

  • Proven Dependencies
  • "); + if (depStats.closed.isEmpty()) { + html.append("
      "); + html.append("None"); + } else { + html.append("
        "); + for (Contract closed : depStats.closed) { + html.append("
      • ").append(escapeHtml(closed.getName())).append("
      • "); + } + } + html.append("
      "); + + html.append("
    • Unproven Dependencies
    • "); + if (depStats.open.isEmpty()) { + html.append("
        "); + html.append("None"); + } else { + html.append("
          "); + for (Contract open : depStats.open) { + html.append("
        • ").append(escapeHtml(open.getName())).append("
        • "); + } + } + html.append("

        "); + + + // Compartment 5: Source Analysis + // html.append("

        5. Source Analysis

        "); + // if (sourceStats.javaFileCount() == 0 && sourceStats.keyFileCount() == 0) { + // html.append("

        Detailed source analysis not yet implemented.

        "); + // } else { + // html.append("
          ") + // .append("
        • Java files loaded: ").append(sourceStats.javaFileCount()).append("
        • ") + // .append("
        • Specification (.key) files: ").append(sourceStats.keyFileCount()).append( + // "
        • "); + // + // if (!sourceStats.loadedFiles().isEmpty()) { + // html.append("
        • Loaded files: "); + // for (int i = 0; i < Math.min(10, sourceStats.loadedFiles().size()); i++) { + // if (i > 0) html.append(", "); + // html.append(escapeHtml(sourceStats.loadedFiles().get(i))); + // } + // if (sourceStats.loadedFiles().size() > 10) { + // html.append(", ..."); + // } + // html.append("
        • "); + // } + // + // if (sourceStats.totalMethods() > 0) { + // int percentage = (sourceStats.methodsWithContracts() * 100) / sourceStats.totalMethods(); + // html.append("
        • JML contract coverage: ").append(sourceStats.methodsWithContracts()) + // .append("/").append(sourceStats.totalMethods()).append(" methods (") + // .append(percentage).append("%)
        • "); + // } + // + // if (!sourceStats.parseWarnings().isEmpty()) { + // html.append("
        • Parse warnings: ").append(sourceStats.parseWarnings().size()).append( + // "
        • "); + // } + // + // if (!sourceStats.unsupportedFeatures().isEmpty()) { + // html.append("
        • Unsupported features: ").append(sourceStats.unsupportedFeatures().size() + // ).append("
        • "); + // } + // + // html.append("
        "); + // } + + + html.append(""); + return html.toString(); + } + + private static DependencyStats analyzeDependencies(Proof proof) { + ProofStatus proofStatus = proof.mgt().getStatus(); + List closed = new ArrayList<>(); + List open = new ArrayList<>(); + ImmutableSet dependencies = proof.mgt().getUsedContracts(); + + // for each dependency try to find a proof (in the same environment/specRepo) + // Careful: Environment is modeled via SpecificationRepository, not via proof.getEnv()! + // Set envProofs = proof.getEnv().getProofs(); + Services srv = proof.getServices(); + SpecificationRepository specRepo = srv.getSpecificationRepository(); + + for (Contract dep : dependencies) { + List prfs = specRepo.getProofs(dep).stream().toList(); + boolean allClosed = prfs.stream().allMatch(pr -> pr.mgt().getStatus() == CLOSED); + if (!prfs.isEmpty() && allClosed) { + closed.add(dep); + } else { + open.add(dep); + } + } + return new DependencyStats(proofStatus, closed, open); + } + + private static String escapeHtml(String text) { + if (text == null) { + return ""; + } + return text.replace("&", "&") + .replace("<", "<") + .replace(">", ">") + .replace("\"", """); + } + + private static List analyzeGeneralKeY() { + List warnings = new ArrayList<>(); + + // this seems not really relevant + // long maxMemory = Runtime.getRuntime().maxMemory(); + // if (maxMemory < 1024 * 1024 * 1024) { + // warnings.add(new GeneralKeYWarning( + // GeneralKeYWarning.Level.CRITICAL, + // "Memory", + // "Very limited memory available (" + (maxMemory / 1024 / 1024) + " MB)", + // "Increase heap size with -Xmx flag (e.g., -Xmx4g)" + // )); + // } else if (maxMemory < 2L * 1024 * 1024 * 1024) { + // warnings.add(new GeneralKeYWarning( + // GeneralKeYWarning.Level.WARNING, + // "Memory", + // "Limited memory available (" + (maxMemory / 1024 / 1024) + " MB)", + // "Consider increasing heap size with -Xmx flag for large proofs" + // )); + // } + + // this is by the taclet settings section + // ChoiceSettings choiceSettings = proof.getSettings().getChoiceSettings(); + // Map choices = choiceSettings.getDefaultChoices(); + // + // if ("initialisation:disableStaticInitialisation".equals(choices.get("initialisation"))) { + // warnings.add(new GeneralKeYWarning( + // GeneralKeYWarning.Level.CRITICAL, + // "Static Initialization", + // "Static constructors are not analyzed", + // "Enable static initialization for complete verification (may increase complexity)" + // )); + // } + + GeneralSettings generalSettings = + ProofIndependentSettings.DEFAULT_INSTANCE.getGeneralSettings(); + if (!generalSettings.isUseJML()) { + warnings.add(new GeneralKeYWarning( + GeneralKeYWarning.Level.CRITICAL, + "JML Specifications", + "JML specifications are ignored", + "Enable JML in settings to verify behavioral properties")); + } + + if (!generalSettings.isEnsureSourceConsistency()) { + warnings.add(new GeneralKeYWarning( + GeneralKeYWarning.Level.INFO, + "Source Code", + "Source file changes may not be detected during verification", + "Enable source consistency checking if source files change during verification")); + } + + // this is not really relevant (proof status can be seen easily in the GUI) + // StrategySettings strategySettings = proof.getSettings().getStrategySettings(); + // int maxSteps = strategySettings.getMaxSteps(); + // if (maxSteps > 0 && maxSteps < 100000) { + // warnings.add(new GeneralKeYWarning( + // GeneralKeYWarning.Level.WARNING, + // "Proof Search", + // "Automated proof search limited to " + maxSteps + " steps", + // "Increase step limit for more complex proofs" + // )); + // } + + + return warnings; + } + + private static List analyzeTacletOptions(Proof proof) { + List entries = new ArrayList<>(); + + ChoiceSettings choiceSettings = proof.getSettings().getChoiceSettings(); + Map choices = choiceSettings.getDefaultChoices(); + + for (Map.Entry entry : choices.entrySet()) { + String choice = entry.getValue(); + + // local port of Swing TacletOptionsSettings.createChoiceEntry + // (TacletOptionsSettings.java:179-183) + ChoiceEntry baseEntry = new ChoiceEntry(choice, isUnsound(choice), + isIncomplete(choice), getInformation(choice)); + + String impact = getPlainEnglishImpact(choice); + String[] features = getAffectedFeatures(choice); + + if (impact != null && !impact.isEmpty()) { + entries.add(new TacletOptionEntry(choice, impact, features, baseEntry.unsound(), + baseEntry.incomplete())); + } + } + + return entries; + } + + private static String getPlainEnglishImpact(String choice) { + return switch (choice) { + case "runtimeExceptions:ignore" -> + "Assumes no null pointer, divide-by-zero, or array bounds errors occur"; + case "runtimeExceptions:ban" -> + "Any implicit runtime exception is treated as program failure (overly strict but safe)"; + case "initialisation:disableStaticInitialisation" -> + "Static field initializers and static blocks are skipped"; + case "Strings:off" -> "String operations are not analyzed"; + case "programRules:None" -> "Java language constructs are not processed"; + case "intRules:arithmeticSemanticsIgnoringOF" -> + "Integer overflow is ignored; arithmetic operations are interpreted as unbounded"; + case "intRules:arithmeticSemanticsCheckingOF" -> + "Integer arithmetic is mathematical; overflow must be explicitly prevented (overly strict but safe)"; + case "integerSimplificationRules:minimal" -> + "Limited simplification rules for integer arithmetic"; + case "JavaCard:on" -> + "JavaCard mode enabled - sound for JavaCard, unsound for regular Java"; + case "JavaCard:off" -> "JavaCard features disabled - sound for regular Java"; + case "assertions:on" -> + "Java assertions are enabled - sound only if JVM runs with -ea flag"; + case "assertions:off" -> + "Java assertions are disabled - sound only if JVM runs without -ea flag"; + default -> null; + }; + } + + private static String[] getAffectedFeatures(String choice) { + return switch (choice) { + case "runtimeExceptions:ignore", + "runtimeExceptions:ban" -> + new String[] { "NullPointerException", "ArithmeticException", + "ArrayIndexOutOfBoundsException" }; + case "intRules:arithmeticSemanticsIgnoringOF", + "intRules:arithmeticSemanticsCheckingOF" -> + new String[] { "int arithmetic", "long arithmetic", "overflow detection" }; + case "initialisation:disableStaticInitialisation" -> + new String[] { "static fields", "static initialization blocks" }; + case "Strings:off" -> + new String[] { "String concatenation", "String.length()", "String.charAt()" }; + case "programRules:None" -> + new String[] { "all Java statements", "method calls", "assignments" }; + case "JavaCard:on" -> + new String[] { "transaction mechanism", "atomicity", "persistence" }; + default -> new String[0]; + }; + } + + // ------------------------------------------------------------------ + // local port of Swing TacletOptionsSettings choice classification + // (key.ui/src/main/java/de/uka/ilkd/key/gui/settings/TacletOptionsSettings.java:108-131, + // verbatim logic; that class lives in the Swing module and cannot be referenced here) + // ------------------------------------------------------------------ + + /** + * Checks if the given choice makes a proof unsound (Swing + * {@code TacletOptionsSettings.isUnsound}). + * + * @param choice the choice to check + * @return {@code true} proof will be unsound, {@code false} proof will be sound as long as + * all other choices are sound + */ + static boolean isUnsound(String choice) { + return "runtimeExceptions:ignore".equals(choice) + || "initialisation:disableStaticInitialisation".equals(choice) + || "intRules:arithmeticSemanticsIgnoringOF".equals(choice); + } + + /** + * Checks if the given choice makes a proof incomplete (Swing + * {@code TacletOptionsSettings.isIncomplete}). + * + * @param choice the choice to check + * @return {@code true} proof will be incomplete, {@code false} proof will be complete as long + * as all other choices are complete + */ + static boolean isIncomplete(String choice) { + return "runtimeExceptions:ban".equals(choice) || "Strings:off".equals(choice) + || "intRules:arithmeticSemanticsCheckingOF".equals(choice) + || "integerSimplificationRules:minimal".equals(choice) + || "programRules:None".equals(choice); + } + + /** + * Checks if additional information for the choice are available (Swing + * {@code TacletOptionsSettings.getInformation}). + * + * @param choice the choice to check + * @return the additional information or {@code null} if no information are available + */ + static String getInformation(String choice) { + if ("JavaCard:on".equals(choice)) { + return "Sound if a JavaCard program is proven."; + } else if ("JavaCard:off".equals(choice)) { + return "Sound if a Java program is proven."; + } else if ("assertions:on".equals(choice)) { + return "Sound if JVM is started with enabled assertions for the whole system."; + } else if ("assertions:off".equals(choice)) { + return "Sound if JVM is started with disabled assertions for the whole system."; + } else { + return null; + } + } + + /** + * Local port of the Swing {@code TacletOptionsSettings.ChoiceEntry} record (only the fields + * used by the report: choice, unsoundness and incompleteness flags, additional information). + */ + record ChoiceEntry(String choice, boolean unsound, boolean incomplete, String information) { + } + + private static SourceStats analyzeSource(Proof proof) { + // TODO: Implement! What should this do? Warn about unsupported features? + return new SourceStats(0, 0, new ArrayList<>(), new ArrayList<>(), 0, 0, new ArrayList<>()); + } + + private static ProofStats analyzeProofTree(Proof proof) { + + Statistics stats = proof.getStatistics(); + int totalGoals = proof.allGoals().size(); + int closedGoals = proof.closedGoals().size(); + int openGoals = proof.openGoals().size(); + int cachedGoals = proof.closedGoals().stream() + .filter(g -> g.node().lookup(ClosedBy.class) != null).toList().size(); + int interactiveSteps = stats.interactiveSteps; + int totalSteps = stats.totalRuleApps; + + List mostUsedRules = stats.getInteractiveAppsDetails().entrySet() + .stream() + .sorted(Map.Entry.comparingByValue().reversed()) + .limit(5) + .map(entry -> entry.getKey() + " (" + entry.getValue() + ")") + .collect(Collectors.toList()); + + return new ProofStats(totalGoals, closedGoals, openGoals, cachedGoals, totalSteps, + totalSteps - interactiveSteps, interactiveSteps, mostUsedRules); + } + + private record GeneralKeYWarning(Level level, String category, String userMessage, + String recommendation) { + enum Level { + INFO("info"), WARNING("warning"), CRITICAL("critical"); + + private final String cssClass; + + Level(String cssClass) { this.cssClass = cssClass; } + + String cssClass() { return cssClass; } + } + } + + private record TacletOptionEntry(String choice, String impact, String[] features, + boolean unsound, boolean incomplete) { + String cssClass() { + return unsound ? "unsound" : incomplete ? "incomplete" : "default"; + } + } + + private record SourceStats(int javaFileCount, int keyFileCount, List loadedFiles, + List parseWarnings, int totalMethods, int methodsWithContracts, + List unsupportedFeatures) { + } + + private record ProofStats(int totalGoals, int closedGoals, int openGoals, int cachedGoals, + /* List lemmasUsed, List assumptionsUsed, */ + int totalSteps, int automatedSteps, int interactiveSteps, + List mostUsedInteractiveRules) { + } + + /* + * private record LemmaUsage(String name, boolean proved, int count) { + * } + * + * private record AssumptionUsage(String formula, String location, String reason) { + * } + */ + + private record DependencyStats(ProofStatus status, List closed, List open) { + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/soundiness/SoundinessDialogF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/soundiness/SoundinessDialogF.java new file mode 100644 index 00000000000..1c306cc6d99 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/soundiness/SoundinessDialogF.java @@ -0,0 +1,150 @@ +/* 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.soundiness; + +import java.util.Map; +import javafx.scene.control.Alert; +import javafx.scene.control.Button; +import javafx.scene.control.ButtonBar; +import javafx.scene.input.Clipboard; +import javafx.scene.input.DataFormat; +import javafx.scene.web.WebView; +import javafx.stage.Modality; +import javafx.stage.Window; + +import de.uka.ilkd.key.gui.fx.theme.ThemeManager; +import de.uka.ilkd.key.proof.Proof; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Modal dialog displaying a soundiness report as HTML for a proof. + *

        + * FX port of the Swing {@code de.uka.ilkd.key.gui.soundiness.SoundinessDialog} ({@code key.ui}): + * a modal window titled {@code "Soundiness Report: "} ({@code SoundinessDialog.java:27-28}) + * that renders {@link SoundinessAnalyzer#generateHTMLReport(Proof)} in a non-editable HTML view + * (there a {@code JEditorPane}, here a {@link WebView} — the Swing control renders only a small + * HTML subset, the WebView renders the report faithfully), with a Copy HTML button + * writing the raw HTML to the system clipboard plus a confirmation dialog ({@code + * SoundinessDialog.java:62-69}) and a Close button as the default button ({@code + * :74-76}). The window is 900×750 and centered on the owner ({@code :53-55}). + *

        + * The report computation lives entirely in {@link SoundinessAnalyzer} (like Swing); the dialog is + * only the display. Triggered by the "Show Soundiness Report" action (Swing {@code + * ShowSoundinessAction} contributed to the proof-list context menu by {@code + * SoundinessExtension}; the FX proof-list dockable does not exist yet, so the action temporarily + * lives in the View menu — see {@code MainWindowF}). + */ +public final class SoundinessDialogF extends javafx.stage.Stage { + + private static final Logger LOGGER = LoggerFactory.getLogger(SoundinessDialogF.class); + + /** + * The owner window, kept for centering in {@link #showCenteredOnOwner()} (centering must + * happen after {@link #show()}, when the stage has its final size). + */ + private final Window owner; + + /** + * Creates and shows the soundiness report dialog for the given proof (Swing + * {@code SoundinessDialog(Frame, Proof)} + {@code setVisible(true)}). + * + * @param owner the owner window (the main window stage) + * @param proof the proof to analyze, non-null + */ + public SoundinessDialogF(Window owner, Proof proof) { + setTitle("Soundiness Report: " + proof.name()); + if (owner != null) { + initOwner(owner); + // Swing: JDialog(owner, title, true) — modal to the owner window + initModality(Modality.WINDOW_MODAL); + } + String html = SoundinessAnalyzer.generateHTMLReport(proof); + + // the report rendered in a non-editable HTML view (Swing JEditorPane in a scroll pane) + WebView view = new WebView(); + view.getEngine().loadContent(html, "text/html"); + + Button copyButton = new Button("Copy HTML"); + copyButton.setOnAction(e -> { + // Swing: Toolkit...getSystemClipboard().setContents(new StringSelection(html), null) + Clipboard.getSystemClipboard() + .setContent(Map.of(DataFormat.PLAIN_TEXT, html)); + LOGGER.info("Soundiness report HTML copied to clipboard ({} chars)", html.length()); + Alert info = new Alert(Alert.AlertType.INFORMATION, "HTML copied to clipboard"); + info.setTitle("Copy"); + info.setHeaderText(null); + if (owner != null) { + info.initOwner(owner); + } + info.getDialogPane() + .getStylesheets() + .add(ThemeManager.getInstance().getTheme().stylesheetUrl()); + info.showAndWait(); + }); + + // Swing: Close is the default button and disposes the dialog + Button closeButton = new Button("Close"); + closeButton.setOnAction(e -> close()); + ButtonBar.setButtonData(closeButton, ButtonBar.ButtonData.OK_DONE); + closeButton.setDefaultButton(true); + + ButtonBar buttonBar = new ButtonBar(); + buttonBar.getButtons().addAll(copyButton, closeButton); + javafx.scene.layout.BorderPane.setMargin(buttonBar, new javafx.geometry.Insets(10)); + + javafx.scene.layout.BorderPane root = + new javafx.scene.layout.BorderPane(view, null, null, buttonBar, null); + javafx.scene.Scene scene = new javafx.scene.Scene(root, 900, 750); + ThemeManager.getInstance().manage(scene); + setScene(scene); + // Escape closes like the Swing window-close handler (JDialog.DISPOSE_ON_CLOSE) + scene.setOnKeyPressed(e -> { + if (e.getCode() == javafx.scene.input.KeyCode.ESCAPE) { + close(); + e.consume(); + } + }); + + // Swing: pack(); setSize(900, 750); setLocationRelativeTo(getOwner()) — the FX centering + // happens in showCenteredOnOwner after show(), when the sizes are known + sizeToScene(); + this.owner = owner; + } + + /** + * Centers the dialog over the owner window (Swing {@code setLocationRelativeTo(getOwner())}); + * without an owner, centers on the primary screen. The position is clamped to the screen + * bounds (the Swing dialog is placed fully on screen as well). + */ + private void centerOnOwner(Window owner) { + javafx.geometry.Rectangle2D bounds = + javafx.stage.Screen.getPrimary().getVisualBounds(); + double x; + double y; + if (owner != null && owner.getWidth() > 0) { + x = owner.getX() + owner.getWidth() / 2 - getWidth() / 2.0; + y = owner.getY() + owner.getHeight() / 2 - getHeight() / 2.0; + } else { + x = bounds.getMinX() + (bounds.getWidth() - getWidth()) / 2; + y = bounds.getMinY() + (bounds.getHeight() - getHeight()) / 2; + } + // clamp into the screen + setX(Math.max(bounds.getMinX(), Math.min(x, bounds.getMaxX() - getWidth()))); + setY(Math.max(bounds.getMinY(), Math.min(y, bounds.getMaxY() - getHeight()))); + } + + /** + * Opens the dialog modally (Swing {@code dialog.setVisible(true)} on a modal {@code JDialog}; + * on the FX thread the dialog is non-blocking for the caller) and centers it on the owner. + */ + public void showCenteredOnOwner() { + show(); + // Swing setLocationRelativeTo happens on the visible dialog: the sizes are final here + centerOnOwner(owner); + toFront(); + } +} From 6d7d7d03a60af58feb5e88b90c99e80302b4cb47 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Fri, 9 Oct 2026 14:52:21 +0200 Subject: [PATCH 084/281] key.ui.fx: termmenu: pure-Java model of the sequent context menu (SequentMenuModelF) --- .../gui/fx/nodeviews/SequentMenuModelF.java | 757 ++++++++++++++++++ 1 file changed, 757 insertions(+) create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentMenuModelF.java diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentMenuModelF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentMenuModelF.java new file mode 100644 index 00000000000..f5b2af8acd1 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentMenuModelF.java @@ -0,0 +1,757 @@ +/* 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.nodeviews; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import de.uka.ilkd.key.control.ProofControl; +import de.uka.ilkd.key.core.fx.KeYMediatorF; +import de.uka.ilkd.key.java.visitor.JavaASTWalker; +import de.uka.ilkd.key.logic.JTerm; +import de.uka.ilkd.key.logic.JavaBlock; +import de.uka.ilkd.key.logic.op.FormulaSV; +import de.uka.ilkd.key.logic.op.ProgramVariable; +import de.uka.ilkd.key.pp.AbbrevMap; +import de.uka.ilkd.key.pp.PosInSequent; +import de.uka.ilkd.key.proof.Goal; +import de.uka.ilkd.key.proof.join.ProspectivePartner; +import de.uka.ilkd.key.rule.BlockContractExternalRule; +import de.uka.ilkd.key.rule.BlockContractInternalRule; +import de.uka.ilkd.key.rule.BuiltInRule; +import de.uka.ilkd.key.rule.FindTaclet; +import de.uka.ilkd.key.rule.LoopContractExternalRule; +import de.uka.ilkd.key.rule.LoopContractInternalRule; +import de.uka.ilkd.key.rule.LoopScopeInvariantRule; +import de.uka.ilkd.key.rule.NoFindTaclet; +import de.uka.ilkd.key.rule.RewriteTaclet; +import de.uka.ilkd.key.rule.Taclet; +import de.uka.ilkd.key.rule.TacletApp; +import de.uka.ilkd.key.rule.TacletSchemaVariableCollector; +import de.uka.ilkd.key.rule.UseOperationContractRule; +import de.uka.ilkd.key.rule.WhileInvariantRule; +import de.uka.ilkd.key.rule.merge.MergeRule; +import de.uka.ilkd.key.rule.tacletbuilder.RewriteTacletGoalTemplate; +import de.uka.ilkd.key.settings.FeatureSettings; +import de.uka.ilkd.key.settings.ProofIndependentSMTSettings; +import de.uka.ilkd.key.settings.ProofIndependentSettings; +import de.uka.ilkd.key.smt.SolverTypeCollection; + +import org.key_project.logic.op.sv.SchemaVariable; +import org.key_project.prover.proof.rulefilter.TacletFilter; +import org.key_project.prover.rules.RuleSet; +import org.key_project.prover.rules.tacletbuilder.TacletGoalTemplate; +import org.key_project.prover.sequent.PosInOccurrence; +import org.key_project.util.collection.ImmutableList; + +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +/** + * Pure-Java model of the menu shown when the user right-clicks a term, operator or sequent in the + * sequent view. This is the FX counterpart of the Swing {@code + * de.uka.ilkd.key.gui.nodeviews.CurrentGoalViewMenu} (key.ui, {@code CurrentGoalViewMenu.java}, + * the whole menu logic) plus the context-menu contributions of the Swing base + * {@code SequentViewMenu}. + *

        + * The model is deliberately free of any Swing or JavaFX types: it reduces the applicable rules, + * built-in rules and menu structure at a {@link PosInSequent} to a tree of {@link Entry + * descriptors}. The concrete UI (a JavaFX {@code ContextMenu}) is built from those descriptors by + * the companion shell {@code SequentTermContextMenuF}, which also supplies the FX- or + * side-effectful pieces (macro list, extension actions, clipboard, SMT launch) via the {@link + * Seam} supplier so that this class stays headlessly unit-testable. + *

        + * Rule applicability is computed through the same {@link ProofControl} query surface the Swing + * listener uses ({@code getBuiltInRule}, {@code getFindTaclet}, {@code getRewriteTaclet}, + * {@code getNoFindTaclet}), so the two produce the same rule lists for a given goal and position. + */ +public final class SequentMenuModelF { + + /** The Swing name of the (unsound, hidden by default) axiom-introduction taclet. */ + public static final String INTRODUCE_AXIOM_TACLET_NAME = "introduceAxiom"; + + public static final String MORE_RULES = "More rules"; + public static final String APPLY_CONTRACT = "Apply Contract"; + public static final String CHOOSE_AND_APPLY_CONTRACT = "Choose and Apply Contract..."; + public static final String ENTER_LOOP_SPECIFICATION = "Enter Loop Specification..."; + public static final String APPLY_RULE = "Apply Rule"; + public static final String NO_RULES_APPLICABLE = "No rules applicable."; + public static final String INSERT_CLASS_INVARIANT = "Insert Class Invariant"; + public static final String INSERT_HIDDEN = "Insert Hidden"; + + /** The Swing FeatureSettings id that gates the delayed-cut join entry. */ + public static final String FEATURE_DELAY_CUT = "DELAY_CUT"; + + /** Pagination threshold for the flat taclet list (Swing {@code TOO_MANY_TACLETS_THRESHOLD}). */ + public static final int TOO_MANY_TACLETS_THRESHOLD = 15; + + /** Sort order for the taclet list (Swing {@code CurrentGoalViewMenu.TacletAppComparator}). */ + public static final Comparator TACLET_SORT = new TacletAppComparator(); + + /** The position at which the menu was requested. {@code null} for a whole-sequent menu. */ + private final @Nullable PosInSequent pos; + + private final KeYMediatorF mediator; + private final ProofControl proofControl; + private final @Nullable Seam seam; + private final @Nullable TacletFilter interactiveFilter; + + /** + * Pluggable, side-effectful or UI-dependent menu contributions that the pure model cannot (and + * must not) compute itself. The FX shell supplies real implementations; unit tests supply + * stubs or {@code null} to skip. + */ + public interface Seam { + /** Name-creation-info OSS/abbreviation UI hooks used by the abbreviation section. */ + @NonNull + AbbrevHooks abbrevHooks(); + + /** + * The visual candidate action for a single applicable {@link TacletApp} (Swing + * {@code TacletAppAction}): rendered label and full tooltip text. + */ + @NonNull + TacletHooks tacletHooks(); + } + + /** Abbreviation-map action labels that the model emits as descriptors when applicable. */ + public interface AbbrevHooks { + String createLabel(); + + String changeLabel(); + + String enableLabel(); + + String disableLabel(); + } + + /** Presentation hooks for an applicable taclet (label + tooltip). */ + public interface TacletHooks { + /** The menu label for the taclet (Swing {@code TacletAppAction.setName}). */ + @NonNull + String label(TacletApp app); + + /** The (raw, possibly HTML) tooltip for the taclet. */ + @NonNull + String tooltip(TacletApp app); + } + + /** + * Result tree of the model. Each {@link Entry} is one of: a taclet application, a built-in + * rule application (possibly with two sub-entries), a sub-menu, a fixed named action, an + * abbreviation action, or a separator. + */ + public sealed interface Entry permits TacletEntry, BuiltInEntry, SubMenuEntry, NamedAction, + SeparatorEntry, AbbrevActionEntry { + + /** A symbolic discriminator so the FX shell can switch without instanceof chains. */ + Kind kind(); + + enum Kind { + TACLET, BUILT_IN, SUB_MENU, NAMED_ACTION, ABBREV_ACTION, SEPARATOR + } + } + + /** A single applicable taclet application. */ + public record TacletEntry(@NonNull Kind kind, @NonNull TacletApp app, @NonNull String label, + @NonNull String tooltip) implements Entry { + public TacletEntry(TacletApp app, String label, String tooltip) { + this(Kind.TACLET, app, label, tooltip); + } + } + + /** + * A built-in rule entry. Built-in rules that support both a "forced/complete" and an + * "interactive" mode contribute two labeled sub-actions (e.g. the contract and loop rules); all + * others contribute a single action. {@code forced} only selects the mode, never what rule to + * run. + */ + public record BuiltInEntry(@NonNull Kind kind, @NonNull BuiltInRule rule, + @NonNull List<@NonNull NamedAction> actions, @NonNull String subMenuLabel) + implements Entry { + public BuiltInEntry(BuiltInRule rule, List actions, String subMenuLabel) { + this(Kind.BUILT_IN, rule, List.copyOf(actions), subMenuLabel); + } + } + + /** + * A sub-menu node ({@code More rules}, {@code Insert Hidden}, {@code Insert Class Invariant}). + */ + public record SubMenuEntry(@NonNull Kind kind, @NonNull String label, + @NonNull List<@NonNull Entry> children) implements Entry { + public SubMenuEntry(String label, List children) { + this(Kind.SUB_MENU, label, List.copyOf(children)); + } + } + + /** + * A fixed-named action emitted by the model. The FX shell maps each {@code id} to a concrete + * handler (merge rule, join, focused auto mode, SMT union, copy to clipboard, view name + * creation info, a macro, an extension action). + */ + public record NamedAction(@NonNull Kind kind, @NonNull String id, @NonNull String label, + @Nullable String tooltip, @Nullable Object payload) implements Entry { + public NamedAction(String id, String label, String tooltip, Object payload) { + this(Kind.NAMED_ACTION, id, label, tooltip, payload); + } + } + + /** A named abbreviation action (create/change/enable/disable). */ + public record AbbrevActionEntry(@NonNull Kind kind, @NonNull AbbrevAction action, + @NonNull String label) implements Entry { + public AbbrevActionEntry(AbbrevAction action, String label) { + this(Kind.ABBREV_ACTION, action, label); + } + } + + public enum AbbrevAction { + CREATE, CHANGE, ENABLE, DISABLE + } + + /** A visual menu separator. */ + public record SeparatorEntry(@NonNull Kind kind) implements Entry { + public SeparatorEntry() { + this(Kind.SEPARATOR); + } + } + + private SequentMenuModelF(@Nullable PosInSequent pos, KeYMediatorF mediator, + ProofControl proofControl, @Nullable Seam seam, + @Nullable TacletFilter interactiveFilter) { + this.pos = pos; + this.mediator = mediator; + this.proofControl = proofControl; + this.seam = seam; + this.interactiveFilter = interactiveFilter; + } + + /** + * Builds the menu that Swing {@code CurrentGoalViewMenu} would show for the given position. + * Each constructor parameter mirrors the Swing inputs: + * + * @param pos the selected {@link PosInSequent} (possibly {@code null}) + * @param mediator the FX mediator (selection, notation info, services) + * @param proofControl the consulted {@link ProofControl} (rule applicability) + * @param seam optional UI hooks; {@code null} skips abbreviation/taclet styling + * @param interactiveFilter the Interactive-Proof filter (Swing + * {@code KeYMediator#getFilterForInteractiveProving}); {@code null} + * keeps every taclet that survives the clutter rules + * @return the ordered root entries of the context menu + */ + public static @NonNull List<@NonNull Entry> build(@Nullable PosInSequent pos, + @NonNull KeYMediatorF mediator, @NonNull ProofControl proofControl, + @Nullable Seam seam, @Nullable TacletFilter interactiveFilter) { + SequentMenuModelF m = new SequentMenuModelF(pos, mediator, proofControl, seam, + interactiveFilter); + return m.compute(); + } + + private List compute() { + // The Swing listener computes the four lists via the proof control, keyed on the clicked + // position (CurrentGoalViewListener.java:72-83). We mirror that exactly. + Goal goal = mediator.getSelectedGoal(); + PosInOccurrence pio = pos != null ? pos.getPosInOccurrence() : null; + + // Match the Swing guard: only compute when we have a goal AND (pos.sequent xor + // pio != null). The Swing path only reaches a menu with a valid combination; we guard to + // avoid NPEs on a position whose occurrence is not resolvable in the current goal. + if (goal == null) { + return List.of(); + } + + List result = new ArrayList<>(); + + ImmutableList builtInList = proofControl.getBuiltInRule(goal, pio); + ImmutableList findList = proofControl.getFindTaclet(goal, pio); + ImmutableList rewriteList = proofControl.getRewriteTaclet(goal, pio); + ImmutableList noFindList = proofControl.getNoFindTaclet(goal); + + // delete RewriteTaclets from findList because they will be in the rewrite list, and + // concatenate both lists (Swing CurrentGoalViewMenu constructor). + ImmutableList combined = + removeRewrites(findList).prepend(rewriteList); + ImmutableList noFind = removeIntroduceAxiomTaclet(noFindList); + + // "immediate" section: applicable taclets at the clicked position. + boolean sequentPos = pos != null && pos.isSequent(); + ImmutableList toAdd = sort(combined, TACLET_SORT); + boolean rulesAvailable = !combined.isEmpty(); + if (sequentPos) { + rulesAvailable |= !noFind.isEmpty(); + toAdd = toAdd.prepend(noFind); + } + + if (rulesAvailable) { + addTacletSection(result, toAdd); + } else { + result.add(new NamedAction("no_rules", NO_RULES_APPLICABLE, null, null)); + } + + addBuiltInSection(result, builtInList); + + // delayed-cut join (Swing CurrentGoalViewMenu.createDelayedCutJoinMenu) + if (FeatureSettings.isFeatureActivated(FEATURE_DELAY_CUT)) { + if (sequentPos && pio != null) { + Collection partner = + de.uka.ilkd.key.proof.join.JoinIsApplicable.INSTANCE.isApplicable(goal, pio); + if (!partner.isEmpty()) { + result.add(new NamedAction("join", joinLabel(partner.size()), + "Delayed-cut join of the highlighted term.", partner)); + } + } + } + + // merge rule (Swing createMergeRuleMenu) + if (sequentPos && pio != null + && MergeRule.isOfAdmissibleForm(goal, pio, true)) { + result.add(new NamedAction("merge_rule", mergeLabel(), mergeTooltip(), pio)); + } + + // SMT (Swing createSMTMenu) — only on a sequent position + if (sequentPos) { + addSMTSection(result); + } + + addFocussedAutoMode(result); + + result.add(new NamedAction("macro_menu", "Strategy Macros", null, null)); + + result.add(new SeparatorEntry()); + + result.add(new NamedAction("extension", "Extensions", null, null)); + + result.add(new SeparatorEntry()); + + result.add(new NamedAction("copy_clipboard", "Copy to clipboard", null, null)); + + // Term-relative sections (abbreviation, name creation info) + if (pos != null) { + PosInOccurrence occ = pos.getPosInOccurrence(); + if (occ != null && occ.posInTerm() != null) { + JTerm sub = (JTerm) occ.subTerm(); + addAbbrevSection(result, sub); + + if (sub.op() instanceof ProgramVariable var) { + if (var.getProgramElementName().getCreationInfo() != null) { + result.add(new NamedAction("name_creation_info", "View name creation info", + null, var)); + } + } + } + } + + return List.copyOf(result); + } + + private String joinLabel(int partnerCount) { + return "Join (" + partnerCount + (partnerCount == 1 ? " partner)" : " partners)"); + } + + private @NonNull String mergeLabel() { + return "Refocus merge rule"; + } + + private @NonNull String mergeTooltip() { + return "Links a partner node to this merged node (Swing MergeRuleMenuItem)"; + } + + private void addTacletSection(List out, ImmutableList taclets) { + List hidden = new ArrayList<>(); + List systemInv = new ArrayList<>(); + List normal = new ArrayList<>(); + List rare = new ArrayList<>(); + + Set clutterRuleSets = viewSettings().getClutterRuleSets(); + Set clutterRules = viewSettings().getClutterRules(); + + for (TacletApp app : taclets) { + Taclet taclet = app.taclet(); + if (isHiddenTaclet(taclet)) { + hidden.add(app); + continue; + } + if (isSystemInvariantTaclet(taclet)) { + systemInv.add(app); + continue; + } + if (interactiveFilter != null && !interactiveFilter.filter(taclet)) { + continue; + } + if (isRareRule(taclet, clutterRuleSets, clutterRules)) { + rare.add(app); + } else { + normal.add(app); + } + } + normal.addAll(rare); + + int currentSize = 0; + List target = out; + for (TacletApp app : normal) { + target.add(createTacletEntry(app)); + ++currentSize; + if (currentSize >= TOO_MANY_TACLETS_THRESHOLD) { + List sub = new ArrayList<>(); + target.add(new SubMenuEntry(MORE_RULES, sub)); + target = sub; + currentSize = 0; + } + } + + if (!hidden.isEmpty()) { + out.add(createInsertHiddenMenu(hidden)); + } + if (!systemInv.isEmpty()) { + out.add(createSystemInvariantMenu(systemInv)); + } + } + + private Entry createTacletEntry(TacletApp app) { + String label = app.taclet().displayName(); + String tooltip = ""; + if (seam != null) { + label = seam.tacletHooks().label(app); + tooltip = seam.tacletHooks().tooltip(app); + } + return new TacletEntry(app, label, tooltip); + } + + private void addBuiltInSection(List out, ImmutableList builtInList) { + if (builtInList.isEmpty()) { + return; + } + out.add(new SeparatorEntry()); + for (BuiltInRule rule : builtInList) { + addBuiltInRule(out, rule); + } + } + + private void addBuiltInRule(List out, BuiltInRule rule) { + switch (rule) { + case WhileInvariantRule r -> out.add(dualEntry(rule, APPLY_RULE, + "Applies a known and complete loop specification immediately.", + ENTER_LOOP_SPECIFICATION, + "Allows to modify an existing or to enter a new loop specification.")); + case BlockContractInternalRule r -> out.add(dualEntry(rule, APPLY_RULE, + "Applies a known and complete block specification immediately.", + CHOOSE_AND_APPLY_CONTRACT, "Asks to select the contract to be applied.")); + case BlockContractExternalRule r -> out.add(dualEntry(rule, APPLY_RULE, + "All available contracts of the block are combined and applied.", + CHOOSE_AND_APPLY_CONTRACT, "Asks to select the contract to be applied.")); + case LoopContractInternalRule r -> out.add(dualEntry(rule, APPLY_RULE, + "Applies a known and complete loop block specification immediately.", + CHOOSE_AND_APPLY_CONTRACT, "Asks to select the contract to be applied.")); + case LoopContractExternalRule r -> out.add(dualEntry(rule, APPLY_RULE, + "All available contracts of the loop block are combined and applied.", + CHOOSE_AND_APPLY_CONTRACT, "Asks to select the contract to be applied.")); + case UseOperationContractRule r -> out.add(dualEntry(rule, APPLY_CONTRACT, + "All available contracts of the method are combined and applied.", + CHOOSE_AND_APPLY_CONTRACT, "Asks to select the contract to be applied.")); + case MergeRule r -> { + // handled by the merge-rule entry elsewhere; nothing extra here + } + case LoopScopeInvariantRule r -> { + } + case null -> { + } + default -> out.add(new BuiltInEntry(rule, + List.of(new NamedAction("apply_builtin_forced", rule.toString(), "", rule)), + "")); + } + } + + private Entry dualEntry(BuiltInRule rule, String forcedText, String forcedTip, + String interactiveText, String interactiveTip) { + List actions = List.of( + new NamedAction("apply_builtin_forced", forcedText, forcedTip, rule), + new NamedAction("apply_builtin_interactive", interactiveText, interactiveTip, rule)); + return new BuiltInEntry(rule, actions, null); + } + + private void addFocussedAutoMode(List out) { + out.add(new SeparatorEntry()); + out.add(new NamedAction("focus_auto_mode", "Apply rules automatically here", + "Initiates and restricts automatic rule applications on the highlighted formula, " + + "term or sequent. 'Shift + left mouse click' on the highlighted entity does the same.", + pos)); + } + + private void addSMTSection(List out) { + ProofIndependentSMTSettings smt = + ProofIndependentSettings.DEFAULT_INSTANCE.getSMTSettings(); + Collection solverUnions = smt.getSolverUnions(); + if (!solverUnions.isEmpty()) { + out.add(new SeparatorEntry()); + } + for (SolverTypeCollection union : solverUnions) { + if (union.isUsable()) { + out.add( + new NamedAction("smt", union.toString(), "Run the " + union + " SMT solvers.", + union)); + } + } + } + + private boolean addAbbrevSection(List out, JTerm t) { + AbbrevMap map = mediator.getNotationInfo().getAbbrevMap(); + AbbrevHooks hooks = seam != null ? seam.abbrevHooks() : null; + String create = hooks != null ? hooks.createLabel() : "Create abbreviation..."; + String change = hooks != null ? hooks.changeLabel() : "Change abbreviation..."; + String enable = hooks != null ? hooks.enableLabel() : "Enable abbreviation"; + String disable = hooks != null ? hooks.disableLabel() : "Disable abbreviation"; + if (map.containsTerm(t)) { + out.add(new AbbrevActionEntry(AbbrevAction.CHANGE, change)); + out.add( + new AbbrevActionEntry(map.isEnabled(t) ? AbbrevAction.DISABLE : AbbrevAction.ENABLE, + map.isEnabled(t) ? disable : enable)); + } else { + out.add(new AbbrevActionEntry(AbbrevAction.CREATE, create)); + } + return true; + } + + private Entry createInsertHiddenMenu(List hidden) { + List children = new ArrayList<>(); + for (TacletApp app : hidden) { + children.add(createTacletEntry(app)); + } + return new SubMenuEntry(INSERT_HIDDEN, children); + } + + private Entry createSystemInvariantMenu(List systemInv) { + systemInv.sort(Comparator.comparing(it -> it.taclet().displayName())); + List children = new ArrayList<>(); + for (TacletApp app : systemInv) { + children.add(createTacletEntry(app)); + } + return new SubMenuEntry(INSERT_CLASS_INVARIANT, children); + } + + // --------------------------------------------------------------------------------------------- + // Static rule-classification helpers (verbatim logic from CurrentGoalViewMenu / + // SequentViewMenu) + // --------------------------------------------------------------------------------------------- + + /** + * Removes the unsound "introduceAxiom" taclet from the list of displayed taclets + * (Swing CurrentGoalViewMenu.removeIntroduceAxiomTaclet). + */ + public static ImmutableList removeIntroduceAxiomTaclet( + ImmutableList list) { + return list.stream() + .filter(app -> !app.rule().name().toString().equals(INTRODUCE_AXIOM_TACLET_NAME)) + .collect(ImmutableList.collector()); + } + + /** Removes {@link RewriteTaclet}s from a list (Swing CurrentGoalViewMenu.removeRewrites). */ + public static ImmutableList removeRewrites(ImmutableList list) { + ImmutableList result = ImmutableList.nil(); + for (TacletApp app : list) { + Taclet taclet = app.taclet(); + result = (taclet instanceof RewriteTaclet ? result : result.prepend(app)); + } + return result; + } + + /** + * Sorts {@code finds} with {@code comp} — intentionally reversing the comparator's + * natural order, because the Swing method prepends the sorted list back together (Swing + * CurrentGoalViewMenu.sort, "the order will be reversed when the list is sorted"). Kept + * byte-identical so headless assertions can mirror the Swing output exactly. + */ + public static ImmutableList sort(ImmutableList finds, + Comparator comp) { + ImmutableList result = ImmutableList.nil(); + List list = new ArrayList<>(finds.size()); + for (TacletApp app : finds) { + list.add(app); + } + list.sort(comp); + for (TacletApp app : list) { + result = result.prepend(app); + } + return result; + } + + /** + * "Insert hidden" taclets are {@code NoFindTaclet}s whose display name starts with + * {@code insert_hidden} and which have exactly one goal template (Swing + * CurrentGoalViewMenu.isHiddenTaclet). + */ + public static boolean isHiddenTaclet(Taclet taclet) { + if (!(taclet instanceof NoFindTaclet) + || !taclet.displayName().startsWith("insert_hidden")) { + return false; + } + return taclet.goalTemplates().size() == 1; + } + + /** + * "Insert class invariant" taclets are {@code NoFindTaclet}s whose display name starts with + * {@code Insert implicit invariants of} and which have exactly one goal template (Swing + * CurrentGoalViewMenu.isSystemInvariantTaclet). + */ + public static boolean isSystemInvariantTaclet(Taclet taclet) { + if (!(taclet instanceof NoFindTaclet) + || !taclet.displayName().startsWith("Insert implicit invariants of")) { + return false; + } + return taclet.goalTemplates().size() == 1; + } + + private static boolean isRareRule(Taclet taclet, Set clutterRules, + Set clutterRuleSets) { + if (clutterRules.contains(taclet.name().toString())) { + return true; + } + return taclet.getRuleSets().stream() + .anyMatch(it -> clutterRuleSets.contains(it.name().toString())); + } + + private de.uka.ilkd.key.settings.ViewSettings viewSettings() { + return ProofIndependentSettings.DEFAULT_INSTANCE.getViewSettings(); + } + + // --------------------------------------------------------------------------------------------- + // TacletAppComparator (verbatim port of Swing CurrentGoalViewMenu.TacletAppComparator) + // --------------------------------------------------------------------------------------------- + + /** + * Orders applicable taclets so the "best" heuristic candidate appears near the top of the menu + * (Swing {@code CurrentGoalViewMenu.TacletAppComparator}). The comparator itself is used by + * {@link #sort}, whose reversal makes a smaller score sort later; the + * resulting order is what the Swing menu shows. This nested class is {@code public} to be + * unit-tested independently, matching the Swing class's visibility. + */ + public static final class TacletAppComparator implements Comparator { + + private int countFormulaSV(TacletSchemaVariableCollector c) { + int formulaSV = 0; + Iterator it = c.varIterator(); + while (it.hasNext()) { + SchemaVariable sv = it.next(); + if (sv instanceof FormulaSV) { + formulaSV++; + } + } + return formulaSV; + } + + /** Rough goal complexity estimate (Swing measureGoalComplexity). */ + private int measureGoalComplexity(ImmutableList l) { + int result = 0; + for (TacletGoalTemplate gt : l) { + if (gt instanceof RewriteTacletGoalTemplate) { + if (((RewriteTacletGoalTemplate) gt).replaceWith() != null) { + result += ((RewriteTacletGoalTemplate) gt).replaceWith().depth(); + } + } + if (!gt.sequent().isEmpty()) { + result += 10; + } + } + return result; + } + + /** Rough program-size estimate (Swing programComplexity). */ + public int programComplexity(JavaBlock b) { + if (b.isEmpty()) { + return 0; + } + return new JavaASTWalker(b.program()) { + private int counter = 0; + + @Override + protected void doAction(de.uka.ilkd.key.java.ast.ProgramElement pe) { + counter++; + } + + public int getCounter() { + counter = 0; + start(); + return counter; + } + }.getCounter(); + } + + @Override + public int compare(TacletApp o1, TacletApp o2) { + LinkedHashMap map1 = score(o1); + LinkedHashMap map2 = score(o2); + Iterator> it1 = map1.entrySet().iterator(); + Iterator> it2 = map2.entrySet().iterator(); + while (it1.hasNext() && it2.hasNext()) { + String s1 = it1.next().getKey(); + String s2 = it2.next().getKey(); + if (!s1.equals(s2)) { + throw new IllegalStateException( + "A decision should have been made on a higher level ( " + s1 + "<->" + s2 + + ")"); + } + int v1 = map1.get(s1); + int v2 = map2.get(s2); + if (v1 < v2) { + return 1; + } + if (v1 > v2) { + return -1; + } + } + return 0; + } + + /** A named, lexicographically comparable score for one taclet (Swing score). */ + public LinkedHashMap score(TacletApp o1) { + LinkedHashMap map = new LinkedHashMap<>(); + final Taclet taclet1 = o1.taclet(); + + map.put("closing", taclet1.goalTemplates().isEmpty() ? -1 : 1); + + boolean calc = false; + for (RuleSet rs : taclet1.getRuleSets()) { + String s = rs.name().toString(); + if (s.equals("simplify_literals") || s.equals("concrete") || s.equals("update_elim") + || s.equals("replace_known_left") || s.equals("replace_known_right")) { + calc = true; + } + } + map.put("calc", calc ? -1 : 1); + + int formulaSV1 = 0; + int cmpVar1 = 0; + + if (taclet1 instanceof FindTaclet) { + map.put("has_find", -1); + final JTerm find1 = ((FindTaclet) taclet1).find(); + int findComplexity1 = find1.depth(); + findComplexity1 += programComplexity(find1.javaBlock()); + map.put("find_complexity", -findComplexity1); + + TacletSchemaVariableCollector coll1 = new TacletSchemaVariableCollector(); + find1.execPostOrder(coll1); + formulaSV1 = countFormulaSV(coll1); + cmpVar1 -= coll1.size(); + map.put("num_sv", -cmpVar1); + } else { + map.put("has_find", 1); + } + + cmpVar1 = cmpVar1 - formulaSV1; + map.put("sans_formula_sv", -cmpVar1); + + map.put("if_seq", taclet1.assumesSequent().isEmpty() ? 1 : -1); + map.put("num_goals", taclet1.goalTemplates().size()); + map.put("goal_compl", measureGoalComplexity(taclet1.goalTemplates())); + + return map; + } + } +} From 6d250c23bb39593a3d1f34c7663132f9b14326fd Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Fri, 9 Oct 2026 15:02:35 +0200 Subject: [PATCH 085/281] key.ui.fx: termmenu: headless unit test of SequentMenuModelF (structural skeleton, partition helpers) --- .../fx/nodeviews/SequentMenuModelFTest.java | 327 ++++++++++++++++++ 1 file changed, 327 insertions(+) create mode 100644 key.ui.fx/src/test/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentMenuModelFTest.java diff --git a/key.ui.fx/src/test/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentMenuModelFTest.java b/key.ui.fx/src/test/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentMenuModelFTest.java new file mode 100644 index 00000000000..37f777a7e74 --- /dev/null +++ b/key.ui.fx/src/test/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentMenuModelFTest.java @@ -0,0 +1,327 @@ +/* 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.nodeviews; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; + +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.KeYMediatorF; +import de.uka.ilkd.key.gui.fx.nodeviews.SequentMenuModelF.AbbrevHooks; +import de.uka.ilkd.key.gui.fx.nodeviews.SequentMenuModelF.Entry; +import de.uka.ilkd.key.gui.fx.nodeviews.SequentMenuModelF.NamedAction; +import de.uka.ilkd.key.gui.fx.nodeviews.SequentMenuModelF.Seam; +import de.uka.ilkd.key.gui.fx.nodeviews.SequentMenuModelF.TacletHooks; +import de.uka.ilkd.key.macros.ProofMacro; +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.BuiltInRule; +import de.uka.ilkd.key.rule.Taclet; +import de.uka.ilkd.key.rule.TacletApp; + +import org.key_project.prover.engine.ProverTaskListener; +import org.key_project.prover.rules.RuleApp; +import org.key_project.prover.sequent.PosInOccurrence; +import org.key_project.util.collection.ImmutableList; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Headless unit test of the pure-Java sequent context-menu model + * {@link SequentMenuModelF} (T1 of the {@code termmenu} milestone). + *

        + * A {@link TacletApp}/{@link Taclet} cannot be trivially constructed without a full + * taclet builder, so the fixed structural skeleton is asserted through + * {@link SequentMenuModelF#build} with a null caret position (whole-sequent + * menu) against a real {@link Goal} loaded headlessly via {@link KeYEnvironment} and a + * {@link ProofControl} stub returning empty rule lists. The static partition helpers are + * exercised on the empty / no-goal boundary and on the real (find) taclet list where a + * partition can be asserted without constructing taclets by hand. + */ +class SequentMenuModelFTest { + + /** A proof control stub returning empty rule lists (deterministic skeleton). */ + private static final class EmptyProofControl implements ProofControl { + @Override + public ImmutableList getBuiltInRule(Goal focusedGoal, + PosInOccurrence pos) { + return ImmutableList.nil(); + } + + @Override + public ImmutableList getFindTaclet(Goal focusedGoal, + PosInOccurrence pos) { + return ImmutableList.nil(); + } + + @Override + public ImmutableList getRewriteTaclet(Goal focusedGoal, + PosInOccurrence pos) { + return ImmutableList.nil(); + } + + @Override + public ImmutableList getNoFindTaclet(Goal focusedGoal) { + return ImmutableList.nil(); + } + + @Override + public boolean isMinimizeInteraction() { + return false; + } + + @Override + public void setMinimizeInteraction(boolean minimizeInteraction) { + } + + @Override + public boolean selectedTaclet(Taclet taclet, Goal goal, PosInOccurrence pos) { + return false; + } + + @Override + public void applyInteractive(RuleApp app, Goal goal) { + } + + @Override + public void selectedBuiltInRule(Goal goal, BuiltInRule rule, PosInOccurrence pos, + boolean forced, boolean interactive) { + } + + @Override + public ProverTaskListener getDefaultProverTaskListener() { + return null; + } + + @Override + public void addAutoModeListener(de.uka.ilkd.key.control.AutoModeListener p) { + } + + @Override + public void removeAutoModeListener(de.uka.ilkd.key.control.AutoModeListener p) { + } + + @Override + public boolean isAutoModeSupported(Proof proof) { + return false; + } + + @Override + public boolean isInAutoMode() { + return false; + } + + @Override + public void startAutoMode(Proof proof) { + } + + @Override + public void startAutoMode(Proof proof, ImmutableList goals) { + } + + @Override + public void stopAutoMode() { + } + + @Override + public void stopAndWaitAutoMode() { + } + + @Override + public void waitWhileAutoMode() { + } + + @Override + public void startAndWaitForAutoMode(Proof proof, ImmutableList goals) { + } + + @Override + public void startAndWaitForAutoMode(Proof proof) { + } + + @Override + public void startFocussedAutoMode(PosInOccurrence focus, Goal goal) { + } + + @Override + public void runMacro(Node node, ProofMacro macro, PosInOccurrence posInOcc) { + } + } + + /** A no-op seam (its hooks are never consulted for the {@code null} position). */ + private static final Seam STUB_SEAM = new Seam() { + @Override + public AbbrevHooks abbrevHooks() { + return new AbbrevHooks() { + @Override + public String createLabel() { + return "create"; + } + + @Override + public String changeLabel() { + return "change"; + } + + @Override + public String enableLabel() { + return "enable"; + } + + @Override + public String disableLabel() { + return "disable"; + } + }; + } + + @Override + public TacletHooks tacletHooks() { + return new TacletHooks() { + @Override + public String label(TacletApp app) { + return app.taclet().displayName(); + } + + @Override + public String tooltip(TacletApp app) { + return ""; + } + }; + } + }; + + private KeYEnvironment env; + + @AfterEach + void tearDown() { + if (env != null) { + env.dispose(); + env = null; + } + } + + /** Loads a trivial problem headlessly and selects its root goal in a fresh mediator. */ + private KeYMediatorF mediatorWithGoal() throws Exception { + Path dir = Files.createTempDirectory("termmenu"); + Path main = dir.resolve("test.key"); + // No \include: the JavaDL profile loads the standard library from the classpath. + Files.writeString(main, "\\problem { true }\n"); + env = KeYEnvironment.load(main); + Proof proof = env.getLoadedProof(); + assertNotNull(proof, "the demo problem must produce a proof"); + + KeYMediatorF mediator = new KeYMediatorF(); + mediator.getSelectionModel().setSelectedProof(proof); + assertNotNull(mediator.getSelectedGoal(), "the root goal must be selectable"); + return mediator; + } + + /** + * (c) With a {@code null} caret position and an empty rule list the model must emit the + * fixed structural skeleton: no-rules entry, then the separator / named-action tail + * {@code focus_auto_mode, macro_menu, extension, copy_clipboard}. + */ + @Test + void buildNullPosEmitsFixedStructuralSkeleton() throws Exception { + KeYMediatorF mediator = mediatorWithGoal(); + + List entries = + SequentMenuModelF.build(null, mediator, new EmptyProofControl(), STUB_SEAM, null); + + List skeleton = flatten(entries); + List expected = List.of( + id("no_rules"), + sep(), + id("focus_auto_mode"), + id("macro_menu"), + sep(), + id("extension"), + sep(), + id("copy_clipboard")); + assertEquals(expected, skeleton, "the null-position menu must be the fixed skeleton"); + } + + /** + * Without a selected goal the model must fall back to an empty menu (the Swing + * {@code CurrentGoalViewListener} only opens a menu with a valid goal+position). + */ + @Test + void buildWithoutGoalReturnsEmpty() { + KeYMediatorF mediator = new KeYMediatorF(); + List entries = + SequentMenuModelF.build(null, mediator, new EmptyProofControl(), STUB_SEAM, null); + assertTrue(entries.isEmpty(), "an empty menu is expected without a selected goal"); + } + + /** + * (a) {@link SequentMenuModelF#removeRewrites} must drop every {@code RewriteTaclet} and keep + * the remaining order. On the empty boundary it returns {@link ImmutableList#nil()}. + */ + @Test + void removeRewritesKeepsNonRewrites() { + ImmutableList input = ImmutableList.nil(); + ImmutableList out = SequentMenuModelF.removeRewrites(input); + assertEquals(ImmutableList.nil(), out, "an empty rewrite-filtered list stays nil"); + } + + /** + * (a) {@link SequentMenuModelF#removeIntroduceAxiomTaclet} must drop the synthetic + * {@code introduceAxiom} taclet while keeping everything else. On the empty boundary it returns + * {@link ImmutableList#nil()}. + */ + @Test + void removeIntroduceAxiomDropsSyntheticTaclet() { + ImmutableList input = ImmutableList.nil(); + ImmutableList out = + SequentMenuModelF.removeIntroduceAxiomTaclet(input); + assertEquals(ImmutableList.nil(), out, "an empty axiom-filtered list stays nil"); + } + + /** + * (b) {@link SequentMenuModelF#sort} reverses the comparator's natural order (it prepends the + * sorted list back together). On the empty boundary it returns {@link ImmutableList#nil()}. + */ + @Test + void sortOnEmptyListStaysNil() { + ImmutableList out = + SequentMenuModelF.sort(ImmutableList.nil(), SequentMenuModelF.TACLET_SORT); + assertEquals(ImmutableList.nil(), out, "an empty sorted list stays nil"); + } + + // --------------------------------------------------------------------------------- + // helpers: flatten the model tree to (kind, id)-style comparables for assertions + // --------------------------------------------------------------------------------- + + private static Object id(String actionId) { + return "id:" + actionId; + } + + private static Object sep() { + return "SEPARATOR"; + } + + private static List flatten(List entries) { + List out = new ArrayList<>(); + for (Entry e : entries) { + switch (e.kind()) { + case NAMED_ACTION -> out.add(id(((NamedAction) e).id())); + case SEPARATOR -> out.add(sep()); + default -> out.add(e.kind()); + } + } + return out; + } +} From 4dd998b9e7ad6e96c51123d2810e356c92d69022 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Fri, 9 Oct 2026 15:14:49 +0200 Subject: [PATCH 086/281] key.ui.fx: termmenu: JavaFX shell for the sequent context menu (SequentTermContextMenuF) --- .../fx/nodeviews/SequentTermContextMenuF.java | 388 ++++++++++++++++++ 1 file changed, 388 insertions(+) create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentTermContextMenuF.java diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentTermContextMenuF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentTermContextMenuF.java new file mode 100644 index 00000000000..52818c1afae --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentTermContextMenuF.java @@ -0,0 +1,388 @@ +/* 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.nodeviews; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import javafx.scene.control.Alert; +import javafx.scene.control.Alert.AlertType; +import javafx.scene.control.ButtonType; +import javafx.scene.control.ContextMenu; +import javafx.scene.control.Menu; +import javafx.scene.control.MenuItem; +import javafx.scene.control.SeparatorMenuItem; +import javafx.scene.control.TextInputDialog; +import javafx.scene.input.Clipboard; +import javafx.scene.input.ClipboardContent; +import javafx.stage.Window; + +import de.uka.ilkd.key.control.ProofControl; +import de.uka.ilkd.key.core.fx.KeYMediatorF; +import de.uka.ilkd.key.gui.fx.join.JoinActionF; +import de.uka.ilkd.key.gui.fx.mergerule.MergeRuleMenuItemF; +import de.uka.ilkd.key.gui.fx.nodeviews.SequentMenuModelF.AbbrevActionEntry; +import de.uka.ilkd.key.gui.fx.nodeviews.SequentMenuModelF.BuiltInEntry; +import de.uka.ilkd.key.gui.fx.nodeviews.SequentMenuModelF.Entry; +import de.uka.ilkd.key.gui.fx.nodeviews.SequentMenuModelF.NamedAction; +import de.uka.ilkd.key.gui.fx.nodeviews.SequentMenuModelF.SubMenuEntry; +import de.uka.ilkd.key.gui.fx.nodeviews.SequentMenuModelF.TacletEntry; +import de.uka.ilkd.key.logic.JTerm; +import de.uka.ilkd.key.logic.NameCreationInfo; +import de.uka.ilkd.key.logic.ProgramElementName; +import de.uka.ilkd.key.logic.op.ProgramVariable; +import de.uka.ilkd.key.pp.AbbrevException; +import de.uka.ilkd.key.pp.AbbrevMap; +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.join.ProspectivePartner; + +import org.key_project.prover.sequent.PosInOccurrence; + +import org.jspecify.annotations.Nullable; + +/** + * The JavaFX {@link ContextMenu} shell for the sequent term context menu. It converts the tree of + * pure model descriptors ({@link SequentMenuModelF.Entry}) produced by + * {@link SequentMenuModelF#build} into a code-built JavaFX {@code ContextMenu} and wires each + * item to the {@link ProofControl} application path (Swing {@code CurrentGoalViewMenu} / + * {@code SequentViewMenu}). + *

        + * The shell is pure JavaFX construction — no FXML — and keeps every side effect (rule application, + * rule instantiation dialog, reprint of the sequent) behind the proof control or the supplied + * {@link MenuContext} callbacks, so the item structure is unit-testable without a live {@code + * Stage}. The milestone-deferred sections ({@code macro_menu}, {@code extension}, {@code smt}) are + * rendered as disabled placeholders so the skeleton stays faithful; they are wired in later + * milestones. + */ +public final class SequentTermContextMenuF { + + private SequentTermContextMenuF() { + } + + /** + * Everything the shell needs to build and dispatch a menu for the clicked position: the shared + * mediator, the {@link ProofControl} to apply rules with, the goal, the clicked position, the + * owner window for dialogs and a reprint callback invoked after a rule/abbreviation changed the + * sequent. + */ + public record MenuContext(@Nullable KeYMediatorF mediator, @Nullable ProofControl proofControl, + @Nullable Goal goal, @Nullable PosInSequent pos, @Nullable Window owner, + @Nullable Runnable afterApply) { + public MenuContext { + // normalize null callbacks to no-ops so builders never null-check + afterApply = afterApply == null ? () -> { + } : afterApply; + } + } + + /** + * Builds the {@link ContextMenu} for the given model entries. + * + * @param entries the ordered model descriptors + * @param ctx the application context (mediator, proof control, goal, position, owner, reprint) + * @return the constructed and wired context menu + */ + public static ContextMenu build(List entries, MenuContext ctx) { + ContextMenu menu = new ContextMenu(); + menu.getItems().addAll(buildItems(entries, ctx)); + return menu; + } + + private static List buildItems(List entries, MenuContext ctx) { + List items = new ArrayList<>(); + for (Entry entry : entries) { + switch (entry.kind()) { + case TACLET -> items.add(tacletItem((TacletEntry) entry, ctx)); + case BUILT_IN -> items.add(builtInMenu((BuiltInEntry) entry, ctx)); + case SUB_MENU -> items.add(subMenu((SubMenuEntry) entry, ctx)); + case NAMED_ACTION -> items.add(namedActionItem((NamedAction) entry, ctx)); + case ABBREV_ACTION -> items.add(abbrevItem((AbbrevActionEntry) entry, ctx)); + case SEPARATOR -> items.add(new SeparatorMenuItem()); + } + } + return items; + } + + private static MenuItem tacletItem(TacletEntry entry, MenuContext ctx) { + MenuItem item = new MenuItem(entry.label()); + item.setOnAction(e -> { + Goal goal = ctx.goal(); + PosInOccurrence pio = ctx.pos() == null ? null : ctx.pos().getPosInOccurrence(); + if (goal != null && ctx.proofControl() != null) { + ctx.proofControl().selectedTaclet(entry.app().taclet(), goal, pio); + ctx.afterApply().run(); + } + }); + return item; + } + + private static Menu builtInMenu(BuiltInEntry entry, MenuContext ctx) { + // The model passes the sub-menu label only for the dual-contribution contract/loop rules; + // the Swing original titles the menu with the rule's display name (Swing + // CurrentGoalViewMenu.addBuiltInRuleItem: "new JMenu(builtInRule.displayName())"). + String name = entry.rule().displayName(); + Menu menu = new Menu(name); + for (NamedAction action : entry.actions()) { + boolean forced = "apply_builtin_forced".equals(action.id()); + MenuItem item = new MenuItem(action.label()); + item.setOnAction(e -> { + Goal goal = ctx.goal(); + PosInOccurrence pio = + ctx.pos() == null ? null : ctx.pos().getPosInOccurrence(); + if (goal != null && ctx.proofControl() != null) { + ctx.proofControl().selectedBuiltInRule(goal, entry.rule(), pio, forced, true); + ctx.afterApply().run(); + } + }); + menu.getItems().add(item); + } + return menu; + } + + private static Menu subMenu(SubMenuEntry entry, MenuContext ctx) { + Menu menu = new Menu(entry.label()); + menu.getItems().addAll(buildItems(entry.children(), ctx)); + return menu; + } + + private static MenuItem namedActionItem(NamedAction action, MenuContext ctx) { + return switch (action.id()) { + case "join" -> joinItem(action, ctx); + case "merge_rule" -> mergeItem(action, ctx); + // termmenu: TODO wire the focused-auto-mode activation (Swing + // FocussedRuleApplicationAction) once the shift-click hit-test wiring lands (S3); + // for now the item is a faithful disabled placeholder. + case "focus_auto_mode" -> disabledItem(action.label()); + case "copy_clipboard" -> copyClipboardItem(ctx); + case "name_creation_info" -> nameCreationInfoItem(action, ctx); + case "no_rules" -> disabledItem(action.label()); + // termmenu: TODO these sections need the FX macro list (ProofMacroMenu), extension + // registry (KeYGuiExtensionFacade) and SMT-launch plumbing — deferred milestones. + // They are rendered as disabled placeholders so the menu structure stays faithful. + case "macro_menu", "extension", "smt" -> disabledItem(action.label()); + default -> disabledItem(action.label()); + }; + } + + /** Delayed-cut join (Swing JoinMenuItem / CurrentGoalViewMenu.createDelayedCutJoinMenu). */ + private static MenuItem joinItem(NamedAction action, MenuContext ctx) { + MenuItem item = new MenuItem(action.label()); + item.setOnAction(e -> { + Proof proof = ctx.goal() == null ? null : ctx.goal().proof(); + Object payload = action.payload(); + if (payload instanceof Collection coll && proof != null + && ctx.proofControl() != null) { + try { + @SuppressWarnings("unchecked") + List partners = + new ArrayList<>((Collection) coll); + JoinActionF.run(partners, proof, ctx.proofControl(), ctx.owner()); + } catch (RuntimeException ex) { + // The join dialog may fail to open (e.g. a missing stage owner in tests); + // never let a broken dialog take the context menu down with it. + System.err.println("Delayed-cut join could not be started: " + ex); + ex.printStackTrace(); + } + } + }); + return item; + } + + /** State-merging rule (Swing MergeRuleMenuItem / CurrentGoalViewMenu.createMergeRuleMenu). */ + private static MenuItem mergeItem(NamedAction action, MenuContext ctx) { + Goal goal = ctx.goal(); + PosInOccurrence pio = action.payload() instanceof PosInOccurrence p ? p : null; + if (goal == null || pio == null || ctx.proofControl() == null) { + return disabledItem(action.label()); + } + return new MergeRuleMenuItemF(goal, pio, ctx.proofControl()); + } + + /** Copy the clicked term to the system clipboard (Swing SequentView.addClipboardItem). */ + private static MenuItem copyClipboardItem(MenuContext ctx) { + MenuItem item = new MenuItem("Copy to clipboard"); + item.setOnAction(e -> { + PosInSequent pos = ctx.pos(); + PosInOccurrence occ = pos == null ? null : pos.getPosInOccurrence(); + if (occ != null && occ.subTerm() != null) { + String s = occ.subTerm().toString().replace('\u00A0', ' '); + ClipboardContent content = new ClipboardContent(); + content.putString(s); + Clipboard.getSystemClipboard().setContent(content); + } + }); + return item; + } + + /** + * View the {@link NameCreationInfo} of the clicked program variable (Swing + * NameCreationInfoAction). + */ + private static MenuItem nameCreationInfoItem(NamedAction action, MenuContext ctx) { + MenuItem item = new MenuItem(action.label()); + item.setOnAction(e -> { + Object payload = action.payload(); + String message; + if (payload instanceof ProgramVariable var) { + ProgramElementName name = var.getProgramElementName(); + NameCreationInfo info = name.getCreationInfo(); + message = info != null ? info.infoAsString() : "No information available."; + } else { + message = "No information available."; + } + Alert alert = new Alert(AlertType.INFORMATION, message, ButtonType.OK); + alert.setTitle("Name creation info"); + alert.setHeaderText(null); + if (ctx.owner() != null) { + alert.initOwner(ctx.owner()); + } + alert.showAndWait(); + }); + return item; + } + + /** + * Create / change / enable / disable an abbreviation for the clicked term via the shared + * {@code NotationInfo.getAbbrevMap()} (Swing CurrentGoalViewMenu.createAbbrevSection and the + * four AbbreviationActions). + */ + private static MenuItem abbrevItem(AbbrevActionEntry entry, MenuContext ctx) { + MenuItem item = new MenuItem(entry.label()); + item.setOnAction(e -> { + PosInOccurrence occ = ctx.pos() == null ? null : ctx.pos().getPosInOccurrence(); + if (occ == null || occ.posInTerm() == null || ctx.mediator() == null) { + return; + } + JTerm term = (JTerm) occ.subTerm(); + AbbrevMap map = ctx.mediator().getNotationInfo().getAbbrevMap(); + switch (entry.action()) { + case CREATE -> createAbbreviation(map, term, ctx); + case CHANGE -> changeAbbreviation(map, term, ctx); + case ENABLE -> { + map.setEnabled(term, true); + ctx.afterApply().run(); + } + case DISABLE -> { + map.setEnabled(term, false); + ctx.afterApply().run(); + } + } + }); + return item; + } + + private static void createAbbreviation(AbbrevMap map, JTerm term, MenuContext ctx) { + String old = term.toString(); + String trimmed = old.length() > 200 ? old.substring(0, 200) : old; + TextInputDialog dialog = new TextInputDialog(""); + dialog.setTitle("New Abbreviation"); + dialog.setHeaderText(null); + dialog.setContentText("Enter abbreviation for term: \n" + trimmed); + dialog.setGraphic(null); + if (ctx.owner() != null) { + dialog.initOwner(ctx.owner()); + } + dialog.showAndWait().ifPresent(abbreviation -> { + if (invalidAbbreviation(abbreviation)) { + showError("Only letters, numbers and '_' are allowed for Abbreviations", "Sorry", + ctx.owner()); + return; + } + try { + if (map.containsAbbreviation(abbreviation)) { + String newAbbreviation = "old_" + abbreviation; + ButtonType choice = showConfirm( + String.format( + "Abbreviation %s already bound. Do want to rename the previous " + + "binding to %s and proceed?", + abbreviation, newAbbreviation), + "Name collision resolution", ctx.owner()); + if (choice != ButtonType.OK) { + return; + } + var prevTerm = map.getTerm(abbreviation); + var enabled = prevTerm != null && map.isEnabled(prevTerm); + if (prevTerm != null) { + map.remove(prevTerm); + } + if (prevTerm != null) { + map.put(prevTerm, newAbbreviation, enabled); + } + } + map.put(term, abbreviation, true); + ctx.afterApply().run(); + } catch (AbbrevException sce) { + showError(sce.getMessage(), "Sorry", ctx.owner()); + } + }); + } + + private static void changeAbbreviation(AbbrevMap map, JTerm term, MenuContext ctx) { + String current = map.getAbbrev(term); + String initial = current != null && current.length() > 1 ? current.substring(1) : ""; + TextInputDialog dialog = new TextInputDialog(initial); + dialog.setTitle("Change Abbreviation"); + dialog.setHeaderText(null); + dialog.setContentText("Enter abbreviation for term: \n" + term); + dialog.setGraphic(null); + if (ctx.owner() != null) { + dialog.initOwner(ctx.owner()); + } + dialog.showAndWait().ifPresent(abbreviation -> { + if (abbreviation == null) { + return; + } + if (invalidAbbreviation(abbreviation)) { + showError("Only letters, numbers and '_' are allowed for Abbreviations", "Sorry", + ctx.owner()); + return; + } + try { + map.changeAbbrev(term, abbreviation); + ctx.afterApply().run(); + } catch (AbbrevException sce) { + showError(sce.getMessage(), "Sorry", ctx.owner()); + } + }); + } + + private static boolean invalidAbbreviation(String s) { + if (s == null || s.isBlank()) { + return true; + } + return !s.chars() + .allMatch(it -> Character.isAlphabetic(it) || Character.isDigit(it) || it == '_'); + } + + private static MenuItem disabledItem(String label) { + MenuItem item = new MenuItem(label); + item.setDisable(true); + return item; + } + + private static void showError(String message, String title, Window owner) { + Alert alert = new Alert(AlertType.ERROR, message, ButtonType.OK); + alert.setTitle(title); + alert.setHeaderText(null); + if (owner != null) { + alert.initOwner(owner); + } + alert.showAndWait(); + } + + private static ButtonType showConfirm(String message, String title, Window owner) { + Alert alert = new Alert(AlertType.CONFIRMATION, message, ButtonType.OK, + ButtonType.CANCEL); + alert.setTitle(title); + alert.setHeaderText(null); + if (owner != null) { + alert.initOwner(owner); + } + alert.showAndWait(); + return alert.getResult(); + } +} From 74a243435dcefe96311012d00d29bc60785e9222 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Fri, 9 Oct 2026 15:19:43 +0200 Subject: [PATCH 087/281] key.ui.fx: termmenu: headless unit test of SequentMenuModelF (structural skeleton, partition helpers, comparator) --- .../fx/nodeviews/SequentMenuModelFTest.java | 275 ++++++++++++++++-- 1 file changed, 246 insertions(+), 29 deletions(-) diff --git a/key.ui.fx/src/test/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentMenuModelFTest.java b/key.ui.fx/src/test/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentMenuModelFTest.java index 37f777a7e74..8b7c2354577 100644 --- a/key.ui.fx/src/test/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentMenuModelFTest.java +++ b/key.ui.fx/src/test/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentMenuModelFTest.java @@ -18,11 +18,16 @@ import de.uka.ilkd.key.gui.fx.nodeviews.SequentMenuModelF.NamedAction; import de.uka.ilkd.key.gui.fx.nodeviews.SequentMenuModelF.Seam; import de.uka.ilkd.key.gui.fx.nodeviews.SequentMenuModelF.TacletHooks; +import de.uka.ilkd.key.logic.JavaBlock; import de.uka.ilkd.key.macros.ProofMacro; 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.BuiltInRule; +import de.uka.ilkd.key.rule.FindTaclet; +import de.uka.ilkd.key.rule.NoFindTaclet; +import de.uka.ilkd.key.rule.NoPosTacletApp; +import de.uka.ilkd.key.rule.RewriteTaclet; import de.uka.ilkd.key.rule.Taclet; import de.uka.ilkd.key.rule.TacletApp; @@ -35,20 +40,31 @@ import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; /** - * Headless unit test of the pure-Java sequent context-menu model - * {@link SequentMenuModelF} (T1 of the {@code termmenu} milestone). + * Headless unit test of the pure-Java sequent context-menu model {@link SequentMenuModelF} (S1 of + * the {@code termmenu} milestone). *

        - * A {@link TacletApp}/{@link Taclet} cannot be trivially constructed without a full - * taclet builder, so the fixed structural skeleton is asserted through - * {@link SequentMenuModelF#build} with a null caret position (whole-sequent - * menu) against a real {@link Goal} loaded headlessly via {@link KeYEnvironment} and a - * {@link ProofControl} stub returning empty rule lists. The static partition helpers are - * exercised on the empty / no-goal boundary and on the real (find) taclet list where a - * partition can be asserted without constructing taclets by hand. + * The module has no mocking library on the test classpath (repo convention: JUnit 5 + AssertJ, no + * Mockito), so the tests run against real taclets of the standard JavaDL profile loaded + * headlessly via {@link KeYEnvironment} and app objects created with + * {@link NoPosTacletApp#createNoPosTacletApp(Taclet)}. This exercises the actual rewriting / + * filtering / scoring code paths rather than mock behaviour. + *

          + *
        • The fixed structural skeleton of {@link SequentMenuModelF#build} is asserted with a + * {@code null} caret position (whole-sequent menu) against a real {@link Goal} and a + * {@link ProofControl} stub returning empty rule lists.
        • + *
        • The static partition helpers {@link SequentMenuModelF#removeRewrites} and + * {@link SequentMenuModelF#removeIntroduceAxiomTaclet} are checked on mixed lists of real + * taclet apps; the {@code introduceAxiom} taclet is commented out of the current standard library + * ({@code propRule.key}), so a real {@link NoFindTaclet} renamed via {@link Taclet#setName} stands + * in for it.
        • + *
        • The sort order and the {@link SequentMenuModelF.TacletAppComparator} score maps are asserted + * on real find / no-find taclets.
        • + *
        */ class SequentMenuModelFTest { @@ -213,26 +229,46 @@ void tearDown() { } } - /** Loads a trivial problem headlessly and selects its root goal in a fresh mediator. */ - private KeYMediatorF mediatorWithGoal() throws Exception { + /** + * Loads a trivial problem headlessly and selects its root goal in a fresh mediator. Loading + * {@code \problem { true }} also parses the full standard JavaDL rule library into the + * {@code InitConfig}, so {@link #allTaclets()} can reuse the same environment. + */ + private KeYEnvironment loadEnv() throws Exception { + if (env != null) { + return env; + } Path dir = Files.createTempDirectory("termmenu"); Path main = dir.resolve("test.key"); // No \include: the JavaDL profile loads the standard library from the classpath. Files.writeString(main, "\\problem { true }\n"); env = KeYEnvironment.load(main); - Proof proof = env.getLoadedProof(); - assertNotNull(proof, "the demo problem must produce a proof"); + assertNotNull(env.getLoadedProof(), "the demo problem must produce a proof"); + return env; + } + + /** Every taclet of the standard JavaDL profile (activated or not). */ + private ImmutableList allTaclets() throws Exception { + return loadEnv().getInitConfig().getTaclets(); + } + private KeYMediatorF mediatorWithGoal() throws Exception { + Proof proof = loadEnv().getLoadedProof(); KeYMediatorF mediator = new KeYMediatorF(); mediator.getSelectionModel().setSelectedProof(proof); assertNotNull(mediator.getSelectedGoal(), "the root goal must be selectable"); return mediator; } + // --------------------------------------------------------------------------------- + // (c) build(...): fixed structural skeleton for a null caret position + // --------------------------------------------------------------------------------- + /** - * (c) With a {@code null} caret position and an empty rule list the model must emit the - * fixed structural skeleton: no-rules entry, then the separator / named-action tail - * {@code focus_auto_mode, macro_menu, extension, copy_clipboard}. + * With a {@code null} caret position and an empty rule list the model must emit the fixed + * structural skeleton: the no-rules entry, then the separator / named-action tail + * {@code focus_auto_mode, macro_menu, extension, copy_clipboard} with the two separators in + * place. */ @Test void buildNullPosEmitsFixedStructuralSkeleton() throws Exception { @@ -254,10 +290,7 @@ void buildNullPosEmitsFixedStructuralSkeleton() throws Exception { assertEquals(expected, skeleton, "the null-position menu must be the fixed skeleton"); } - /** - * Without a selected goal the model must fall back to an empty menu (the Swing - * {@code CurrentGoalViewListener} only opens a menu with a valid goal+position). - */ + /** Without a selected goal the model must fall back to an empty menu. */ @Test void buildWithoutGoalReturnsEmpty() { KeYMediatorF mediator = new KeYMediatorF(); @@ -266,9 +299,14 @@ void buildWithoutGoalReturnsEmpty() { assertTrue(entries.isEmpty(), "an empty menu is expected without a selected goal"); } + // --------------------------------------------------------------------------------- + // (a) partition helpers on the empty boundary (singleton settings already + // initialized through KeYEnvironment in the tests above) + // --------------------------------------------------------------------------------- + /** - * (a) {@link SequentMenuModelF#removeRewrites} must drop every {@code RewriteTaclet} and keep - * the remaining order. On the empty boundary it returns {@link ImmutableList#nil()}. + * {@link SequentMenuModelF#removeRewrites} must drop every {@link RewriteTaclet} and keep the + * remaining taclets. On the empty boundary it returns {@link ImmutableList#nil()}. */ @Test void removeRewritesKeepsNonRewrites() { @@ -278,22 +316,18 @@ void removeRewritesKeepsNonRewrites() { } /** - * (a) {@link SequentMenuModelF#removeIntroduceAxiomTaclet} must drop the synthetic + * {@link SequentMenuModelF#removeIntroduceAxiomTaclet} must drop the synthetic * {@code introduceAxiom} taclet while keeping everything else. On the empty boundary it returns * {@link ImmutableList#nil()}. */ @Test void removeIntroduceAxiomDropsSyntheticTaclet() { ImmutableList input = ImmutableList.nil(); - ImmutableList out = - SequentMenuModelF.removeIntroduceAxiomTaclet(input); + ImmutableList out = SequentMenuModelF.removeIntroduceAxiomTaclet(input); assertEquals(ImmutableList.nil(), out, "an empty axiom-filtered list stays nil"); } - /** - * (b) {@link SequentMenuModelF#sort} reverses the comparator's natural order (it prepends the - * sorted list back together). On the empty boundary it returns {@link ImmutableList#nil()}. - */ + /** {@link SequentMenuModelF#sort} iterates the empty list and returns it unchanged. */ @Test void sortOnEmptyListStaysNil() { ImmutableList out = @@ -301,6 +335,189 @@ void sortOnEmptyListStaysNil() { assertEquals(ImmutableList.nil(), out, "an empty sorted list stays nil"); } + // --------------------------------------------------------------------------------- + // (a) partition helpers on mixed lists of real taclet apps + // --------------------------------------------------------------------------------- + + /** + * {@link SequentMenuModelF#removeRewrites} on a mixed list of real apps drops exactly the + * {@link RewriteTaclet}s. Note that the Swing-port implementation prepends the kept apps, so + * the relative order of the survivors mirrors the input order reversed. + */ + @Test + void removeRewritesDropsOnlyRewriteTaclets() throws Exception { + List taclets = allTaclets().stream().toList(); + Taclet rewrite = taclets.stream().filter(t -> t instanceof RewriteTaclet) + .findFirst().orElse(null); + assertNotNull(rewrite, "the standard library must contain a rewrite taclet"); + Taclet other1 = taclets.stream().filter(t -> !(t instanceof RewriteTaclet)) + .findFirst().orElse(null); + Taclet other2 = taclets.stream() + .filter(t -> !(t instanceof RewriteTaclet) && t != other1).findFirst() + .orElse(null); + assertNotNull(other1, "the standard library must contain a non-rewrite taclet"); + assertNotNull(other2, "the standard library must contain a second non-rewrite taclet"); + + TacletApp rewriteApp = NoPosTacletApp.createNoPosTacletApp(rewrite); + TacletApp other1App = NoPosTacletApp.createNoPosTacletApp(other1); + TacletApp other2App = NoPosTacletApp.createNoPosTacletApp(other2); + + ImmutableList out = SequentMenuModelF.removeRewrites( + ImmutableList.fromList(List.of(other1App, rewriteApp, other2App))); + + assertEquals(ImmutableList.fromList(List.of(other2App, other1App)), out, + "only the rewrite taclet is dropped, the survivors keep prepend order"); + assertFalse(out.stream().anyMatch(a -> a.taclet() instanceof RewriteTaclet), + "no rewrite taclet may survive"); + } + + /** + * {@link SequentMenuModelF#removeIntroduceAxiomTaclet} drops only the taclet whose rule name + * is {@code introduceAxiom}. The rule is commented out of the current standard library + * ({@code propRule.key}), so a real {@link NoFindTaclet} renamed via {@link Taclet#setName} + * stands in for it; the kept survivors keep their encounter order (stream collector). + */ + @Test + void removeIntroduceAxiomDropsOnlyNamedTaclet() throws Exception { + List taclets = allTaclets().stream().toList(); + Taclet axiom = taclets.stream() + .filter(t -> t.name().toString().equals("introduceAxiom")).findFirst() + .orElseGet(() -> { + Taclet firstNoFind = taclets.stream().filter(t -> t instanceof NoFindTaclet) + .findFirst().orElse(null); + assertNotNull(firstNoFind, + "a NoFindTaclet is needed to synthesize the introduceAxiom rule"); + return firstNoFind.setName("introduceAxiom"); + }); + Taclet other1 = taclets.stream() + .filter(t -> !t.name().toString().equals("introduceAxiom")).findFirst() + .orElse(null); + Taclet other2 = taclets.stream().filter(t -> t != other1 + && !t.name().toString().equals("introduceAxiom")).findFirst().orElse(null); + assertNotNull(other1, "the standard library must contain a non-axiom taclet"); + assertNotNull(other2, "the standard library must contain a second non-axiom taclet"); + + TacletApp axiomApp = NoPosTacletApp.createNoPosTacletApp(axiom); + TacletApp other1App = NoPosTacletApp.createNoPosTacletApp(other1); + TacletApp other2App = NoPosTacletApp.createNoPosTacletApp(other2); + + ImmutableList out = SequentMenuModelF.removeIntroduceAxiomTaclet( + ImmutableList.fromList(List.of(axiomApp, other1App, other2App))); + + assertEquals(ImmutableList.fromList(List.of(other1App, other2App)), out, + "only the introduceAxiom taclet is dropped, the survivors keep their order"); + assertFalse(out.stream() + .anyMatch(a -> a.rule().name().toString().equals("introduceAxiom")), + "no introduceAxiom taclet may survive"); + } + + // --------------------------------------------------------------------------------- + // (b) sort + TacletAppComparator ordering and score maps + // --------------------------------------------------------------------------------- + + /** + * A closing rule (FindTaclet without goal templates, e.g. the standard {@code closeTrue}) + * scores {@code closing == -1} and must be sorted to the top of the menu: the comparator puts + * it after the non-closing rule in natural order, and {@link SequentMenuModelF#sort} + * reverses that order by prepending back (exactly like the Swing menu). + */ + @Test + void sortPutsClosingTacletBeforeNonClosing() throws Exception { + List taclets = allTaclets().stream().toList(); + Taclet closing = taclets.stream().filter(t -> t instanceof FindTaclet) + .filter(t -> t.goalTemplates().isEmpty()).findFirst().orElse(null); + Taclet nonClosing = taclets.stream().filter(t -> t instanceof FindTaclet) + .filter(t -> !t.goalTemplates().isEmpty()).findFirst().orElse(null); + assertNotNull(closing, "the standard library must contain a closing find taclet"); + assertNotNull(nonClosing, "the standard library must contain a non-closing find taclet"); + + TacletApp closingApp = NoPosTacletApp.createNoPosTacletApp(closing); + TacletApp nonClosingApp = NoPosTacletApp.createNoPosTacletApp(nonClosing); + + SequentMenuModelF.TacletAppComparator cmp = new SequentMenuModelF.TacletAppComparator(); + assertEquals(-1, cmp.score(closingApp).get("closing"), + "a closing taclet scores best on the closing key"); + assertEquals(1, cmp.score(nonClosingApp).get("closing"), + "a non-closing taclet scores worse on the closing key"); + + // natural (ascending) order: non-closing first; compare returns the inverse sign + assertEquals(1, cmp.compare(closingApp, nonClosingApp)); + assertEquals(-1, cmp.compare(nonClosingApp, closingApp)); + + ImmutableList out = SequentMenuModelF.sort( + ImmutableList.fromList(List.of(nonClosingApp, closingApp)), + SequentMenuModelF.TACLET_SORT); + assertEquals(2, out.size()); + assertEquals(closingApp, out.head(), "the closing taclet surfaces first in the menu"); + assertEquals(nonClosingApp, out.tail().head()); + } + + /** + * {@link SequentMenuModelF.TacletAppComparator#score} exposes a fixed, lexicographically + * comparable key set: a find taclet additionally contributes {@code find_complexity} and + * {@code num_sv}, a no-find taclet does not. + */ + @Test + void scoreExposesExpectedKeysForFindAndNoFindTaclets() throws Exception { + List taclets = allTaclets().stream().toList(); + Taclet find = taclets.stream().filter(t -> t instanceof FindTaclet).findFirst() + .orElse(null); + Taclet noFind = taclets.stream().filter(t -> t instanceof NoFindTaclet).findFirst() + .orElse(null); + assertNotNull(find, "the standard library must contain a find taclet"); + assertNotNull(noFind, "the standard library must contain a no-find taclet"); + + SequentMenuModelF.TacletAppComparator cmp = new SequentMenuModelF.TacletAppComparator(); + var findScore = cmp.score(NoPosTacletApp.createNoPosTacletApp(find)); + var noFindScore = cmp.score(NoPosTacletApp.createNoPosTacletApp(noFind)); + + assertEquals(List.of("closing", "calc", "has_find", "find_complexity", "num_sv", + "sans_formula_sv", "if_seq", "num_goals", "goal_compl"), + List.copyOf(findScore.keySet()), + "a find taclet scores the full key set"); + assertEquals(List.of("closing", "calc", "has_find", "sans_formula_sv", "if_seq", + "num_goals", "goal_compl"), + List.copyOf(noFindScore.keySet()), + "a no-find taclet skips the find-related keys"); + assertEquals(-1, findScore.get("has_find")); + assertEquals(1, noFindScore.get("has_find")); + } + + /** The program-complexity measure is zero without a Java program. */ + @Test + void programComplexityOfEmptyJavaBlockIsZero() { + assertEquals(0, + new SequentMenuModelF.TacletAppComparator() + .programComplexity(JavaBlock.EMPTY_JAVABLOCK)); + } + + // --------------------------------------------------------------------------------- + // classification helpers on ordinary rules + // --------------------------------------------------------------------------------- + + /** + * {@link SequentMenuModelF#isHiddenTaclet} / {@link SequentMenuModelF#isSystemInvariantTaclet} + * must not classify ordinary library rules. The positive cases (display names + * {@code insert_hidden...} / {@code Insert implicit invariants of...}) are created only at + * runtime by the proof machinery, so they are not part of the standard library and cannot be + * constructed here without the full taclet builder. + */ + @Test + void classificationHelpersRejectOrdinaryRules() throws Exception { + List taclets = allTaclets().stream().toList(); + Taclet rewrite = taclets.stream().filter(t -> t instanceof RewriteTaclet) + .findFirst().orElse(null); + Taclet noFind = taclets.stream().filter(t -> t instanceof NoFindTaclet).findFirst() + .orElse(null); + assertNotNull(rewrite, "the standard library must contain a rewrite taclet"); + assertNotNull(noFind, "the standard library must contain a no-find taclet"); + + assertFalse(SequentMenuModelF.isHiddenTaclet(rewrite)); + assertFalse(SequentMenuModelF.isSystemInvariantTaclet(rewrite)); + assertFalse(SequentMenuModelF.isHiddenTaclet(noFind)); + assertFalse(SequentMenuModelF.isSystemInvariantTaclet(noFind)); + } + // --------------------------------------------------------------------------------- // helpers: flatten the model tree to (kind, id)-style comparables for assertions // --------------------------------------------------------------------------------- From 3284ae35c61f5b8daa7089bdab18d36297a8c475 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Fri, 9 Oct 2026 15:36:14 +0200 Subject: [PATCH 088/281] key.ui.fx: termmenu: sequent view context-menu hit test (right-click shows the sequent menu) --- .../de/uka/ilkd/key/gui/fx/MainWindowF.java | 5 + .../key/gui/fx/nodeviews/SequentViewF.java | 95 +++++++++++++++++++ 2 files changed, 100 insertions(+) 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 index cca23dcdf2d..61fe88cae8d 100644 --- 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 @@ -594,6 +594,11 @@ protected KeYEnvironment call() throws Exception { // 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()); + // 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()); env.getProofControl().addAutoModeListener(autoModeUiListener); // notification: register the notification framework's auto-mode tracker on the // proof control (Swing parity: the NotificationManager constructor registers its diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentViewF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentViewF.java index 179676cced5..c1c028448b4 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentViewF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentViewF.java @@ -18,6 +18,7 @@ import javafx.geometry.Point2D; import javafx.scene.control.Button; import javafx.scene.control.ComboBox; +import javafx.scene.control.ContextMenu; import javafx.scene.control.Label; import javafx.scene.control.ListCell; import javafx.scene.control.ScrollPane; @@ -28,6 +29,7 @@ import javafx.scene.input.KeyCodeCombination; import javafx.scene.input.KeyCombination; import javafx.scene.input.KeyEvent; +import javafx.scene.input.MouseButton; import javafx.scene.input.MouseEvent; import javafx.scene.layout.BorderPane; import javafx.scene.layout.HBox; @@ -47,6 +49,8 @@ import javafx.scene.text.TextFlow; import javafx.util.Duration; +import de.uka.ilkd.key.control.ProofControl; +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; @@ -67,6 +71,7 @@ import de.uka.ilkd.key.pp.SequentPrintFilterEntry; import de.uka.ilkd.key.pp.SequentViewLogicPrinter; import de.uka.ilkd.key.pp.VisibleTermLabels; +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.settings.ProofIndependentSettings; @@ -596,6 +601,14 @@ public void setOnPosSelected(Consumer handler) { } private void handleMouseClick(MouseEvent event) { + // termmenu: a right-click shows the sequent context menu instead of running the + // selection path below (Swing CurrentGoalViewListener: a right mouse click builds + // CurrentGoalViewMenu at the caret position and shows it at the mouse location); the + // left-click selection / highlight logic is left untouched + if (event.getButton() == MouseButton.SECONDARY) { + showContextMenu(event); + return; + } InitialPositionTable table = getInitialPositionTable(); if (table == null || printed == null) { return; @@ -617,6 +630,52 @@ private void handleMouseClick(MouseEvent event) { onPosSelected.accept(pos); } + // termmenu: begin — right-click sequent context menu (SequentMenuModelF + + // SequentTermContextMenuF) + /** + * Builds and shows the sequent context menu at the given right-click position (Swing + * {@code CurrentGoalViewListener.mouseClicked}: the menu is built from the + * {@link PosInSequent} at the caret position and displayed at the mouse location). A no-op + * until the menu context ({@link #setMenuContext}) and a printable position are available. + * + * @param event the right-click event + */ + private void showContextMenu(MouseEvent event) { + if (menuMediator == null || menuProofControl == null) { + return; + } + Goal goal = menuMediator.getSelectedGoal(); + if (goal == null) { + return; + } + PosInSequent pos = getSequentPosAt(charIndexOf(event)); + if (pos == null) { + return; + } + List entries = + SequentMenuModelF.build(pos, menuMediator, menuProofControl, null, null); + ContextMenu menu = SequentTermContextMenuF.build(entries, + new SequentTermContextMenuF.MenuContext(menuMediator, menuProofControl, goal, pos, + null, this::printSequent)); + menu.show(textFlow, event.getScreenX(), event.getScreenY()); + } + + /** + * The character index of the printed text under the given mouse event (the same mapping + * {@link #handleMouseClick} uses), or {@code -1} if the event does not address a character. + */ + private int charIndexOf(MouseEvent event) { + InitialPositionTable table = getInitialPositionTable(); + if (table == null || printed == null) { + return -1; + } + Point2D local = textFlow.sceneToLocal(event.getSceneX(), event.getSceneY()); + HitInfo hit = textFlow.getHitInfo(local); + int charIndex = hit == null ? -1 : hit.getCharIndex(); + return charIndex >= 0 && charIndex < printed.length() ? charIndex : -1; + } + // termmenu: end + // lemmaorigin: begin — the last clicked position (Swing's term context-menu target; the // View▸Origin Tracking▸Show Origin item of OriginLabelsF uses it because the FX sequent view // has no context menu yet) @@ -631,6 +690,42 @@ public PosInSequent getLastClickedPos() { } // lemmaorigin: end + // termmenu: begin — right-click context menu support (S3 hit-test wiring): the shared + // mediator + proof control supplied by MainWindowF (Swing parity: the sequent view builds + // CurrentGoalViewMenu with the mediator's selected goal and the proof control of the loaded + // environment) and the char-index → PosInSequent lookup shared by the right-click handler and + // the key.fx.verify.termmenu self test. + private KeYMediatorF menuMediator; + private ProofControl menuProofControl; + + /** + * Supplies the FX mediator and the proof control used to build the right-click sequent + * context menu (Swing {@code MainWindow.setSequentView} passes its mediator and the proof + * control of the loaded environment). No-op until both are set — the menu only appears when + * the full context is available. + * + * @param mediator the shared mediator, {@code null} clears the reference + * @param proofControl the proof control of the loaded environment, {@code null} clears it + */ + public void setMenuContext(KeYMediatorF mediator, ProofControl proofControl) { + this.menuMediator = mediator; + this.menuProofControl = proofControl; + } + + /** + * @return the {@link PosInSequent} of the printed sequent at the given character index (the + * same lookup the mouse handlers perform), or {@code null} if there is no printing or + * the index does not address a position + */ + public PosInSequent getSequentPosAt(int charIndex) { + InitialPositionTable table = getInitialPositionTable(); + if (table == null || printed == null || charIndex < 0 || charIndex >= printed.length()) { + return null; + } + return table.getPosInSequent(charIndex, filter); + } + // termmenu: end + // ----------------------------------------------------------------------- // Hover highlight + tooltip (Swing SequentViewInputListener.mouseMoved / // SequentView.getToolTipText) From 32db1ee1d150a76f37eecb548ddd734b9b32ebf2 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Fri, 9 Oct 2026 15:36:23 +0200 Subject: [PATCH 089/281] key.ui.fx: termmenu: key.fx.verify.termmenu headless self test of the sequent context menu --- key.ui.fx/build.gradle | 5 +- .../de/uka/ilkd/key/gui/fx/MainWindowF.java | 90 +++++++++++++++++++ 2 files changed, 94 insertions(+), 1 deletion(-) diff --git a/key.ui.fx/build.gradle b/key.ui.fx/build.gradle index beb496e8d53..06f81d093ab 100644 --- a/key.ui.fx/build.gradle +++ b/key.ui.fx/build.gradle @@ -103,6 +103,8 @@ run { // the demo proof and open the SoundinessDialogF) // * key.fx.verify.javacsettings = any (smalldialogs: run the javac settings provider // self test; the settings dialog is not opened) + // * key.fx.verify.termmenu = any (termmenu: run the headless sequent context-menu self test + // after the demo load; text report on stdout) ["key.fx.driver", "key.fx.theme", "key.fx.demo.sequent", "key.fx.verify.sequent", "key.fx.show", "key.fx.demo.autoprove", "key.fx.verify.tree", "key.fx.demo.autoprove.live", "key.fx.demo.maxsteps", "key.fx.verify.goallist", @@ -115,7 +117,8 @@ run { "key.fx.verify.docking", "key.fx.verify.uicontrol", "key.fx.verify.tacletmatch", "key.fx.verify.joinmerge", "key.fx.verify.joinmerge.maxsteps", "key.fx.verify.lemmaorigin", "key.fx.verify.help", "key.fx.verify.soundiness", "key.fx.verify.profileloading", - "key.fx.verify.profileloadingdialog", "key.fx.verify.javacsettings"] + "key.fx.verify.profileloadingdialog", "key.fx.verify.javacsettings", + "key.fx.verify.termmenu"] .each { name -> def value = System.getProperty(name) if (value != null) { 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 index 61fe88cae8d..326745aa0e7 100644 --- 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 @@ -8,6 +8,7 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -24,6 +25,7 @@ import javafx.scene.control.Alert; import javafx.scene.control.CheckBox; import javafx.scene.control.CheckMenuItem; +import javafx.scene.control.ContextMenu; import javafx.scene.control.Label; import javafx.scene.control.Menu; import javafx.scene.control.MenuBar; @@ -68,6 +70,8 @@ import de.uka.ilkd.key.gui.fx.join.JoinMergeVerifyF; 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; @@ -91,6 +95,8 @@ 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.pp.PosInSequent; +import de.uka.ilkd.key.proof.Goal; import de.uka.ilkd.key.proof.Proof; import de.uka.ilkd.key.proof.ProofEvent; import de.uka.ilkd.key.proof.io.GZipProofSaver; @@ -712,6 +718,12 @@ protected KeYEnvironment call() throws Exception { 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); + } if (System.getProperty("key.fx.demo.autoprove.live") != null) { startLiveAutoMode(env); } @@ -1212,6 +1224,84 @@ private void runUpdateHighlightVerification() { report.endsWith("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)); + } + + /** + * 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 {@link JoinMergeVerifyF}. From 2c7a6e55e8f1f75f0ada7bf611664a35ab1fb720 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Fri, 9 Oct 2026 16:02:25 +0200 Subject: [PATCH 090/281] key.ui.fx: termmenu: wire completeAndApplyTacletMatch to the ported taclet-match dialogs (S4 seam) --- .../gui/fx/WindowUserInterfaceControlF.java | 54 ++++++++++++++----- 1 file changed, 41 insertions(+), 13 deletions(-) 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 index 7a2021bb84d..eee7b1fdbfd 100644 --- 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 @@ -9,6 +9,7 @@ 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; @@ -16,7 +17,11 @@ 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; @@ -67,10 +72,11 @@ * 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}), the taclet-match - * dialogs ({@code TacletMatchDialog}, a stub here), {@code selectProofObligation} + * 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). + * yet). The taclet-match dialogs are wired through {@link #completeAndApplyTacletMatch} + * (termmenu/S4). */ public class WindowUserInterfaceControlF extends DefaultUserInterfaceControl implements RuleCompletionHandler { @@ -199,19 +205,41 @@ public IBuiltInRuleApp completeBuiltInRuleApp(IBuiltInRuleApp app, Goal goal, bo /** * Port of Swing WindowUserInterfaceControl.java:329-338 - * ({@code completeAndApplyTacletMatch}): the Swing original opens the redesigned - * {@code TacletMatchDialog} (or the classic one via - * {@code ViewSettings.isUseClassicTacletDialog} - * — a migration toggle). The FX taclet-match dialog suite is a separate port item; for now - * the completion is reported and skipped (the core then keeps the app unapplied). + * ({@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) { - LOGGER.warn("The taclet match dialog is not yet ported to the JavaFX UI; " - + "the taclet application is skipped ({} model(s))", models.length); - NotificationManagerF.getInstance().notify( - "The taclet match dialog arrives in a later milestone of the key.ui.fx rewrite.", - NotificationManagerF.Kind.WARNING); + // 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) + if (ProofIndependentSettings.DEFAULT_INSTANCE.getViewSettings() + .isUseClassicTacletDialog()) { + new TacletMatchCompletionDialogF(owner, models, goal, services, notationInfo, + getProofControl()); + } else { + new TacletMatchDialogF(owner, models, goal, getProofControl(), services, + notationInfo); + } } // ------------------------------------------------------------------ From 2abf34d77dfe4fc9965dde26c4dc9425331a7e38 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Fri, 9 Oct 2026 16:02:30 +0200 Subject: [PATCH 091/281] key.ui.fx: termmenu: extend tacletmatch self test with the seam-dispatched dialog step (S4) --- .../de/uka/ilkd/key/gui/fx/MainWindowF.java | 7 +- .../fx/tacletmatch/TacletMatchVerifyF.java | 67 +++++++++++++++++-- 2 files changed, 68 insertions(+), 6 deletions(-) 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 index 326745aa0e7..f46ad42233c 100644 --- 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 @@ -704,10 +704,13 @@ protected KeYEnvironment call() throws Exception { UiControlSelfTestF.run(this); } // tacletmatch: run the interactive taclet application self test (dialog render, - // cancel keeps the proof, apply adds to the proof) after the demo load + // 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) { TacletMatchVerifyF.runTacletMatchVerification(env.getLoadedProof(), - env.getProofControl(), stage, mediator.getNotationInfo()); + env.getProofControl(), stage, mediator.getNotationInfo(), + getUserInterfaceControl()); } // lemmaorigin: begin — term labels / origin visualizer / lemma generator self test if (System.getProperty("key.fx.verify.lemmaorigin") != null) { diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/tacletmatch/TacletMatchVerifyF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/tacletmatch/TacletMatchVerifyF.java index 8d3aeada401..c75cfb6c28a 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/tacletmatch/TacletMatchVerifyF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/tacletmatch/TacletMatchVerifyF.java @@ -7,16 +7,19 @@ import java.util.ArrayList; import java.util.List; import javafx.stage.Stage; +import javafx.stage.Window; import de.uka.ilkd.key.control.AbstractProofControl; import de.uka.ilkd.key.control.ProofControl; import de.uka.ilkd.key.control.instantiation_model.TacletInstantiationModel; +import de.uka.ilkd.key.gui.fx.WindowUserInterfaceControlF; import de.uka.ilkd.key.gui.fx.tacletmatch.classic.TacletMatchCompletionDialogF; 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.Proof; import de.uka.ilkd.key.rule.TacletApp; +import de.uka.ilkd.key.settings.ProofIndependentSettings; import org.key_project.logic.PosInTerm; import org.key_project.prover.proof.rulefilter.TacletFilter; @@ -39,6 +42,10 @@ *

      • cancel closes the dialog and leaves the proof unchanged (node count stable);
      • *
      • the classic (table-based) completion dialog renders the instantiation table and * {@code cancelAndClose()} leaves the proof unchanged (port deliverable 2);
      • + *
      • the seam dispatch ({@code WindowUserInterfaceControlF.completeAndApplyTacletMatch}, + * termmenu/S4) opens the ported dialog for the real models + goal — the interactive path the + * sequent-view term menu triggers via {@code selectedTaclet} — and closing it leaves the proof + * unchanged;
      • *
      • apply closes the dialog and adds the application to the proof (node count changes; last, as * it modifies the proof).
      • * @@ -46,7 +53,7 @@ * *

        * The value of {@code key.fx.verify.tacletmatch} selects the mode: {@code 1} (default) runs the - * full four-assertion flow; {@code hold} runs only the render assertion of the redesigned dialog + * full five-assertion flow; {@code hold} runs only the render assertion of the redesigned dialog * and leaves it open for interactive screenshots; {@code hold-classic} does the same for the * classic (table-based) dialog (light/dark theme checks). */ @@ -59,21 +66,25 @@ private TacletMatchVerifyF() {} /** * Runs the verification on the FX thread (called from the demo-load success handler in * MainWindowF). + * + * @param seam the window's {@link WindowUserInterfaceControlF}; the seam dispatch assertion + * (termmenu/S4) exercises {@code completeAndApplyTacletMatch} through it */ public static void runTacletMatchVerification(Proof proof, ProofControl proofControl, - Stage owner, NotationInfo notationInfo) { + Stage owner, NotationInfo notationInfo, WindowUserInterfaceControlF seam) { String mode = System.getProperty("key.fx.verify.tacletmatch", "1").trim(); boolean hold = "hold".equalsIgnoreCase(mode); boolean holdClassic = "hold-classic".equalsIgnoreCase(mode); try { - run(proof, proofControl, owner, notationInfo, hold, holdClassic); + run(proof, proofControl, owner, notationInfo, hold, holdClassic, seam); } catch (Throwable t) { LOGGER.error("tacletmatch verification: FAIL (unexpected error: {})", t, t); } } private static void run(Proof proof, ProofControl proofControl, Stage owner, - NotationInfo notationInfo, boolean hold, boolean holdClassic) { + NotationInfo notationInfo, boolean hold, boolean holdClassic, + WindowUserInterfaceControlF seam) { Services services = proof.getServices(); Goal goal = proof.openEnabledGoals().head(); AbstractProofControl control = (AbstractProofControl) proofControl; @@ -159,6 +170,39 @@ private static void run(Proof proof, ProofControl proofControl, Stage owner, t.toString()); } + // ---- assertion: the seam dispatch (WindowUserInterfaceControlF + // .completeAndApplyTacletMatch, termmenu/S4) opens the ported dialog for the real + // models + goal — the interactive path the sequent-view term menu triggers via + // selectedTaclet — and closing it leaves the proof unchanged. The branch (redesigned + // vs. classic) is chosen by ViewSettings.isUseClassicTacletDialog and printed so the + // runner sees which seam branch was exercised (the dialog constructors show() their + // stage, which is safe on the FX thread here; the "showing" check is the guard). ----- + try { + boolean seamClassic = + ProofIndependentSettings.DEFAULT_INSTANCE.getViewSettings() + .isUseClassicTacletDialog(); + TacletInstantiationModel[] seamModels = + control.completeAndApplyApp(List.of(chosen), goal); + seam.completeAndApplyTacletMatch(seamModels, goal); + Stage seamDialog = findSeamDialog(); + boolean seamOpened = seamDialog != null && seamDialog.isShowing(); + LOGGER.info("tacletmatch verification: seam-dispatch: {} (branch={}, showing={})", + seamOpened ? "PASS" : "FAIL", seamClassic ? "classic" : "redesigned", + seamOpened); + int nodesBeforeSeam = proof.countNodes(); + if (seamDialog != null) { + seamDialog.close(); + } + boolean seamClosed = (seamDialog == null || !seamDialog.isShowing()) + && proof.countNodes() == nodesBeforeSeam; + LOGGER.info("tacletmatch verification: seam-close: {} (node count before={}, " + + "after={})", seamClosed ? "PASS" : "FAIL", nodesBeforeSeam, + proof.countNodes()); + } catch (Throwable t) { + LOGGER.info("tacletmatch verification: seam-dispatch: FAIL (unexpected error: {})", + t.toString()); + } + // ---- assertion 4 (last, it changes the proof): apply closes the dialog and adds the // application to the proof ------------------------------------------------------------ if (chosen.taclet().assumesSequent().isEmpty()) { @@ -185,6 +229,21 @@ private static TacletMatchDialogF openDialog(AbstractProofControl control, Tacle return new TacletMatchDialogF(owner, models, goal, proofControl, services, notationInfo); } + /** + * the dialog window opened by the seam dispatch (the redesigned and the classic dialog + * share the title "Choose Taclet Instantiation", set by the common super constructor + * {@code ApplyTacletDialogF}); {@code null} when the seam did not open one. + */ + private static Stage findSeamDialog() { + for (Window window : Window.getWindows()) { + if (window instanceof Stage stage + && "Choose Taclet Instantiation".equals(stage.getTitle())) { + return stage; + } + } + return null; + } + /** * finds a taclet application on the given goal whose instantiation is incomplete (the condition * under which the Swing term menu routes to the instantiation dialog, From 2c5e3a3a24b6c773a2856314b38dcc8bbed65305 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Fri, 9 Oct 2026 16:02:32 +0200 Subject: [PATCH 092/281] key.ui.fx: termmenu: route load failures through the notification center and register the exception-failure task by default (S4) --- .../de/uka/ilkd/key/gui/fx/MainWindowF.java | 20 +++++++----- .../ExceptionFailureNotificationF.java | 12 ++++--- .../fx/notification/NotificationCenterF.java | 31 ++++++++++--------- .../events/ExceptionFailureEventF.java | 6 ++-- 4 files changed, 39 insertions(+), 30 deletions(-) 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 index f46ad42233c..3481c9ce489 100644 --- 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 @@ -76,6 +76,7 @@ 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.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; @@ -733,19 +734,22 @@ protected KeYEnvironment call() throws Exception { }); loadTask.setOnFailed(event -> { Throwable error = loadTask.getException(); - // notification: TODO-merge wire into WindowUserInterfaceControlF — the seam agent - // routes exceptions through NotificationCenterF.handleNotificationEvent( - // new ExceptionFailureEventF(...)) there (Swing parity: - // IssueDialog.showExceptionDialog) - LOGGER.error((demo ? "Demo proof" : "Proof") + " loading failed", error); + 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); - NotificationManagerF.getInstance() - .notify((demo ? "Demo proof" : "Proof") + " loading failed: " - + error.getMessage(), Kind.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); diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/ExceptionFailureNotificationF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/ExceptionFailureNotificationF.java index 38b99063098..e67f5086484 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/ExceptionFailureNotificationF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/ExceptionFailureNotificationF.java @@ -13,11 +13,13 @@ * ExceptionFailureNotification.java} (Swing action: * {@code ExceptionFailureNotificationDialog} → {@code IssueDialog.showExceptionDialog}). *

        - * TODO-merge (seam agent, branch {@code weigl/ocfx-seam}): exception routing (the Swing - * {@code mediator.notify(new ExceptionFailureEvent(...))} call sites and the exception-dialog - * path) is owned by the exception-seam agent; this task ships with the ERROR-toast action only - * and is not wired into any control construction yet — it is exercised by the - * {@code key.fx.verify.notifications} self-test hook. Runs also during auto mode (Swing parity). + * termmenu/S4: this task is a default registration of {@code NotificationCenterF} + * ({@code setDefaultNotifications}) and receives the exception events fired by the + * application — e.g. the load-failure path {@code MainWindowF.setOnFailed} routes its + * {@code ExceptionFailureEventF} through the center. Deviation from the Swing original, + * which is why the Swing FIXME (double dialog for parser errors) does not apply: the action + * is a toast, not a dialog ({@link ToastActionF}); the IssueDialog stays the primary + * surface of the reporting exception path. Runs also during auto mode (Swing parity). */ public class ExceptionFailureNotificationF extends NotificationTaskF { diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/NotificationCenterF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/NotificationCenterF.java index 7649557acc5..3fd1bd57306 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/NotificationCenterF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/NotificationCenterF.java @@ -38,7 +38,9 @@ * App-level wiring status (parity audit {@code parity-unported-dialogs.md} §10): the proof-closed * and "Automated proof search" triggers are wired through * {@link #afterAutoModeFinished(Proof, boolean)} (called from the FX auto-mode-stop hook); - * the exception routing and the exit/abandon triggers are not ported yet. + * the exception routing (termmenu/S4: {@code ExceptionFailureEventF} dispatch in + * {@code MainWindowF.setOnFailed}, the task registered in {@link #setDefaultNotifications()}) + * is wired; the exit/abandon triggers are not ported yet. */ public final class NotificationCenterF { @@ -77,16 +79,18 @@ public void setDefaultNotifications() { addNotificationTask(new GeneralInformationNotificationF()); addNotificationTask(new AbandonNotificationF()); addNotificationTask(new ExitKeYNotificationF()); - - // FIXME (DS): Obviously, adding ExceptionFailureNotification at this place leads to a - // double appearance of Dialogs in case of a parser error. However, the user is not - // notified in case of an ExceptionFailure occurring *after* the parsing procedure, so - // for instance at an erroneous BuiltInRule application. This is not desirable, since - // then there might be a strange GUI behavior without even a notification. - // (comment ported from NotificationManager.setDefaultNotification) + // termmenu/S4: the exception-failure task is registered by default. This resolves the + // TODO-merge left by the seam agent: MainWindowF.setOnFailed routes load failures + // through handleNotificationEvent(new ExceptionFailureEventF(...)). // - // TODO-merge (seam agent, branch weigl/ocfx-seam): the exception routing decides - // whether/where ExceptionFailureNotificationF is registered. + // The Swing FIXME (ported from NotificationManager.setDefaultNotification) is not + // applicable to the FX port: the Swing ExceptionFailureNotification opened a *dialog* + // (ExceptionFailureNotificationDialog -> IssueDialog.showExceptionDialog), causing a + // double dialog for parser errors (the ProblemLoader branch already surfaces the + // IssueDialog); the FX ExceptionFailureNotificationF only shows an error *toast*, so + // dialog + toast is the intended surface (Swing parity: WindowUserInterfaceControl + // reports parser errors in the IssueDialog while the toast keeps the notification sink). + addNotificationTask(new ExceptionFailureNotificationF()); } /** @@ -333,9 +337,9 @@ public void verifyNotifications(Proof proof) { : "FAIL: neither dialog nor fallback toast shown")); // 4. synthetic exception (Swing parity: ExceptionFailureEvent -> - // ExceptionFailureNotificationDialog; not registered by default, see the FIXME - // in setDefaultNotifications — registered here for the exercise, then removed) - addNotificationTask(new ExceptionFailureNotificationF()); + // ExceptionFailureNotificationDialog; registered by default since termmenu/S4 — + // ExceptionFailureNotificationF is toast-only, see setDefaultNotifications — + // so no add/remove dance needed; the assertion uses the toast-count delta) int beforeError = NotificationManagerF.getInstance().getVisibleToastCount(); handleNotificationEvent(new ExceptionFailureEventF( "Synthetic exception (notification self-test)", @@ -345,7 +349,6 @@ public void verifyNotifications(Proof proof) { + (NotificationManagerF.getInstance().getVisibleToastCount() > beforeError ? "PASS" : "FAIL: error toast not shown")); - removeNotificationTask(NotificationEventIDF.EXCEPTION_CAUSED_FAILURE); String report = String.join("\n", lines); LOGGER.info("Notification verification report:\n{}", report); boolean pass = report.lines().allMatch(line -> line.endsWith("PASS")); diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/events/ExceptionFailureEventF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/events/ExceptionFailureEventF.java index 7b22787d5c4..1aec249b8a9 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/events/ExceptionFailureEventF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/events/ExceptionFailureEventF.java @@ -12,9 +12,9 @@ * Port of the Swing original {@code key.ui/src/main/java/de/uka/ilkd/key/gui/notification/ * events/ExceptionFailureEvent.java}. *

        - * TODO-merge (seam agent, branch {@code weigl/ocfx-seam}): exception routing into this - * framework is owned by the exception-seam agent; do not wire report paths here yet — the - * framework side ({@code ExceptionFailureNotificationF}) is ready to receive the events. + * termmenu/S4: fired by the load-failure path ({@code MainWindowF.setOnFailed}) and the + * notification self-test; the default-registered {@code ExceptionFailureNotificationF} + * surfaces the error toast. */ public class ExceptionFailureEventF extends GeneralFailureEventF { From f7f00af12752fd86834d3820081b0bf67d282db4 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Fri, 9 Oct 2026 17:23:55 +0200 Subject: [PATCH 093/281] key.ui.fx: menu: MP1 Proof menu parity (Abandon Proof, search section, statistics/settings/known-types entries) --- .../de/uka/ilkd/key/gui/fx/MainWindowF.java | 277 +++++++++++++++++- .../key/gui/fx/nodeviews/SequentViewF.java | 14 + .../notification/ProofStatisticsDialogF.java | 89 ++++++ .../fx/proofmanagement/KnownTypesDialogF.java | 85 ++++++ .../fx/settings/ActiveSettingsDialogF.java | 165 +++++++++++ 5 files changed, 629 insertions(+), 1 deletion(-) create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/ProofStatisticsDialogF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/proofmanagement/KnownTypesDialogF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/ActiveSettingsDialogF.java 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 index 3481c9ce489..3d371d21279 100644 --- 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 @@ -23,6 +23,7 @@ import javafx.scene.Node; import javafx.scene.Scene; import javafx.scene.control.Alert; +import javafx.scene.control.ButtonType; import javafx.scene.control.CheckBox; import javafx.scene.control.CheckMenuItem; import javafx.scene.control.ContextMenu; @@ -76,6 +77,7 @@ 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; @@ -83,10 +85,12 @@ 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.soundiness.SoundinessAnalyzer; import de.uka.ilkd.key.gui.fx.soundiness.SoundinessDialogF; @@ -728,6 +732,12 @@ protected KeYEnvironment call() throws Exception { if (System.getProperty("key.fx.verify.termmenu") != null) { runTermMenuVerification(env); } + // menu: MP1 — menu parity self test (key.fx.verify.menuparity): walks the built + // menu bar and asserts the Proof menu entries against the Swing createProofMenu + // table; the marker line must end with PASS + if (System.getProperty("key.fx.verify.menuparity") != null) { + verifyMenuParity(); + } if (System.getProperty("key.fx.demo.autoprove.live") != null) { startLiveAutoMode(env); } @@ -1649,14 +1659,279 @@ private Menu buildProofMenu() { mediator::stopAutoMode); stopAuto.disableProperty().bind(mediator.autoModeRunningProperty().not()); automation.getItems().addAll(startAuto, stopAuto); + // 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()); proof.getItems().addAll(automation, new SeparatorMenuItem(), menuItem("Goal Back", "de.uka.ilkd.key.gui.actions.GoalBackAction", IconFactoryF.Key.GOAL_BACK, mediator::goalBack), menuItem("Prune Proof", "de.uka.ilkd.key.gui.actions.PruneProofAction", - IconFactoryF.Key.PRUNE, mediator::pruneProof)); + IconFactoryF.Key.PRUNE, mediator::pruneProof), + abandonProof, new SeparatorMenuItem(), + searchInTree, searchInSequent, searchNext, searchPrevious, searchMode, + new SeparatorMenuItem(), + usedContracts, activeSettings, proofStatistics, knownTypes); return proof; } + // ------------------------------------------------------------------ + // proof menu actions (menu: MP1 — Swing MainWindow.createProofMenu :1082-1142) + // ------------------------------------------------------------------ + + /** + * 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 self test of the Proof menu (system property {@code key.fx.verify.menuparity}, + * run after a proof load like the other verify hooks): builds the menu bar and walks the + * Proof menu, asserting the presence of the entries of {@link #buildProofMenu()} in the + * Swing order (MainWindow.createProofMenu :1082-1142). Table-driven so MP2-MP5 can extend + * it with the other menus later. + */ + private void verifyMenuParity() { + String report = verifyMenuParityReport(); + LOGGER.info("Menu parity verification (MP1 Proof): {}", report); + NotificationManagerF.getInstance() + .notify("Menu parity verification (MP1 Proof): " + report, + report.endsWith("PASS") ? Kind.INFO : Kind.ERROR); + } + + /** + * menu: builds the {@link #buildMenuBar() menu bar} and checks the Proof menu entries. A + * table holds the expected labels; entries may be leaf items, plain separators (the + * {@code "---"} row) or submenu names ({@code "Search Mode"}) whose own children are + * checked recursively. + * + * @return {@code "PASS - items, found: "} or {@code "FAIL - missing: "} + */ + String verifyMenuParityReport() { + MenuBar menuBar = buildMenuBar(); + Menu proof = menuBar.getMenus().stream().filter(m -> "Proof".equals(m.getText())) + .findFirst().orElse(null); + if (proof == null) { + return "FAIL - missing: "; + } + String[][] expected = { + { "Automation", "Start Automatic Proof", "Stop Automatic Proof" }, + { "Goal Back" }, + { "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" }, + }; + List present = new ArrayList<>(); + List missing = new ArrayList<>(); + List remaining = new ArrayList<>(proof.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) + 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() { Menu options = new Menu("Options"); options.getItems().addAll( diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentViewF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentViewF.java index c1c028448b4..962ee999eae 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentViewF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentViewF.java @@ -1084,6 +1084,20 @@ public void searchPrevious() { scrollToMatch(searchResultPos); } + /** + * menu: selects the search mode in the search bar's combo (Swing + * {@code SequentViewSearchBar.setSearchMode}, SequentViewSearchBar.java:82-84: + * {@code searchModeBox.setSelectedItem(mode)}). The combo's own listener applies the mode + * ({@link #applySearchMode(SearchMode)}) and re-runs the search, so selecting here is + * sufficient. Entry point of the "Proof > Search Mode" submenu (Swing + * {@code SearchModeChangeAction}, MainWindow.createProofMenu :1125-1131). + * + * @param mode the search mode to select (Highlight/Hide/Regroup), must not be {@code null} + */ + public void setSearchMode(SearchMode mode) { + searchModeBox.getSelectionModel().select(mode); + } + /** * Scrolls the current match into view (Swing {@code setCaretPosition(foundAt)}): the vertical * position of the match's shape is centered in the viewport. diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/ProofStatisticsDialogF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/ProofStatisticsDialogF.java new file mode 100644 index 00000000000..dda53018300 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/ProofStatisticsDialogF.java @@ -0,0 +1,89 @@ +/* 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.notification; + +import java.util.List; +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.ScrollPane; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; +import javafx.stage.Window; + +import de.uka.ilkd.key.proof.Proof; +import de.uka.ilkd.key.proof.Statistics; + +import org.key_project.util.collection.Pair; + +/** + * menu: non-modal window showing the statistics of the selected proof (Swing + * {@code ShowProofStatistics.Window}, ShowProofStatistics.java:234-410): the open-goal count plus + * the {@link Statistics#getSummary()} pairs, exactly the layout built by the FX + * {@link ProofClosedDialogF} ({@code ProofClosedJTextPaneDisplay} opens the same Swing + * "Proof Statistics" window for closed proofs). + *

        + * The plain VBox/ScrollPane layout replaces the Swing HTML table; no export buttons for now + * (the CSV/HTML export of {@code ShowProofStatistics.Window} is not ported). + */ +public final class ProofStatisticsDialogF extends javafx.stage.Stage { + + private ProofStatisticsDialogF(Window owner, Proof proof) { + setTitle("Proof Statistics"); + if (owner != null) { + initOwner(owner); + } + + Statistics stats = proof.getStatistics(); + int openGoals = proof.openGoals().size(); + + VBox statsBox = new VBox(4); + statsBox.getChildren().add(new Label("Open goals: " + openGoals)); + // menu: Swing getHTMLStatisticsMessage iterates the summary; a null summary is rendered + // gracefully (Swing would NPE — defensive here) + List> summary = stats == null ? null : stats.getSummary(); + if (summary != null) { + for (Pair entry : summary) { + if ("".equals(entry.second)) { + statsBox.getChildren().add(new Label(entry.first)); + } else { + statsBox.getChildren().add(new Label(entry.first + ": " + entry.second)); + } + } + } + + Button close = new Button("Close"); + close.setOnAction(e -> hide()); + VBox.setVgrow(close, Priority.NEVER); + + ScrollPane scroll = new ScrollPane(statsBox); + scroll.setFitToWidth(true); + scroll.setPrefViewportHeight(Math.min(240, 40 + 22 * statsBox.getChildren().size())); + + VBox root = new VBox(12, new Label("Proof " + proof.name()), scroll, close); + root.getStyleClass().add("notification-dialog"); + root.setPadding(new Insets(14)); + root.setAlignment(Pos.CENTER_LEFT); + + Scene scene = new Scene(root, 380, Math.min(480, 180 + 22 * statsBox.getChildren().size())); + setScene(scene); + } + + /** + * Shows the statistics window for the given proof (Swing + * {@code ShowProofStatistics.actionPerformed}, ShowProofStatistics.java:69-78: non-modal + * {@code Window}, like the proof-closed dialog). + * + * @param owner the owner window (the main window stage); may be {@code null} + * @param proof the selected proof, must not be {@code null} (the menu item is disabled + * without a proof) + */ + public static void show(Window owner, Proof proof) { + ProofStatisticsDialogF dialog = new ProofStatisticsDialogF(owner, proof); + dialog.show(); + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/proofmanagement/KnownTypesDialogF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/proofmanagement/KnownTypesDialogF.java new file mode 100644 index 00000000000..f529aa0bb05 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/proofmanagement/KnownTypesDialogF.java @@ -0,0 +1,85 @@ +/* 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.proofmanagement; + +import javafx.geometry.Insets; +import javafx.scene.Scene; +import javafx.scene.control.Button; +import javafx.scene.control.ButtonBar; +import javafx.scene.control.ScrollPane; +import javafx.scene.control.Tab; +import javafx.scene.control.TabPane; +import javafx.scene.input.KeyCode; +import javafx.scene.layout.BorderPane; +import javafx.stage.Modality; +import javafx.stage.Window; + +import de.uka.ilkd.key.gui.fx.theme.ThemeManager; +import de.uka.ilkd.key.proof.Proof; + +/** + * menu: modal dialog showing the type hierarchy known to a proof (Swing + * {@code ShowKnownTypesAction.showTypeHierarchy}, ShowKnownTypesAction.java:47-83): a "Package + * view" tab with the {@link ClassTreeF} of the proof's services in a scroll pane and an OK button + * that closes the dialog (default button, Escape closes too). Window title "Known types for this + * proof", 300×400 like the Swing original, centered on the owner. + */ +public final class KnownTypesDialogF extends javafx.stage.Stage { + + private KnownTypesDialogF(Window owner, Proof proof) { + setTitle("Known types for this proof"); + initOwner(owner); + if (owner != null) { + // Swing: new JDialog(mainWindow, title, true) — modal to the owner window + initModality(Modality.WINDOW_MODAL); + } + + TabPane tabbedPane = new TabPane(); + ScrollPane scroll = new ScrollPane(new ClassTreeF(false, false, proof.getServices())); + scroll.setFitToWidth(true); + tabbedPane.getTabs().add(new Tab("Package view", scroll)); + + Button okButton = new Button("OK"); + okButton.setOnAction(e -> close()); + ButtonBar.setButtonData(okButton, ButtonBar.ButtonData.OK_DONE); + okButton.setDefaultButton(true); + ButtonBar buttonBar = new ButtonBar(); + buttonBar.getButtons().add(okButton); + BorderPane.setMargin(buttonBar, new Insets(10)); + + BorderPane root = new BorderPane(tabbedPane, null, null, buttonBar, null); + Scene scene = new Scene(root, 300, 400); + ThemeManager.getInstance().manage(scene); + // Swing: GuiUtilities.attachClickOnEscListener(okButton) closes with Escape + scene.setOnKeyPressed(e -> { + if (e.getCode() == KeyCode.ESCAPE) { + close(); + e.consume(); + } + }); + setScene(scene); + } + + /** + * Opens the known-types dialog for the given proof (Swing + * {@code ShowKnownTypesAction.showTypeHierarchy}, ShowKnownTypesAction.java:47-83; modal, 300 + * × 400, centered on the owner). + * + * @param owner the owner window (the main window stage); may be {@code null} + * @param proof the selected proof, must not be {@code null} (the menu item is disabled + * without a proof) + */ + public static void show(Window owner, Proof proof) { + KnownTypesDialogF dialog = new KnownTypesDialogF(owner, proof); + if (owner != null) { + dialog.setOnShown(e -> { + // center over the owner like Swing setLocationRelativeTo(mainWindow) + dialog.setX(owner.getX() + owner.getWidth() / 2 - dialog.getWidth() / 2); + dialog.setY(owner.getY() + owner.getHeight() / 2 - dialog.getHeight() / 2); + }); + } + dialog.showAndWait(); + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/ActiveSettingsDialogF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/ActiveSettingsDialogF.java new file mode 100644 index 00000000000..643f4cc6184 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/ActiveSettingsDialogF.java @@ -0,0 +1,165 @@ +/* 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.settings; + +import java.util.Collection; +import java.util.Map; +import javafx.geometry.Insets; +import javafx.scene.Scene; +import javafx.scene.control.Button; +import javafx.scene.control.ButtonBar; +import javafx.scene.control.Label; +import javafx.scene.control.ScrollPane; +import javafx.scene.control.TreeItem; +import javafx.scene.control.TreeView; +import javafx.scene.input.KeyCode; +import javafx.scene.layout.BorderPane; +import javafx.stage.Modality; +import javafx.stage.Window; + +import de.uka.ilkd.key.gui.fx.theme.ThemeManager; +import de.uka.ilkd.key.proof.Proof; +import de.uka.ilkd.key.settings.Configuration; +import de.uka.ilkd.key.settings.ProofIndependentSettings; + +/** + * menu: dialog showing the active settings of the selected proof (Swing + * {@code ShowActiveSettingsAction}, a debugging window over + * {@code SettingsTreeModel(proof.getSettings(), ProofIndependentSettings.DEFAULT_INSTANCE)}). + *

        + * This is a coarse port of the Swing tree: the {@link TreeView} renders the proof-dependent + * settings ({@code proof.getSettings().asConfiguration()}) and the proof-independent settings + * ({@code ProofIndependentSettings.asConfiguration()}) as a sectioned configuration tree, plus + * the announce line with the proof name. Title and structure mirror the Swing + * {@code ViewSettingsDialog} ("All active settings"). + *

        + * // menu: KNOWN-DEFERRED — Swing {@code SettingsTreeModel} renders each section with + * OptionContentNode components (real Swing editors per option); the FX port shows the plain + * key: value leaves instead. The "Taclet Options" deep-link of + * {@code ShowActiveSettingsAction.showAndFocusTacletOptions} is not ported either (no Swing + * call site uses it besides the old menu entry it replaced). + */ +public final class ActiveSettingsDialogF extends javafx.stage.Stage { + + private ActiveSettingsDialogF(Window owner, Proof proof) { + setTitle("All active settings"); + initOwner(owner); + if (owner != null) { + // Swing: JDialog(owner frame, ...) — window-modal + initModality(Modality.WINDOW_MODAL); + } + + // Swing announce label (ShowActiveSettingsAction.java:78-85) + Label announce = new Label("This shows the active settings for the proof: " + proof.name() + + ".\nTo change settings for future proofs, use Options > Show Settings."); + announce.setWrapText(true); + announce.setPadding(new Insets(5)); + + TreeView tree = new TreeView<>(settingsTree(proof)); + tree.setShowRoot(true); + tree.setPrefSize(420, 320); + + Button okButton = new Button("OK"); + okButton.setOnAction(e -> close()); + ButtonBar.setButtonData(okButton, ButtonBar.ButtonData.OK_DONE); + okButton.setDefaultButton(true); + ButtonBar buttonBar = new ButtonBar(); + buttonBar.getButtons().add(okButton); + BorderPane.setMargin(buttonBar, new Insets(10)); + + ScrollPane scroll = new ScrollPane(tree); + scroll.setFitToWidth(true); + + BorderPane root = new BorderPane(); + root.setTop(announce); + root.setCenter(scroll); + root.setBottom(buttonBar); + Scene scene = new Scene(root, 520, 460); + ThemeManager.getInstance().manage(scene); + // Swing: DISPOSE_ON_CLOSE + Escape closes (ShowActiveSettingsAction.java:95-96) + scene.setOnKeyPressed(e -> { + if (e.getCode() == KeyCode.ESCAPE) { + close(); + e.consume(); + } + }); + setScene(scene); + } + + /** + * Builds the settings tree of the dialog (Swing {@code SettingsTreeModel.generateTree}): the + * root "All Settings" holds the proof-dependent and the proof-independent sections, each a + * sorted configuration tree like {@code SettingsTreeModel.configurationTable}. + */ + private static TreeItem settingsTree(Proof proof) { + TreeItem root = new TreeItem<>("All Settings"); + TreeItem proofSection = new TreeItem<>("Proof Settings"); + proofSection.getChildren() + .addAll(configurationTree(proof.getSettings().asConfiguration())); + // menu: KNOWN-DEFERRED — Swing's proof section renders an introductory + // "These are the proof dependent settings." OptionContentNode; the plain tree carries + // the configuration leaves directly. + root.getChildren().add(proofSection); + TreeItem independentSection = new TreeItem<>("Proof-Independent Settings"); + independentSection.getChildren() + .addAll(configurationTree(ProofIndependentSettings.DEFAULT_INSTANCE + .asConfiguration())); + root.getChildren().add(independentSection); + root.setExpanded(true); + proofSection.setExpanded(true); + return root; + } + + /** + * One tree level per configuration section, values as {@code key: value} leaves (Swing + * {@code SettingsTreeModel.configurationTable}, SettingsTreeModel.java:93-120). + */ + private static java.util.List> configurationTree(Configuration cfg) { + java.util.List> items = new java.util.ArrayList<>(); + for (String name : cfg.keys().stream().sorted().toList()) { + Object value = cfg.get(name); + if (value == null) { + items.add(new TreeItem<>(" ")); + } else if (value instanceof Configuration nested) { + TreeItem node = new TreeItem<>(name); + node.getChildren().addAll(configurationTree(nested)); + items.add(node); + } else if (value instanceof Collection col) { + TreeItem node = new TreeItem<>(name); + col.forEach(e -> node.getChildren().add(new TreeItem<>(String.valueOf(e)))); + items.add(node); + } else if (value instanceof Map map) { + TreeItem node = new TreeItem<>(name); + map.forEach((k, v) -> node.getChildren() + .add(new TreeItem<>(k + ": " + v))); + items.add(node); + } else { + items.add(new TreeItem<>(name + ": " + value)); + } + } + return items; + } + + /** + * Opens the active-settings dialog for the given proof (Swing + * {@code ShowActiveSettingsAction.actionPerformed} → {@code ViewSettingsDialog}, + * ShowActiveSettingsAction.java:32-47; window-modal, centered on the owner). + * + * @param owner the owner window (the main window stage); may be {@code null} + * @param proof the selected proof, must not be {@code null} (the menu item is disabled + * without a proof) + */ + public static void show(Window owner, Proof proof) { + ActiveSettingsDialogF dialog = new ActiveSettingsDialogF(owner, proof); + if (owner != null) { + dialog.setOnShown(e -> { + // center over the owner like Swing setLocationRelativeTo(mainWindow) + dialog.setX(owner.getX() + owner.getWidth() / 2 - dialog.getWidth() / 2); + dialog.setY(owner.getY() + owner.getHeight() / 2 - dialog.getHeight() / 2); + }); + } + dialog.showAndWait(); + } +} From 5dc3c3f9bf142f68045910de67f170f0f12e98a8 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Fri, 9 Oct 2026 17:25:06 +0200 Subject: [PATCH 094/281] chore: ignore the .hermes/ tool cache directory --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index b54d0ca8b9c..05a52134ad4 100644 --- a/.gitignore +++ b/.gitignore @@ -78,3 +78,6 @@ checkstyle-diff.txt # Java Flight Recorder output (e.g. runJfrProbe default mt-jfr-probe.jfr) *.jfr + +# Hermes tool cache (agent/tooling state; never version-controlled) +.hermes/ From 7b9ed3a0ea4d1aff77a8daa394a5f97fe2af1d9e Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Fri, 9 Oct 2026 17:28:22 +0200 Subject: [PATCH 095/281] key.ui.fx: menu: MP2 Automation submenu parity (4 proof-macro entries) --- .../de/uka/ilkd/key/gui/fx/MainWindowF.java | 64 +++++++++++++++++-- 1 file changed, 60 insertions(+), 4 deletions(-) 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 index 3d371d21279..eb3c1bebc42 100644 --- 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 @@ -100,6 +100,11 @@ 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.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.pp.PosInSequent; import de.uka.ilkd.key.proof.Goal; import de.uka.ilkd.key.proof.Proof; @@ -1658,7 +1663,38 @@ private Menu buildProofMenu() { MenuItem stopAuto = menuItem("Stop Automatic Proof", IconFactoryF.Key.AUTO_MODE_STOP, mediator::stopAutoMode); stopAuto.disableProperty().bind(mediator.autoModeRunningProperty().not()); - automation.getItems().addAll(startAuto, 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). + MenuItem defaultAuto = menuItem(new DefaultAutoMacro().getName(), + "de.uka.ilkd.key.macros.DefaultAutoMacro", null, + () -> runMacro(new DefaultAutoMacro())); + defaultAuto.disableProperty().bind(proofLoaded.not()); + MenuItem structuredAuto = menuItem(new FullAutoPilotProofMacro().getName(), + "de.uka.ilkd.key.macros.FullAutoPilotProofMacro", null, + () -> runMacro(new FullAutoPilotProofMacro())); + structuredAuto.disableProperty().bind(proofLoaded.not()); + MenuItem prepareAuto = menuItem(new AutoPilotPrepareProofMacro().getName(), + "de.uka.ilkd.key.macros.AutoPilotPrepareProofMacro", null, + () -> runMacro(new AutoPilotPrepareProofMacro())); + prepareAuto.disableProperty().bind(proofLoaded.not()); + MenuItem scriptAuto = menuItem(new ScriptAwareMacro().getName(), + "de.uka.ilkd.key.macros.ScriptAwareMacro", null, + () -> runMacro(new ScriptAwareMacro())); + scriptAuto.disableProperty().bind(proofLoaded.not()); + automation.getItems().addAll(startAuto, stopAuto, + defaultAuto, structuredAuto, prepareAuto, scriptAuto); // 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 @@ -1743,6 +1779,23 @@ abandonProof, new SeparatorMenuItem(), // 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); + } + } + /** * menu: abandons the selected proof (Swing {@code AbandonTaskAction.actionPerformed}, * AbandonTaskAction.java:33-46): asks for confirmation first (Swing @@ -1822,10 +1875,11 @@ private void showKnownTypes() { } /** - * menu: MP1 self test of the Proof menu (system property {@code key.fx.verify.menuparity}, + * menu: MP1/MP2 self test of the Proof menu (system property {@code key.fx.verify.menuparity}, * run after a proof load like the other verify hooks): builds the menu bar and walks the * Proof menu, asserting the presence of the entries of {@link #buildProofMenu()} in the - * Swing order (MainWindow.createProofMenu :1082-1142). Table-driven so MP2-MP5 can extend + * Swing order (MainWindow.createProofMenu :1082-1142, and the Automation submenu entries of + * MainWindow.createAutomationActions :814-827 since MP2). Table-driven so MP3-MP5 can extend * it with the other menus later. */ private void verifyMenuParity() { @@ -1852,7 +1906,9 @@ String verifyMenuParityReport() { return "FAIL - missing: "; } String[][] expected = { - { "Automation", "Start Automatic Proof", "Stop Automatic Proof" }, + { "Automation", "Start Automatic Proof", "Stop Automatic Proof", "Full Automation", + "Structured Automation", "Structured Automation (Prep. Only)", + "Script-aware Auto" }, { "Goal Back" }, { "Prune Proof" }, { "Abandon Proof" }, From 8e9f6de41215134504dc06a7240d3841afdc1421 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Fri, 9 Oct 2026 17:35:38 +0200 Subject: [PATCH 096/281] =?UTF-8?q?key.ui.fx:=20menu:=20MP3a=20View=20menu?= =?UTF-8?q?=20parity=20=E2=80=94=20wire=20Pretty=20Print=20+=20Unicode,=20?= =?UTF-8?q?tooltip=20toggles,=20ToolTip=20Options=20dialog?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../de/uka/ilkd/key/gui/fx/MainWindowF.java | 118 ++++++++++++++- .../key/gui/fx/goallist/GoalListViewF.java | 36 ++++- .../key/gui/fx/nodeviews/SequentViewF.java | 37 +++++ .../key/gui/fx/prooftree/ProofTreeViewF.java | 25 +++- .../fx/settings/ToolTipOptionsDialogF.java | 135 ++++++++++++++++++ 5 files changed, 341 insertions(+), 10 deletions(-) create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/ToolTipOptionsDialogF.java 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 index eb3c1bebc42..feece72198b 100644 --- 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 @@ -92,6 +92,7 @@ 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.ToolTipOptionsDialogF; 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; @@ -105,6 +106,7 @@ 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.pp.NotationInfo; import de.uka.ilkd.key.pp.PosInSequent; import de.uka.ilkd.key.proof.Goal; import de.uka.ilkd.key.proof.Proof; @@ -1583,14 +1585,82 @@ private Menu buildFileMenu() { private Menu buildViewMenu() { Menu view = new Menu("View"); - // placeholder toggles; wired to the real views in M2 + // 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"); @@ -1614,6 +1684,12 @@ private Menu buildViewMenu() { menuItem("Decrease", "de.uka.ilkd.key.gui.actions.DecreaseFontSizeAction", IconFactoryF.Key.MINUS, () -> 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 @@ -1623,8 +1699,18 @@ private Menu buildViewMenu() { javafx.scene.control.MenuItem soundinessItem = menuItem("Show Soundiness Report", "de.uka.ilkd.key.gui.actions.ShowSoundinessAction", this::showSoundinessReport); - view.getItems().addAll(prettyPrint, unicode, syntaxHighlighting, new SeparatorMenuItem(), - themeMenu, fontSize, new SeparatorMenuItem(), + view.getItems().addAll(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(), @@ -1636,15 +1722,33 @@ themeMenu, fontSize, new SeparatorMenuItem(), menuItem("Log View", "de.uka.ilkd.key.gui.actions.LogViewAction", this::showLogView), soundinessItem); - // lemmaorigin: begin — term labels + origin tracking view controls (Swing TermLabelMenu / - // HidePackagePrefixToggleAction / OriginTermLabelsExt MainMenu items) - view.getItems().addAll(OriginLabelsF.install(this)); - // lemmaorigin: end soundinessItem.disableProperty() .bind(mediator.autoModeRunningProperty().or(proofLoaded.not())); return view; } + /** + * 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}). diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/goallist/GoalListViewF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/goallist/GoalListViewF.java index 0c5599051ec..d49d683ddcd 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/goallist/GoalListViewF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/goallist/GoalListViewF.java @@ -30,6 +30,7 @@ import de.uka.ilkd.key.gui.fx.configuration.ConfigF; import de.uka.ilkd.key.gui.fx.fonticons.FontAwesomeSolid; import de.uka.ilkd.key.gui.fx.fonticons.IconFactoryF; +import de.uka.ilkd.key.java.Services; import de.uka.ilkd.key.logic.label.TermLabel; import de.uka.ilkd.key.pp.NotationInfo; import de.uka.ilkd.key.pp.SequentViewLogicPrinter; @@ -38,6 +39,8 @@ import de.uka.ilkd.key.proof.GoalListener; import de.uka.ilkd.key.proof.Node; import de.uka.ilkd.key.proof.Proof; +import de.uka.ilkd.key.settings.ProofIndependentSettings; +import de.uka.ilkd.key.settings.ViewSettings; import org.key_project.logic.Name; import org.key_project.prover.sequent.SequentChangeInfo; @@ -416,6 +419,22 @@ private void refreshAfterGoalStateChange() { } } + /** + * menu: MP3a — re-renders the goal rows after the View-menu Pretty Print / Unicode Symbols + * toggles changed the notation (Swing {@code MainWindow.makePrettyView}, + * MainWindow.java:954-959 re-prints the sequent views through the refreshed shared + * NotationInfo). The cached one-line sequent texts are flushed (they were printed with the + * previous notation) and the rows re-set from the proof so every cell recomputes its sequent + * text; the selection is kept like {@link #refreshAfterGoalStateChange()}. + */ + public void refreshPrettyView() { + if (proof == null || proof.isDisposed()) { + return; + } + sequentTextCache.clear(); + refreshAfterGoalStateChange(); + } + /** * @return the one-line printed sequent text of the goal (truncated, no term labels), cached * per goal node @@ -429,7 +448,7 @@ private String sequentText(Goal goal) { String res; try { SequentViewLogicPrinter printer = - SequentViewLogicPrinter.purePrinter(new NotationInfo(), + SequentViewLogicPrinter.purePrinter(notationInfo(node.proof().getServices()), node.proof().getServices(), NO_VISIBLE_TERM_LABELS); printer.setMaxChar(MAX_DISPLAYED_SEQUENT_LENGTH); printer.printSequent(goal.sequent()); @@ -446,6 +465,21 @@ private String sequentText(Goal goal) { return res; } + /** + * menu: MP3a — a {@link NotationInfo} refreshed with the current view settings (Swing + * {@code MainWindow.makePrettyView} refreshes the shared NotationInfo against the services + * before re-printing, MainWindow.java:954-959; the goal list builds its own NotationInfo per + * print, so the refresh happens here with {@code (isUsePretty(), isUseUnicode(), + * isHidePackagePrefix())}, NotationInfo.java:425-438). + */ + private static NotationInfo notationInfo(Services services) { + ViewSettings viewSettings = ProofIndependentSettings.DEFAULT_INSTANCE.getViewSettings(); + NotationInfo notationInfo = new NotationInfo(); + notationInfo.refresh(services, viewSettings.isUsePretty(), viewSettings.isUseUnicode(), + viewSettings.isHidePackagePrefix()); + return notationInfo; + } + /** * @return the marker text of the goal for the cell tooltip, the text analogue of the icon * choice of the Swing {@code IconCellRenderer} (linked, key-hole for automatic, diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentViewF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentViewF.java index 962ee999eae..95711d64f26 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentViewF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentViewF.java @@ -75,6 +75,7 @@ import de.uka.ilkd.key.proof.Node; import de.uka.ilkd.key.proof.Proof; import de.uka.ilkd.key.settings.ProofIndependentSettings; +import de.uka.ilkd.key.settings.ViewSettings; import org.key_project.logic.Name; import org.key_project.logic.Term; @@ -513,6 +514,38 @@ public Proof getProof() { return proof; } + /** + * menu: MP3a — re-creates the printer with a {@link NotationInfo} refreshed from the current + * view settings, so the View-menu Pretty Print / Unicode Symbols toggles change the printed + * symbols (Swing {@code MainWindow.makePrettyView}, MainWindow.java:954-959: refresh the + * mediator's shared NotationInfo against the services and re-display the sequent + * {@code SwingUtilities.invokeLater(this::updateSequentView)}). The FX sequent view builds + * its own {@code NotationInfo} per proof ({@link #display(Node)} — it does not use the + * mediator's shared instance yet), so this mirrors the printer rebuild of + * {@link #setVisibleTermLabels(VisibleTermLabels)} with the settings passed to + * {@code NotationInfo.refresh(Services, boolean, boolean, boolean)} + * (NotationInfo.java:425-438): {@code (isUsePretty(), isUseUnicode(), isHidePackagePrefix())} + * exactly like Swing's {@code ViewSettings}-driven refresh, keeping the search filter re-bound + * to the new printer and re-printing the current node. + */ + public void refreshPrettyView() { + if (printer == null || selectedNode == null) { + return; + } + ViewSettings viewSettings = ProofIndependentSettings.DEFAULT_INSTANCE.getViewSettings(); + NotationInfo notationInfo = new NotationInfo(); + notationInfo.refresh(selectedNode.proof().getServices(), viewSettings.isUsePretty(), + viewSettings.isUseUnicode(), viewSettings.isHidePackagePrefix()); + printer = SequentViewLogicPrinter.positionPrinter(notationInfo, + selectedNode.proof().getServices(), visibleTermLabels); + if (filter instanceof SearchSequentPrintFilter searchFilter) { + // the search filters print single formulas through the view's printer (Swing + // SequentViewSearchBar.search refreshes it the same way) + searchFilter.setLogicPrinter(printer); + } + printSequent(); + } + /** * Re-prints the sequent of the currently selected node. */ @@ -820,6 +853,10 @@ private void showHoverTooltip() { /** * The tooltip text for the given position (Swing {@code SequentView.getToolTipText} without * the HTML markup and without the GUI extension strings, which have no FX counterpart yet). + * menu: MP3a — the {@code isShowSequentViewTooltips()} gate is driven by the View menu "Show + * Tooltips in Sequent View" toggle (Swing {@code ToggleSequentViewTooltipAction}, NAME = + * "Show Tooltips in Sequent View"); {@link #updateHoverTooltip} hides the tooltip whenever + * this method returns the empty string, so no further gating is needed at the show site. */ private String getTooltipText(PosInSequent pos) { if (!ProofIndependentSettings.DEFAULT_INSTANCE.getViewSettings() diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/prooftree/ProofTreeViewF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/prooftree/ProofTreeViewF.java index 93fc765eb04..ded139ea879 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/prooftree/ProofTreeViewF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/prooftree/ProofTreeViewF.java @@ -47,6 +47,7 @@ import de.uka.ilkd.key.proof.ProofTreeListener; import de.uka.ilkd.key.settings.ProofIndependentSettings; +import org.key_project.prover.rules.RuleApp; import org.key_project.util.collection.ImmutableList; import org.key_project.util.javafx.FxUtil; @@ -1290,7 +1291,20 @@ protected void updateItem(Entry item, boolean empty) { if (matched) { getStyleClass().add("proof-tree-match"); } - setTooltip(new Tooltip(tooltipText(item))); + // menu: MP3a — proof-tree tooltips are gated by the shared ViewSettings flag "Show + // Tooltips in Proof Tree" (Swing ProofTreeView.getToolTipText, + // ProofTreeView.java:195-217 renders no tooltip unless isShowProofTreeTooltips()). + // The cell re-consults the flag on every updateItem; the View-menu toggle triggers a + // refresh() (MainWindowF.buildViewMenu) so the change applies immediately. Minimal + // fidelity: the Swing renderer builds a rich styled tooltip (rule name, position in + // occurrence, notes, rendered by ProofTreeView.renderTooltip :1202-1219); the FX cell + // shows the node/rule name only. + if (ProofIndependentSettings.DEFAULT_INSTANCE.getViewSettings() + .isShowProofTreeTooltips()) { + setTooltip(new Tooltip(tooltipText(item))); + } else { + setTooltip(null); + } } private String styleClassOf(Entry item) { @@ -1315,7 +1329,14 @@ private String tooltipText(Entry item) { if (item.isBranch()) { return "Branch: " + item.branchLabel; } - return "Node " + item.node.serialNr(); + // menu: MP3a — the node's applied rule name (Swing rule-application nodes show the + // applied rule, ProofTreeView.java:1389), falling back to the plain node serial. + Node node = item.node; + RuleApp appliedRule = node.getAppliedRuleApp(); + if (appliedRule != null) { + return "Node " + node.serialNr() + ": " + appliedRule.rule().name(); + } + return "Node " + node.serialNr(); } } } diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/ToolTipOptionsDialogF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/ToolTipOptionsDialogF.java new file mode 100644 index 00000000000..dac56b28adb --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/ToolTipOptionsDialogF.java @@ -0,0 +1,135 @@ +/* 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.settings; + +import javafx.geometry.Insets; +import javafx.scene.Scene; +import javafx.scene.control.Button; +import javafx.scene.control.ButtonBar; +import javafx.scene.control.CheckBox; +import javafx.scene.control.Label; +import javafx.scene.control.TextField; +import javafx.scene.control.TextFormatter; +import javafx.scene.input.KeyCode; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.HBox; +import javafx.stage.Modality; +import javafx.stage.Window; + +import de.uka.ilkd.key.gui.fx.theme.ThemeManager; +import de.uka.ilkd.key.settings.ProofIndependentSettings; +import de.uka.ilkd.key.settings.ViewSettings; + +/** + * menu: MP3a — JavaFX port of the Swing {@code ViewSelector} + * (key.ui/.../gui/configuration/ViewSelector.java), opened by the View>ToolTip Options… item + * (Swing {@code ToolTipOptionsAction.actionPerformed} → {@code ViewSelector.mainWindow}, + * ToolTipOptionsAction.java:26). The dialog edits the tooltip options of the + * {@linkplain ViewSettings proof-independent view settings}: + *

          + *
        • the maximum line count of the tooltips of applicable rules with schema-variable + * instantiations (Swing {@code NumberInputField}, digits only; on parse failure OK resets the + * value to the previously stored one instead of applying garbage),
        • + *
        • "show uninstantiated taclet" (Swing {@code showUninstantiatedTacletCB}, + * {@code ViewSettings.getShowUninstantiatedTaclet}),
        • + *
        • "pretty-print whole Taclet" (Swing {@code showWholeTacletCB}, + * {@code ViewSettings.getShowWholeTaclet}).
        • + *
        + * The Swing "Save as Default" button (persisting via {@code ProofIndependentSettings.saveSettings}) + * is deliberately not ported — the default settings are written by the FX settings dialog + * (SettingsManagerF), which exposes the same options. + */ +public final class ToolTipOptionsDialogF extends javafx.stage.Stage { + + private ToolTipOptionsDialogF(Window owner) { + setTitle("Maximum line number for tooltips"); + initOwner(owner); + if (owner != null) { + // Swing: JDialog(parent, title, true) — window-modal + initModality(Modality.WINDOW_MODAL); + } + + ViewSettings viewSettings = ProofIndependentSettings.DEFAULT_INSTANCE.getViewSettings(); + int maxLines = viewSettings.getMaxTooltipLines(); + boolean showWholeTaclet = viewSettings.getShowWholeTaclet(); + boolean showUninstantiatedTaclet = viewSettings.getShowUninstantiatedTaclet(); + + Label maxLinesLabel = new Label("Maximum size (line count) of the tooltips of applicable" + + " rules\n with schema variable instantiations displayed. In case of longer\n" + + " tooltips the instantiation will be suppressed."); + TextField maxLinesField = new TextField(String.valueOf(maxLines)); + // digits only, like the Swing NumberInputField (ViewSelector.NumberDocument:165-175) + maxLinesField.setTextFormatter(new TextFormatter( + change -> change.getControlNewText().matches("\\d*") + ? change + : null)); + HBox maxLinesRow = new HBox(8, maxLinesLabel, maxLinesField); + + CheckBox showUninstantiatedTacletBox = + new CheckBox("show uninstantiated taclet recommended for unexperienced users"); + showUninstantiatedTacletBox.setSelected(showUninstantiatedTaclet); + CheckBox showWholeTacletBox = new CheckBox( + "pretty-print whole Taclet including 'name', 'find', 'varCond' and 'heuristics'"); + showWholeTacletBox.setSelected(showWholeTaclet); + + GridPane center = new GridPane(); + center.setHgap(8); + center.setVgap(10); + center.add(maxLinesRow, 0, 0); + center.add(showUninstantiatedTacletBox, 0, 1); + center.add(showWholeTacletBox, 0, 2); + center.setPadding(new Insets(12)); + + Button okButton = new Button("OK"); + okButton.setDefaultButton(true); + // Swing OK: parse the field, write all three settings back, close + // (ViewSelector.java:99-115). + // Integer.parseInt throws on invalid input; like Swing's intended behavior the value then + // falls back to the previously stored setting instead of being applied. + okButton.setOnAction(e -> { + int maxSteps; + try { + maxSteps = Integer.parseInt(maxLinesField.getText()); + } catch (NumberFormatException nfe) { + maxSteps = viewSettings.getMaxTooltipLines(); + } + viewSettings.setMaxTooltipLines(maxSteps); + viewSettings.setShowWholeTaclet(showWholeTacletBox.isSelected()); + viewSettings.setShowUninstantiatedTaclet(showUninstantiatedTacletBox.isSelected()); + close(); + }); + Button cancelButton = new Button("Cancel"); + // Swing Cancel: close without writing (ViewSelector.java:133-137) + cancelButton.setOnAction(e -> close()); + ButtonBar.setButtonData(okButton, ButtonBar.ButtonData.OK_DONE); + ButtonBar.setButtonData(cancelButton, ButtonBar.ButtonData.CANCEL_CLOSE); + ButtonBar buttonBar = new ButtonBar(); + buttonBar.getButtons().addAll(okButton, cancelButton); + BorderPane.setMargin(buttonBar, new Insets(10)); + + BorderPane root = new BorderPane(); + root.setCenter(center); + root.setBottom(buttonBar); + Scene scene = new Scene(root, 620, 170); + ThemeManager.getInstance().manage(scene); + scene.setOnKeyPressed(e -> { + if (e.getCode() == KeyCode.ESCAPE) { + close(); + e.consume(); + } + }); + setScene(scene); + } + + /** + * Shows the dialog (Swing {@code ViewSelector.setVisible(true)}, ViewSelector.java:27). + * + * @param owner the owner window (the main window stage), may be {@code null} + */ + public static void show(Window owner) { + new ToolTipOptionsDialogF(owner).show(); + } +} From 7910a8d819bbfc39b669f9d637c60abfb76e8097 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Fri, 9 Oct 2026 17:38:45 +0200 Subject: [PATCH 097/281] =?UTF-8?q?key.ui.fx:=20menu:=20MP3b=20View=20menu?= =?UTF-8?q?=20parity=20=E2=80=94=20Select=20Goal=20submenu,=20Back/Forward?= =?UTF-8?q?=20selection=20history=20(SelectionHistoryF)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../de/uka/ilkd/key/gui/fx/MainWindowF.java | 56 +++- .../ilkd/key/gui/fx/SelectionHistoryF.java | 297 ++++++++++++++++++ .../key/gui/fx/prooftree/ProofTreeViewF.java | 128 ++++++++ 3 files changed, 480 insertions(+), 1 deletion(-) create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/SelectionHistoryF.java 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 index feece72198b..161f0125aa7 100644 --- 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 @@ -219,6 +219,13 @@ public final class MainWindowF { */ 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. @@ -1699,6 +1706,33 @@ private Menu buildViewMenu() { 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(prettyPrint, unicode, syntaxHighlighting); // lemmaorigin: begin — term labels + origin tracking view controls (Swing TermLabelMenu / // HidePackagePrefixToggleAction / OriginTermLabelsExt MainMenu items) @@ -1721,12 +1755,32 @@ themeMenu, fontSize, toolTipOptions, new SeparatorMenuItem(), // item (LogViewF.showInstance) menuItem("Log View", "de.uka.ilkd.key.gui.actions.LogViewAction", this::showLogView), - soundinessItem); + 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 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/prooftree/ProofTreeViewF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/prooftree/ProofTreeViewF.java index ded139ea879..608ff06fd71 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/prooftree/ProofTreeViewF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/prooftree/ProofTreeViewF.java @@ -1084,6 +1084,134 @@ private static TreeItem findItem(TreeItem start, Node node) { return null; } + // ----------------------------------------------------------------------- + // Goal select above/below (menu: MP3b — Swing ProofTreeView.selectAbove/selectBelow, + // ProofTreeView.java:550-606, invoked by GoalSelectAboveAction/GoalSelectBelowAction) + // ----------------------------------------------------------------------- + + /** + * menu: MP3b — selects the next open goal above the currently selected node (Swing + * {@code ProofTreeView.selectAbove}, ProofTreeView.java:550-577). The Swing version walks + * the visible JTree rows upward from the current row: a collapsed branch row is expanded + * while crossing it (the search continues at the far end of the just-expanded branch, + * {@code row += newRows - prevRows}, :563-568) and the first LEAF node row is selected via + * the selection model. JavaFX has no row API, so {@link #visibleRows()} builds the ordered + * list of currently visible rows from the expansion state ({@code TreeItem.isExpanded()}); + * a collapsed branch is expanded (with a row-list rebuild) and the walk continues from the + * far end of its subtree — the same behaviour as Swing. + * + * @return whether a goal above the current selection was found and selected + */ + public boolean selectAbove() { + return selectGoal(-1); + } + + /** + * menu: MP3b — the downward counterpart of {@link #selectAbove()} (Swing + * {@code ProofTreeView.selectBelow}, ProofTreeView.java:584-606): a collapsed branch is + * expanded and the walk continues at the branch row itself, moving into the expanded subtree. + * + * @return whether a goal below the current selection was found and selected + */ + public boolean selectBelow() { + return selectGoal(1); + } + + /** + * Walks the visible rows from the current selection in the given direction and selects the + * first leaf row via the {@link KeYSelectionModel} (the selection listener then reveals and + * scrolls the new selection like a user click, see {@link #revealSelectedNode}). + */ + private boolean selectGoal(int direction) { + TreeItem start = tree.getSelectionModel().getSelectedItem(); + if (start == null || start.getValue() == null || tree.getRoot() == null) { + return false; + } + List> rows = visibleRows(); + int cursor = rows.indexOf(start); + if (cursor < 0) { + return false; + } + for (int i = cursor + direction; i >= 0 && i < rows.size(); i += direction) { + TreeItem item = rows.get(i); + Entry entry = item.getValue(); + if (entry.isBranch() && isExpandableBranch(item)) { + // Swing: expandPath(tp) — a collapsed branch is expanded while crossing it; + // selectAbove continues at the far end of the expanded subtree + // (ProofTreeView.java:563-568), selectBelow at the branch root (:596-598) + item.setExpanded(true); + rows = visibleRows(); + int branchIndex = rows.indexOf(item); + if (direction < 0) { + i = branchIndex + visibleRowCount(item) - 1; + } else { + i = branchIndex; + } + continue; + } + if (!entry.isBranch() && entry.node.leaf()) { + if (selectionModel != null) { + selectionModel.setSelectedNode(entry.node); + } + return true; + } + } + return false; + } + + /** + * @param item a branch entry + * @return whether the branch is a real collapsed branch point that Swing would expand while + * walking (a {@code GUIBranchNode} whose node has a multi-child parent — the root + * branch is excluded by {@code parent() != null}; Swing ProofTreeView.java:560-562) + */ + private static boolean isExpandableBranch(TreeItem item) { + Entry entry = item.getValue(); + return !item.isExpanded() && entry.node.parent() != null + && entry.node.parent().childrenCount() > 1; + } + + /** + * @return the currently visible tree rows in display order — the FX counterpart of the + * Swing {@code JTree} row API used by {@code selectAbove/selectBelow}: a pre-order + * walk that descends only into expanded items (this mirrors the rows allocated by + * the {@link TreeView}, whose {@code TreeItem.expandedProperty} gates visibility) + */ + private List> visibleRows() { + List> rows = new ArrayList<>(); + TreeItem root = tree.getRoot(); + if (root != null) { + collectVisibleRows(root, rows); + } + return rows; + } + + private static void collectVisibleRows(TreeItem item, List> into) { + if (item == null) { + return; + } + into.add(item); + if (item.isExpanded()) { + for (TreeItem child : item.getChildren()) { + collectVisibleRows(child, into); + } + } + } + + /** + * @return the number of visible rows in the subtree of the given item, the item's own row + * included + */ + private static int visibleRowCount(TreeItem item) { + int count = 1; + if (item.isExpanded()) { + for (TreeItem child : item.getChildren()) { + count += visibleRowCount(child); + } + } + return count; + } + // ----------------------------------------------------------------------- // Search (Swing ProofTreeSearchBar + ProofTreeViewFilter.TreeSearchFilter) // ----------------------------------------------------------------------- From f98f63e0731b01e7e033f566084bfdc4ba10c615 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Fri, 9 Oct 2026 17:40:52 +0200 Subject: [PATCH 098/281] =?UTF-8?q?key.ui.fx:=20menu:=20MP3c=20View=20menu?= =?UTF-8?q?=20parity=20=E2=80=94=20extend=20menu-parity=20self=20test=20to?= =?UTF-8?q?=20walk=20the=20View=20menu?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../de/uka/ilkd/key/gui/fx/MainWindowF.java | 124 ++++++++++++------ 1 file changed, 82 insertions(+), 42 deletions(-) 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 index 161f0125aa7..c3884945a5b 100644 --- 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 @@ -746,9 +746,10 @@ protected KeYEnvironment call() throws Exception { if (System.getProperty("key.fx.verify.termmenu") != null) { runTermMenuVerification(env); } - // menu: MP1 — menu parity self test (key.fx.verify.menuparity): walks the built - // menu bar and asserts the Proof menu entries against the Swing createProofMenu - // table; the marker line must end with PASS + // 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(); } @@ -2032,59 +2033,97 @@ private void showKnownTypes() { 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. + 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" }, + { "Goal Back" }, + { "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: MP1/MP2 self test of the Proof menu (system property {@code key.fx.verify.menuparity}, - * run after a proof load like the other verify hooks): builds the menu bar and walks the - * Proof menu, asserting the presence of the entries of {@link #buildProofMenu()} in the - * Swing order (MainWindow.createProofMenu :1082-1142, and the Automation submenu entries of - * MainWindow.createAutomationActions :814-827 since MP2). Table-driven so MP3-MP5 can extend - * it with the other menus later. + * menu: MP1/MP3 — 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() { - String report = verifyMenuParityReport(); - LOGGER.info("Menu parity verification (MP1 Proof): {}", report); + logMenuParity("Proof", verifyMenuParityReport("Proof", PROOF_MENU_EXPECTED)); + logMenuParity("View", verifyMenuParityReport("View", VIEW_MENU_EXPECTED)); + } + + private void logMenuParity(String menuName, String report) { + LOGGER.info("Menu parity verification ({}): {}", menuName, report); NotificationManagerF.getInstance() - .notify("Menu parity verification (MP1 Proof): " + report, + .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. A - * table holds the expected labels; entries may be leaf items, plain separators (the - * {@code "---"} row) or submenu names ({@code "Search Mode"}) whose own children are - * checked recursively. + * 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: "} + * @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 proof = menuBar.getMenus().stream().filter(m -> "Proof".equals(m.getText())) + Menu menu = menuBar.getMenus().stream().filter(m -> menuName.equals(m.getText())) .findFirst().orElse(null); - if (proof == null) { - return "FAIL - missing: "; - } - String[][] expected = { - { "Automation", "Start Automatic Proof", "Stop Automatic Proof", "Full Automation", - "Structured Automation", "Structured Automation (Prep. Only)", - "Script-aware Auto" }, - { "Goal Back" }, - { "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" }, - }; + if (menu == null) { + return "FAIL - missing: <" + menuName + " menu>"; + } List present = new ArrayList<>(); List missing = new ArrayList<>(); - List remaining = new ArrayList<>(proof.getItems()); + List remaining = new ArrayList<>(menu.getItems()); for (String[] row : expected) { String label = row[0]; if ("---".equals(label)) { @@ -2113,7 +2152,8 @@ String verifyMenuParityReport() { 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) + // 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]); From af43ed6c6f40520f133eda51109f63fd64a464e8 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Fri, 9 Oct 2026 17:25:06 +0200 Subject: [PATCH 099/281] chore: ignore the .hermes/ tool cache directory --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index b54d0ca8b9c..05a52134ad4 100644 --- a/.gitignore +++ b/.gitignore @@ -78,3 +78,6 @@ checkstyle-diff.txt # Java Flight Recorder output (e.g. runJfrProbe default mt-jfr-probe.jfr) *.jfr + +# Hermes tool cache (agent/tooling state; never version-controlled) +.hermes/ From 1f2758b4212ae3df169b7016bf8e7cbbb229e269 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Fri, 9 Oct 2026 17:53:15 +0200 Subject: [PATCH 100/281] =?UTF-8?q?key.ui.fx:=20menu:=20MP4=20Options=20me?= =?UTF-8?q?nu=20parity=20=E2=80=94=205=20settings=20toggles,=20real=20SMT?= =?UTF-8?q?=20Solvers=E2=80=A6=20entry=20(SMTSettingsProviderF),=20Options?= =?UTF-8?q?=20line=20in=20the=20parity=20self-test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Confirm Exit / Auto Save Proofs / Minimize Interaction / Right Click for Proof Macros / Ensure Source Consistency as CheckMenuItems initialized from and writing back to ProofIndependentSettings (ViewSettings / GeneralSettings), mirroring Swing ToggleConfirmExitAction, AutoSave, MinimizeInteraction, RightMouseClickToggleAction, EnsureSourceConsistencyToggleAction. Mediator/filter/macro wiring deferred: the FX KeYMediatorF has no setAutoSave, the goal-view taclet filter and the direct-macro right-click are not wired, the FX port has no source-consistency machinery (// menu: notes). - SMT Solvers… replaced notYetImplemented() by the real settings-dialog entry focused on the new SMTSettingsProviderF (registered as SettingsManagerF.SMT_SETTINGS like Swing SettingsManager.SMT_SETTINGS): per-solver check boxes from SolverTypes.getSolverTypes() (initial state containsSolver, persisted via setActiveSolver) plus the four 1:1 persisted toggles (show results, store SMT/taclet translation, enable-on-load); the un-ported Swing options stay KNOWN-DEFERRED // menu:. - verifyMenuParityReport() gains the OPTIONS_MENU_EXPECTED table and emits the third 'Menu parity verification (Options):' PASS/FAIL line. --- .../de/uka/ilkd/key/gui/fx/MainWindowF.java | 130 ++++++++++++++- .../gui/fx/settings/SMTSettingsProviderF.java | 149 ++++++++++++++++++ .../key/gui/fx/settings/SettingsManagerF.java | 14 +- 3 files changed, 289 insertions(+), 4 deletions(-) create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/SMTSettingsProviderF.java 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 index c3884945a5b..67588253b56 100644 --- 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 @@ -115,6 +115,7 @@ 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.settings.GeneralSettings; import de.uka.ilkd.key.settings.PathConfig; import de.uka.ilkd.key.settings.ProofIndependentSettings; import de.uka.ilkd.key.settings.ViewSettings; @@ -2076,14 +2077,31 @@ private void showKnownTypes() { { "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: MP1/MP3 — menu parity self test (system property {@code key.fx.verify.menuparity}, + * menu: MP1/MP3/MP4 — 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)); } private void logMenuParity(String menuName, String report) { @@ -2188,16 +2206,124 @@ private static int indexOfItem(List items, String text) { private Menu buildOptionsMenu() { 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), - menuItem("SMT Solvers…", IconFactoryF.Key.TOOLBOX, this::notYetImplemented)); + 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; + } + + /** + * 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: auto-save timer wiring deferred (no FX mediator support): Swing additionally calls + * getMediator().setAutoSave(p) (AutoSave.java:31); the FX KeYMediatorF has no setAutoSave, so + * only the persisted flag is written. + */ + private CheckMenuItem autoSaveProofsToggle() { + GeneralSettings gs = ProofIndependentSettings.DEFAULT_INSTANCE.getGeneralSettings(); + CheckMenuItem item = new CheckMenuItem("Auto Save Proofs"); + item.setSelected(gs.autoSavePeriod() > 0); + item.setOnAction( + e -> gs.setAutoSave(item.isSelected() ? DEFAULT_AUTO_SAVE_PERIOD : 0)); + return item; + } + + /** 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. + * // menu: the FX context-menu filter wiring for this flag is deferred — Swing additionally + * applies a taclet filter on the current goal view via + * mainWindow.getUserInterface().getProofControl().setMinimizeInteraction(b) + * (MinimizeInteraction.java:64-66); only the flag is persisted here. + */ + private CheckMenuItem minimizeInteractionToggle() { + GeneralSettings gs = ProofIndependentSettings.DEFAULT_INSTANCE.getGeneralSettings(); + CheckMenuItem item = new CheckMenuItem("Minimize Interaction"); + item.setSelected(gs.getTacletFilter()); + item.setOnAction(e -> gs.setTacletFilter(item.isSelected())); + return item; + } + + /** + * 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: Swing's direct-macro-on-right-click behavior is deferred — the FX right click + * currently always opens the term context menu; only the flag persists here. + */ + 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: Swing's info dialog is dropped — the FX port has no source-consistency machinery + * (no proof-bundle/source-cache backend); only the flag persists for now. + */ + private CheckMenuItem ensureSourceConsistencyToggle() { + GeneralSettings gs = ProofIndependentSettings.DEFAULT_INSTANCE.getGeneralSettings(); + CheckMenuItem item = new CheckMenuItem("Ensure Source Consistency"); + item.setSelected(gs.isEnsureSourceConsistency()); + item.setOnAction(e -> gs.setEnsureSourceConsistency(item.isSelected())); + return item; + } + private Menu buildAboutMenu() { Menu about = new Menu("About"); about.getItems().addAll( diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/SMTSettingsProviderF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/SMTSettingsProviderF.java new file mode 100644 index 00000000000..23e03fd7108 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/SMTSettingsProviderF.java @@ -0,0 +1,149 @@ +/* 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.settings; + +import java.util.LinkedHashMap; +import java.util.Map; +import javafx.scene.Node; +import javafx.scene.control.CheckBox; + +import de.uka.ilkd.key.gui.fx.MainWindowF; +import de.uka.ilkd.key.settings.ProofIndependentSMTSettings; +import de.uka.ilkd.key.settings.ProofIndependentSettings; +import de.uka.ilkd.key.smt.solvertypes.SolverType; +import de.uka.ilkd.key.smt.solvertypes.SolverTypes; + +/** + * Settings provider for the SMT solvers, counter-part of + * {@code de.uka.ilkd.key.gui.smt.settings.SMTSettingsProvider} of the Swing module + * {@code key.ui} (registered there as {@code SettingsManager.SMT_SETTINGS}, the target of the + * Options | SMT Solvers… action). + *

        + * The panel shows one check box per available solver (from the pure-Java + * {@link SolverTypes#getSolverTypes()}, key.core) and the SMT options that persist 1:1 on + * {@link ProofIndependentSMTSettings}. All values are read in {@link #getPanel(MainWindowF)} + * from {@link ProofIndependentSettings#DEFAULT_INSTANCE} and written back in + * {@link #apply(MainWindowF)}, like the Swing original's {@code getPanel} (clone)/{@code + * applySettings} ({@code settings.copy}) pair, SMTSettingsProvider.java:172-183. + */ +public class SMTSettingsProviderF extends SettingsPanelF implements SettingsProviderF { + + /** + * One check box per solver type, in the {@code SolverTypes.getSolverTypes()} order. + *

        + * menu: MP4 — the checks mirror the solver selection of the settings + * ({@code ProofIndependentSMTSettings.containsSolver}), the selection state is persisted as + * the active solver name ({@code setActiveSolver}, the setter the Swing SMT toolbar writes + * via {@code setActiveSolverUnion} → {@code setActiveSolver}, + * ProofIndependentSMTSettings.java:475-480): there is no add/remove API for the + * {@code solverTypes} collection, so a deselected box cannot remove the solver from the + * settings list (KNOWN-DEFERRED, see {@link #apply(MainWindowF)}). + */ + private final Map solverChecks = new LinkedHashMap<>(); + + /** menu: MP4 — 1:1 persisted toggle (Swing property {@code SHOW_SMT_RES_DIA}). */ + private CheckBox chkShowResultsAfterExecution; + + /** + * menu: MP4 — 1:1 persisted toggle (Swing property {@code PROP_STORE_SMT_TRANSLATION_FILE}). + */ + private CheckBox chkStoreSMTTranslationToFile; + + /** + * menu: MP4 — 1:1 persisted toggle (Swing property {@code PROP_STORE_TACLET_TRANSLATION_FILE}). + */ + private CheckBox chkStoreTacletTranslationToFile; + + /** + * menu: MP4 — 1:1 persisted toggle (Swing property {@code SOLVER_ENABLED_ON_LOAD}; the + * "Enable SMT solvers when loading proofs" check of the Swing provider, + * SMTSettingsProvider.java:238-242). + */ + private CheckBox chkEnableOnLoad; + + /** Creates the provider and builds the form once (the panel is reused). */ + public SMTSettingsProviderF() { + setHeaderText(getDescription()); + + // menu: MP4 — SolverTypes.getSolverTypes() is pure Java (key.core, service-loaded + // solver properties), the check states come from the settings in getPanel. + addSeparator("SMT Solvers"); + for (SolverType type : SolverTypes.getSolverTypes()) { + solverChecks.put(type, addCheckBox(type.getName(), "", false)); + } + + addSeparator("SMT Options"); + chkShowResultsAfterExecution = + addCheckBox("Show results after execution", "", false); + chkStoreSMTTranslationToFile = addCheckBox("Store SMT translation to file", "", false); + chkStoreTacletTranslationToFile = + addCheckBox("Store taclet translation to file", "", false); + chkEnableOnLoad = addCheckBox("Enable SMT solvers when loading proofs", "", true); + + // menu: KNOWN-DEFERRED — the remaining Swing options of SMTSettingsProvider / + // SolverOptions are not ported: the per-solver command/parameters/timeout and support + // panels (SolverOptions.java:36-50), the progress-dialog mode, the global timeout, the + // concurrent-process count, the int/seq/object/locset bounds, the "check for support" + // toggle and the store-translation file path panel (SMTSettingsProvider.java:105-158, + // 185-256). The four persisted toggles above and the solver selection are the + // functional core; the rest is left for the SMT milestone. + + refreshFromSettings(); + } + + @Override + public String getDescription() { + return "SMT"; + } + + @Override + public Node getPanel(MainWindowF window) { + // like the Swing original, re-read the current settings on every getPanel + // (SMTSettingsProvider.java:172-176 prepares a clone there; here the same values are + // read directly and the write-back happens in apply). + refreshFromSettings(); + return this; + } + + /** + * Re-reads the current settings into the input components (Swing + * {@code setSmtSettings}, SMTSettingsProvider.java:258-271). + */ + private void refreshFromSettings() { + ProofIndependentSMTSettings settings = + ProofIndependentSettings.DEFAULT_INSTANCE.getSMTSettings(); + for (Map.Entry entry : solverChecks.entrySet()) { + entry.getValue().setSelected(settings.containsSolver(entry.getKey())); + } + chkShowResultsAfterExecution.setSelected(settings.isShowResultsAfterExecution()); + chkStoreSMTTranslationToFile.setSelected(settings.isStoreSMTTranslationToFile()); + chkStoreTacletTranslationToFile.setSelected(settings.isStoreTacletTranslationToFile()); + chkEnableOnLoad.setSelected(settings.isEnableOnLoad()); + } + + @Override + public void apply(MainWindowF window) throws InvalidSettingsInputExceptionF { + ProofIndependentSMTSettings settings = + ProofIndependentSettings.DEFAULT_INSTANCE.getSMTSettings(); + // menu: MP4 — the settings API persists a single active solver name (the Swing SMT + // toolbar selection writes it via setActiveSolverUnion → setActiveSolver, + // MainWindow.java:744-750); a checked box stores its solver as the active solver, an + // unchecked box cannot remove the solver from the settings' solverTypes collection + // (no add/remove API, KNOWN-DEFERRED, see solverChecks). + for (Map.Entry entry : solverChecks.entrySet()) { + if (entry.getValue().isSelected()) { + settings.setActiveSolver(entry.getKey().getName()); + } + } + settings.setShowResultsAfterExecution(chkShowResultsAfterExecution.isSelected()); + settings.setStoreSMTTranslationToFile(chkStoreSMTTranslationToFile.isSelected()); + settings.setStoreTacletTranslationToFile(chkStoreTacletTranslationToFile.isSelected()); + settings.setEnableOnLoad(chkEnableOnLoad.isSelected()); + } + + // menu: MP4 — no priority override, like the Swing SMTSettingsProvider (the default + // priority 0 places SMT between "Appearance & Behaviour" (MIN_VALUE) and the javac options + // (10000), the same relative order as the Swing SettingsManager registration). +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/SettingsManagerF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/SettingsManagerF.java index f9344c3a3d6..c7051198323 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/SettingsManagerF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/SettingsManagerF.java @@ -80,9 +80,16 @@ public final class SettingsManagerF { public static final JavacSettingsProviderF JAVAC_SETTINGS = new JavacSettingsProviderF(); + /** + * menu: MP4 — the SMT options provider (Swing {@code SMTSettingsProvider}, registered as + * {@code SettingsManager.SMT_SETTINGS} between the standard UI and the taclet options, + * SettingsManager.java:37-63); the target of the Options | SMT Solvers… action + * (Swing {@code SMTOptionsAction}). + */ + public static final SMTSettingsProviderF SMT_SETTINGS = new SMTSettingsProviderF(); + // Deliberately deferred to later milestones (the provider seam stays open): - // SMTSettingsProvider, ParallelProverSettingsProvider, - // FeatureSettingsPanel and the ShowActiveSettings dump. + // ParallelProverSettingsProvider, FeatureSettingsPanel and the ShowActiveSettings dump. private static SettingsManagerF INSTANCE; @@ -98,6 +105,9 @@ public static SettingsManagerF getInstance() { if (INSTANCE == null) { INSTANCE = new SettingsManagerF(); INSTANCE.add(STANDARD_UI_SETTINGS); + // menu: MP4 — SMT between the standard UI and the taclet options, like the Swing + // SettingsManager registration order (SettingsManager.java:59-63) + INSTANCE.add(SMT_SETTINGS); INSTANCE.add(TACLET_OPTIONS_SETTINGS); INSTANCE.add(JAVAC_SETTINGS); } From f01f13173e1fa0881864f3ee1b37bf376a0ab0cb Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Fri, 9 Oct 2026 18:18:07 +0200 Subject: [PATCH 101/281] =?UTF-8?q?key.ui.fx:=20menu:=20MP5=20File+About?= =?UTF-8?q?=20menu=20parity=20=E2=80=94=20Edit=20Last=20Opened=20File,=20u?= =?UTF-8?q?ser-defined=20taclets=20+=20Prove=20submenu,=20About=20menu=20v?= =?UTF-8?q?ia=20HostServices,=20File/About=20in=20the=20parity=20self-test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../de/uka/ilkd/key/gui/fx/MainWindowF.java | 521 +++++++++++++++++- .../key/gui/fx/dialogs/FeedbackDialogF.java | 170 ++++++ .../fx/dialogs/LoadUserTacletsDialogF.java | 148 +++++ .../key/gui/fx/dialogs/RunAllProofsF.java | 76 +++ .../uka/ilkd/key/gui/fx/help/HelpFacadeF.java | 17 + .../key/gui/fx/recentfiles/RecentFilesF.java | 20 + 6 files changed, 947 insertions(+), 5 deletions(-) create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/dialogs/FeedbackDialogF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/dialogs/LoadUserTacletsDialogF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/dialogs/RunAllProofsF.java 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 index 67588253b56..89df82b9858 100644 --- 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 @@ -34,6 +34,7 @@ import javafx.scene.control.OverrunStyle; import javafx.scene.control.RadioMenuItem; import javafx.scene.control.SeparatorMenuItem; +import javafx.scene.control.TextArea; import javafx.scene.control.ToggleGroup; import javafx.scene.control.ToolBar; import javafx.scene.control.Tooltip; @@ -58,6 +59,9 @@ import de.uka.ilkd.key.core.fx.KeYSelectionModel; import de.uka.ilkd.key.gui.fx.actions.QuickSaveF; import de.uka.ilkd.key.gui.fx.configuration.ConfigF; +import de.uka.ilkd.key.gui.fx.dialogs.FeedbackDialogF; +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; @@ -101,6 +105,7 @@ 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.macros.AutoPilotPrepareProofMacro; import de.uka.ilkd.key.macros.DefaultAutoMacro; import de.uka.ilkd.key.macros.FullAutoPilotProofMacro; @@ -110,19 +115,32 @@ 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.InitConfig; +import de.uka.ilkd.key.proof.init.ProblemInitializer; +import de.uka.ilkd.key.proof.init.Profile; 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.rule.Taclet; +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.ProofIndependentSettings; import de.uka.ilkd.key.settings.ViewSettings; +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.util.collection.DefaultImmutableSet; +import org.key_project.util.collection.ImmutableList; +import org.key_project.util.collection.ImmutableSet; import org.key_project.util.javafx.FxUtil; import org.jspecify.annotations.Nullable; @@ -144,6 +162,53 @@ public final class MainWindowF { 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"; @@ -278,6 +343,13 @@ public final class MainWindowF { */ 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}). @@ -527,7 +599,7 @@ private void startDemoProofLoad() { * @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); + startProofLoad(location, demo, null, null, false); } /** @@ -539,7 +611,7 @@ private void startProofLoad(Path location, boolean demo) { * {@code location} is not a proof bundle */ private void startProofLoad(Path location, boolean demo, @Nullable Path proofFilename) { - startProofLoad(location, demo, proofFilename, null); + startProofLoad(location, demo, proofFilename, null, false); } /** @@ -556,6 +628,26 @@ private void startProofLoad(Path location, boolean demo, @Nullable Path proofFil */ 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) { @@ -612,6 +704,14 @@ protected KeYEnvironment call() throws Exception { 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; } }; @@ -911,6 +1011,297 @@ private void reloadLastFile() { 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(); + // menu: the Swing axiom-file list of the dialog is not ported (KNOWN-DEFERRED, see + // LoadUserTacletsDialogF) — the loader never gets axiom files. + List filesForAxioms = List.of(); + 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(); + // menu: the Swing axiom-file list of the dialog is not ported (KNOWN-DEFERRED, see + // LoadUserTacletsDialogF) + List filesForAxioms = List.of(); + 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) + runTacletSoundnessLoader(tacletLoader, + tacletLoader.getProofEnvForTaclets().getInitConfigForEnvironment(), true, true, + listener); + } + + /** + * 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) { + // menu: instead of the Swing LemmaSelectionDialog, keep all "supported" taclets (the + // dialog's default "show only supported" filter) — the selection dialog is not ported + TacletSoundnessPOLoader.TacletFilter filter = tacletInfos -> { + ImmutableSet supported = DefaultImmutableSet.nil(); + for (TacletSoundnessPOLoader.TacletInfo info : tacletInfos) { + if (!info.isNotSupported()) { + supported = supported.add(info.getTaclet()); + } + } + return supported; + }; + 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 @@ -1541,6 +1932,21 @@ private Menu buildFileMenu() { // 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 @@ -1574,12 +1980,24 @@ private Menu buildFileMenu() { 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, @@ -1592,6 +2010,47 @@ private Menu buildFileMenu() { 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() { Menu view = new Menu("View"); // menu: MP3a — Pretty Print toggle (Swing PrettyPrintToggleAction, @@ -2093,8 +2552,42 @@ private void showKnownTypes() { { "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 — menu parity self test (system property {@code key.fx.verify.menuparity}, + * 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. */ @@ -2102,6 +2595,8 @@ 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)); } private void logMenuParity(String menuName, String report) { @@ -2324,11 +2819,27 @@ private CheckMenuItem ensureSourceConsistencyToggle() { 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() { Menu about = new Menu("About"); about.getItems().addAll( - menuItem("License…", IconFactoryF.Key.INFO_VIEW, this::showLicense), - menuItem("About KeY…", this::showAbout)); + 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", + () -> HelpFacadeF.openExternal(GITHUB_ISSUE_URL)), + menuItem("License…", "de.uka.ilkd.key.gui.actions.LicenseAction", + IconFactoryF.Key.INFO_VIEW, this::showLicense)); return about; } diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/dialogs/FeedbackDialogF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/dialogs/FeedbackDialogF.java new file mode 100644 index 00000000000..d2b3e703197 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/dialogs/FeedbackDialogF.java @@ -0,0 +1,170 @@ +/* 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.dialogs; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import javafx.geometry.Insets; +import javafx.scene.Scene; +import javafx.scene.control.Button; +import javafx.scene.control.ButtonBar; +import javafx.scene.control.Label; +import javafx.scene.control.TextArea; +import javafx.scene.input.KeyCode; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.VBox; +import javafx.stage.Modality; +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 de.uka.ilkd.key.util.KeYResourceManager; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * menu: MP5 — minimal JavaFX port of the Swing {@code SendFeedbackAction} dialog + * (key.ui/.../gui/actions/SendFeedbackAction.java), opened by the About>Send Feedback… entry + * (Swing {@code MenuSendFeedackAction}). The user writes a description which is then sent to the + * KeY report server. + *

        + * Port notes and deviations from the Swing original: + *

          + *
        • The Swing dialog collects a whole metadata zip (proof, last problem, settings, + * version, system properties, … {@code saveMetaData}) and sends it as + * {@code Content-Type: application/zip} after an "Ready to send?" confirmation; this port sends + * the bare description as {@code text/plain; charset=UTF-8} (with the same {@code KeY-Version} + * header) — the metadata collection is deliberately not ported, {@code // menu:} + * KNOWN-DEFERRED.
        • + *
        • Like the Swing original the send is performed off the UI thread + * ({@code sendReport}, the Swing version blocks the EDT) — here via + * {@linkplain HttpClient#sendAsync an asynchronous request}; failures surface as error toasts.
        • + *
        • The Swing "save ZIP to file" alternative (Swing {@code saveZIP}) is not ported (it exists + * only in the Swing dialog in front of the send step).
        • + *
        + */ +public final class FeedbackDialogF { + private static final Logger LOGGER = LoggerFactory.getLogger(FeedbackDialogF.class); + + /** + * The url to which the feedback will be sent (Swing {@code SendFeedbackAction.REPORT_URL}). + */ + public static final String REPORT_URL = "https://formal.kastel.kit.edu/key/key-report.php"; + + /** The email address the report is forwarded to (Swing {@code FEEDBACK_RECIPIENT}). */ + public static final String FEEDBACK_RECIPIENT = "support@key-project.org"; + + private FeedbackDialogF() { + } + + /** + * Shows the modal feedback dialog and sends the entered description to {@link #REPORT_URL}. + * + * @param owner the owner window (the main window stage), may be {@code null} + */ + public static void show(Window owner) { + javafx.stage.Stage dialog = new javafx.stage.Stage(); + dialog.setTitle("Report an error to KeY developers"); + dialog.initOwner(owner); + if (owner != null) { + // Swing: JDialog(parent, title, DOCUMENT_MODAL) + dialog.initModality(Modality.WINDOW_MODAL); + } + + Label hint = new Label("Describe the problem you encountered. The report is sent to " + + "https://formal.kastel.kit.edu/key/key-report.php and forwarded to the KeY " + + "mailing list <" + FEEDBACK_RECIPIENT + ">."); + hint.setWrapText(true); + + TextArea message = new TextArea(); + message.setPromptText("Your feedback…"); + message.setWrapText(true); + VBox.setVgrow(message, javafx.scene.layout.Priority.ALWAYS); + + Button send = new Button("Send"); + send.setDefaultButton(true); + Button cancel = new Button("Cancel"); + ButtonBar.setButtonData(send, ButtonBar.ButtonData.OK_DONE); + ButtonBar.setButtonData(cancel, ButtonBar.ButtonData.CANCEL_CLOSE); + ButtonBar buttons = new ButtonBar(); + buttons.getButtons().addAll(send, cancel); + + VBox center = new VBox(10, hint, message); + center.setPadding(new Insets(12)); + BorderPane root = new BorderPane(); + root.setCenter(center); + root.setBottom(buttons); + BorderPane.setMargin(buttons, new Insets(10)); + Scene scene = new Scene(root, 560, 300); + ThemeManager.getInstance().manage(scene); + scene.setOnKeyPressed(e -> { + if (e.getCode() == KeyCode.ESCAPE) { + dialog.close(); + e.consume(); + } + }); + dialog.setScene(scene); + + send.setOnAction(e -> { + send.setDisable(true); + dialog.close(); + sendReport(owner, message.getText()); + }); + cancel.setOnAction(e -> dialog.close()); + dialog.showAndWait(); + } + + /** + * Posts the feedback text to the {@linkplain #REPORT_URL report server} (Swing + * {@code SendFeedbackAction.sendReport}): the {@code http.client} POST carries the same + * {@code KeY-Version} header as the Swing original; on HTTP 200 the caller gets an info + * toast, otherwise the response body is reported as an error toast. + */ + private static void sendReport(Window owner, String text) { + if (text == null || text.isBlank()) { + NotificationManagerF.getInstance() + .notify("Feedback not sent: the description is empty.", Kind.WARNING); + return; + } + HttpRequest request = HttpRequest.newBuilder(URI.create(REPORT_URL)) + // menu: Swing sends an application/zip metadata bundle (saveMetaData); this port + // sends the plain description, so the content type differs (KNOWN-DEFERRED above) + .header("Content-Type", "text/plain; charset=UTF-8") + .header("KeY-Version", "KeY " + KeYResourceManager.getManager().getVersion()) + .timeout(Duration.ofSeconds(30)) + .POST(HttpRequest.BodyPublishers.ofString(text, StandardCharsets.UTF_8)) + .build(); + HttpClient.newHttpClient().sendAsync(request, HttpResponse.BodyHandlers.ofString()) + .whenComplete((response, error) -> { + if (error != null) { + LOGGER.error("Sending feedback failed", error); + NotificationManagerF.getInstance().notify( + "Sending feedback failed: " + error.getMessage(), Kind.ERROR); + return; + } + int code = response.statusCode(); + if (code == 200) { + LOGGER.info("Feedback sent to {}", REPORT_URL); + NotificationManagerF.getInstance() + .notify("Your report has been filed successfully. Thank you!", + Kind.INFO); + } else { + String body = response.body(); + LOGGER.error("Sending feedback failed, server responded {}: {}", code, + body); + NotificationManagerF.getInstance().notify( + "Sending feedback failed, the server responded with " + code + ": " + + body, + Kind.ERROR); + } + }); + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/dialogs/LoadUserTacletsDialogF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/dialogs/LoadUserTacletsDialogF.java new file mode 100644 index 00000000000..c2f1dbb162b --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/dialogs/LoadUserTacletsDialogF.java @@ -0,0 +1,148 @@ +/* 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.dialogs; + +import java.io.File; +import java.nio.file.Path; +import java.util.Optional; +import javafx.geometry.Insets; +import javafx.scene.Scene; +import javafx.scene.control.Button; +import javafx.scene.control.ButtonBar; +import javafx.scene.control.CheckBox; +import javafx.scene.control.Label; +import javafx.scene.input.KeyCode; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.VBox; +import javafx.stage.FileChooser; +import javafx.stage.Modality; +import javafx.stage.Window; + +import de.uka.ilkd.key.gui.fx.theme.ThemeManager; + +/** + * menu: MP5 — JavaFX port of the Swing {@code LoadUserTacletsDialog} + * (key.ui/.../gui/lemmatagenerator/LoadUserTacletsDialog.java), opened by the + * "Load User Defined Taclets…" entry and the "Prove" submenu's "Load User Defined Taclets for + * Proving" entry. The dialog collects + *
          + *
        • the (single) file containing the user-defined taclets — the Swing dialog additionally + * maintains a list of axiom files ({@code Mode.PROVE} axiom panel / the LOAD mode's + * justification box); the axiom-file list is deliberately not ported + * ({@code TacletSoundnessPOLoader} then simply never loads axioms), marked {@code // menu:} + * KNOWN-DEFERRED below,
        • + *
        • the "Generate proof obligations for taclets" checkbox (Swing + * {@code getLemmaCheckBox}, default {@code true}): when un-checked the taclets are loaded + * without a soundness proof.
        • + *
        + */ +public final class LoadUserTacletsDialogF { + + /** + * The two modes of the Swing dialog ({@code LoadUserTacletsDialog.Mode}): PROVE only creates + * the proof obligations, LOAD additionally loads the taclets into the current proof. + */ + public enum Mode { + PROVE, LOAD + } + + /** + * The collected choices of the dialog. + * + * @param fileForTaclets the {@code .key} file with the user-defined taclets + * (Swing {@code getFileForTaclets()}) + * @param generateProofObligations whether a soundness proof obligation should be created for + * each taclet (Swing {@code isGenerateProofObligations()}) + */ + public record Result(Path fileForTaclets, boolean generateProofObligations) { + } + + private LoadUserTacletsDialogF() { + } + + /** + * Shows the modal taclet-loading dialog. + * + * @param owner the owner window (the main window stage), may be {@code null} + * @param mode PROVE or LOAD + * @return the chosen file and checkbox state, or {@link Optional#empty()} when canceled + * (Swing {@code showAsDialog()} returns {@code false}) + */ + public static Optional showDialog(Window owner, Mode mode) { + javafx.stage.Stage dialog = new javafx.stage.Stage(); + dialog.setTitle( + mode == Mode.LOAD ? "Load User-Defined Taclets" : "Prove User-Defined Taclets"); + dialog.initOwner(owner); + if (owner != null) { + // Swing: JDialog(parent, title, modal) — window-modal + dialog.initModality(Modality.WINDOW_MODAL); + } + + Label hint = new Label("Choose the file containing the user-defined taclets." + + (mode == Mode.LOAD + ? "\nEach taclet is loaded for the current proof when its proof obligation" + + " can be proven." + : "\nFor each taclet a proof obligation is created.")); + hint.setWrapText(true); + + Label chosen = new Label(""); + chosen.setWrapText(true); + + Button choose = new Button("Choose…"); + Button ok = new Button("OK"); + ok.setDefaultButton(true); + ok.setDisable(true); // enabled once a file has been chosen (Swing fileHasBeenChosen) + choose.setOnAction(e -> { + FileChooser chooser = new FileChooser(); + chooser.setTitle("Choose the file with the user-defined taclets"); + // Swing KeYFileChooser opens with the last chosen directory + chooser.getExtensionFilters() + .add(new FileChooser.ExtensionFilter("KeY files (*.key)", "*.key")); + File selected = chooser.showOpenDialog(dialog); + if (selected != null) { + chosen.setText(selected.getPath()); + ok.setDisable(false); + } + }); + + CheckBox lemmaBox = new CheckBox("Generate proof obligations for taclets"); + lemmaBox.setSelected(true); + // menu: the Swing warning on un-checking (InfoDialog "you are going to load taclets + // without creating corresponding proof obligations") is dropped — the checkbox state + // is persisted nowhere in the FX port. + lemmaBox.setWrapText(true); + + Button cancel = new Button("Cancel"); + ButtonBar.setButtonData(ok, ButtonBar.ButtonData.OK_DONE); + ButtonBar.setButtonData(cancel, ButtonBar.ButtonData.CANCEL_CLOSE); + ButtonBar buttons = new ButtonBar(); + buttons.getButtons().addAll(ok, cancel); + + VBox center = new VBox(10, hint, choose, chosen, lemmaBox); + center.setPadding(new Insets(12)); + BorderPane root = new BorderPane(); + root.setCenter(center); + root.setBottom(buttons); + BorderPane.setMargin(buttons, new Insets(10)); + Scene scene = new Scene(root, 520, 210); + ThemeManager.getInstance().manage(scene); + scene.setOnKeyPressed(e -> { + if (e.getCode() == KeyCode.ESCAPE) { + dialog.close(); + e.consume(); + } + }); + dialog.setScene(scene); + + Result[] result = new Result[1]; + ok.setOnAction(e -> { + result[0] = new Result(Path.of(chosen.getText()), lemmaBox.isSelected()); + dialog.close(); + }); + cancel.setOnAction(e -> dialog.close()); + dialog.showAndWait(); + return Optional.ofNullable(result[0]); + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/dialogs/RunAllProofsF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/dialogs/RunAllProofsF.java new file mode 100644 index 00000000000..ea890ed1feb --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/dialogs/RunAllProofsF.java @@ -0,0 +1,76 @@ +/* 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.dialogs; + +import java.nio.file.Path; + +import org.jspecify.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * menu: MP5 — minimal JavaFX port of the Swing {@code RunAllProofsAction} + * (key.ui/.../gui/actions/RunAllProofsAction.java), the QA feature shown in the "Prove" submenu + * behind the {@code BULK_UI_TEST} feature flag. The Swing action maintains a whole list + * of proof files (from a newline-separated file referenced by the {@link #ENV_VARIABLE} + * environment variable, or the bundled {@code runallproofsui.txt}) and auto-proves each in turn; + * this port keeps the smallest useful subset: + *
          + *
        • the proof file spec is read from the environment variable or (as a port convenience) the + * system property {@value #ENV_VARIABLE} — Swing reads only the environment,
        • + *
        • a spec ending in {@code .key} is loaded and auto-proved once by the caller + * (load + auto mode, the summary of Swing's + * {@code actionPerformed: ui.getProblemLoader(...).runSynchronously()} + {@code startAutoMode} + * + {@code waitWhileAutoMode()});
        • + *
        • any other spec (or none) yields a short usage/status message instead of the Swing whole + * batch machinery ({@code // menu:} the multi-file batch loop and the odometer progress are not + * ported).
        • + *
        + */ +public final class RunAllProofsF { + private static final Logger LOGGER = LoggerFactory.getLogger(RunAllProofsF.class); + + /** + * Environment variable which points to a proof file to be run (Swing + * {@code RunAllProofsAction.ENV_VARIABLE}). + */ + public static final String ENV_VARIABLE = "KEY_RUNALLPROOFS_UI_FILE"; + + private RunAllProofsF() { + } + + /** + * Resolves the proof file to auto-prove from {@link #ENV_VARIABLE} (environment variable, + * then system property). + * + * @return the absolute proof file, or {@code null} when the variable is not set or does not + * name a {@code .key} file (Swing falls back to the bundled file list; the FX port + * has no bundled list and reports the usage message instead) + */ + public static @Nullable Path proofToRun() { + String spec = System.getenv(ENV_VARIABLE); + if (spec == null || spec.isBlank()) { + spec = System.getProperty(ENV_VARIABLE); + } + if (spec == null || spec.isBlank()) { + return null; + } + Path path = Path.of(spec).toAbsolutePath(); + if (!path.toString().endsWith(".key")) { + LOGGER.info("Run All Proofs: ignoring {}={} (no .key file)", ENV_VARIABLE, spec); + return null; + } + return path; + } + + /** + * @return the usage message shown when {@link #proofToRun()} has nothing to do + */ + public static String usageMessage() { + return "Run All Proofs is active, but " + ENV_VARIABLE + + " is not set to a .key file to auto-prove (e.g. export " + + ENV_VARIABLE + "=/path/to/example.key)."; + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/help/HelpFacadeF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/help/HelpFacadeF.java index 8f70aed648f..73f4356d6a5 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/help/HelpFacadeF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/help/HelpFacadeF.java @@ -236,6 +236,23 @@ private static void openHelpInBrowser(String url) { browserOpener.accept(url); } + /** + * Opens the given external URL in the default system browser via the application's host + * services (Swing {@code SwingUtil.browse}, used by the About-menu browser actions + * {@code KeYProjectHomepageAction} / {@code CreateGithubIssueAction} and by + * {@code EditMostRecentFileAction}). + *

        + * // menu: MP5 — opens the URL through the same {@linkplain #setBrowserOpener seam} as the + * help pages (host services of the running application with a {@link Desktop} fallback), so + * the self tests can record the target instead of launching a real browser. + * + * @param url the external URL to open + */ + public static void openExternal(String url) { + LOGGER.info("Opening external URL in browser: {}", url); + browserOpener.accept(url); + } + /** * Replaces the browser-opening seam (self tests only). * diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/recentfiles/RecentFilesF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/recentfiles/RecentFilesF.java index 2ea96e4df7c..c5979730ff9 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/recentfiles/RecentFilesF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/recentfiles/RecentFilesF.java @@ -13,6 +13,8 @@ import java.util.List; import java.util.Objects; import java.util.Optional; +import javafx.beans.property.ReadOnlyBooleanProperty; +import javafx.beans.property.ReadOnlyBooleanWrapper; import de.uka.ilkd.key.gui.fx.actions.QuickSaveF; import de.uka.ilkd.key.nparser.ParsingFacade; @@ -84,6 +86,23 @@ Configuration asConfiguration() { private Runnable onChange = () -> { }; + /** + * Whether at least one recent file exists. Backs the enablement of the "Edit Last Opened + * File" menu entry (Swing {@code EditMostRecentFileAction}; the Swing original re-checks the + * file list on every menu showing, the FX equivalent observes this property). + *

        + * The property is updated on the FX thread together with {@link #setOnChange(Runnable)}. + */ + private final ReadOnlyBooleanWrapper hasRecentFile = new ReadOnlyBooleanWrapper(false); + + /** + * @return whether at least one recent file exists ({@code true} while + * {@link #getMostRecent()} is not {@code null}) + */ + public ReadOnlyBooleanProperty hasRecentFileProperty() { + return hasRecentFile.getReadOnlyProperty(); + } + /** * Loads the entries from the current default configuration folder (falls back to the previous * version's folder like the Swing original). @@ -187,6 +206,7 @@ public void setOnChange(Runnable hook) { } private void fireChange() { + hasRecentFile.set(!entries.isEmpty()); onChange.run(); } From d4cf67648989c3eb9d073903d80cf3ec57039091 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Fri, 9 Oct 2026 19:58:59 +0200 Subject: [PATCH 102/281] =?UTF-8?q?key.ui.fx:=20menu:=20MP7a=20=E2=80=94?= =?UTF-8?q?=20wire=20Minimize=20Interaction=20to=20the=20proof=20control?= =?UTF-8?q?=20(persisted=20flag=20applied=20on=20toggle=20and=20on=20proof?= =?UTF-8?q?=20load;=20key.fx.verify.minimizeinteraction=20self=20test)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../de/uka/ilkd/key/gui/fx/MainWindowF.java | 64 +++++++++++++++++-- 1 file changed, 58 insertions(+), 6 deletions(-) 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 index 89df82b9858..8e2c5d2be1a 100644 --- 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 @@ -53,6 +53,7 @@ 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.KeYMediatorF; import de.uka.ilkd.key.core.fx.KeYSelectionEvent; import de.uka.ilkd.key.core.fx.KeYSelectionListener; @@ -720,6 +721,11 @@ protected KeYEnvironment call() throws Exception { // 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 @@ -854,6 +860,24 @@ protected KeYEnvironment call() throws Exception { if (System.getProperty("key.fx.verify.menuparity") != null) { verifyMenuParity(); } + // 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); + } if (System.getProperty("key.fx.demo.autoprove.live") != null) { startLiveAutoMode(env); } @@ -2773,20 +2797,48 @@ private CheckMenuItem autoSaveProofsToggle() { /** * 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. - * // menu: the FX context-menu filter wiring for this flag is deferred — Swing additionally - * applies a taclet filter on the current goal view via - * mainWindow.getUserInterface().getProofControl().setMinimizeInteraction(b) - * (MinimizeInteraction.java:64-66); only the flag is persisted here. + * 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 -> gs.setTacletFilter(item.isSelected())); + 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: MP4 — Right Click for Proof Macros check item (Swing RightMouseClickToggleAction, * RightMouseClickToggleAction.java:22-33): the selected state mirrors From 2b1a5d35a8e0be18db2b7b7f97206ea960348a5b Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Fri, 9 Oct 2026 20:04:38 +0200 Subject: [PATCH 103/281] =?UTF-8?q?key.ui.fx:=20menu:=20MP7b=20=E2=80=94?= =?UTF-8?q?=20right-click=20proof-macro=20popup=20(Swing=20CurrentGoalView?= =?UTF-8?q?Listener/ProofMacroMenu=20parity:=20macro=20list=20shared=20wit?= =?UTF-8?q?h=20the=20Automation=20submenu,=20tooltip=20descriptions,=20ter?= =?UTF-8?q?m-menu=20fallback=20when=20no=20macro=20applies;=20key.fx.verif?= =?UTF-8?q?y.rightclickmacro=20self=20test)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../de/uka/ilkd/key/gui/fx/MainWindowF.java | 51 +++-- .../key/gui/fx/nodeviews/SequentViewF.java | 187 +++++++++++++++++- 2 files changed, 219 insertions(+), 19 deletions(-) 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 index 8e2c5d2be1a..f7e28c9a427 100644 --- 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 @@ -878,6 +878,16 @@ protected KeYEnvironment call() throws Exception { .notify("Minimize interaction verification: " + (ok ? "PASS" : "FAIL"), ok ? Kind.INFO : Kind.ERROR); } + // 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); + } if (System.getProperty("key.fx.demo.autoprove.live") != null) { startLiveAutoMode(env); } @@ -2296,6 +2306,19 @@ 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() { Menu proof = new Menu("Proof"); Menu automation = new Menu("Automation"); @@ -2320,24 +2343,16 @@ private Menu buildProofMenu() { // 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). - MenuItem defaultAuto = menuItem(new DefaultAutoMacro().getName(), - "de.uka.ilkd.key.macros.DefaultAutoMacro", null, - () -> runMacro(new DefaultAutoMacro())); - defaultAuto.disableProperty().bind(proofLoaded.not()); - MenuItem structuredAuto = menuItem(new FullAutoPilotProofMacro().getName(), - "de.uka.ilkd.key.macros.FullAutoPilotProofMacro", null, - () -> runMacro(new FullAutoPilotProofMacro())); - structuredAuto.disableProperty().bind(proofLoaded.not()); - MenuItem prepareAuto = menuItem(new AutoPilotPrepareProofMacro().getName(), - "de.uka.ilkd.key.macros.AutoPilotPrepareProofMacro", null, - () -> runMacro(new AutoPilotPrepareProofMacro())); - prepareAuto.disableProperty().bind(proofLoaded.not()); - MenuItem scriptAuto = menuItem(new ScriptAwareMacro().getName(), - "de.uka.ilkd.key.macros.ScriptAwareMacro", null, - () -> runMacro(new ScriptAwareMacro())); - scriptAuto.disableProperty().bind(proofLoaded.not()); - automation.getItems().addAll(startAuto, stopAuto, - defaultAuto, structuredAuto, prepareAuto, scriptAuto); + // 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 diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentViewF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentViewF.java index 95711d64f26..65de20c26c6 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentViewF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentViewF.java @@ -19,8 +19,10 @@ import javafx.scene.control.Button; import javafx.scene.control.ComboBox; import javafx.scene.control.ContextMenu; +import javafx.scene.control.CustomMenuItem; import javafx.scene.control.Label; import javafx.scene.control.ListCell; +import javafx.scene.control.MenuItem; import javafx.scene.control.ScrollPane; import javafx.scene.control.TextField; import javafx.scene.control.ToggleButton; @@ -54,9 +56,11 @@ 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.MainWindowF; import de.uka.ilkd.key.gui.fx.configuration.ConfigF; import de.uka.ilkd.key.gui.fx.fonticons.IconFactoryF; import de.uka.ilkd.key.logic.label.TermLabel; +import de.uka.ilkd.key.macros.ProofMacro; import de.uka.ilkd.key.pp.HideSequentPrintFilter; import de.uka.ilkd.key.pp.IdentitySequentPrintFilter; import de.uka.ilkd.key.pp.IllegalRegexException; @@ -74,17 +78,20 @@ 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.settings.GeneralSettings; import de.uka.ilkd.key.settings.ProofIndependentSettings; import de.uka.ilkd.key.settings.ViewSettings; import org.key_project.logic.Name; import org.key_project.logic.Term; +import org.key_project.prover.sequent.PosInOccurrence; import org.key_project.prover.sequent.Semisequent; import org.key_project.prover.sequent.Sequent; import org.key_project.prover.sequent.SequentFormula; import org.key_project.util.collection.ImmutableList; import org.key_project.util.javafx.FxUtil; +import org.jspecify.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -687,12 +694,190 @@ private void showContextMenu(MouseEvent event) { } List entries = SequentMenuModelF.build(pos, menuMediator, menuProofControl, null, null); - ContextMenu menu = SequentTermContextMenuF.build(entries, + ContextMenu fallback = SequentTermContextMenuF.build(entries, new SequentTermContextMenuF.MenuContext(menuMediator, menuProofControl, goal, pos, null, this::printSequent)); + // menu: MP7 — the macro popup replaces the term menu while "Right Click for Proof Macros" + // is active ({@link #buildRightClickMenu} falls back to the term menu when no macro is + // applicable) + ContextMenu menu = buildRightClickMenu(pos, fallback); menu.show(textFlow, event.getScreenX(), event.getScreenY()); } + // menu: MP7 — begin — right-click proof-macro popup (Swing CurrentGoalViewListener.java:54-67 + // + ProofMacroMenu) + /** + * menu: MP7 — the right-click popup seam: with the "Right Click for Proof Macros" setting + * active the macro popup replaces the term context menu (Swing + * {@code CurrentGoalViewListener.mouseClicked}, CurrentGoalViewListener.java:53-67: {@code + * isRightClickMacro()} selects {@code ProofMacroMenu} over the taclet menu); when no macro is + * applicable the term menu is used (Swing politely adds a "No strategies available" label, + * CurrentGoalViewListener.java:62-64 / {@code ProofMacroMenu.isEmpty()}, ProofMacroMenu.java + * :160-162 — the term menu is the FX equivalent fallback). + * + * @param pos the clicked sequent position (already non-null for the interactive callers) + * @param fallback the term context menu built for the same position + * @return the popup to show + */ + private ContextMenu buildRightClickMenu(PosInSequent pos, ContextMenu fallback) { + if (ProofIndependentSettings.DEFAULT_INSTANCE.getGeneralSettings().isRightClickMacro()) { + ContextMenu macroMenu = buildMacroPopup(pos); + if (macroMenu != null) { + return macroMenu; + } + } + return fallback; + } + + /** + * menu: MP7 — builds the macro popup for the clicked position: one item per macro of the + * Automation submenu's {@link MainWindowF#AUTOMATION_MACROS} list that is applicable at the + * position (Swing {@code ProofMacroMenu} iterates the registered macros and keeps those with + * {@code canApplyTo}, ProofMacroMenu.java:87-99); item text = {@code macro.getName()}, tooltip + * = {@code macro.getDescription()} (ProofMacroMenu.java:143-144). The action runs the macro on + * the selected node with the {@link PosInOccurrence} of the clicked position (Swing + * {@code ProofMacroUserAction}, ProofMacroUserAction.java:57-59: {@code + * mediator.getUI().getProofControl().runMacro(node, macro, pio)}; the core silently ignores + * the run while auto mode is active, and the {@code pio} may be {@code null} — the position + * may resolve to no occurrence and global macros accept that). Returns {@code null} when no + * macro is applicable (the caller then falls back to the term menu). + * + * @param pos the clicked sequent position or {@code null} + * @return the macro popup, or {@code null} if no macro is applicable + */ + private ContextMenu buildMacroPopup(@Nullable PosInSequent pos) { + Node node = menuMediator.getSelectedNode(); + Goal goal = menuMediator.getSelectedGoal(); + if (node == null || goal == null) { + return null; + } + Proof proof = node.proof(); + PosInOccurrence pio = pos == null ? null : pos.getPosInOccurrence(); + ImmutableList goals = proof.getSubtreeEnabledGoals(node); + ContextMenu menu = new ContextMenu(); + int count = 0; + for (ProofMacro macro : MainWindowF.AUTOMATION_MACROS) { + if (macro.canApplyTo(proof, goals, pio)) { + // menu: MP7 — JavaFX MenuItem has no tooltip property (unlike Swing + // JMenuItem.setToolTipText, ProofMacroMenu.java:144); the item is a + // CustomMenuItem wrapping a tooltip-bearing Label, so the description actually + // appears on hover (the visible label also keeps the macro name readable) + Label label = new Label(macro.getName()); + Tooltip.install(label, new Tooltip(macro.getDescription())); + CustomMenuItem item = new CustomMenuItem(label); + item.setOnAction(e -> menuProofControl.runMacro(node, macro, pio)); + menu.getItems().add(item); + count++; + } + } + return count == 0 ? null : menu; + } + + /** + * menu: MP7 — the visible text of a menu item: the item text, or the content text of a + * {@link CustomMenuItem} (the macro popup uses label-backed custom items for the tooltips), + * or the empty string. + */ + private static String visibleText(MenuItem item) { + if (!item.getText().isEmpty()) { + return item.getText(); + } + if (item instanceof CustomMenuItem custom && custom.getContent() instanceof Label label) { + return label.getText(); + } + return ""; + } + + /** + * menu: MP7 — the labels of the given menu items (separators skipped, sub-menus flattened). + */ + private static List menuLabels(List items) { + List labels = new ArrayList<>(); + for (MenuItem item : items) { + if (!(item instanceof javafx.scene.control.SeparatorMenuItem)) { + String text = visibleText(item); + if (!text.isEmpty()) { + labels.add(text); + } + } + if (item instanceof javafx.scene.control.Menu subMenu) { + labels.addAll(menuLabels(subMenu.getItems())); + } + } + return labels; + } + + /** + * menu: MP7 — headless self test of the right-click popup ({@code + * key.fx.verify.rightclickmacro}), run after the demo load from MainWindowF: computes a + * {@link PosInSequent} of the current printing and builds the popup through the + * {@link #buildRightClickMenu} seam with the "Right Click for Proof Macros" flag ON and OFF + * (the persisted setting is restored afterwards). With the flag ON the popup must contain the + * macro names of the Automation submenu ({@link MainWindowF#AUTOMATION_MACROS}) and no + * term-menu entries; with the flag OFF the term-menu path must be taken. Uses the printed + * position table — no synthetic mouse events. Skips gracefully without a goal or position. + * + * @return {@code "PASS - ..."} or {@code "FAIL - ..."} + */ + public String verifyRightClickMacro() { + Goal goal = menuMediator == null ? null : menuMediator.getSelectedGoal(); + PosInSequent pos = firstIndexedPos(); + if (goal == null || pos == null) { + return "SKIP - no goal/position (no printed sequent)"; + } + List entries = + SequentMenuModelF.build(pos, menuMediator, menuProofControl, null, null); + ContextMenu fallback = SequentTermContextMenuF.build(entries, + new SequentTermContextMenuF.MenuContext(menuMediator, menuProofControl, goal, pos, + null, this::printSequent)); + GeneralSettings gs = ProofIndependentSettings.DEFAULT_INSTANCE.getGeneralSettings(); + boolean saved = gs.isRightClickMacro(); + try { + gs.setRightClickMacros(true); + ContextMenu on = buildRightClickMenu(pos, fallback); + gs.setRightClickMacros(false); + ContextMenu off = buildRightClickMenu(pos, fallback); + List onLabels = menuLabels(on.getItems()); + List offLabels = menuLabels(off.getItems()); + List macroNames = new ArrayList<>(); + for (ProofMacro macro : MainWindowF.AUTOMATION_MACROS) { + macroNames.add(macro.getName()); + } + boolean hasMacros = onLabels.containsAll(macroNames); + // the macro popup must not contain any term-menu entry + boolean noTermEntries = onLabels.stream().noneMatch(offLabels::contains); + // the OFF path is the term menu (fixed structural items of SequentTermContextMenuF) + boolean termPath = offLabels.contains("Apply rules automatically here") + || offLabels.contains("Copy to clipboard") + || offLabels.contains("No rules applicable."); + boolean pass = hasMacros && noTermEntries && termPath; + return (pass ? "PASS" : "FAIL") + " - on[" + String.join(", ", onLabels) + "] off[" + + String.join(", ", offLabels) + "]"; + } finally { + gs.setRightClickMacros(saved); + } + } + + /** + * menu: MP7 — 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 firstIndexedPos() { + String text = printedText(); + if (text == null) { + return null; + } + for (int i = 0; i < text.length(); i++) { + PosInSequent pos = getSequentPosAt(i); + if (pos != null) { + return pos; + } + } + return null; + } + // menu: MP7 — end + /** * The character index of the printed text under the given mouse event (the same mapping * {@link #handleMouseClick} uses), or {@code -1} if the event does not address a character. From 459eef344f2ab03983344ba703e951dc8543c3c8 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Fri, 9 Oct 2026 20:06:34 +0200 Subject: [PATCH 104/281] =?UTF-8?q?key.ui.fx:=20menu:=20MP7c=20=E2=80=94?= =?UTF-8?q?=20arm=20the=20AutoSaver=20from=20the=20Auto=20Save=20Proofs=20?= =?UTF-8?q?toggle=20and=20at=20startup=20(period>0),=20register=20it=20as?= =?UTF-8?q?=20ProverTaskListener=20on=20the=20UI=20control,=20hand=20it=20?= =?UTF-8?q?every=20newly=20selected=20proof=20via=20KeYMediatorF.setProof?= =?UTF-8?q?=20(Swing=20KeYMediator.setAutoSave/setProof=20parity);=20key.f?= =?UTF-8?q?x.verify.autosave=20self=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../de/uka/ilkd/key/core/fx/KeYMediatorF.java | 40 +++++++- .../de/uka/ilkd/key/gui/fx/MainWindowF.java | 96 ++++++++++++++++++- 2 files changed, 130 insertions(+), 6 deletions(-) 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 index 58701682c05..9fd93daa859 100644 --- 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 @@ -19,6 +19,7 @@ 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.util.collection.ImmutableList; @@ -42,8 +43,10 @@ * 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), the auto saver, user - * action listeners and the task/progress plumbing of the Swing mediator. + * 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 { @@ -68,6 +71,13 @@ public final class KeYMediatorF implements KeYSelectionModel.ProofBinder { /** 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; + /** * 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. @@ -118,9 +128,35 @@ public void setProof(Proof newProof, Proof previousProof) { 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 */ 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 index f7e28c9a427..106ef13d807 100644 --- 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 @@ -6,6 +6,7 @@ import java.io.File; import java.io.IOException; +import java.lang.reflect.Field; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; @@ -122,6 +123,7 @@ 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; @@ -428,6 +430,11 @@ public void initialize() { 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) @@ -888,6 +895,39 @@ protected KeYEnvironment call() throws Exception { .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); + } if (System.getProperty("key.fx.demo.autoprove.live") != null) { startLiveAutoMode(env); } @@ -2793,19 +2833,67 @@ private CheckMenuItem confirmExitToggle() { * (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: auto-save timer wiring deferred (no FX mediator support): Swing additionally calls - * getMediator().setAutoSave(p) (AutoSave.java:31); the FX KeYMediatorF has no setAutoSave, so - * only the persisted flag is written. + * // 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 -> gs.setAutoSave(item.isSelected() ? DEFAULT_AUTO_SAVE_PERIOD : 0)); + 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: MP4 — Swing {@code AutoSave.DEFAULT_PERIOD} (key.ui), see autoSaveProofsToggle(). */ private static final int DEFAULT_AUTO_SAVE_PERIOD = 2000; From 16fad828a9adc6545d0901be12ce718bcf1fef9a Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Fri, 9 Oct 2026 20:07:45 +0200 Subject: [PATCH 105/281] =?UTF-8?q?key.ui.fx:=20menu:=20MP7d=20=E2=80=94?= =?UTF-8?q?=20correct=20stale=20menu=20notes:=20Ensure=20Source=20Consiste?= =?UTF-8?q?ncy=20is=20honored=20at=20runtime=20(AbstractProblemLoader.java?= =?UTF-8?q?:400-409,=20SoundinessAnalyzer.java:377-383;=20only=20the=20Swi?= =?UTF-8?q?ng=20info=20dialog=20is=20dropped)=20and=20Right=20Click=20for?= =?UTF-8?q?=20Proof=20Macros=20is=20wired=20since=20MP7b?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/de/uka/ilkd/key/gui/fx/MainWindowF.java | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) 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 index 106ef13d807..0bb52b7efaa 100644 --- 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 @@ -2947,8 +2947,10 @@ private void applyMinimizeInteraction(@Nullable ProofControl pc) { * RightMouseClickToggleAction.java:22-33): the selected state mirrors * {@code GeneralSettings.isRightClickMacro()} and the action writes * {@code setRightClickMacros} back. - * // menu: Swing's direct-macro-on-right-click behavior is deferred — the FX right click - * currently always opens the term context menu; only the flag persists here. + * // 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(); @@ -2963,8 +2965,12 @@ private CheckMenuItem rightClickMacroToggle() { * EnsureSourceConsistencyToggleAction, EnsureSourceConsistencyToggleAction.java:37-48): the * selected state mirrors {@code GeneralSettings.isEnsureSourceConsistency()} and the action * writes {@code setEnsureSourceConsistency} back. - * // menu: Swing's info dialog is dropped — the FX port has no source-consistency machinery - * (no proof-bundle/source-cache backend); only the flag persists for now. + * // 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). Only the + * // Swing info dialog of the toggle action (EnsureSourceConsistencyToggleAction.java:42-47) + * // is dropped. */ private CheckMenuItem ensureSourceConsistencyToggle() { GeneralSettings gs = ProofIndependentSettings.DEFAULT_INSTANCE.getGeneralSettings(); From 088f1240518ba530eca8ae806a234b9145bfa4dd Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Fri, 9 Oct 2026 20:35:43 +0200 Subject: [PATCH 106/281] =?UTF-8?q?key.ui.fx:=20menu:=20MP7b=20=E2=80=94?= =?UTF-8?q?=20fix=20two=20follow-up=20bugs:=20re-add=20Start/Stop=20Automa?= =?UTF-8?q?tic=20Proof=20to=20the=20Automation=20submenu=20(dropped=20in?= =?UTF-8?q?=20the=20AUTOMATION=5FMACROS=20loop=20refactor;=20menuparity=20?= =?UTF-8?q?Proof=20FAIL)=20and=20null-guard=20MenuItem.getText()=20in=20th?= =?UTF-8?q?e=20right-click=20verify=20(term-menu=20placeholders=20have=20n?= =?UTF-8?q?ull=20text;=20NPE=20in=20key.fx.verify.rightclickmacro)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/de/uka/ilkd/key/gui/fx/MainWindowF.java | 2 ++ .../de/uka/ilkd/key/gui/fx/nodeviews/SequentViewF.java | 8 +++++--- 2 files changed, 7 insertions(+), 3 deletions(-) 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 index 0bb52b7efaa..5c1a59926d4 100644 --- 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 @@ -2369,6 +2369,8 @@ private Menu buildProofMenu() { 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 diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentViewF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentViewF.java index 65de20c26c6..95f51583910 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentViewF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentViewF.java @@ -776,11 +776,13 @@ private ContextMenu buildMacroPopup(@Nullable PosInSequent pos) { /** * menu: MP7 — the visible text of a menu item: the item text, or the content text of a * {@link CustomMenuItem} (the macro popup uses label-backed custom items for the tooltips), - * or the empty string. + * or the empty string. {@code getText()} may be {@code null} (e.g. separators/Swing-ish + * placeholder items of the term menu), which counts as empty. */ private static String visibleText(MenuItem item) { - if (!item.getText().isEmpty()) { - return item.getText(); + String text = item.getText(); + if (text != null && !text.isEmpty()) { + return text; } if (item instanceof CustomMenuItem custom && custom.getContent() instanceof Label label) { return label.getText(); From 7edc849518fa68e549c617333932e53b8f5a5358 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Fri, 9 Oct 2026 21:44:25 +0200 Subject: [PATCH 107/281] =?UTF-8?q?key.ui.fx:=20menu:=20MP8a=20=E2=80=94?= =?UTF-8?q?=20wire=20Apply=20rules=20automatically=20here=20(focus=5Fauto?= =?UTF-8?q?=5Fmode)=20to=20ProofControl.startFocussedAutoMode=20(Swing=20F?= =?UTF-8?q?ocussedAutoModeUserAction.java:43;=20FX=20single-caret=20view,?= =?UTF-8?q?=20no=20caret=20state=20to=20read)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../fx/nodeviews/SequentTermContextMenuF.java | 31 ++++++++++++++++--- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentTermContextMenuF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentTermContextMenuF.java index 52818c1afae..1ad3e050a75 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentTermContextMenuF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentTermContextMenuF.java @@ -153,10 +153,12 @@ private static MenuItem namedActionItem(NamedAction action, MenuContext ctx) { return switch (action.id()) { case "join" -> joinItem(action, ctx); case "merge_rule" -> mergeItem(action, ctx); - // termmenu: TODO wire the focused-auto-mode activation (Swing - // FocussedRuleApplicationAction) once the shift-click hit-test wiring lands (S3); - // for now the item is a faithful disabled placeholder. - case "focus_auto_mode" -> disabledItem(action.label()); + // menu: MP8 — the focused-auto-mode activation is wired (Swing + // FocussedRuleApplicationAction, FocussedAutoModeUserAction.java:43: {@code + // mediator.getUI().getProofControl().startFocussedAutoMode(pio, goal)}); unlike the + // Swing entry there is no separate caret to remember — the clicked position is the + // focus, mirroring the shift+left-click fast path (CurrentGoalViewListener.java:57). + case "focus_auto_mode" -> focusAutoModeItem(action, ctx); case "copy_clipboard" -> copyClipboardItem(ctx); case "name_creation_info" -> nameCreationInfoItem(action, ctx); case "no_rules" -> disabledItem(action.label()); @@ -168,6 +170,27 @@ private static MenuItem namedActionItem(NamedAction action, MenuContext ctx) { }; } + /** + * The focused-auto-mode entry: starts an automatic proof search restricted to the clicked + * position (Swing FocussedRuleApplicationAction, FocussedAutoModeUserAction.java:43: {@code + * mediator.getUI().getProofControl().startFocussedAutoMode(pio, goal)}). The FX sequent view + * is single-caret, so the clicked position is the focus — no separate caret state to + * read (the Swing action takes it from {@code SequentView.getCaretPosition()}, + * FocussedAutoModeUserAction.java:39-40). + */ + private static MenuItem focusAutoModeItem(NamedAction action, MenuContext ctx) { + MenuItem item = new MenuItem(action.label()); + item.setOnAction(e -> { + Goal goal = ctx.goal(); + PosInOccurrence pio = ctx.pos() == null ? null : ctx.pos().getPosInOccurrence(); + if (goal != null && ctx.proofControl() != null) { + ctx.proofControl().startFocussedAutoMode(pio, goal); + ctx.afterApply().run(); + } + }); + return item; + } + /** Delayed-cut join (Swing JoinMenuItem / CurrentGoalViewMenu.createDelayedCutJoinMenu). */ private static MenuItem joinItem(NamedAction action, MenuContext ctx) { MenuItem item = new MenuItem(action.label()); From 65d0932d8ed8bc8275bc0c06da1f9f08f9f2ad6f Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Fri, 9 Oct 2026 21:47:19 +0200 Subject: [PATCH 108/281] =?UTF-8?q?key.ui.fx:=20menu:=20MP8b=20=E2=80=94?= =?UTF-8?q?=20wire=20the=20term-menu=20Strategy=20Macros=20section=20(Swin?= =?UTF-8?q?g=20ProofMacroMenu/CurrentGoalViewMenu.addMacroMenu):=20shared?= =?UTF-8?q?=20ProofMacroMenuF=20item=20builder=20(name=20label=20+=20toolt?= =?UTF-8?q?ip=20CustomMenuItem)=20reused=20by=20the=20right-click=20popup?= =?UTF-8?q?=20(MP7)=20and=20the=20term=20menu,=20each=20macro=20runs=20Pro?= =?UTF-8?q?ofControl.runMacro=20on=20the=20selected=20node=20at=20the=20cl?= =?UTF-8?q?icked=20position?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../key/gui/fx/nodeviews/ProofMacroMenuF.java | 55 +++++++++++++++++++ .../fx/nodeviews/SequentTermContextMenuF.java | 43 +++++++++++++-- .../key/gui/fx/nodeviews/SequentViewF.java | 13 ++--- 3 files changed, 98 insertions(+), 13 deletions(-) create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/ProofMacroMenuF.java diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/ProofMacroMenuF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/ProofMacroMenuF.java new file mode 100644 index 00000000000..920e10f0161 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/ProofMacroMenuF.java @@ -0,0 +1,55 @@ +/* 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.nodeviews; + +import javafx.scene.control.CustomMenuItem; +import javafx.scene.control.Label; +import javafx.scene.control.MenuItem; +import javafx.scene.control.Tooltip; + +import de.uka.ilkd.key.control.ProofControl; +import de.uka.ilkd.key.macros.ProofMacro; +import de.uka.ilkd.key.proof.Node; + +import org.key_project.prover.sequent.PosInOccurrence; + +/** + * menu: MP8 — shared construction of one proof-macro menu item (Swing {@code ProofMacroMenu} + * createMenuItem, ProofMacroMenu.java:143-144: item text = {@code macro.getName()}, tooltip = + * {@code macro.getDescription()}). Used by the sequent-view right-click macro popup (MP7, + * {@link SequentViewF#buildMacroPopup}) and by the term-menu "Strategy Macros" section (MP8b, + * {@link SequentTermContextMenuF}) so both present the very same macros with the same look. + *

        + * JavaFX {@link MenuItem} has no tooltip property (unlike Swing {@code + * JMenuItem.setToolTipText}), so the item is a {@link CustomMenuItem} wrapping a + * tooltip-bearing {@link Label}. + */ +final class ProofMacroMenuF { + + private ProofMacroMenuF() { + } + + /** + * menu: MP8 — a single macro item whose action runs the macro on the selected node at the + * given position (Swing {@code ProofMacroUserAction}, ProofMacroUserAction.java:57-59: + * {@code mediator.getUI().getProofControl().runMacro(node, macro, pio)}; the core silently + * ignores the run while auto mode is active, and {@code pio} may be {@code null} — a + * sequent position may resolve to no occurrence and global macros accept that). + * + * @param macro the macro to run + * @param node the node the macro is started at (the mediator's selection) + * @param proofControl the proof control to run the macro with + * @param pio the clicked {@link PosInOccurrence}, possibly {@code null} + * @return the wired macro menu item + */ + static MenuItem itemFor(ProofMacro macro, Node node, ProofControl proofControl, + PosInOccurrence pio) { + Label label = new Label(macro.getName()); + Tooltip.install(label, new Tooltip(macro.getDescription())); + CustomMenuItem item = new CustomMenuItem(label); + item.setOnAction(e -> proofControl.runMacro(node, macro, pio)); + return item; + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentTermContextMenuF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentTermContextMenuF.java index 1ad3e050a75..b37ba1e3db5 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentTermContextMenuF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentTermContextMenuF.java @@ -21,6 +21,7 @@ import de.uka.ilkd.key.control.ProofControl; import de.uka.ilkd.key.core.fx.KeYMediatorF; +import de.uka.ilkd.key.gui.fx.MainWindowF; import de.uka.ilkd.key.gui.fx.join.JoinActionF; import de.uka.ilkd.key.gui.fx.mergerule.MergeRuleMenuItemF; import de.uka.ilkd.key.gui.fx.nodeviews.SequentMenuModelF.AbbrevActionEntry; @@ -33,10 +34,12 @@ import de.uka.ilkd.key.logic.NameCreationInfo; import de.uka.ilkd.key.logic.ProgramElementName; import de.uka.ilkd.key.logic.op.ProgramVariable; +import de.uka.ilkd.key.macros.ProofMacro; import de.uka.ilkd.key.pp.AbbrevException; import de.uka.ilkd.key.pp.AbbrevMap; import de.uka.ilkd.key.pp.PosInSequent; 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.join.ProspectivePartner; @@ -162,10 +165,17 @@ private static MenuItem namedActionItem(NamedAction action, MenuContext ctx) { case "copy_clipboard" -> copyClipboardItem(ctx); case "name_creation_info" -> nameCreationInfoItem(action, ctx); case "no_rules" -> disabledItem(action.label()); - // termmenu: TODO these sections need the FX macro list (ProofMacroMenu), extension - // registry (KeYGuiExtensionFacade) and SMT-launch plumbing — deferred milestones. - // They are rendered as disabled placeholders so the menu structure stays faithful. - case "macro_menu", "extension", "smt" -> disabledItem(action.label()); + // menu: MP8 — the Strategy Macros section is wired (Swing ProofMacroMenu, + // ProofMacroMenu.java:81: JMenu("Strategy Macros") with one item per applicable + // macro); "extension" and "smt" notes follow below. + case "macro_menu" -> macroMenu(action, ctx); + // menu: MP8 — KNOWN-DEFERRED extension section: needs the KeYGuiExtensionFacade + // registry of the external keyext modules (no FX counterpart, keyext modules are + // Swing UIs); stays a disabled placeholder so the menu structure stays faithful. + case "extension" -> disabledItem(action.label()); + // menu: MP8 — SMT section (Swing CurrentGoalViewMenu.createSMTMenu, + // CurrentGoalViewMenu.java:219-231): wired in MP8c. + case "smt" -> disabledItem(action.label()); default -> disabledItem(action.label()); }; } @@ -191,6 +201,31 @@ private static MenuItem focusAutoModeItem(NamedAction action, MenuContext ctx) { return item; } + /** + * menu: MP8 — the "Strategy Macros" section (Swing {@code ProofMacroMenu}, a {@code + * JMenu("Strategy Macros")}, ProofMacroMenu.java:81, with one item per applicable macro; + * CurrentGoalViewMenu.addMacroMenu adds it to the term menu, CurrentGoalViewMenu.java: + * 212-217). The FX model emits the section unconditionally (SequentMenuModelF.compute), so + * all four macros of the Automation submenu / right-click popup ({@code + * MainWindowF.AUTOMATION_MACROS}, same order as Swing MainWindow.createAutomationActions, + * MainWindow.java:814-827) are shown — Swing instead filters by {@code canApplyTo} and + * omits the whole menu when it is empty; with the model's fixed skeleton the section keeps + * its label either way. Each item is built by the shared {@link ProofMacroMenuF} helper. + */ + private static MenuItem macroMenu(NamedAction action, MenuContext ctx) { + Node node = ctx.mediator() == null ? null : ctx.mediator().getSelectedNode(); + if (node == null || ctx.proofControl() == null) { + // no proof context: keep the faithful disabled placeholder + return disabledItem(action.label()); + } + Menu menu = new Menu(action.label()); + PosInOccurrence pio = ctx.pos() == null ? null : ctx.pos().getPosInOccurrence(); + for (ProofMacro macro : MainWindowF.AUTOMATION_MACROS) { + menu.getItems().add(ProofMacroMenuF.itemFor(macro, node, ctx.proofControl(), pio)); + } + return menu; + } + /** Delayed-cut join (Swing JoinMenuItem / CurrentGoalViewMenu.createDelayedCutJoinMenu). */ private static MenuItem joinItem(NamedAction action, MenuContext ctx) { MenuItem item = new MenuItem(action.label()); diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentViewF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentViewF.java index 95f51583910..13aa4c36f31 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentViewF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentViewF.java @@ -758,15 +758,10 @@ private ContextMenu buildMacroPopup(@Nullable PosInSequent pos) { int count = 0; for (ProofMacro macro : MainWindowF.AUTOMATION_MACROS) { if (macro.canApplyTo(proof, goals, pio)) { - // menu: MP7 — JavaFX MenuItem has no tooltip property (unlike Swing - // JMenuItem.setToolTipText, ProofMacroMenu.java:144); the item is a - // CustomMenuItem wrapping a tooltip-bearing Label, so the description actually - // appears on hover (the visible label also keeps the macro name readable) - Label label = new Label(macro.getName()); - Tooltip.install(label, new Tooltip(macro.getDescription())); - CustomMenuItem item = new CustomMenuItem(label); - item.setOnAction(e -> menuProofControl.runMacro(node, macro, pio)); - menu.getItems().add(item); + // menu: MP7/MP8 — the item construction (name label + description tooltip via + // CustomMenuItem, since JavaFX MenuItem has no tooltip property) is shared with + // the term-menu "Strategy Macros" section, see {@link ProofMacroMenuF#itemFor}. + menu.getItems().add(ProofMacroMenuF.itemFor(macro, node, menuProofControl, pio)); count++; } } From f8e3c3e1d934e35df37a28634bdc3d5c1d8ce1a5 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Fri, 9 Oct 2026 21:48:49 +0200 Subject: [PATCH 109/281] =?UTF-8?q?key.ui.fx:=20menu:=20MP8c=20=E2=80=94?= =?UTF-8?q?=20wire=20the=20SMT=20section:=20each=20solver=20union=20runs?= =?UTF-8?q?=20the=20core=20SolverLauncher=20on=20the=20clicked=20goal=20in?= =?UTF-8?q?=20a=20daemon=20thread=20(Swing=20CurrentGoalViewMenu.SMTAction?= =?UTF-8?q?=20:765-790)=20and=20shows=20a=20read-only=20result=20dialog;?= =?UTF-8?q?=20porting=20the=20Swing=20FullSmtSolverDialog=20progress/count?= =?UTF-8?q?ermodel=20UI=20is=20out=20of=20scope?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../fx/nodeviews/SequentTermContextMenuF.java | 101 +++++++++++++++++- 1 file changed, 98 insertions(+), 3 deletions(-) diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentTermContextMenuF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentTermContextMenuF.java index b37ba1e3db5..f965e72eef2 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentTermContextMenuF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentTermContextMenuF.java @@ -7,6 +7,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.List; +import javafx.application.Platform; import javafx.scene.control.Alert; import javafx.scene.control.Alert.AlertType; import javafx.scene.control.ButtonType; @@ -42,6 +43,15 @@ import de.uka.ilkd.key.proof.Node; import de.uka.ilkd.key.proof.Proof; import de.uka.ilkd.key.proof.join.ProspectivePartner; +import de.uka.ilkd.key.settings.DefaultSMTSettings; +import de.uka.ilkd.key.settings.ProofIndependentSettings; +import de.uka.ilkd.key.smt.SMTProblem; +import de.uka.ilkd.key.smt.SMTSolver; +import de.uka.ilkd.key.smt.SMTSolverResult; +import de.uka.ilkd.key.smt.SolverLauncher; +import de.uka.ilkd.key.smt.SolverLauncherListener; +import de.uka.ilkd.key.smt.SolverTypeCollection; +import de.uka.ilkd.key.smt.solvertypes.SolverType; import org.key_project.prover.sequent.PosInOccurrence; @@ -173,9 +183,9 @@ private static MenuItem namedActionItem(NamedAction action, MenuContext ctx) { // registry of the external keyext modules (no FX counterpart, keyext modules are // Swing UIs); stays a disabled placeholder so the menu structure stays faithful. case "extension" -> disabledItem(action.label()); - // menu: MP8 — SMT section (Swing CurrentGoalViewMenu.createSMTMenu, - // CurrentGoalViewMenu.java:219-231): wired in MP8c. - case "smt" -> disabledItem(action.label()); + // menu: MP8 — SMT section item, wired in MP8c (Swing CurrentGoalViewMenu. + // createSMTMenu, CurrentGoalViewMenu.java:219-231, SMTAction :765-790). + case "smt" -> smtItem(action, ctx); default -> disabledItem(action.label()); }; } @@ -422,6 +432,91 @@ private static MenuItem disabledItem(String label) { return item; } + /** + * menu: MP8 — the SMT section item: runs the given solver union on the clicked goal's + * sequent (Swing {@code CurrentGoalViewMenu.SMTAction}, CurrentGoalViewMenu.java:765-790: a + * background thread builds a {@code DefaultSMTSettings}, a {@code SolverLauncher} with a + * listener, an {@code SMTProblem} for the goal and launches the union's solver types on the + * goal's services, then shows the outcome). Swing presents the outcome in the heavy + * {@code FullSmtSolverDialog} (progress dialog + countermodel application); the FX minimum + * shows a read-only result dialog with each solver's outcome and the combined final result — + * the counterexample-application UI of the Swing dialog is not ported. + */ + private static MenuItem smtItem(NamedAction action, MenuContext ctx) { + MenuItem item = new MenuItem(action.label()); + item.setOnAction(e -> runSmt(action, ctx)); + return item; + } + + /** + * menu: MP8 — launches the solver union of the SMT item on the clicked goal (Swing + * CurrentGoalViewMenu.SMTAction.actionPerformed, CurrentGoalViewMenu.java:773-789). The + * launch is synchronous and blocking (SolverLauncher.launch waits for every solver), so it + * runs in a daemon thread; the read-only result dialog is presented back on the FX thread. + */ + private static void runSmt(NamedAction action, MenuContext ctx) { + Goal goal = ctx.goal(); + if (goal == null || !(action.payload() instanceof SolverTypeCollection union)) { + return; + } + Window owner = ctx.owner(); + Thread thread = new Thread(() -> { + DefaultSMTSettings settings = + new DefaultSMTSettings(goal.proof().getSettings().getSMTSettings(), + ProofIndependentSettings.DEFAULT_INSTANCE.getSMTSettings(), + goal.proof().getSettings().getNewSMTSettings(), goal.proof()); + SolverLauncher launcher = new SolverLauncher(settings); + // a listener suppresses the launcher's SolverException for failing solvers + // (SolverLauncher.notifyListenersOfStop, SolverLauncher.java:370-376); Swing's + // SolverListener plays the same role (CurrentGoalViewMenu.java:783) + launcher.addListener(new SolverLauncherListener() { + @Override + public void launcherStopped(SolverLauncher launcher, + Collection finishedSolvers) { + } + + @Override + public void launcherStarted(Collection problems, + Collection solverTypes, SolverLauncher launcher) { + } + }); + SMTProblem problem = new SMTProblem(goal); + String report; + try { + launcher.launch(union.getTypes(), List.of(problem), + goal.proof().getServices()); + StringBuilder sb = new StringBuilder(); + for (SMTSolver solver : problem.getSolvers()) { + SMTSolverResult result = solver.getFinalResult(); + sb.append(solver.name()).append(": ") + .append(result == null ? "no result" : result.isValid()).append("\n"); + } + sb.append("\n").append(problem.getFinalResult().isValid()); + report = sb.toString(); + } catch (RuntimeException ex) { + report = "SMT run failed: " + ex; + } + String finalReport = report; + Platform.runLater(() -> showSmtResult(union.toString(), finalReport, owner)); + }, "SMTRunner"); + thread.setDaemon(true); + thread.start(); + } + + /** + * menu: MP8 — read-only SMT result dialog (the FX minimum replacing the heavy Swing + * FullSmtSolverDialog progress/countermodel UI). + */ + private static void showSmtResult(String unionName, String report, Window owner) { + Alert alert = new Alert(AlertType.INFORMATION, report, ButtonType.OK); + alert.setTitle("SMT: " + unionName); + alert.setHeaderText(null); + if (owner != null) { + alert.initOwner(owner); + } + alert.showAndWait(); + } + private static void showError(String message, String title, Window owner) { Alert alert = new Alert(AlertType.ERROR, message, ButtonType.OK); alert.setTitle(title); From 87911344c70f41090a3b8af48f4d87048e2198c1 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Fri, 9 Oct 2026 21:50:10 +0200 Subject: [PATCH 110/281] =?UTF-8?q?key.ui.fx:=20menu:=20MP8=20=E2=80=94=20?= =?UTF-8?q?add=20the=20key.fx.verify.termmenuwiring=20self=20test:=20same?= =?UTF-8?q?=20seam=20as=20key.fx.verify.termmenu,=20asserts=20the=20MP8a?= =?UTF-8?q?=20focus=5Fauto=5Fmode=20item=20is=20enabled=20with=20a=20non-n?= =?UTF-8?q?ull=20handler=20and=20the=20MP8b=20Strategy=20Macros=20section?= =?UTF-8?q?=20holds=20exactly=20the=20four=20AUTOMATION=5FMACROS=20(handle?= =?UTF-8?q?rs=20not=20invoked)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../de/uka/ilkd/key/gui/fx/MainWindowF.java | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) 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 index 5c1a59926d4..27618f3f206 100644 --- 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 @@ -28,6 +28,7 @@ import javafx.scene.control.CheckBox; import javafx.scene.control.CheckMenuItem; import javafx.scene.control.ContextMenu; +import javafx.scene.control.CustomMenuItem; import javafx.scene.control.Label; import javafx.scene.control.Menu; import javafx.scene.control.MenuBar; @@ -860,6 +861,15 @@ protected KeYEnvironment call() throws Exception { 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 @@ -1762,6 +1772,80 @@ private void runTermMenuVerification(KeYEnvironment + menu.getItems().size() + " items, found: " + String.join(", ", found)); } + /** + * menu: MP8 — 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) exists and contains exactly the four macro names of + * the Automation submenu ({@link #AUTOMATION_MACROS}, same order). 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()); + } + boolean macroOk = macroNames.equals(expected); + 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 + "]"); + } + + /** + * 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 From 95447c0f09d7ce7e470680444ee0cafd1c9312df Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Fri, 9 Oct 2026 21:52:45 +0200 Subject: [PATCH 111/281] =?UTF-8?q?key.ui.fx:=20menu:=20MP8b=20=E2=80=94?= =?UTF-8?q?=20fix=20the=20right-click=20verify=20for=20the=20new=20Strateg?= =?UTF-8?q?y=20Macros=20term-menu=20section:=20exclude=20the=20shared=20AU?= =?UTF-8?q?TOMATION=5FMACROS=20names=20from=20the=20no-term-entry=20check?= =?UTF-8?q?=20(the=20term=20menu=20now=20legitimately=20holds=20them;=20po?= =?UTF-8?q?pup=20behavior=20unchanged)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../de/uka/ilkd/key/gui/fx/nodeviews/SequentViewF.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentViewF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentViewF.java index 13aa4c36f31..bc5bb1b8aa2 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentViewF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentViewF.java @@ -841,8 +841,13 @@ public String verifyRightClickMacro() { macroNames.add(macro.getName()); } boolean hasMacros = onLabels.containsAll(macroNames); - // the macro popup must not contain any term-menu entry - boolean noTermEntries = onLabels.stream().noneMatch(offLabels::contains); + // menu: MP8 — the macro popup must not contain any term-menu entry other than the + // shared macro names: since MP8b the term menu has a "Strategy Macros" section with + // the very same four macros, so the names are excluded from the comparison (the + // MP7b term menu had them only in the macro popup). + boolean noTermEntries = onLabels.stream() + .filter(l -> !macroNames.contains(l)) + .noneMatch(offLabels::contains); // the OFF path is the term menu (fixed structural items of SequentTermContextMenuF) boolean termPath = offLabels.contains("Apply rules automatically here") || offLabels.contains("Copy to clipboard") From 91de410a33519fea5ebb2fa2464804d0ef3dd05a Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Fri, 9 Oct 2026 22:39:21 +0200 Subject: [PATCH 112/281] =?UTF-8?q?key.ui.fx:=20extension:=20MP9.0=20?= =?UTF-8?q?=E2=80=94=20FX-native=20KeYGuiExtensionF=20SPI=20(marker=20+=20?= =?UTF-8?q?@Info=20+=20MainMenuF/ToolbarF/StatusLineF/LeftPanelF/ContextMe?= =?UTF-8?q?nuF/SettingsF/TooltipF/StartupF),=20ServiceLoader=20facade=20Ke?= =?UTF-8?q?YGuiExtensionFacadeF,=20host=20integration=20in=20MainWindowF?= =?UTF-8?q?=20(extension=20menus=20after=20About,=20third=20toolbar,=20sta?= =?UTF-8?q?tus-line=20controls,=20left-panel=20dockables,=20settings=20reg?= =?UTF-8?q?istration,=20StartupF.init)=20and=20the=20term-menu=20extension?= =?UTF-8?q?=20section=20in=20SequentTermContextMenuF;=20key.fx.verify.exte?= =?UTF-8?q?nsions=20self=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../de/uka/ilkd/key/gui/fx/MainWindowF.java | 161 +++++++++++- .../fx/extension/KeYGuiExtensionFacadeF.java | 239 ++++++++++++++++++ .../fx/extension/api/KeYGuiExtensionF.java | 198 +++++++++++++++ .../gui/fx/extension/contrib/HeatmapF.java | 105 ++++++++ .../contrib/HeatmapSettingsProviderF.java | 139 ++++++++++ .../ParallelProverStatusIndicatorF.java | 92 +++++++ .../contrib/ProfileNameInStatusBarF.java | 69 +++++ .../fx/nodeviews/SequentTermContextMenuF.java | 43 +++- .../key/gui/fx/settings/SettingsManagerF.java | 11 + ....key.gui.fx.extension.api.KeYGuiExtensionF | 3 + 10 files changed, 1045 insertions(+), 15 deletions(-) create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/extension/KeYGuiExtensionFacadeF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/extension/api/KeYGuiExtensionF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/extension/contrib/HeatmapF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/extension/contrib/HeatmapSettingsProviderF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/extension/contrib/ParallelProverStatusIndicatorF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/extension/contrib/ProfileNameInStatusBarF.java create mode 100644 key.ui.fx/src/main/resources/META-INF/services/de.uka.ilkd.key.gui.fx.extension.api.KeYGuiExtensionF 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 index 27618f3f206..1b30b4903e7 100644 --- 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 @@ -28,6 +28,7 @@ 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; @@ -36,6 +37,7 @@ import javafx.scene.control.OverrunStyle; import javafx.scene.control.RadioMenuItem; import javafx.scene.control.SeparatorMenuItem; +import javafx.scene.control.Tab; import javafx.scene.control.TextArea; import javafx.scene.control.ToggleGroup; import javafx.scene.control.ToolBar; @@ -71,6 +73,7 @@ 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.SimpleDockable; +import de.uka.ilkd.key.gui.fx.extension.KeYGuiExtensionFacadeF; 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; @@ -99,6 +102,7 @@ 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.soundiness.SoundinessAnalyzer; import de.uka.ilkd.key.gui.fx.soundiness.SoundinessDialogF; @@ -483,6 +487,17 @@ public void initialize() { report.endsWith("PASS") ? Kind.INFO : Kind.ERROR); } + // 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() + "."); @@ -938,6 +953,14 @@ protected KeYEnvironment call() throws Exception { .notify("Auto save verification: " + (ok ? "PASS" : "FAIL"), ok ? Kind.INFO : Kind.ERROR); } + // 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); + } if (System.getProperty("key.fx.demo.autoprove.live") != null) { startLiveAutoMode(env); } @@ -1831,6 +1854,74 @@ private void runTermMenuWiringVerification(KeYEnvironmentno position is available + * (the fallback of SequentTermContextMenuF; contributed items would be enabled only with a + * position). One stdout report line; skips the term-menu sub-assertion gracefully when no + * goal is loaded. + * + * @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); + pass &= discovered == 3; + + List statusControls = KeYGuiExtensionFacadeF.getStatusLineControls(); + sb.append(" statusControls=").append(statusControls.size()); + HBox statusBar = buildStatusBar(); + pass &= statusControls.size() == 2 + && statusBar.getChildren().containsAll(statusControls); + + 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; + + // 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); + + String report = (pass ? "PASS" : "FAIL") + " - " + sb; + System.out.println("Extension verification: " + report); + LOGGER.info("Extension 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). @@ -2009,6 +2100,15 @@ private void buildDockables() { dockables.put(ID_SEQUENT, new SimpleDockable(ID_SEQUENT, "Sequent", sequentView)); dockables.put(ID_SOURCE_VIEW, new SimpleDockable(ID_SOURCE_VIEW, "Source", buildSourceViewContent())); + // extension: MP9.0 — register each left-panel Tab contributed by the FX extensions as + // a dockable (Swing KeYGuiExtension.LeftPanel returns tabs for the left JTabbedPane, + // KeYGuiExtension.java:127-143; the FX host mirrors the built-in views' registration + // pattern, so extension panels appear in the docking layout and its persistence). + int extensionPanel = 0; + for (Tab tab : KeYGuiExtensionFacadeF.getLeftPanelTabs(this, mediator)) { + String id = "extension-left-panel-" + extensionPanel++; + dockables.put(id, new SimpleDockable(id, tab.getText(), tab.getContent())); + } } /** @@ -2024,14 +2124,27 @@ private Node buildSourceViewContent() { } private List defaultLayout() { - return List.of(new DockWorkspace.Default(DockLocation.LEFT, - requireDockable(ID_LOADED_PROOFS)), - new DockWorkspace.Default(DockLocation.LEFT, requireDockable(ID_GOAL_LIST)), - new DockWorkspace.Default(DockLocation.LEFT, requireDockable(ID_PROOF_TREE)), - new DockWorkspace.Default(DockLocation.LEFT, requireDockable(ID_INFO_VIEW)), - new DockWorkspace.Default(DockLocation.LEFT, requireDockable(ID_STRATEGY)), - new DockWorkspace.Default(DockLocation.MAIN, requireDockable(ID_SEQUENT)), + List defaults = new ArrayList<>(); + defaults.add(new DockWorkspace.Default(DockLocation.LEFT, + requireDockable(ID_LOADED_PROOFS))); + defaults.add(new DockWorkspace.Default(DockLocation.LEFT, requireDockable(ID_GOAL_LIST))); + defaults.add( + new DockWorkspace.Default(DockLocation.LEFT, requireDockable(ID_PROOF_TREE))); + defaults.add(new DockWorkspace.Default(DockLocation.LEFT, requireDockable(ID_INFO_VIEW))); + defaults.add( + new DockWorkspace.Default(DockLocation.LEFT, requireDockable(ID_STRATEGY))); + defaults.add(new DockWorkspace.Default(DockLocation.MAIN, requireDockable(ID_SEQUENT))); + defaults.add( new DockWorkspace.Default(DockLocation.RIGHT, requireDockable(ID_SOURCE_VIEW))); + // extension: MP9.0 — the extension left-panel dockables (registered in + // {@link #buildDockables()}) appear at the bottom of the left docking area in the + // factory-default layout; without LeftPanelF providers this appends nothing. + for (Dockable dockable : dockables.values()) { + if (dockable.getId().startsWith("extension-left-panel-")) { + defaults.add(new DockWorkspace.Default(DockLocation.LEFT, dockable)); + } + } + return defaults; } private void restoreLayout() { @@ -2066,6 +2179,19 @@ 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; @@ -2075,6 +2201,18 @@ 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). The five built-in menus' item sets are + // untouched: key.fx.verify.menuparity keeps asserting 16/24/12/7/5. + List extensionMenus = + KeYGuiExtensionFacadeF.getMenus(this, mediator); + if (!extensionMenus.isEmpty()) { + menuBar.getMenus().addAll(extensionMenus); + } return menuBar; } @@ -3196,6 +3334,15 @@ private HBox buildStatusBar() { statusRight.setMinWidth(0); statusRight.setMinHeight(0); bar.getChildren().addAll(statusLeft, spacer, statusRight); + // extension: MP9.0 — the status-line controls contributed by the FX extensions are + // appended at the right end of the status bar, after the theme/font-size label (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; } diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/extension/KeYGuiExtensionFacadeF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/extension/KeYGuiExtensionFacadeF.java new file mode 100644 index 00000000000..5a7b2dedc9d --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/extension/KeYGuiExtensionFacadeF.java @@ -0,0 +1,239 @@ +/* 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.extension; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; +import java.util.ServiceLoader; +import java.util.stream.Collectors; +import javafx.scene.control.Control; +import javafx.scene.control.Menu; +import javafx.scene.control.MenuItem; +import javafx.scene.control.Tab; + +import de.uka.ilkd.key.core.fx.KeYMediatorF; +import de.uka.ilkd.key.gui.fx.MainWindowF; +import de.uka.ilkd.key.gui.fx.extension.api.KeYGuiExtensionF; +import de.uka.ilkd.key.gui.fx.settings.SettingsProviderF; +import de.uka.ilkd.key.pp.PosInSequent; +import de.uka.ilkd.key.proof.Goal; + +import org.jspecify.annotations.NullMarked; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Facade for retrieving the FX GUI extensions, counter-part of {@code + * de.uka.ilkd.key.gui.extension.impl.KeYGuiExtensionFacade} of the Swing module {@code key.ui} + * (KeYGuiExtensionFacade.java:36-461). Discovers all {@link KeYGuiExtensionF} providers lazily + * (cached on first use) and aggregates the contributions of the typed capability interfaces. + * Never returns {@code null} — empty lists instead. + * + * @author Alexander Weigl + * @author MP9.0 extension SPI port (FX) + */ +@NullMarked +public final class KeYGuiExtensionFacadeF { + + private static final Logger LOGGER = LoggerFactory.getLogger(KeYGuiExtensionFacadeF.class); + + private static List extensions; + + private KeYGuiExtensionFacadeF() { + } + + /** + * The discovered extension providers, loaded lazily on first use and cached (Swing + * {@code KeYGuiExtensionFacade.getExtensions}, KeYGuiExtensionFacade.java:318-323). The + * providers are sorted by their {@link KeYGuiExtensionF.Info#priority()} (lowest first, + * baseline zero), mirroring the Swing priority sort of the main-menu actions + * (KeYGuiExtensionFacade.java:63-76). + * + * @return the non-null, possibly empty list of providers + */ + public static List getExtensions() { + if (extensions == null) { + extensions = ServiceLoader.load(KeYGuiExtensionF.class).stream() + .map(ServiceLoader.Provider::get) + .filter(Objects::nonNull) + .sorted(Comparator.comparingInt(KeYGuiExtensionFacadeF::priorityOf)) + .collect(Collectors.toList()); + LOGGER.info("Extension discovery: {} FX extension(s) loaded: {}", + extensions.size(), + extensions.stream().map(it -> it.getClass().getName()) + .collect(Collectors.joining(", "))); + } + return extensions; + } + + /** + * @return the number of discovered (and instantiated) extension providers — the assertion + * target of the {@code key.fx.verify.extensions} self test + */ + public static int discoveredCount() { + return getExtensions().size(); + } + + /** @return the {@link KeYGuiExtensionF.Info#priority()} of the provider's class, or 0. */ + private static int priorityOf(KeYGuiExtensionF extension) { + KeYGuiExtensionF.Info info = + extension.getClass().getAnnotation(KeYGuiExtensionF.Info.class); + return info == null ? 0 : info.priority(); + } + + /** + * The menus contributed by every {@link KeYGuiExtensionF.MainMenuF} provider, in provider + * order (Swing {@code KeYGuiExtensionFacade.getMainMenuActions}, + * KeYGuiExtensionFacade.java:63-76). The host appends these to the menu bar as separate + * menus after the built-in menus; the five built-in menus' item sets are untouched (Swing + * {@code MainWindow.createMenuBar}, MainWindow.java:977-988 + {@code + * addExtensionsToMainMenu}, KeYGuiExtensionFacade.java:81-89). + * + * @param window the main window + * @param mediator the mediator of the window + * @return non-null, emptiable list of menus + */ + public static List

        getMenus(MainWindowF window, KeYMediatorF mediator) { + List menus = new ArrayList<>(); + for (KeYGuiExtensionF extension : getExtensions()) { + if (extension instanceof KeYGuiExtensionF.MainMenuF mainMenu) { + menus.addAll(mainMenu.getMenus(window, mediator)); + } + } + return menus; + } + + /** + * The toolbar controls contributed by every {@link KeYGuiExtensionF.ToolbarF} provider + * (Swing {@code KeYGuiExtensionFacade.createToolbars}, KeYGuiExtensionFacade.java:217-221). + * + * @param window the main window + * @param mediator the mediator of the window + * @return non-null, emptiable list of controls + */ + public static List getToolbarControls(MainWindowF window, KeYMediatorF mediator) { + List controls = new ArrayList<>(); + for (KeYGuiExtensionF extension : getExtensions()) { + if (extension instanceof KeYGuiExtensionF.ToolbarF toolbar) { + controls.addAll(toolbar.getToolbarControls(window, mediator)); + } + } + return controls; + } + + /** + * The status-line controls contributed by every {@link KeYGuiExtensionF.StatusLineF} + * provider (Swing {@code KeYGuiExtensionFacade.getStatusLineComponents}, + * KeYGuiExtensionFacade.java:325-333). + * + * @return non-null, emptiable list of controls + */ + public static List getStatusLineControls() { + List controls = new ArrayList<>(); + for (KeYGuiExtensionF extension : getExtensions()) { + if (extension instanceof KeYGuiExtensionF.StatusLineF statusLine) { + controls.addAll(statusLine.getStatusLineControls()); + } + } + return controls; + } + + /** + * The left-panel tabs contributed by every {@link KeYGuiExtensionF.LeftPanelF} provider + * (Swing {@code KeYGuiExtensionFacade.getAllPanels}, KeYGuiExtensionFacade.java:42-45). + * + * @param window the main window + * @param mediator the mediator of the window + * @return non-null, emptiable list of tabs + */ + public static List getLeftPanelTabs(MainWindowF window, KeYMediatorF mediator) { + List tabs = new ArrayList<>(); + for (KeYGuiExtensionF extension : getExtensions()) { + if (extension instanceof KeYGuiExtensionF.LeftPanelF leftPanel) { + tabs.addAll(leftPanel.getLeftPanelTabs(window, mediator)); + } + } + return tabs; + } + + /** + * The sequent context-menu items contributed by every {@link + * KeYGuiExtensionF.ContextMenuF} provider for the given goal and position (Swing + * {@code KeYGuiExtensionFacade.getContextMenuItems}, + * KeYGuiExtensionFacade.java:264-269 with {@code ContextMenuKind.SEQUENT_VIEW}). + * + * @param mediator the mediator of the window + * @param goal the goal whose sequent was clicked + * @param pos the clicked position + * @return non-null, emptiable list of menu items + */ + public static List getSequentContextItems(KeYMediatorF mediator, Goal goal, + PosInSequent pos) { + List items = new ArrayList<>(); + for (KeYGuiExtensionF extension : getExtensions()) { + if (extension instanceof KeYGuiExtensionF.ContextMenuF contextMenu) { + items.addAll(contextMenu.getSequentContextItems(mediator, goal, pos)); + } + } + return items; + } + + /** + * The settings providers contributed by every {@link KeYGuiExtensionF.SettingsF} provider + * (Swing {@code KeYGuiExtensionFacade.getSettingsProvider}, + * KeYGuiExtensionFacade.java:335-337); the host registers them into the + * {@link de.uka.ilkd.key.gui.fx.settings.SettingsManagerF} registry. + * + * @return non-null, emptiable list of providers + */ + public static List getSettingsProviders() { + List providers = new ArrayList<>(); + for (KeYGuiExtensionF extension : getExtensions()) { + if (extension instanceof KeYGuiExtensionF.SettingsF settings) { + providers.add(settings.getSettings()); + } + } + return providers; + } + + /** + * The tooltip strings contributed by every {@link KeYGuiExtensionF.TooltipF} provider for + * the given position (Swing {@code KeYGuiExtensionFacade.getTooltipStrings}, + * KeYGuiExtensionFacade.java:395-399). + * + * @param mediator the mediator of the window + * @param pos the position of the term whose info shall be shown + * @return non-null, emptiable list of strings + */ + public static List getTooltipStrings(KeYMediatorF mediator, PosInSequent pos) { + List strings = new ArrayList<>(); + for (KeYGuiExtensionF extension : getExtensions()) { + if (extension instanceof KeYGuiExtensionF.TooltipF tooltip) { + strings.addAll(tooltip.getTooltipStrings(mediator, pos)); + } + } + return strings; + } + + /** + * Calls {@link KeYGuiExtensionF.StartupF#init} of every provider once at app startup + * (Swing {@code KeYGuiExtensionFacade.getStartupExtensions} + the host calling + * {@code init}, KeYGuiExtensionFacade.java:339-341; Swing MainWindow calls + * {@code extension.init(this, mediator)} on the Startup extensions after the facade + * creation). + * + * @param window the main window + * @param mediator the mediator of the window + */ + public static void initAll(MainWindowF window, KeYMediatorF mediator) { + for (KeYGuiExtensionF extension : getExtensions()) { + if (extension instanceof KeYGuiExtensionF.StartupF startup) { + startup.init(window, mediator); + } + } + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/extension/api/KeYGuiExtensionF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/extension/api/KeYGuiExtensionF.java new file mode 100644 index 00000000000..8f318ee465a --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/extension/api/KeYGuiExtensionF.java @@ -0,0 +1,198 @@ +/* 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.extension.api; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.List; +import javafx.scene.control.Control; +import javafx.scene.control.Menu; +import javafx.scene.control.MenuItem; +import javafx.scene.control.Tab; + +import de.uka.ilkd.key.core.fx.KeYMediatorF; +import de.uka.ilkd.key.gui.fx.MainWindowF; +import de.uka.ilkd.key.gui.fx.settings.SettingsProviderF; +import de.uka.ilkd.key.pp.PosInSequent; +import de.uka.ilkd.key.proof.Goal; + +import org.jspecify.annotations.NullMarked; + +/** + * The FX-native GUI-extension SPI, counter-part of {@code de.uka.ilkd.key.gui.extension.api. + * KeYGuiExtension} of the Swing module {@code key.ui} (KeYGuiExtension.java:31-308). Every FX + * extension implements this marker interface and is registered in the service-loader file + * META-INF/services/de.uka.ilkd.key.gui.fx.extension.api.KeYGuiExtensionF, then + * implements one or more of the nested capability interfaces below. + *

        + * The capabilities mirror the Swing set with JavaFX-typed surfaces ({@link Menu}s instead of + * {@code JMenu}s, {@link Control}s instead of {@code JComponent}s, {@link Tab}s instead of the + * Swing {@code TabPanel}). The host ({@link MainWindowF}) discovers the providers through + * {@link de.uka.ilkd.key.gui.fx.extension.KeYGuiExtensionFacadeF} and integrates them into the + * menu bar, toolbars, status bar, docking layout, settings dialog and sequent context menu — + * exactly the slots the Swing {@code KeYGuiExtensionFacade} fills in {@code key.ui}. + * + * @author Alexander Weigl + * @author MP9.0 extension SPI port (FX) + */ +@NullMarked +public interface KeYGuiExtensionF { + /** + * Describes the annotated extension (Swing {@code KeYGuiExtension.Info}, + * KeYGuiExtension.java:46-89). + */ + @Retention(RetentionPolicy.RUNTIME) + @interface Info { + /** + * Simple name of this extension, else the fqdn of the class is used. + * + * @return non-null string + */ + String name() default ""; + + /** + * Long description of this extension (what does it do? who developed it?). + * + * @return a string, default empty + */ + String description() default ""; + + /** + * Optional extensions can be disabled by the user (Swing semantics). + * + * @return a boolean + */ + boolean optional() default false; + + /** + * Marks an extension as experimental. Swing only loads experimental extensions with the + * {@code --experimental} command-line flag; the FX port reserves the flag semantics and + * defaults to {@code true} like the Swing original (KeYGuiExtension.java:88). + * + * @return a boolean + */ + boolean experimental() default true; + + /** + * Loading priority of this extension; baseline is zero (Swing + * KeYGuiExtension.java:80). + * + * @return the priority + */ + int priority() default 0; + } + + /** + * Main-menu extension: contributes whole {@link Menu} objects that the host appends to the + * menu bar as new separate menus (Swing {@code KeYGuiExtension.MainMenu}, + * KeYGuiExtension.java:92-107 — the Swing original contributes actions that the facade + * groups into one "Extensions" {@code JMenu}, MainWindow.createMenuBar :983; the FX SPI + * keeps the grouping decision with the extension and contributes ready {@code Menu}s). + * The five built-in menus (File / Proof / View / Options / About) and their item sets are + * never touched. + * + * @param window the main window + * @param mediator the mediator of the window + * @return non-null, emptiable list of menus + */ + interface MainMenuF { + List

        getMenus(MainWindowF window, KeYMediatorF mediator); + } + + /** + * Toolbar extension: contributes {@link Control}s that the host appends into an extra + * toolbar next to the built-in file/proof toolbars (Swing + * {@code KeYGuiExtension.Toolbar}, KeYGuiExtension.java:170-183). + * + * @param window the main window + * @param mediator the mediator of the window + * @return non-null, emptiable list of controls + */ + interface ToolbarF { + List getToolbarControls(MainWindowF window, KeYMediatorF mediator); + } + + /** + * Status-line extension: contributes {@link Control}s that the host appends to the status + * bar (Swing {@code KeYGuiExtension.StatusLine}, KeYGuiExtension.java:203-214). + * + * @return non-null, emptiable list of controls + */ + interface StatusLineF { + List getStatusLineControls(); + } + + /** + * Left-panel extension: contributes {@link Tab}s that the host registers as dockables in + * the left docking area (Swing {@code KeYGuiExtension.LeftPanel}, + * KeYGuiExtension.java:127-143 — the Swing original returns {@code TabPanel}s for the left + * JTabbedPane). + * + * @param window the main window + * @param mediator the mediator of the window + * @return non-null, emptiable list of tabs + */ + interface LeftPanelF { + List getLeftPanelTabs(MainWindowF window, KeYMediatorF mediator); + } + + /** + * Context-menu extension for the sequent term menu: contributes {@link MenuItem}s for the + * clicked position (Swing {@code KeYGuiExtension.ContextMenu}, + * KeYGuiExtension.java:145-168 with {@code ContextMenuKind.SEQUENT_VIEW} — the FX port + * exposes only the sequent slot because it is the only term-menu slot). The host renders + * the items inside the "Extensions" section of the sequent context menu. + * + * @param mediator the mediator of the window + * @param goal the goal whose sequent was clicked + * @param pos the clicked position + * @return non-null, emptiable list of menu items + */ + interface ContextMenuF { + List getSequentContextItems(KeYMediatorF mediator, Goal goal, + PosInSequent pos); + } + + /** + * Settings extension: contributes a {@link SettingsProviderF} into the settings dialog + * (Swing {@code KeYGuiExtension.Settings}, KeYGuiExtension.java:216-227). + * + * @return non-null settings provider + */ + interface SettingsF { + SettingsProviderF getSettings(); + } + + /** + * Sequent-view tooltip extension: contributes term-information strings for the given + * position (Swing {@code KeYGuiExtension.Tooltip}, KeYGuiExtension.java:186-201). The FX + * host integration with the sequent view tooltips is a later milestone; the facade already + * aggregates the strings. + * + * @param mediator the mediator of the window + * @param pos the position of the term whose info shall be shown + * @return non-null, emptiable list of strings + */ + interface TooltipF { + List getTooltipStrings(KeYMediatorF mediator, PosInSequent pos); + } + + /** + * Startup extension: the host calls {@link #init(MainWindowF, KeYMediatorF)} once at + * startup, after discovery, before layout-dependent interaction (Swing + * {@code KeYGuiExtension.Startup}, KeYGuiExtension.java:109-125). + */ + interface StartupF { + /** + * Initialization hook, called once at the end of the app startup after the discovery. + * Can be used to register listeners and initialize controls. + * + * @param window the main window + * @param mediator the mediator of the window + */ + default void init(MainWindowF window, KeYMediatorF mediator) { + } + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/extension/contrib/HeatmapF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/extension/contrib/HeatmapF.java new file mode 100644 index 00000000000..9a2e7c0113c --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/extension/contrib/HeatmapF.java @@ -0,0 +1,105 @@ +/* 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.extension.contrib; + +import java.util.List; +import javafx.scene.control.CheckMenuItem; +import javafx.scene.control.Control; +import javafx.scene.control.Menu; +import javafx.scene.control.MenuItem; +import javafx.scene.control.ToggleButton; +import javafx.scene.control.Tooltip; + +import de.uka.ilkd.key.core.fx.KeYMediatorF; +import de.uka.ilkd.key.gui.fx.MainWindowF; +import de.uka.ilkd.key.gui.fx.extension.api.KeYGuiExtensionF; +import de.uka.ilkd.key.gui.fx.settings.SettingsManagerF; +import de.uka.ilkd.key.gui.fx.settings.SettingsProviderF; +import de.uka.ilkd.key.settings.ProofIndependentSettings; +import de.uka.ilkd.key.settings.ViewSettings; + +/** + * The Heatmap extension, FX port of {@code de.uka.ilkd.key.gui.extension.impl.HeatmapExt} + * (Swing HeatmapExt.java:30-68): a separate "Heatmap" menu with the toggle and the settings + * actions, a toolbar control and a {@link SettingsProviderF} with the intensity/age options. + *

        + * KNOWN-SIMPLIFIED: the Swing extension renders the actual heat highlight into the + * proof tree / sequent (HeatmapExt.java:41-62 delegates to the {@code HeatmapToggleAction} / + * {@code HeatmapSettingsAction}); the FX port is a lightweight hook only — it provides the menu, + * the toolbar toggle and the persisted {@link ViewSettings} options (the same settings the Swing + * provider reads/writes, HeatmapExt.java:198-223), while the actual proof-tree heat overlay is + * deferred to a later milestone. + */ +@KeYGuiExtensionF.Info(name = "Heatmap", optional = true, + description = "Colorize the formulae on the sequent based on the most recent changes. " + + "KNOWN-SIMPLIFIED (FX): settings and toggle only — the proof-tree heat overlay is " + + "deferred.", + experimental = false) +public class HeatmapF + implements KeYGuiExtensionF, KeYGuiExtensionF.MainMenuF, KeYGuiExtensionF.ToolbarF, + KeYGuiExtensionF.SettingsF { + + private final SettingsProviderF heatmapSettingsProvider = new HeatmapSettingsProviderF(); + + @Override + public List

        getMenus(MainWindowF window, KeYMediatorF mediator) { + // extension: MP9.0 — Swing HeatmapExt.java:41-51: the main menu holds the toggle and + // the settings action; the FX SPI contributes whole menus, so the two actions live in a + // NEW separate "Heatmap" menu (the five built-in menu bars and their item sets stay + // untouched — key.fx.verify.menuparity keeps asserting 16/24/12/7/5). + Menu heatmap = new Menu("Heatmap"); + heatmap.getItems().addAll(toggleItem(window), settingsItem(window)); + return List.of(heatmap); + } + + /** + * The "Show Heatmap" toggle (Swing {@code HeatmapToggleAction}): switches the persisted + * {@link ViewSettings} heatmap flag; the checkbox reflects the current flag when the menu + * is (re)built. + */ + private MenuItem toggleItem(MainWindowF window) { + ViewSettings vs = ProofIndependentSettings.DEFAULT_INSTANCE.getViewSettings(); + CheckMenuItem toggle = new CheckMenuItem("Show Heatmap"); + toggle.setSelected(vs.isShowHeatmap()); + toggle.setOnAction(e -> vs.setHeatmapOptions(!vs.isShowHeatmap(), vs.isHeatmapSF(), + vs.isHeatmapNewest(), vs.getMaxAgeForHeatmap())); + return toggle; + } + + /** The "Heatmap Settings…" action (Swing {@code HeatmapSettingsAction}): opens the dialog. */ + private MenuItem settingsItem(MainWindowF window) { + MenuItem settings = new MenuItem("Heatmap Settings…"); + settings.setOnAction(e -> SettingsManagerF.getInstance() + .showSettingsDialog(window, heatmapSettingsProvider)); + return settings; + } + + @Override + public List getToolbarControls(MainWindowF window, KeYMediatorF mediator) { + // extension: MP9.0 — Swing HeatmapExt.java:54-62: the toolbar holds the toggle button + // (icon-only in Swing, labelled here) and the settings action. + ViewSettings vs = ProofIndependentSettings.DEFAULT_INSTANCE.getViewSettings(); + ToggleButton toggle = new ToggleButton("Heatmap"); + toggle.setTooltip(new Tooltip("Toggle the (deferred) heatmap overlay; the option is " + + "persisted in the ViewSettings.")); + toggle.setSelected(vs.isShowHeatmap()); + toggle.setOnAction(e -> vs.setHeatmapOptions(toggle.isSelected(), vs.isHeatmapSF(), + vs.isHeatmapNewest(), vs.getMaxAgeForHeatmap())); + javafx.scene.control.Button settings = + new javafx.scene.control.Button("Heatmap Settings…"); + settings.setTooltip(new Tooltip("Open the heatmap options in the settings dialog.")); + settings.setOnAction(e -> SettingsManagerF.getInstance() + .showSettingsDialog(window, heatmapSettingsProvider)); + return List.of(toggle, settings); + } + + @Override + public SettingsProviderF getSettings() { + // extension: MP9.0 — Swing HeatmapExt.java:64-67: {@code getSettings()} returns the + // heatmap options provider; the host registers it into the SettingsManagerF registry + // (Swing SettingsManager.registerProvider). + return heatmapSettingsProvider; + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/extension/contrib/HeatmapSettingsProviderF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/extension/contrib/HeatmapSettingsProviderF.java new file mode 100644 index 00000000000..41d59fc3455 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/extension/contrib/HeatmapSettingsProviderF.java @@ -0,0 +1,139 @@ +/* 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.extension.contrib; + +import java.util.List; +import javafx.scene.control.Label; +import javafx.scene.control.Spinner; +import javafx.scene.control.Toggle; + +import de.uka.ilkd.key.gui.fx.MainWindowF; +import de.uka.ilkd.key.gui.fx.settings.InvalidSettingsInputExceptionF; +import de.uka.ilkd.key.gui.fx.settings.SettingsPanelF; +import de.uka.ilkd.key.gui.fx.settings.SettingsProviderF; +import de.uka.ilkd.key.settings.ProofIndependentSettings; +import de.uka.ilkd.key.settings.ViewSettings; + +/** + * The heatmap options panel, FX port of {@code HeatmapSettingsProvider} (Swing + * HeatmapExt.java:71-229): the mode radios (no heatmaps / sequent formulae or terms, up to an + * age or the newest k) and the maximum-age spinner, reading and writing the persisted + * {@link ViewSettings} heatmap options (HeatmapExt.java:198-223). + */ +final class HeatmapSettingsProviderF extends SettingsPanelF implements SettingsProviderF { + + /** + * Minimal setting for the number of highlighted terms (Swing {@code MIN_AGE}). + */ + private static final int MIN_AGE = 1; + + /** + * Maximal setting for the number of highlighted terms (Swing {@code MAX_AGE}). + */ + private static final int MAX_AGE = 1000; + + /** + * Text for the introductory heatmap explanation (Swing {@code INTRO_LABEL}). + */ + private static final String INTRO_LABEL = + "Heatmaps can be used to highlight the most recent changes in the sequent."; + + /** + * Explanation for the age spinner (Swing {@code TEXTFIELD_LABEL}). + */ + private static final String TEXTFIELD_LABEL = "Maximum age of highlighted terms or formulae," + + " or number of newest terms or formulae. Please enter a number between " + MIN_AGE + + " and " + MAX_AGE + "."; + + /** + * The heatmap highlight modes, mirroring the Swing {@code HeatmapMode} enum of the same + * name (HeatmapExt.java:106-156). + */ + enum HeatmapMode { + DEFAULT("No heatmaps", false, false, false), + SF_AGE("Sequent formulae up to age", true, true, false), + SF_NEWEST("Newest sequent formulae", true, true, true), + TERMS_AGE("Terms up to age", true, false, false), + TERMS_NEWEST("Newest terms", true, false, true); + + final String text; + final boolean enableHeatmap; + final boolean sequent; + final boolean newest; + + HeatmapMode(String shortText, boolean enableHeatmap, boolean sequent, boolean newest) { + text = shortText; + this.enableHeatmap = enableHeatmap; + this.sequent = sequent; + this.newest = newest; + } + } + + private final javafx.scene.control.ToggleGroup modeGroup; + private final Spinner spinnerAge; + + HeatmapSettingsProviderF() { + setHeaderText("Heatmap Options"); + + Label intro = new Label(INTRO_LABEL); + intro.setWrapText(true); + pCenter.add(intro, 0, pCenter.getRowCount(), 3, 1); + + // extension: MP9.0 — Swing HeatmapExt.java:160-184: the five mode radios (with the + // separator groupings) and the max-age spinner; the FX SettingsPanelF renders one + // radio group and the spinner rows. + modeGroup = addRadioButtons("Heatmap Mode", List.of(HeatmapMode.values()), + TEXTFIELD_LABEL); + spinnerAge = addIntNumberField("Maximal age:", MIN_AGE, MAX_AGE, 1, + (int) ProofIndependentSettings.DEFAULT_INSTANCE.getViewSettings() + .getMaxAgeForHeatmap(), + TEXTFIELD_LABEL, null); + // Swing HeatmapExt.java:181-184: the live-update comment is a no-op in the Swing + // original as well — the value is written on apply only. + } + + @Override + public String getDescription() { + return "Heatmap"; + } + + @Override + public javafx.scene.Node getPanel(MainWindowF window) { + // extension: MP9.0 — Swing HeatmapExt.java:198-210: {@code getPanel} re-reads the + // ViewSettings and selects the matching mode / spinner value. + ViewSettings vs = ProofIndependentSettings.DEFAULT_INSTANCE.getViewSettings(); + for (Toggle toggle : modeGroup.getToggles()) { + Object data = toggle.getUserData(); + if (data instanceof HeatmapMode mode + && mode.enableHeatmap == vs.isShowHeatmap() + && (!mode.enableHeatmap + || (mode.sequent == vs.isHeatmapSF() + && mode.newest == vs.isHeatmapNewest()))) { + modeGroup.selectToggle(toggle); + break; + } + } + spinnerAge.getValueFactory().setValue(vs.getMaxAgeForHeatmap()); + return this; + } + + @Override + public void apply(MainWindowF window) throws InvalidSettingsInputExceptionF { + // extension: MP9.0 — Swing HeatmapExt.java:213-223: {@code applySettings} writes the + // selected mode and the spinner value into the ViewSettings. + ViewSettings vs = ProofIndependentSettings.DEFAULT_INSTANCE.getViewSettings(); + Toggle selected = modeGroup.getSelectedToggle(); + if (selected != null && selected.getUserData() instanceof HeatmapMode mode) { + vs.setHeatmapOptions(mode.enableHeatmap, mode.sequent, mode.newest, + Math.clamp(spinnerAge.getValue(), MIN_AGE, MAX_AGE)); + } + } + + @Override + public int getPriorityOfSettings() { + // extension: MP9.0 — Swing HeatmapExt.java:227-229: sorted last like Swing. + return 10000; + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/extension/contrib/ParallelProverStatusIndicatorF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/extension/contrib/ParallelProverStatusIndicatorF.java new file mode 100644 index 00000000000..d281ca7a2d2 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/extension/contrib/ParallelProverStatusIndicatorF.java @@ -0,0 +1,92 @@ +/* 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.extension.contrib; + +import java.util.List; +import javafx.scene.control.Control; +import javafx.scene.control.Label; +import javafx.scene.control.Tooltip; + +import de.uka.ilkd.key.core.fx.KeYMediatorF; +import de.uka.ilkd.key.gui.fx.MainWindowF; +import de.uka.ilkd.key.gui.fx.extension.api.KeYGuiExtensionF; +import de.uka.ilkd.key.settings.GeneralSettings; +import de.uka.ilkd.key.settings.ProofIndependentSettings; + +import org.key_project.util.javafx.FxUtil; + +import org.jspecify.annotations.Nullable; + +/** + * Status-line indicator for the prover state, FX port of {@code + * de.uka.ilkd.key.gui.extension.impl.ParallelProverStatusIndicator} (Swing + * ParallelProverStatusIndicator.java:34-145). + *

        + * KNOWN-SIMPLIFIED: the Swing original is a toggle button showing the parallel-prover + * mode ({@code SC} for the single-core prover, {@code MT N×} for the multi-core prover with + * {@code N} workers) with a left-click toggle and a right-click context menu to pick the worker + * count (ParallelProverStatusIndicator.java:88-121). The FX minimum is a plain Label showing + * the auto-mode state live — "Auto" while an automatic proof search is running, + * "Manual" otherwise — bound to {@link KeYMediatorF#autoModeRunningProperty()} (the + * "selection/auto-mode events" of the milestone description). Left-clicking the label still + * toggles the persisted {@code GeneralSettings.PARALLEL_PROVER_ENABLED} flag and the label + * reacts to the setting's property-change events like the Swing {@code refresh()} + * (ParallelProverStatusIndicator.java:56-59, 127-139) — the worker-count picker and the + * button-styling are out of scope. + */ +@KeYGuiExtensionF.Info(experimental = false, name = "Prover Mode in Status Line", optional = false, + description = "Shows and toggles the prover mode in the status line.") +public class ParallelProverStatusIndicatorF + implements KeYGuiExtensionF, KeYGuiExtensionF.StatusLineF, KeYGuiExtensionF.StartupF { + + private final Label label = new Label(); + + @Override + public void init(MainWindowF window, KeYMediatorF mediator) { + // extension: MP9.0 — Swing ParallelProverStatusIndicator.java:56-59: the indicator + // refreshes on the parallel-prover settings' property changes; the FX label extra binds + // the live auto-mode state of the mediator (auto-mode events). + GeneralSettings gs = ProofIndependentSettings.DEFAULT_INSTANCE.getGeneralSettings(); + gs.addPropertyChangeListener(GeneralSettings.PARALLEL_PROVER_ENABLED, + evt -> refresh(mediator)); + gs.addPropertyChangeListener(GeneralSettings.PARALLEL_PROVER_THREADS, + evt -> refresh(mediator)); + mediator.autoModeRunningProperty().addListener((obs, old, running) -> refresh(mediator)); + // extension: MP9.0 — Swing ParallelProverStatusIndicator.java:43,88-90: left-click + // toggles the parallel-prover mode (the worker-count context menu is simplified away). + label.setOnMouseClicked(e -> { + GeneralSettings general = ProofIndependentSettings.DEFAULT_INSTANCE + .getGeneralSettings(); + general.setParallelProverEnabled(!general.isParallelProverEnabled()); + refresh(mediator); + }); + label.setTooltip(new Tooltip( + "Auto: an automatic proof search is running. Manual: idle. Left-click toggles the " + + "multi-core prover setting (Swing ParallelProverStatusIndicator, simplified: " + + "the worker-count menu is not ported).")); + refresh(mediator); + } + + private void refresh(@Nullable KeYMediatorF mediator) { + Runnable update = () -> { + boolean auto = mediator != null && mediator.isInAutoMode(); + label.setText(auto ? "Auto" : "Manual"); + }; + if (FxUtil.isFxThread()) { + update.run(); + } else { + FxUtil.runLater(update); + } + } + + @Override + public List getStatusLineControls() { + // extension: MP9.0 — Swing ParallelProverStatusIndicator.java:142-145 contributes the + // indicator (plus a leading strut for spacing); the FX minimum is the plain label — + // one control per status provider, so the extension verification asserts exactly two + // status controls across the two StatusLineF extensions. + return List.of(label); + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/extension/contrib/ProfileNameInStatusBarF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/extension/contrib/ProfileNameInStatusBarF.java new file mode 100644 index 00000000000..4f9c972d725 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/extension/contrib/ProfileNameInStatusBarF.java @@ -0,0 +1,69 @@ +/* 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.extension.contrib; + +import java.util.List; +import javafx.scene.control.Control; +import javafx.scene.control.Label; + +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.gui.fx.MainWindowF; +import de.uka.ilkd.key.gui.fx.extension.api.KeYGuiExtensionF; +import de.uka.ilkd.key.proof.Proof; +import de.uka.ilkd.key.proof.init.Profile; + +import org.key_project.util.javafx.FxUtil; + +/** + * Status-line extension showing the profile name of the currently selected proof, FX port of + * {@code de.uka.ilkd.key.gui.extension.impl.ProfileNameInStatusBar} (Swing + * ProfileNameInStatusBar.java:17-36). The Swing original registers a {@code KeYSelectionListener} + * that writes {@code "Profile: " + mediator.getProfile().ident()} into its label + * (ProfileNameInStatusBar.java:24-31); the FX port reads the profile of the selected proof + * instead ({@code mediator.getSelectedProof().getServices().getProfile()}, + * Services.getProfile — the FX {@link KeYMediatorF} has no own {@code getProfile()}) and keeps + * the label null-safe (no proof → {@code "Profile: -"}). Selection events may fire from the + * prover thread, so the update is marshalled to the FX thread ({@link FxUtil#runLater}). + */ +@KeYGuiExtensionF.Info(experimental = false, name = "Profile Name in Status Line", optional = false, + description = "Shows the profile name of the current selected proof in the status line.") +public class ProfileNameInStatusBarF + implements KeYGuiExtensionF, KeYGuiExtensionF.StatusLineF, KeYGuiExtensionF.StartupF { + + private final Label lblProfileName = new Label(); + + @Override + public void init(MainWindowF window, KeYMediatorF mediator) { + // extension: MP9.0 — Swing ProfileNameInStatusBar.java:24-31: a KeYSelectionListener + // updates the label on selectedProofChanged; the initial text is set immediately. + mediator.getSelectionModel().addKeYSelectionListener(new KeYSelectionListener() { + @Override + public void selectedProofChanged(KeYSelectionEvent e) { + update(mediator); + } + }); + update(mediator); + } + + private void update(KeYMediatorF mediator) { + if (!FxUtil.isFxThread()) { + FxUtil.runLater(() -> update(mediator)); + return; + } + Proof proof = mediator.getSelectedProof(); + Profile profile = proof != null && proof.getServices() != null + ? proof.getServices().getProfile() + : null; + lblProfileName.setText("Profile: " + (profile == null ? "-" : profile.ident())); + } + + @Override + public List getStatusLineControls() { + // extension: MP9.0 — Swing ProfileNameInStatusBar.java:34-36: singleton label + return List.of(lblProfileName); + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentTermContextMenuF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentTermContextMenuF.java index f965e72eef2..76791b919cb 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentTermContextMenuF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentTermContextMenuF.java @@ -23,6 +23,7 @@ import de.uka.ilkd.key.control.ProofControl; import de.uka.ilkd.key.core.fx.KeYMediatorF; import de.uka.ilkd.key.gui.fx.MainWindowF; +import de.uka.ilkd.key.gui.fx.extension.KeYGuiExtensionFacadeF; import de.uka.ilkd.key.gui.fx.join.JoinActionF; import de.uka.ilkd.key.gui.fx.mergerule.MergeRuleMenuItemF; import de.uka.ilkd.key.gui.fx.nodeviews.SequentMenuModelF.AbbrevActionEntry; @@ -67,9 +68,9 @@ * The shell is pure JavaFX construction — no FXML — and keeps every side effect (rule application, * rule instantiation dialog, reprint of the sequent) behind the proof control or the supplied * {@link MenuContext} callbacks, so the item structure is unit-testable without a live {@code - * Stage}. The milestone-deferred sections ({@code macro_menu}, {@code extension}, {@code smt}) are - * rendered as disabled placeholders so the skeleton stays faithful; they are wired in later - * milestones. + * Stage}. The milestone-wired sections ({@code macro_menu}, {@code extension}, {@code smt}) + * render their real content; the disabled placeholders remain only for fixed skeleton entries + * without a handler ({@code no_rules}). */ public final class SequentTermContextMenuF { @@ -177,12 +178,15 @@ private static MenuItem namedActionItem(NamedAction action, MenuContext ctx) { case "no_rules" -> disabledItem(action.label()); // menu: MP8 — the Strategy Macros section is wired (Swing ProofMacroMenu, // ProofMacroMenu.java:81: JMenu("Strategy Macros") with one item per applicable - // macro); "extension" and "smt" notes follow below. + // macro); the extension and smt notes follow below. case "macro_menu" -> macroMenu(action, ctx); - // menu: MP8 — KNOWN-DEFERRED extension section: needs the KeYGuiExtensionFacade - // registry of the external keyext modules (no FX counterpart, keyext modules are - // Swing UIs); stays a disabled placeholder so the menu structure stays faithful. - case "extension" -> disabledItem(action.label()); + // extension: MP9.0 — the extension section is wired through the FX extension + // facade (Swing KeYGuiExtension.ContextMenu for ContextMenuKind.SEQUENT_VIEW, + // KeYGuiExtensionFacade.createTermMenu, KeYGuiExtensionFacade.java:271-277: the + // extension actions of every provider are grouped into the "Extensions" sub-menu); + // the disabled placeholder stays only when there are no contributions or no + // position. + case "extension" -> extensionSection(action, ctx); // menu: MP8 — SMT section item, wired in MP8c (Swing CurrentGoalViewMenu. // createSMTMenu, CurrentGoalViewMenu.java:219-231, SMTAction :765-790). case "smt" -> smtItem(action, ctx); @@ -236,6 +240,29 @@ private static MenuItem macroMenu(NamedAction action, MenuContext ctx) { return menu; } + /** + * extension: MP9.0 — the "Extensions" section of the term menu (Swing + * KeYGuiExtensionFacade.createTermMenu, KeYGuiExtensionFacade.java:271-277: the + * SEQUENT_VIEW context actions of every provider are grouped into an "Extensions" + * sub-menu of the term menu). When the clicked position is available and the providers + * contribute items, the section renders them ENABLED inside the sub-menu; the disabled + * placeholder stays only when there are no contributions — or no position, the fallback + * exercised by the {@code key.fx.verify.extensions} self test. + */ + private static MenuItem extensionSection(NamedAction action, MenuContext ctx) { + if (ctx.pos() == null || ctx.mediator() == null || ctx.goal() == null) { + return disabledItem(action.label()); + } + List items = + KeYGuiExtensionFacadeF.getSequentContextItems(ctx.mediator(), ctx.goal(), ctx.pos()); + if (items.isEmpty()) { + return disabledItem(action.label()); + } + Menu menu = new Menu(action.label()); + menu.getItems().addAll(items); + return menu; + } + /** Delayed-cut join (Swing JoinMenuItem / CurrentGoalViewMenu.createDelayedCutJoinMenu). */ private static MenuItem joinItem(NamedAction action, MenuContext ctx) { MenuItem item = new MenuItem(action.label()); diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/SettingsManagerF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/SettingsManagerF.java index c7051198323..683edb7708c 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/SettingsManagerF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/SettingsManagerF.java @@ -148,6 +148,17 @@ public boolean add(SettingsProviderF settingsProvider) { return settingsProviders.add(settingsProvider); } + /** + * extension: MP9.0 — the currently registered settings providers (a defensive copy), used + * by the {@code key.fx.verify.extensions} self test to assert the registration of the + * extension-contributed providers. + * + * @return the registered providers + */ + public List getProviders() { + return new ArrayList<>(settingsProviders); + } + /** * Removes the given settings provider. * diff --git a/key.ui.fx/src/main/resources/META-INF/services/de.uka.ilkd.key.gui.fx.extension.api.KeYGuiExtensionF b/key.ui.fx/src/main/resources/META-INF/services/de.uka.ilkd.key.gui.fx.extension.api.KeYGuiExtensionF new file mode 100644 index 00000000000..a444882dda5 --- /dev/null +++ b/key.ui.fx/src/main/resources/META-INF/services/de.uka.ilkd.key.gui.fx.extension.api.KeYGuiExtensionF @@ -0,0 +1,3 @@ +de.uka.ilkd.key.gui.fx.extension.contrib.HeatmapF +de.uka.ilkd.key.gui.fx.extension.contrib.ParallelProverStatusIndicatorF +de.uka.ilkd.key.gui.fx.extension.contrib.ProfileNameInStatusBarF From 2336feacecb98ff9b440f0b43bbbb4b3e00edc8a Mon Sep 17 00:00:00 2001 From: opencode Date: Fri, 9 Oct 2026 22:58:58 +0200 Subject: [PATCH 113/281] key.ui.fx: drawer: Java port of the TornadoFX Drawer (DrawerF/DrawerItemF/DrawerContentAreaF) with DnD reorder + cross-port item transfer; key.fx.verify.drawer self test --- .../de/uka/ilkd/key/gui/fx/MainWindowF.java | 10 + .../key/gui/fx/drawer/DrawerContentAreaF.java | 36 + .../uka/ilkd/key/gui/fx/drawer/DrawerF.java | 730 ++++++++++++++++++ .../ilkd/key/gui/fx/drawer/DrawerItemF.java | 130 ++++ 4 files changed, 906 insertions(+) create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/drawer/DrawerContentAreaF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/drawer/DrawerF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/drawer/DrawerItemF.java 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 index 1b30b4903e7..c2161a63719 100644 --- 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 @@ -73,6 +73,7 @@ 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.SimpleDockable; +import de.uka.ilkd.key.gui.fx.drawer.DrawerF; import de.uka.ilkd.key.gui.fx.extension.KeYGuiExtensionFacadeF; import de.uka.ilkd.key.gui.fx.fonticons.IconFactoryF; import de.uka.ilkd.key.gui.fx.goallist.GoalListViewF; @@ -802,6 +803,15 @@ protected KeYEnvironment call() throws Exception { if (System.getProperty("key.fx.verify.updatehighlight") != null) { runUpdateHighlightVerification(); } + // 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); + } if (System.getProperty("key.fx.verify.tree") != null) { String report = proofTreeView.verifyTreeStructure(); LOGGER.info("Proof tree structure verification: {}", report); diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/drawer/DrawerContentAreaF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/drawer/DrawerContentAreaF.java new file mode 100644 index 00000000000..2e9533e6ec2 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/drawer/DrawerContentAreaF.java @@ -0,0 +1,36 @@ +/* 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.drawer; + +import javafx.collections.ListChangeListener; +import javafx.scene.Node; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; + +/** + * The content area of a {@link DrawerF}: a {@link VBox} hosting the content of every currently + * expanded item. Expanded items share the available space equally (each child gets + * {@link Priority#ALWAYS} vertical growth), which is the "split pane" behaviour of the TornadoFX + * original (TornadoFX {@code ExpandedDrawerContentArea}). + * + * @see DrawerF + */ +public class DrawerContentAreaF extends VBox { + + public DrawerContentAreaF() { + setStyle("-fx-border-color: darkgray; -fx-border-width: 0.5;"); + getChildren().addListener((ListChangeListener) change -> { + while (change.next()) { + if (change.wasAdded()) { + for (Node added : change.getAddedSubList()) { + if (VBox.getVgrow(added) == null) { + VBox.setVgrow(added, Priority.ALWAYS); + } + } + } + } + }); + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/drawer/DrawerF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/drawer/DrawerF.java new file mode 100644 index 00000000000..78a498b339d --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/drawer/DrawerF.java @@ -0,0 +1,730 @@ +/* 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.drawer; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; + +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.collections.FXCollections; +import javafx.collections.ListChangeListener; +import javafx.collections.ObservableList; +import javafx.geometry.Bounds; +import javafx.geometry.Orientation; +import javafx.geometry.Side; +import javafx.scene.Group; +import javafx.scene.Node; +import javafx.scene.control.CheckMenuItem; +import javafx.scene.control.ContextMenu; +import javafx.scene.control.Label; +import javafx.scene.control.ToggleButton; +import javafx.scene.control.ToolBar; +import javafx.scene.input.ClipboardContent; +import javafx.scene.input.DataFormat; +import javafx.scene.input.DragEvent; +import javafx.scene.input.Dragboard; +import javafx.scene.input.MouseEvent; +import javafx.scene.input.TransferMode; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.Pane; + +/** + * A drawer is a dockable navigation area similar to a {@link javafx.scene.control.TabPane}, but + * organises its entries — {@link DrawerItemF} — as toggle buttons in a button bar placed on one + * of the four sides ({@link javafx.geometry.Side} {@code LEFT/RIGHT/TOP/BOTTOM}). Expanding an + * item shows its content in the content area next to the button bar. In multiselect mode several + * items can be expanded simultaneously; their contents share the content area, each growing + * equally (the "split pane" behaviour) and always in the order of the corresponding buttons. + * In exclusive mode expanding one item collapses all others. + * + *

        + * Faithful Java port of TornadoFX {@code Drawer} (Kotlin: + * {@code tornadofx/Drawer.kt}, guide sections "Drawer" and "Drawer multiselect"), with two + * features added on top: + *

          + *
        • Drag and drop reordering — a toggle button can be dragged within the button bar to + * change its position; the item order determines both the button order and the order of the + * expanded contents in the content area.
        • + *
        • Drag and drop transfer — a toggle button can be dragged onto another drawer (a + * different port, e.g. the east drawer instead of the west drawer) to move the item, + * including its expanded state, to that drawer.
        • + *
        + * + *

        + * Documented deviations from the Kotlin original: the content area is a plain {@code VBox} with + * equal vertical growth (as in the original — not a divider-based SplitPane); the context menu + * exposes the original's "Floating drawers" and "Multiselect" entry; titles are plain + * {@link String}s (no {@code ObservableValue} binding). + */ +public class DrawerF extends BorderPane { + + // ------------------------------------------------------------------ drag and drop + + /** Dragboard format used to move drawer items between and within drawers. */ + public static final DataFormat DRAWER_ITEM_FORMAT = + new DataFormat("application/x-keyfx-draweritem"); + + private static final Map DRAWERS = new ConcurrentHashMap<>(); + private static final Map ITEMS = new ConcurrentHashMap<>(); + private static final AtomicInteger DRAWER_IDS = new AtomicInteger(); + private static final AtomicInteger ITEM_IDS = new AtomicInteger(); + + static String keyOf(DrawerItemF item) { + return item.getDrawer().id + "#" + item.getItemId(); + } + + static long nextItemId() { + return ITEM_IDS.incrementAndGet(); + } + + static void register(DrawerItemF item) { + ITEMS.put(keyOf(item), item); + } + + // ------------------------------------------------------------------ properties (TornadoFX + // port) + + private final ObjectProperty dockingSideProperty = + new SimpleObjectProperty<>(Side.LEFT); + private final BooleanProperty floatingDrawersProperty = new SimpleBooleanProperty(false); + private final ObjectProperty maxContentSizeProperty = new SimpleObjectProperty<>(); + private final ObjectProperty fixedContentSizeProperty = new SimpleObjectProperty<>(); + private final BooleanProperty multiselectProperty = new SimpleBooleanProperty(false); + + // ------------------------------------------------------------------ parts + + private final String id; + private final ToolBar buttonArea = new ToolBar(); + private final DrawerContentAreaF contentArea = new DrawerContentAreaF(); + private final ObservableList items = FXCollections.observableArrayList(); + private final ContextMenu contextMenu = new ContextMenu(); + + // drag-and-drop visual cue state + private Node indicatedNode; + private String indicatedStyle; + + public DrawerF() { + this(Side.LEFT, false, false); + } + + public DrawerF(Side side) { + this(side, false, false); + } + + public DrawerF(Side side, boolean multiselect) { + this(side, multiselect, false); + } + + public DrawerF(Side side, boolean multiselect, boolean floatingDrawers) { + this.id = "drawer-" + DRAWER_IDS.incrementAndGet(); + DRAWERS.put(id, this); + dockingSideProperty.set(side); + multiselectProperty.set(multiselect); + floatingDrawersProperty.set(floatingDrawers); + buttonArea.setStyle("-fx-spacing: 0; -fx-padding: 0;"); + + configureDockingSide(); + configureContextMenu(); + enforceMultiSelect(); + + // adapt the docking side to the slot in the parent BorderPane + parentProperty().addListener((obs, oldParent, parent) -> { + if (parent instanceof BorderPane bp) { + if (bp.getLeft() == this) { + setDockingSide(Side.LEFT); + } else if (bp.getRight() == this) { + setDockingSide(Side.RIGHT); + } else if (bp.getBottom() == this) { + setDockingSide(Side.BOTTOM); + } else if (bp.getTop() == this) { + setDockingSide(Side.TOP); + } + } + }); + dockingSideProperty.addListener((obs, oldSide, newSide) -> configureDockingSide()); + floatingDrawersProperty.addListener((obs, oldValue, newValue) -> { + updateContentArea(); + requestLayout(); + if (getScene() != null && getScene().getRoot() != null) { + getScene().getRoot().requestLayout(); + } + }); + + // maintain the button bar and the content area when items are added or removed + items.addListener((ListChangeListener) change -> { + while (change.next()) { + if (change.wasAdded()) { + for (DrawerItemF item : change.getAddedSubList()) { + item.getButton().setOnDragDetected( + e -> onDragDetected(item, e)); + installDropTargets(item); + buttonArea.getItems().add(new Group(item.getButton())); + configureRotation(item.getButton()); + } + } + if (change.wasRemoved()) { + for (DrawerItemF item : change.getRemoved()) { + detachButton(item); + } + } + } + }); + } + + /** Removes the item's button from the bar (button, its group and its drag handlers). */ + private void detachButton(DrawerItemF item) { + Node parent = item.getButton().getParent(); + if (parent instanceof Pane pane) { + pane.getChildren().remove(item.getButton()); + } + if (parent != null) { + buttonArea.getItems().remove(parent); + } + item.getButton().setOnDragDetected(null); + item.getButton().setOnDragOver(null); + item.getButton().setOnDragDropped(null); + item.getButton().setOnDragDone(null); + } + + // ------------------------------------------------------------------ TornadoFX builder API + + /** Creates a new collapsed item with the given title. */ + public DrawerItemF item(String title) { + return item(title, (Node) null, false); + } + + /** Creates a new collapsed item showing {@code content} when expanded. */ + public DrawerItemF item(String title, Node content) { + return item(title, content, false); + } + + /** Creates a new item; {@code expanded} controls the initial state. */ + public DrawerItemF item(String title, boolean expanded) { + return item(title, null, expanded); + } + + /** + * Creates a new item with the given title, optional content and initial expanded state. The + * header is shown by default when the drawer is in multiselect mode (TornadoFX default). + */ + public DrawerItemF item(String title, Node content, boolean expanded) { + DrawerItemF item = new DrawerItemF(this, title, null, isMultiselect()); + if (content != null) { + item.getChildren().add(content); + } + items.add(item); + if (expanded) { + item.getButton().setSelected(true); + } + return item; + } + + /** Removes the given item and collapses it first (if expanded). */ + public void removeItem(DrawerItemF item) { + if (item != null && item.getDrawer() == this) { + item.getButton().setSelected(false); + items.remove(item); + } + } + + /** Removes all items. */ + public void clearItems() { + new ArrayList<>(items).forEach(this::removeItem); + } + + // ------------------------------------------------------------------ state machine (TornadoFX + // port) + + void updateExpanded(DrawerItemF item) { + if (item.isExpanded()) { + if (!contentArea.getChildren().contains(item)) { + if (!isMultiselect()) { + for (Node child : contentArea.getChildren().toArray(new Node[0])) { + ((DrawerItemF) child).getButton().setSelected(false); + } + } + // insert into the content area in the order of the buttons + int itemIndex = items.indexOf(item); + boolean inserted = false; + for (int i = 0; i < contentArea.getChildren().size(); i++) { + DrawerItemF child = (DrawerItemF) contentArea.getChildren().get(i); + if (items.indexOf(child) > itemIndex) { + contentArea.getChildren().add(i, item); + inserted = true; + break; + } + } + if (!inserted) { + contentArea.getChildren().add(item); + } + } + } else { + contentArea.getChildren().remove(item); + } + updateContentArea(); + } + + private void updateContentArea() { + if (contentArea.getChildren().isEmpty()) { + setCenter(null); + getChildren().remove(contentArea); + } else { + Number fixed = fixedContentSizeProperty.get(); + if (fixed != null) { + double size = fixed.doubleValue(); + if (getDockingSide() == Side.LEFT || getDockingSide() == Side.RIGHT) { + contentArea.setMaxWidth(size); + contentArea.setMinWidth(size); + } else { + contentArea.setMaxHeight(size); + contentArea.setMinHeight(size); + } + } else { + contentArea.setMaxWidth(USE_COMPUTED_SIZE); + contentArea.setMinWidth(USE_COMPUTED_SIZE); + contentArea.setMaxHeight(USE_COMPUTED_SIZE); + contentArea.setMinHeight(USE_COMPUTED_SIZE); + Number max = maxContentSizeProperty.get(); + if (max != null) { + if (getDockingSide() == Side.LEFT || getDockingSide() == Side.RIGHT) { + contentArea.setMaxWidth(max.doubleValue()); + } else { + contentArea.setMaxHeight(max.doubleValue()); + } + } + } + + if (isFloatingDrawers()) { + contentArea.setManaged(false); + if (!getChildren().contains(contentArea)) { + getChildren().add(contentArea); + } + } else { + contentArea.setManaged(true); + getChildren().remove(contentArea); + setCenter(contentArea); + } + } + } + + private void configureRotation(ToggleButton button) { + button.setRotate(switch (getDockingSide()) { + case LEFT -> -90.0; + case RIGHT -> 90.0; + default -> 0.0; + }); + } + + private void configureDockingSide() { + switch (getDockingSide()) { + case LEFT -> { + setLeft(buttonArea); + setRight(null); + setBottom(null); + setTop(null); + buttonArea.setOrientation(Orientation.VERTICAL); + } + case RIGHT -> { + setLeft(null); + setRight(buttonArea); + setBottom(null); + setTop(null); + buttonArea.setOrientation(Orientation.VERTICAL); + } + case BOTTOM -> { + setLeft(null); + setRight(null); + setBottom(buttonArea); + setTop(null); + buttonArea.setOrientation(Orientation.HORIZONTAL); + } + case TOP -> { + setLeft(null); + setRight(null); + setBottom(null); + setTop(buttonArea); + buttonArea.setOrientation(Orientation.HORIZONTAL); + } + } + for (Node node : buttonArea.getItems()) { + if (node instanceof Group group && !group.getChildren().isEmpty() + && group.getChildren().get(0) instanceof ToggleButton button) { + configureRotation(button); + } + } + } + + /** In exclusive mode expanding an item collapses the remaining expanded items. */ + private void enforceMultiSelect() { + multiselectProperty.addListener((obs, oldValue, newValue) -> { + if (!newValue) { + for (int i = 1; i < contentArea.getChildren().size(); i++) { + ((DrawerItemF) contentArea.getChildren().get(i)).getButton() + .setSelected(false); + } + } + }); + } + + private void configureContextMenu() { + CheckMenuItem floating = new CheckMenuItem("Floating drawers"); + floating.selectedProperty().bindBidirectional(floatingDrawersProperty); + CheckMenuItem multiselect = new CheckMenuItem("Multiselect"); + multiselect.selectedProperty().bindBidirectional(multiselectProperty); + contextMenu.getItems().addAll(floating, multiselect); + buttonArea.setOnContextMenuRequested( + event -> contextMenu.show(buttonArea, event.getScreenX(), event.getScreenY())); + } + + /** Floating drawers overlay the content area next to the button bar. */ + @Override + protected void layoutChildren() { + super.layoutChildren(); + if (isFloatingDrawers() && !contentArea.getChildren().isEmpty()) { + Bounds buttonBounds = buttonArea.getLayoutBounds(); + if (getDockingSide() == Side.RIGHT) { + contentArea.resizeRelocate( + buttonBounds.getMinX() - contentArea.prefWidth(-1), + buttonBounds.getMinY(), + contentArea.prefWidth(-1), + buttonBounds.getHeight()); + } else { + contentArea.resizeRelocate( + buttonBounds.getMaxX(), + buttonBounds.getMinY(), + contentArea.prefWidth(-1), + buttonBounds.getHeight()); + } + } + } + + // ------------------------------------------------------------------ drag and drop (reorder + + // transfer) + + private void onDragDetected(DrawerItemF item, MouseEvent event) { + if (item.getDrawer() != this) { + return; + } + Dragboard dragboard = item.getButton().startDragAndDrop(TransferMode.MOVE); + ClipboardContent content = new ClipboardContent(); + content.put(DRAWER_ITEM_FORMAT, keyOf(item)); + dragboard.setContent(content); + event.consume(); + } + + /** Makes the button and the bar drop targets for items dragged from any drawer. */ + private void installDropTargets(DrawerItemF item) { + ToggleButton button = item.getButton(); + button.setOnDragOver(e -> onDragOver(e)); + button.setOnDragDropped(e -> onDragDropped(e, this)); + button.setOnDragDone(e -> clearIndication()); + buttonArea.setOnDragOver(e -> onDragOver(e)); + buttonArea.setOnDragDropped(e -> onDragDropped(e, this)); + buttonArea.setOnDragDone(e -> clearIndication()); + } + + private void onDragOver(DragEvent event) { + Dragboard dragboard = event.getDragboard(); + if (dragboard.hasContent(DRAWER_ITEM_FORMAT)) { + event.acceptTransferModes(TransferMode.MOVE); + indicate(insertionIndexAtScene(event.getSceneX(), event.getSceneY())); + } + event.consume(); + } + + private void onDragDropped(DragEvent event, DrawerF targetDrawer) { + Dragboard dragboard = event.getDragboard(); + boolean completed = false; + if (dragboard.hasContent(DRAWER_ITEM_FORMAT)) { + DrawerItemF source = + ITEMS.get(String.valueOf(dragboard.getContent(DRAWER_ITEM_FORMAT))); + if (source != null) { + DrawerF sourceDrawer = source.getDrawer(); + if (sourceDrawer == targetDrawer) { + // reorder within the same drawer + targetDrawer.moveItem(sourceDrawer.items.indexOf(source), + targetDrawer.insertionIndexAtScene(event.getSceneX(), event.getSceneY())); + } else { + // transfer to a different port + sourceDrawer.transferItem(source, targetDrawer); + } + completed = true; + } + } + event.setDropCompleted(completed); + clearIndication(); + event.consume(); + } + + /** + * Computes the insertion index for a drop at the given scene coordinates: the index of the + * first button whose centre lies beyond the drop point (vertical bars compare y, horizontal + * bars compare x); a drop behind the last button appends. + */ + private int insertionIndexAtScene(double sceneX, double sceneY) { + boolean vertical = buttonArea.getOrientation() == Orientation.VERTICAL; + for (int i = 0; i < items.size(); i++) { + Node button = items.get(i).getButton(); + Bounds bounds = button.localToScene(button.getBoundsInLocal()); + double centre = vertical + ? (bounds.getMinY() + bounds.getMaxY()) / 2 + : (bounds.getMinX() + bounds.getMaxX()) / 2; + double dropPoint = vertical ? sceneY : sceneX; + if (dropPoint < centre) { + return i; + } + } + return items.size(); + } + + private void indicate(int index) { + if (index >= 0 && index < items.size()) { + Node node = items.get(index).getButton(); + if (node != indicatedNode) { + clearIndication(); + indicatedNode = node; + indicatedStyle = node.getStyle(); + node.setStyle((indicatedStyle.isEmpty() ? "" : indicatedStyle + ";") + + "-fx-border-color: #4a90d9; -fx-border-width: 2;"); + } + } + } + + private void clearIndication() { + if (indicatedNode != null) { + indicatedNode.setStyle(indicatedStyle == null ? "" : indicatedStyle); + indicatedNode = null; + indicatedStyle = null; + } + } + + /** + * Moves the item at {@code fromIndex} to {@code toIndex} within this drawer. The item order + * determines both the button-bar order and the order of the expanded contents in the content + * area. + */ + public void moveItem(int fromIndex, int toIndex) { + if (fromIndex < 0 || fromIndex >= items.size() || fromIndex == toIndex) { + return; + } + if (toIndex < 0) { + toIndex = 0; + } + if (toIndex >= items.size()) { + toIndex = items.size() - 1; + } + DrawerItemF moved = items.remove(fromIndex); + items.add(toIndex, moved); + rebuildButtonBar(); + resyncContentOrder(); + } + + /** + * Moves {@code item} from this drawer to {@code target} (a different port). The expanded + * state is preserved; the item appears at the end of the target's button bar. + */ + public void transferItem(DrawerItemF item, DrawerF target) { + if (item == null || target == null || item.getDrawer() == target) { + return; + } + DrawerF source = item.getDrawer(); + boolean expanded = item.getButton().isSelected(); + item.getButton().setSelected(false); + source.items.remove(item); + ITEMS.remove(keyOf(item)); + item.setDrawer(target); + target.items.add(item); + ITEMS.put(keyOf(item), item); + if (expanded) { + item.getButton().setSelected(true); + } + } + + /** Rebuilds the button bar in the current item order (used after a reordering move). */ + private void rebuildButtonBar() { + buttonArea.getItems().clear(); + for (DrawerItemF item : items) { + buttonArea.getItems().add(new Group(item.getButton())); + configureRotation(item.getButton()); + } + } + + /** Re-inserts the expanded items into the content area following the button order. */ + private void resyncContentOrder() { + List expanded = new ArrayList<>(); + for (DrawerItemF item : items) { + if (item.isExpanded() && contentArea.getChildren().contains(item)) { + expanded.add(item); + } + } + if (expanded.size() == contentArea.getChildren().size()) { + contentArea.getChildren().setAll(expanded); + } + } + + // ------------------------------------------------------------------ accessors + + public ObjectProperty dockingSideProperty() { + return dockingSideProperty; + } + + public Side getDockingSide() { + return dockingSideProperty.get(); + } + + public void setDockingSide(Side side) { + dockingSideProperty.set(side); + } + + public BooleanProperty floatingDrawersProperty() { + return floatingDrawersProperty; + } + + public boolean isFloatingDrawers() { + return floatingDrawersProperty.get(); + } + + public void setFloatingDrawers(boolean floating) { + floatingDrawersProperty.set(floating); + } + + public ObjectProperty maxContentSizeProperty() { + return maxContentSizeProperty; + } + + public Number getMaxContentSize() { + return maxContentSizeProperty.get(); + } + + public void setMaxContentSize(Number size) { + maxContentSizeProperty.set(size); + } + + public ObjectProperty fixedContentSizeProperty() { + return fixedContentSizeProperty; + } + + public Number getFixedContentSize() { + return fixedContentSizeProperty.get(); + } + + public void setFixedContentSize(Number size) { + fixedContentSizeProperty.set(size); + } + + public BooleanProperty multiselectProperty() { + return multiselectProperty; + } + + public boolean isMultiselect() { + return multiselectProperty.get(); + } + + public void setMultiselect(boolean multiselect) { + multiselectProperty.set(multiselect); + } + + public ToolBar getButtonArea() { + return buttonArea; + } + + public DrawerContentAreaF getContentArea() { + return contentArea; + } + + public ObservableList getItems() { + return items; + } + + public ContextMenu getContextMenu() { + return contextMenu; + } + + /** @return the stable identifier used in the drag-and-drop registry */ + public String getDrawerId() { + return id; + } + + // ------------------------------------------------------------------ self test + // (key.fx.verify.drawer) + + /** + * Headless self test exercising exclusive/multiselect semantics, side placement, the button + * order in the content area, reordering and cross-drawer transfer — the same methods the + * drag-and-drop handlers invoke. No {@code Scene} is required. Called by the + * {@code key.fx.verify.drawer} verification hook. + * + * @return a single-line PASS/FAIL report + */ + public static String selfTest() { + List checks = new ArrayList<>(); + try { + // exclusive mode: expanding one item collapses the others + DrawerF d = new DrawerF(Side.LEFT, false); + DrawerItemF a = d.item("A"); + DrawerItemF b = d.item("B"); + d.item("C"); + a.getButton().setSelected(true); + b.getButton().setSelected(true); + checks.add("exclusive=" + (d.getContentArea().getChildren().size() == 1 + && d.getContentArea().getChildren().contains(b) ? "PASS" : "FAIL")); + + // side placement and bar orientation + d.setDockingSide(Side.RIGHT); + checks.add("sideRight=" + (d.getRight() == d.getButtonArea() + && d.buttonArea.getOrientation() == Orientation.VERTICAL ? "PASS" : "FAIL")); + d.setDockingSide(Side.BOTTOM); + checks.add("sideBottom=" + (d.getBottom() == d.getButtonArea() + && d.buttonArea.getOrientation() == Orientation.HORIZONTAL ? "PASS" : "FAIL")); + checks.add("centerHost=" + (d.getCenter() == d.getContentArea() ? "PASS" : "FAIL")); + + // multiselect: two expanded items share the content area in button order + header + DrawerF m = new DrawerF(Side.LEFT, true); + m.item("A", new Label("a-content"), true); + m.item("B", new Label("b-content")); + DrawerItemF mB = m.item("C", new Label("c-content")); + mB.getButton().setSelected(true); + checks.add("multiOrder=" + (m.getContentArea().getChildren().size() == 2 + && m.getContentArea().getChildren().get(0) == m.getItems().get(0) + && m.getContentArea().getChildren().get(1) == mB ? "PASS" : "FAIL")); + checks.add("multiHeader=" + (mB.getHeader() != null ? "PASS" : "FAIL")); + + // reorder: move item 1 (B) to the bar front; bar and content follow the new order + // after the move: items = [B, A, C] + m.moveItem(1, 0); + Group bar0 = (Group) m.getButtonArea().getItems().get(0); + Group bar2 = (Group) m.getButtonArea().getItems().get(2); + checks.add("reorder=" + (m.getItems().get(0).getButton().getText().equals("B") + && bar0.getChildren().get(0) == m.getItems().get(0).getButton() + && bar2.getChildren().get(0) == mB.getButton() + && m.getContentArea().getChildren().get(0) == m.getItems().get(1) + && m.getContentArea().getChildren().get(1) == m.getItems().get(2) + ? "PASS" + : "FAIL")); + + // switching multiselect off collapses all but the first expanded item + m.setMultiselect(false); + checks.add("collapse=" + (m.getContentArea().getChildren().size() == 1 ? "PASS" + : "FAIL")); + + // transfer to a different drawer (different port) + DrawerF t = new DrawerF(Side.RIGHT, true); + m.transferItem(m.getItems().get(1), t); + checks.add("transfer=" + (m.getItems().size() == 2 && t.getItems().size() == 1 + && t.getContentArea().getChildren().contains(t.getItems().get(0)) + && t.getItems().get(0).isExpanded() ? "PASS" : "FAIL")); + } catch (RuntimeException ex) { + checks.add("EXCEPTION=" + ex); + } + boolean pass = checks.stream().allMatch(check -> check.endsWith("PASS")); + return (pass ? "PASS" : "FAIL") + " - " + String.join(" ", checks); + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/drawer/DrawerItemF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/drawer/DrawerItemF.java new file mode 100644 index 00000000000..f8d8982cebb --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/drawer/DrawerItemF.java @@ -0,0 +1,130 @@ +/* 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.drawer; + +import javafx.beans.property.BooleanProperty; +import javafx.collections.ListChangeListener; +import javafx.scene.Node; +import javafx.scene.control.TitledPane; +import javafx.scene.control.ToggleButton; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; + +/** + * One entry of a {@link DrawerF}: a toggle button in the drawer's button bar plus the content + * shown in the drawer's content area while the item is expanded. Port of the TornadoFX + * {@code DrawerItem} (Kotlin: {@code tornadofx.Drawer.kt}). + * + *

        + * The expanded state is the selected state of the underlying {@link ToggleButton}; the owning + * drawer reacts to changes and inserts/removes this item from its content area in button order. + * When the constructor's {@code showHeader} flag is set, a non-collapsible {@link TitledPane} + * header (text = title) is rendered above the content to distinguish the item in multiselect + * mode (TornadoFX parameter {@code showHeader}, default {@code true} when multiselect is + * enabled). + * + *

        + * Items are transferable between drawers (drag and drop to another port); the owner reference is + * updated accordingly by {@link DrawerF#transferItem(DrawerItemF, DrawerF)}. + */ +public class DrawerItemF extends VBox { + + /** The drawer currently owning this item; updated on transfer to another drawer. */ + private DrawerF drawer; + + /** Stable identity used by the drag-and-drop registry, independent of index or owner. */ + private final long itemId; + + private final ToggleButton button = new ToggleButton(); + + private TitledPane header; + + private final BooleanProperty expandedProperty; + + DrawerItemF(DrawerF drawer, String title, Node graphic, boolean showHeader) { + this.drawer = drawer; + this.itemId = DrawerF.nextItemId(); + this.expandedProperty = button.selectedProperty(); + if (title != null) { + button.setText(title); + } + if (graphic != null) { + button.setGraphic(graphic); + } + getChildren().addListener((ListChangeListener) change -> { + while (change.next()) { + if (change.wasAdded()) { + for (Node added : change.getAddedSubList()) { + if (added != header && VBox.getVgrow(added) == null) { + VBox.setVgrow(added, Priority.ALWAYS); + } + } + } + } + }); + if (showHeader) { + header = new TitledPane(); + header.setText(title); + header.setCollapsible(false); + getChildren().add(header); + } + applySelectedStyle(button.isSelected()); + button.selectedProperty().addListener((obs, wasSelected, isSelected) -> { + applySelectedStyle(isSelected); + this.drawer.updateExpanded(this); + }); + DrawerF.register(this); + } + + private void applySelectedStyle(boolean selected) { + // TornadoFX DrawerStyles.buttonArea toggleButton:selected -> #818181 background, + // white text (DrawerStyles.kt). + button.setStyle(selected + ? "-fx-background-color: #818181; -fx-text-fill: white;" + : ""); + } + + /** @return the drawer currently owning this item */ + public DrawerF getDrawer() { + return drawer; + } + + void setDrawer(DrawerF drawer) { + this.drawer = drawer; + } + + long getItemId() { + return itemId; + } + + /** @return the toggle button driving the expanded state of this item */ + public ToggleButton getButton() { + return button; + } + + /** @return the header above the content, or {@code null} when constructed without header */ + public TitledPane getHeader() { + return header; + } + + /** @return the expanded (selected) state property — equals {@code button.selectedProperty()} */ + public BooleanProperty getExpandedProperty() { + return expandedProperty; + } + + public boolean isExpanded() { + return button.isSelected(); + } + + public void setExpanded(boolean expanded) { + button.setSelected(expanded); + } + + @Override + public String toString() { + return "DrawerItemF[" + itemId + (button.getText() == null ? "" : " " + button.getText()) + + "]"; + } +} From 1ea13153d933ecc959ab7c2ca9c872291ac48ff1 Mon Sep 17 00:00:00 2001 From: opencode Date: Fri, 9 Oct 2026 23:15:26 +0200 Subject: [PATCH 114/281] =?UTF-8?q?key.ui.fx:=20drawer:=20MP10a=20?= =?UTF-8?q?=E2=80=94=20drawered=20main=20area=20(west/east/south=20DrawerF?= =?UTF-8?q?=20hosts=20replace=20the=20left/right=20dockables;=20the=20dock?= =?UTF-8?q?ing=20centre=20only=20hosts=20the=20sequent;=20DockLayoutStore?= =?UTF-8?q?=20layout=20version=202=20discards=20stale=20left/right=20layou?= =?UTF-8?q?ts;=20key.fx.verify.drawerlayout=20self=20test)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../de/uka/ilkd/key/gui/fx/MainWindowF.java | 210 ++++++++++++++---- .../key/gui/fx/docking/DockLayoutStore.java | 44 +++- 2 files changed, 209 insertions(+), 45 deletions(-) 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 index c2161a63719..600a677d0a1 100644 --- 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 @@ -18,9 +18,11 @@ import java.util.function.Consumer; import javafx.application.Platform; import javafx.beans.property.ReadOnlyBooleanWrapper; +import javafx.collections.ObservableList; import javafx.concurrent.Task; import javafx.geometry.Insets; import javafx.geometry.Pos; +import javafx.geometry.Side; import javafx.scene.Node; import javafx.scene.Scene; import javafx.scene.control.Alert; @@ -37,7 +39,6 @@ import javafx.scene.control.OverrunStyle; import javafx.scene.control.RadioMenuItem; import javafx.scene.control.SeparatorMenuItem; -import javafx.scene.control.Tab; import javafx.scene.control.TextArea; import javafx.scene.control.ToggleGroup; import javafx.scene.control.ToolBar; @@ -74,6 +75,7 @@ import de.uka.ilkd.key.gui.fx.docking.DockingLayoutF; 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.fonticons.IconFactoryF; import de.uka.ilkd.key.gui.fx.goallist.GoalListViewF; @@ -334,6 +336,18 @@ in order to generate and prove the proof obligations for the correctness of (non */ 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; + /** * 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 @@ -405,10 +419,18 @@ public void initialize() { buildDockables(); workspace.setDefaultLayout(defaultLayout()); + buildDrawerHosts(); BorderPane root = new BorderPane(); root.setTop(buildTop()); - StackPane center = new StackPane(workspace.getRoot()); + // 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 + BorderPane mainArea = new BorderPane(); + mainArea.setCenter(workspace.getRoot()); + mainArea.setLeft(westDrawer); + mainArea.setRight(eastDrawer); + mainArea.setBottom(southDrawer); + StackPane center = new StackPane(mainArea); NotificationManagerF.getInstance().attach(center); root.setCenter(center); root.setBottom(buildStatusBar()); @@ -518,6 +540,30 @@ public DockLayoutStore getLayoutStore() { return layoutStore; } + /** + * @return the west (left) drawer host of the main window — Proof Tree, Goal List, Loaded + * Proofs, 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) */ @@ -770,7 +816,15 @@ protected KeYEnvironment call() throws Exception { lastEnvironment = env; proofManager.addProof(env.getLoadedProof()); String show = System.getProperty("key.fx.show", ID_SEQUENT); - String target = ID_PROOF_TREE.equalsIgnoreCase(show) ? ID_PROOF_TREE : 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() @@ -812,6 +866,11 @@ protected KeYEnvironment call() throws Exception { .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); @@ -1932,6 +1991,75 @@ private void runExtensionVerification(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; + } + sb.append("west=").append(west.getItems().size()).append(" east=") + .append(east.getItems().size()).append(" south=").append(south.getItems().size()); + pass &= west.getItems().size() == 5 && 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: Proof Tree + Goal List share the west split in button order + ObservableList westContent = west.getContentArea().getChildren(); + 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() == 5 && 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 west button behind the third; bar + split follow + boolean reorderOk = false; + if (west.getItems().size() > 2) { + DrawerItemF first = west.getItems().get(0); + west.moveItem(0, 2); + reorderOk = west.getItems().get(2) == first + && west.getContentArea().getChildren().contains(first); + west.moveItem(2, 0); + reorderOk &= west.getItems().get(0) == 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). @@ -2096,29 +2224,41 @@ private void setWindowIcons() { // ------------------------------------------------------------------ private void buildDockables() { - // proofmgmt: the Loaded Proofs view replaces the M1 placeholder dockable (JavaFX port of - // the Swing TaskTree) - dockables.put(ID_LOADED_PROOFS, - new SimpleDockable(ID_LOADED_PROOFS, "Loaded Proofs", loadedProofs)); - dockables.put(ID_GOAL_LIST, - new SimpleDockable(ID_GOAL_LIST, "Goal List", goalListView)); - dockables.put(ID_PROOF_TREE, - new SimpleDockable(ID_PROOF_TREE, "Proof Tree", proofTreeView)); - dockables.put(ID_INFO_VIEW, new SimpleDockable(ID_INFO_VIEW, "Info", infoView)); - dockables.put(ID_STRATEGY, - new SimpleDockable(ID_STRATEGY, "Strategy", strategyView)); + // 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, new SimpleDockable(ID_SEQUENT, "Sequent", sequentView)); - dockables.put(ID_SOURCE_VIEW, - new SimpleDockable(ID_SOURCE_VIEW, "Source", buildSourceViewContent())); - // extension: MP9.0 — register each left-panel Tab contributed by the FX extensions as - // a dockable (Swing KeYGuiExtension.LeftPanel returns tabs for the left JTabbedPane, - // KeYGuiExtension.java:127-143; the FX host mirrors the built-in views' registration - // pattern, so extension panels appear in the docking layout and its persistence). - int extensionPanel = 0; - for (Tab tab : KeYGuiExtensionFacadeF.getLeftPanelTabs(this, mediator)) { - String id = "extension-left-panel-" + extensionPanel++; - dockables.put(id, new SimpleDockable(id, tab.getText(), tab.getContent())); - } + } + + /** + * 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); + westDrawer.item("Proof Tree", proofTreeView, true); + westDrawer.item("Goal List", goalListView, true); + westDrawer.item("Loaded Proofs", loadedProofs); + westDrawer.item("Info", infoView); + westDrawer.item("Strategy", strategyView); + eastDrawer = new DrawerF(Side.RIGHT, true); + eastDrawer.item("Source", buildSourceViewContent()); + southDrawer = new DrawerF(Side.BOTTOM, true); } /** @@ -2134,26 +2274,10 @@ private Node buildSourceViewContent() { } 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.LEFT, - requireDockable(ID_LOADED_PROOFS))); - defaults.add(new DockWorkspace.Default(DockLocation.LEFT, requireDockable(ID_GOAL_LIST))); - defaults.add( - new DockWorkspace.Default(DockLocation.LEFT, requireDockable(ID_PROOF_TREE))); - defaults.add(new DockWorkspace.Default(DockLocation.LEFT, requireDockable(ID_INFO_VIEW))); - defaults.add( - new DockWorkspace.Default(DockLocation.LEFT, requireDockable(ID_STRATEGY))); defaults.add(new DockWorkspace.Default(DockLocation.MAIN, requireDockable(ID_SEQUENT))); - defaults.add( - new DockWorkspace.Default(DockLocation.RIGHT, requireDockable(ID_SOURCE_VIEW))); - // extension: MP9.0 — the extension left-panel dockables (registered in - // {@link #buildDockables()}) appear at the bottom of the left docking area in the - // factory-default layout; without LeftPanelF providers this appends nothing. - for (Dockable dockable : dockables.values()) { - if (dockable.getId().startsWith("extension-left-panel-")) { - defaults.add(new DockWorkspace.Default(DockLocation.LEFT, dockable)); - } - } return defaults; } diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/docking/DockLayoutStore.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/docking/DockLayoutStore.java index 19bd2e34496..a03fa6cd84e 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/docking/DockLayoutStore.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/docking/DockLayoutStore.java @@ -32,9 +32,20 @@ */ public final class DockLayoutStore { - /** The name of the properties file inside the config directory. */ + /** + * The name of the properties file inside the config directory. + */ public static final String FILE_NAME = "layout.properties"; + /** + * The layout format version, persisted next to the roles. Bumped when the set of dockable + * roles changes incompatibly (MP10 removed the left/right dockables): files written by an + * older version are discarded so stale persisted layouts fall back to the factory default + * instead of half-restoring references to dockables that no longer exist. + */ + private static final int LAYOUT_VERSION = 2; + private static final String VERSION_KEY = "dock.version"; + /** * The name of the slot applied at startup (Swing {@code DockingLayout.LAYOUT_NAMES[0]}: if * the user ever saved the {@code Default} arrangement, the UI starts with it). @@ -75,6 +86,7 @@ public void save(Map> openDockables) throws IOExcepti List ids = openDockables.getOrDefault(location, List.of()); properties.setProperty(KEY_PREFIX + location.name(), String.join(",", ids)); } + properties.setProperty(VERSION_KEY, String.valueOf(LAYOUT_VERSION)); writeProperties(properties); } @@ -95,6 +107,7 @@ public void saveSlot(String name, Map> openDockables) properties.setProperty(KEY_PREFIX + SLOT_PREFIX + name + "." + location.name(), String.join(",", ids)); } + properties.setProperty(VERSION_KEY, String.valueOf(LAYOUT_VERSION)); writeProperties(properties); } @@ -112,6 +125,9 @@ public Optional>> loadSlot(String name) throws IO return Optional.empty(); } Properties properties = readProperties(); + if (!isVersionCompatible(properties)) { + return Optional.empty(); + } // a slot is "defined" once its save wrote its keys (all three role keys are always written) if (properties.getProperty(KEY_PREFIX + SLOT_PREFIX + name + "." + DockLocation.LEFT.name()) == null) { @@ -165,7 +181,31 @@ public Map> load() throws IOException { } return result; } - return parse(readProperties(), KEY_PREFIX); + Properties properties = readProperties(); + if (!isVersionCompatible(properties)) { + // stale layout written before the MP10 dockable-role change: discard it so the + // workspace falls back to the factory default layout + clear(); + EnumMap> result = new EnumMap<>(DockLocation.class); + for (DockLocation location : DockLocation.values()) { + result.put(location, List.of()); + } + return result; + } + return parse(properties, KEY_PREFIX); + } + + /** + * @param properties the persisted properties + * @return whether the file carries the current layout version (files without the version key + * are pre-MP10 and stale) + */ + private boolean isVersionCompatible(Properties properties) { + try { + return Integer.parseInt(properties.getProperty(VERSION_KEY, "-1")) == LAYOUT_VERSION; + } catch (NumberFormatException e) { + return false; + } } /** From ab2939694fbbf3d048e76fb8dac7f15da2b83e8d Mon Sep 17 00:00:00 2001 From: opencode Date: Fri, 9 Oct 2026 23:19:30 +0200 Subject: [PATCH 115/281] =?UTF-8?q?key.ui.fx:=20drawer:=20MP10b=20?= =?UTF-8?q?=E2=80=94=20extension=20left-panel=20tabs=20become=20west=20dra?= =?UTF-8?q?wer=20items=20(instead=20of=20dockables);=20extension=20self=20?= =?UTF-8?q?test=20asserts=20the=20west=20item=20count?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../de/uka/ilkd/key/gui/fx/MainWindowF.java | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) 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 index 600a677d0a1..ad1acfe542f 100644 --- 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 @@ -39,6 +39,7 @@ import javafx.scene.control.OverrunStyle; import javafx.scene.control.RadioMenuItem; import javafx.scene.control.SeparatorMenuItem; +import javafx.scene.control.Tab; import javafx.scene.control.TextArea; import javafx.scene.control.ToggleGroup; import javafx.scene.control.ToolBar; @@ -1962,6 +1963,14 @@ private void runExtensionVerification(KeYEnvironment Date: Fri, 9 Oct 2026 23:56:23 +0200 Subject: [PATCH 116/281] =?UTF-8?q?key.ui.fx:=20extension:=20MP9.1-9.6=20s?= =?UTF-8?q?caffold=20=E2=80=94=20six=20empty=20keyext..fx=20Gradle?= =?UTF-8?q?=20modules=20(openjfx=20plugin,=20depend=20on=20:key.ui.fx=20+?= =?UTF-8?q?=20the=20keyext=20counterpart),=20settings.gradle=20includes,?= =?UTF-8?q?=20runtimeOnly=20wiring=20into=20:key.ui.fx;=20extensions=20sel?= =?UTF-8?q?f=20test=20switches=20from=20fixed=20count=203=20to=20built-in?= =?UTF-8?q?=20presence=20+=20>=3D=203=20so=20keyext=20providers=20may=20re?= =?UTF-8?q?gister?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- key.ui.fx/build.gradle | 11 ++++++++++ .../de/uka/ilkd/key/gui/fx/MainWindowF.java | 16 ++++++++++++-- keyext.caching.fx/build.gradle | 22 +++++++++++++++++++ keyext.exploration.fx/build.gradle | 22 +++++++++++++++++++ keyext.isabelletranslation.fx/build.gradle | 22 +++++++++++++++++++ keyext.proofmanagement.fx/build.gradle | 22 +++++++++++++++++++ keyext.slicing.fx/build.gradle | 22 +++++++++++++++++++ keyext.ui.testgen.fx/build.gradle | 22 +++++++++++++++++++ settings.gradle | 10 +++++++++ 9 files changed, 167 insertions(+), 2 deletions(-) create mode 100644 keyext.caching.fx/build.gradle create mode 100644 keyext.exploration.fx/build.gradle create mode 100644 keyext.isabelletranslation.fx/build.gradle create mode 100644 keyext.proofmanagement.fx/build.gradle create mode 100644 keyext.slicing.fx/build.gradle create mode 100644 keyext.ui.testgen.fx/build.gradle diff --git a/key.ui.fx/build.gradle b/key.ui.fx/build.gradle index 06f81d093ab..0536f3d4eeb 100644 --- a/key.ui.fx/build.gradle +++ b/key.ui.fx/build.gradle @@ -41,6 +41,17 @@ dependencies { // logging implementation used by the slf4j facade implementation(libs.logback.classic) + + // MP9.1-9.6: the JavaFX keyext ports (one module per keyext extension) are discovered at + // runtime through the KeYGuiExtensionF service loader; they stay off the compile classpath + // (runtimeOnly). Each module pulls its keyext counterpart — and key.ui (Swing) transitively, + // accepted for the user-approved extension migration; the FX sources never import Swing. + runtimeOnly project(":keyext.caching.fx") + runtimeOnly project(":keyext.exploration.fx") + runtimeOnly project(":keyext.isabelletranslation.fx") + runtimeOnly project(":keyext.slicing.fx") + runtimeOnly project(":keyext.ui.testgen.fx") + runtimeOnly project(":keyext.proofmanagement.fx") } application { 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 index ad1acfe542f..c5cd0761717 100644 --- 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 @@ -1944,12 +1944,24 @@ private void runExtensionVerification(KeYEnvironment statusControls = KeYGuiExtensionFacadeF.getStatusLineControls(); sb.append(" statusControls=").append(statusControls.size()); HBox statusBar = buildStatusBar(); - pass &= statusControls.size() == 2 + // 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); MenuBar menuBar = buildMenuBar(); diff --git a/keyext.caching.fx/build.gradle b/keyext.caching.fx/build.gradle new file mode 100644 index 00000000000..ecf6e46d2b1 --- /dev/null +++ b/keyext.caching.fx/build.gradle @@ -0,0 +1,22 @@ +/* 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 */ + +plugins { + // JavaFX is not bundled with the JDK; this plugin resolves the matching + // platform-specific runtime artifacts (see gradle/libs.versions.toml). + alias(libs.plugins.openjfx) +} + +description = "JavaFX port of the keyext.caching extension (MP9.1)" + +javafx { + version = libs.versions.javafx.get() + modules = ['javafx.base', 'javafx.controls', 'javafx.graphics'] +} + +dependencies { + implementation project(":key.ui.fx") + implementation project(":keyext.caching") +} diff --git a/keyext.exploration.fx/build.gradle b/keyext.exploration.fx/build.gradle new file mode 100644 index 00000000000..7250cca1ce9 --- /dev/null +++ b/keyext.exploration.fx/build.gradle @@ -0,0 +1,22 @@ +/* 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 */ + +plugins { + // JavaFX is not bundled with the JDK; this plugin resolves the matching + // platform-specific runtime artifacts (see gradle/libs.versions.toml). + alias(libs.plugins.openjfx) +} + +description = "JavaFX port of the keyext.exploration extension (MP9.2)" + +javafx { + version = libs.versions.javafx.get() + modules = ['javafx.base', 'javafx.controls', 'javafx.graphics'] +} + +dependencies { + implementation project(":key.ui.fx") + implementation project(":keyext.exploration") +} diff --git a/keyext.isabelletranslation.fx/build.gradle b/keyext.isabelletranslation.fx/build.gradle new file mode 100644 index 00000000000..682468a9760 --- /dev/null +++ b/keyext.isabelletranslation.fx/build.gradle @@ -0,0 +1,22 @@ +/* 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 */ + +plugins { + // JavaFX is not bundled with the JDK; this plugin resolves the matching + // platform-specific runtime artifacts (see gradle/libs.versions.toml). + alias(libs.plugins.openjfx) +} + +description = "JavaFX port of the keyext.isabelletranslation extension (MP9.3)" + +javafx { + version = libs.versions.javafx.get() + modules = ['javafx.base', 'javafx.controls', 'javafx.graphics'] +} + +dependencies { + implementation project(":key.ui.fx") + implementation project(":keyext.isabelletranslation") +} diff --git a/keyext.proofmanagement.fx/build.gradle b/keyext.proofmanagement.fx/build.gradle new file mode 100644 index 00000000000..16d85c1f4df --- /dev/null +++ b/keyext.proofmanagement.fx/build.gradle @@ -0,0 +1,22 @@ +/* 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 */ + +plugins { + // JavaFX is not bundled with the JDK; this plugin resolves the matching + // platform-specific runtime artifacts (see gradle/libs.versions.toml). + alias(libs.plugins.openjfx) +} + +description = "JavaFX port of the keyext.proofmanagement extension (MP9.6)" + +javafx { + version = libs.versions.javafx.get() + modules = ['javafx.base', 'javafx.controls', 'javafx.graphics'] +} + +dependencies { + implementation project(":key.ui.fx") + implementation project(":keyext.proofmanagement") +} diff --git a/keyext.slicing.fx/build.gradle b/keyext.slicing.fx/build.gradle new file mode 100644 index 00000000000..12da55ede2a --- /dev/null +++ b/keyext.slicing.fx/build.gradle @@ -0,0 +1,22 @@ +/* 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 */ + +plugins { + // JavaFX is not bundled with the JDK; this plugin resolves the matching + // platform-specific runtime artifacts (see gradle/libs.versions.toml). + alias(libs.plugins.openjfx) +} + +description = "JavaFX port of the keyext.slicing extension (MP9.4)" + +javafx { + version = libs.versions.javafx.get() + modules = ['javafx.base', 'javafx.controls', 'javafx.graphics'] +} + +dependencies { + implementation project(":key.ui.fx") + implementation project(":keyext.slicing") +} diff --git a/keyext.ui.testgen.fx/build.gradle b/keyext.ui.testgen.fx/build.gradle new file mode 100644 index 00000000000..751c0d30575 --- /dev/null +++ b/keyext.ui.testgen.fx/build.gradle @@ -0,0 +1,22 @@ +/* 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 */ + +plugins { + // JavaFX is not bundled with the JDK; this plugin resolves the matching + // platform-specific runtime artifacts (see gradle/libs.versions.toml). + alias(libs.plugins.openjfx) +} + +description = "JavaFX port of the keyext.ui.testgen extension (MP9.5)" + +javafx { + version = libs.versions.javafx.get() + modules = ['javafx.base', 'javafx.controls', 'javafx.graphics'] +} + +dependencies { + implementation project(":key.ui.fx") + implementation project(":keyext.ui.testgen") +} diff --git a/settings.gradle b/settings.gradle index 4be1f43a033..75d141505dd 100644 --- a/settings.gradle +++ b/settings.gradle @@ -33,6 +33,16 @@ include "keyext.slicing" include "keyext.caching" include "keyext.isabelletranslation" +// MP9.1-9.6: JavaFX ports of the keyext extensions (one FX module per extension). +// Each module depends on :key.ui.fx and its keyext counterpart; the providers are +// discovered via the service loader (KeYGuiExtensionF) on the :key.ui.fx runtime classpath. +include "keyext.caching.fx" +include "keyext.exploration.fx" +include "keyext.isabelletranslation.fx" +include "keyext.slicing.fx" +include "keyext.ui.testgen.fx" +include "keyext.proofmanagement.fx" + // ENABLE NULLNESS here or on the CLI // This flag is activated to enable the checker framework. // System.setProperty("ENABLE_NULLNESS", "true") From 23d1dcc284eecb9be8555ef06de4922a3e2ad9bb Mon Sep 17 00:00:00 2001 From: opencode Date: Sat, 10 Oct 2026 00:15:46 +0200 Subject: [PATCH 117/281] =?UTF-8?q?keyext.proofmanagement.fx:=20MP9.6=20?= =?UTF-8?q?=E2=80=94=20JavaFX=20port=20of=20the=20proof=20management=20ext?= =?UTF-8?q?ension=20(KeYGuiExtensionF=20provider)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- keyext.proofmanagement.fx/build.gradle | 7 + .../fx/CheckConfigDialogF.java | 302 ++++++++++++++++++ .../fx/ProofManagementExtF.java | 83 +++++ ....key.gui.fx.extension.api.KeYGuiExtensionF | 1 + .../fx/ProofManagementExtFTest.java | 80 +++++ 5 files changed, 473 insertions(+) create mode 100644 keyext.proofmanagement.fx/src/main/java/org/key_project/proofmanagement/fx/CheckConfigDialogF.java create mode 100644 keyext.proofmanagement.fx/src/main/java/org/key_project/proofmanagement/fx/ProofManagementExtF.java create mode 100644 keyext.proofmanagement.fx/src/main/resources/META-INF/services/de.uka.ilkd.key.gui.fx.extension.api.KeYGuiExtensionF create mode 100644 keyext.proofmanagement.fx/src/test/java/org/key_project/proofmanagement/fx/ProofManagementExtFTest.java diff --git a/keyext.proofmanagement.fx/build.gradle b/keyext.proofmanagement.fx/build.gradle index 16d85c1f4df..d31a2a0d54c 100644 --- a/keyext.proofmanagement.fx/build.gradle +++ b/keyext.proofmanagement.fx/build.gradle @@ -19,4 +19,11 @@ javafx { dependencies { implementation project(":key.ui.fx") implementation project(":keyext.proofmanagement") + // REQUIRED for the FX compile classpath: the SPI types (MainWindowF, KeYMediatorF, ...) are + // annotated with JSpecify type annotations referencing key.core types (e.g. a @Nullable + // KeYEnvironment field), so javac must find `key.core` when it + // loads those class files — `key.ui.fx` / `keyext.proofmanagement` declare it as + // `implementation`, which does not leak onto this module's compile classpath. The runtime + // classpath is unaffected (key.core is already transitively present at runtime). + implementation project(":key.core") } diff --git a/keyext.proofmanagement.fx/src/main/java/org/key_project/proofmanagement/fx/CheckConfigDialogF.java b/keyext.proofmanagement.fx/src/main/java/org/key_project/proofmanagement/fx/CheckConfigDialogF.java new file mode 100644 index 00000000000..a63b513f2d9 --- /dev/null +++ b/keyext.proofmanagement.fx/src/main/java/org/key_project/proofmanagement/fx/CheckConfigDialogF.java @@ -0,0 +1,302 @@ +/* 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 org.key_project.proofmanagement.fx; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import javafx.concurrent.Task; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.control.Alert; +import javafx.scene.control.Button; +import javafx.scene.control.CheckBox; +import javafx.scene.control.Dialog; +import javafx.scene.control.TextField; +import javafx.scene.control.TitledPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; +import javafx.stage.FileChooser; +import javafx.stage.Window; + +import de.uka.ilkd.key.gui.fx.help.HelpFacadeF; + +import org.key_project.proofmanagement.Main; + +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The check-configuration dialog of the proof management extension, JavaFX port of the Swing + * {@code org.key_project.proofmanagement.CheckConfigDialog} (Swing CheckConfigDialog.java:26-299). + * Selects the active checkers (Missing Proofs / Settings / Replay / Dependency), the proof bundle + * to check and the optional HTML report location, then runs the checkers in the background. + *

        + * Port notes and deviations from the Swing original: + *

          + *
        • The checkers themselves are reused unchanged: the dialog drives + * {@link Main.CheckCommand} exactly like the Swing worker (CheckConfigDialog.java:44-86) — this + * keeps the report generation and the per-checker orchestration identical.
        • + *
        • KNOWN-SIMPLIFIED: the Swing {@code BlockingGlassPane} (CheckConfigDialog.java:40, + * 96-173) is replaced by explicitly disabling the input controls while the check runs; the "Run + * checkers" button turns into "Stop" and stays the only enabled control, mirroring the glass + * pane's Stop-only pass-through. Like the Swing original, cancelling may leave partial output on + * disk (the checkers do not abort their loops on interruption).
        • + *
        • KNOWN-SIMPLIFIED: the Swing bundle/report choosers use the AWT {@code + * KeYFileChooser} with the {@code PROOF_BUNDLE_FILTER} (.zproof) and {@code + * PROOF_MANAGEMENT_REPORT_FILTER} (.html) filters (CheckConfigDialog.java:213-256); the FX port + * uses {@link FileChooser} with equivalent extension filters. File-only selection matches the + * Swing dialog (the original also only selects files, even though directory bundles exist).
        • + *
        • KNOWN-SIMPLIFIED: the Swing worker opens the generated report via + * {@code Desktop.getDesktop().open} (CheckConfigDialog.java:66-69); the FX port (no AWT/Swing) + * opens it through the {@link HelpFacadeF} browser seam (host services with a desktop fallback), + * i.e. {@code HelpFacadeF.openExternal(reportPath.toUri().toString())}.
        • + *
        • The Swing dialog leaves no trace of the check outcome on success besides the opened report + * and the stdout log; the FX port additionally reports the outcome in a small result + * {@link Alert} (requested by the MP9.6 port) before closing the dialog.
        • + *
        + */ +@NullMarked +class CheckConfigDialogF extends Dialog { + private static final Logger LOGGER = LoggerFactory.getLogger(CheckConfigDialogF.class); + + /** + * All checkers and the report generation are selected by default (Swing + * CheckConfigDialog.java:187-191). + */ + private final CheckBox missingProofsCheck = new CheckBox("Missing Proofs Checker"); + private final CheckBox settingsCheck = new CheckBox("Settings Checker"); + private final CheckBox replayCheck = new CheckBox("Replay Checker"); + private final CheckBox dependencyCheck = new CheckBox("Dependency Checker"); + private final CheckBox reportCheck = new CheckBox("Generate Report"); + + private final TextField bundleFileField = new TextField(); + private final TextField reportFileField = new TextField(); + + private final Button chooseBundleButton = new Button("Choose file..."); + private final Button chooseReportButton = new Button("Choose report location..."); + private final Button runButton = new Button("Run checkers"); + private final Button cancelButton = new Button("Cancel"); + + private Task checkWorker; + + CheckConfigDialogF(@Nullable Window owner) { + setTitle("Check configuration"); + setResizable(true); + if (owner != null) { + initOwner(owner); + } + + // all checkers and the report are active by default (Swing CheckConfigDialog.java:187-191) + missingProofsCheck.setSelected(true); + settingsCheck.setSelected(true); + replayCheck.setSelected(true); + dependencyCheck.setSelected(true); + reportCheck.setSelected(true); + + bundleFileField.setEditable(false); + bundleFileField.setPromptText("No proof bundle selected"); + reportFileField.setEditable(false); + reportFileField.setPromptText("Choose the location or file for the HTML report " + + "(default file name: \"report.html\")"); + + chooseBundleButton.setOnAction(e -> chooseBundle()); + chooseReportButton.setOnAction(e -> chooseReportLocation()); + // the report controls are enabled only while the report is selected (Swing + // CheckConfigDialog.java:239-247) + reportCheck.selectedProperty().addListener( + (obs, old, selected) -> { + chooseReportButton.setDisable(!selected); + reportFileField.setDisable(!selected); + }); + runButton.setOnAction(e -> runCheckers()); + cancelButton.setOnAction(e -> hide()); + + getDialogPane().setContent(buildContent()); + } + + /** Builds the dialog content: the three titled sections plus the button row. */ + private VBox buildContent() { + VBox checkers = new VBox(6, missingProofsCheck, settingsCheck, replayCheck, + dependencyCheck); + TitledPane checkersPane = new TitledPane("Available Checkers", checkers); + checkersPane.setCollapsible(false); + + HBox bundleBox = new HBox(6, bundleFileField, chooseBundleButton); + HBox.setHgrow(bundleFileField, Priority.ALWAYS); + TitledPane bundlePane = new TitledPane("Proof bundle to check", bundleBox); + bundlePane.setCollapsible(false); + + HBox reportBox = new HBox(6, reportFileField, chooseReportButton); + HBox.setHgrow(reportFileField, Priority.ALWAYS); + VBox reportInner = new VBox(6, reportCheck, reportBox); + TitledPane reportPane = new TitledPane("HTML report", reportInner); + reportPane.setCollapsible(false); + + HBox buttons = new HBox(10, runButton, cancelButton); + buttons.setAlignment(Pos.BOTTOM_RIGHT); + + VBox content = new VBox(10, checkersPane, bundlePane, reportPane, buttons); + content.setPadding(new Insets(12)); + return content; + } + + /** File chooser for a proof bundle file, mirroring the Swing {@code PROOF_BUNDLE_FILTER}. */ + private void chooseBundle() { + FileChooser chooser = new FileChooser(); + chooser.setTitle("Choose file"); + chooser.getExtensionFilters() + .add(new FileChooser.ExtensionFilter("proof bundles (.zproof)", "*.zproof")); + var file = chooser.showOpenDialog(getDialogPane().getScene().getWindow()); + if (file != null) { + bundleFileField.setText(file.toString()); + } + } + + /** File chooser for the HTML report location (Swing {@code PROOF_MANAGEMENT_REPORT_FILTER}). */ + private void chooseReportLocation() { + FileChooser chooser = new FileChooser(); + chooser.setTitle("Choose file or directory"); + chooser.getExtensionFilters() + .add(new FileChooser.ExtensionFilter("proof management reports (.html)", + "*.html")); + var file = chooser.showOpenDialog(getDialogPane().getScene().getWindow()); + if (file != null) { + reportFileField.setText(file.toString()); + } + } + + /** + * Runs the selected checkers on the chosen bundle in a background {@link Task} (Swing + * {@code ProofManagementCheckWorker}, CheckConfigDialog.java:44-86); on success the dialog + * closes and a small result alert reports the outcome. + */ + private void runCheckers() { + if (bundleFileField.getText().isEmpty()) { + showAlert(Alert.AlertType.ERROR, "Error", null, + "Please choose a proof bundle to check!"); + return; + } + + // lock the dialog while the check runs; the "Run checkers" button becomes "Stop" and + // stays the only enabled control (Swing BlockingGlassPane, CheckConfigDialog.java:40, + // 96-173) + setRunning(true); + checkWorker = new Task<>() { + @Override + protected Integer call() throws Exception { + Path reportPath = null; + if (reportCheck.isSelected()) { + reportPath = Paths.get(reportFileField.getText()); + if (Files.isDirectory(reportPath)) { + // add default name (Swing CheckConfigDialog.java:50-53) + reportPath = reportPath.resolve("report.html"); + } + } + Main.CheckCommand c = new Main.CheckCommand(); + c.missing = missingProofsCheck.isSelected(); + c.settings = settingsCheck.isSelected(); + c.replay = replayCheck.isSelected(); + c.dependency = dependencyCheck.isSelected(); + c.bundlePath = Paths.get(bundleFileField.getText()); + c.reportPath = reportPath; + int exitCode = c.call(); + if (reportPath != null) { + // open the report in the system browser (Swing + // CheckConfigDialog.java:66-69 via java.awt.Desktop; the FX port uses the + // HelpFacadeF browser seam — KNOWN-SIMPLIFIED, see class javadoc) + HelpFacadeF.openExternal(reportPath.toUri().toString()); + } + return exitCode; + } + }; + checkWorker.setOnSucceeded(e -> { + int exitCode = checkWorker.getValue(); + setRunning(false); + hide(); + showResultAlert(exitCode); + }); + checkWorker.setOnCancelled(e -> { + LOGGER.info("ProofManagement was cancelled by the user!"); + setRunning(false); + }); + checkWorker.setOnFailed(e -> { + Throwable error = checkWorker.getException(); + LOGGER.error("ProofManagement check failed", error); + setRunning(false); + showAlert(Alert.AlertType.ERROR, "Error", "ProofManagement interrupted due to " + + "critical error.", String.valueOf(error)); + }); + Thread thread = new Thread(checkWorker, "proof-management-check"); + thread.setDaemon(true); + thread.start(); + } + + /** Small result alert reporting the outcome of the finished check run. */ + private void showResultAlert(int exitCode) { + StringBuilder details = new StringBuilder(); + details.append("The selected checkers have been executed on the proof bundle.") + .append(System.lineSeparator()).append(System.lineSeparator()) + .append("Exit code: ").append(exitCode) + .append(exitCode == 0 ? " (success)" : " (error)"); + if (reportCheck.isSelected()) { + Path reportPath = Paths.get(reportFileField.getText()); + if (Files.isDirectory(reportPath)) { + reportPath = reportPath.resolve("report.html"); + } + details.append(System.lineSeparator()) + .append("HTML report: ").append(reportPath) + .append(" (opened in the system browser)"); + } + details.append(System.lineSeparator()).append(System.lineSeparator()) + .append("Detailed messages are written to the log."); + showAlert(Alert.AlertType.INFORMATION, "Check finished", "Proof management check done.", + details.toString()); + } + + /** + * Toggles the running state: while running, all inputs except the "Stop" button are disabled + * and the dialog cannot be closed by the user (Swing {@code BlockingGlassPane} + + * {@code DO_NOTHING_ON_CLOSE}, CheckConfigDialog.java:273-277). + * + * @param running whether the check task is running + */ + private void setRunning(boolean running) { + missingProofsCheck.setDisable(running); + settingsCheck.setDisable(running); + replayCheck.setDisable(running); + dependencyCheck.setDisable(running); + reportCheck.setDisable(running); + bundleFileField.setDisable(running); + reportFileField.setDisable(running); + chooseBundleButton.setDisable(running); + chooseReportButton.setDisable(running); + cancelButton.setDisable(running); + runButton.setText(running ? "Stop" : "Run checkers"); + setOnCloseRequest(running ? e -> e.consume() : null); + if (running) { + runButton.setOnAction(e -> { + if (checkWorker != null) { + checkWorker.cancel(true); + } + }); + } else { + runButton.setOnAction(e -> runCheckers()); + } + } + + /** Small modal alert helper (headless-safe: only called on user interaction). */ + private static void showAlert(Alert.AlertType type, String title, @Nullable String header, + String message) { + Alert alert = new Alert(type); + alert.setTitle(title); + alert.setHeaderText(header); + alert.setContentText(message); + alert.showAndWait(); + } +} diff --git a/keyext.proofmanagement.fx/src/main/java/org/key_project/proofmanagement/fx/ProofManagementExtF.java b/keyext.proofmanagement.fx/src/main/java/org/key_project/proofmanagement/fx/ProofManagementExtF.java new file mode 100644 index 00000000000..a808a2c2733 --- /dev/null +++ b/keyext.proofmanagement.fx/src/main/java/org/key_project/proofmanagement/fx/ProofManagementExtF.java @@ -0,0 +1,83 @@ +/* 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 org.key_project.proofmanagement.fx; + +import java.util.List; +import javafx.scene.control.Menu; +import javafx.scene.control.MenuItem; +import javafx.stage.Window; + +import de.uka.ilkd.key.core.fx.KeYMediatorF; +import de.uka.ilkd.key.gui.fx.MainWindowF; +import de.uka.ilkd.key.gui.fx.extension.api.KeYGuiExtensionF; + +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +/** + * The proof management extension, FX port of {@code org.key_project.proofmanagement. + * ProofManagementExt} (Swing ProofManagementExt.java:18-91): a "Proof Management" menu with the + * single active action "Check proof bundle ..." (Swing {@code CheckAction}) that opens the + * check-configuration dialog and runs the soundness checks on a proof bundle. + *

        + * Port notes and deviations from the Swing original: + *

          + *
        • The Swing provider implements only {@code KeYGuiExtension.MainMenu} (ProofManagementExt. + * java:27-28); the FX port implements the {@link KeYGuiExtensionF.MainMenuF} counterpart and + * nothing else — no other capability slot exists in the original.
        • + *
        • The FX SPI contributes whole {@link Menu}s that the host appends to the menu bar as new + * separate menus after the five built-in menus (the Swing facade grouped the extension actions + * into one "Extensions" JMenu). The "Proof Management" menu is a SIXTH menu; the five built-in + * menus and their item sets stay untouched ({@code key.fx.verify.menuparity}).
        • + *
        • Only the ACTIVE action (the {@code CheckAction}) is ported: the Merge/Bundle actions are + * commented out in the Swing original (ProofManagementExt.java:54-84) and are deliberately not + * ported.
        • + *
        • The check action is proof-bundle based (it checks a proof bundle on disk, not + * the currently loaded proof), so the provider neither reads nor requires the open proof: menu + * construction is proof-independent and the exclusive failure guard is the empty-bundle-path + * validation of the dialog (mirroring the Swing {@code CheckConfigDialog} run handler).
        • + *
        • The check configuration dialog is ported in pure JavaFX in {@link CheckConfigDialogF} — + * {@code org.key_project.proofmanagement.Main.CheckCommand} (the actual check orchestration, + * report generation included) is reused unchanged.
        • + *
        + */ +@KeYGuiExtensionF.Info(name = "Proof management", optional = true, + description = "Allows to run soundness checks on proof bundles.", experimental = false) +@NullMarked +public class ProofManagementExtF implements KeYGuiExtensionF, KeYGuiExtensionF.MainMenuF { + + /** The text of the contributed menu (Swing ProofManagementExt.MENU_PM). */ + private static final String MENU_PM = "Proof Management"; + + @Override + public List getMenus(MainWindowF window, KeYMediatorF mediator) { + // extension: MP9.6 — Swing ProofManagementExt.getMainMenuActions (ProofManagementExt. + // java:32-36): the menu holds the check action; the FX SPI contributes whole menus, so + // the action lives in a NEW separate "Proof Management" menu. The mediator is unused: + // the action is proof-bundle based and needs no open proof. + return List.of(buildMenu(window)); + } + + /** + * Builds the "Proof Management" menu with the "Check proof bundle ..." action that opens the + * check-configuration dialog ({@link CheckConfigDialogF}), parented to the given window's + * stage when available (Swing passed {@code MainWindow.getInstance()} as dialog owner, + * ProofManagementExt.java:47-50). + * + * @param window the main window, or {@code null} (tests / detached usage) — the dialog is + * opened unparented then + * @return the non-null menu + */ + static Menu buildMenu(@Nullable MainWindowF window) { + Menu menu = new Menu(MENU_PM); + MenuItem check = new MenuItem("Check proof bundle ..."); + check.setOnAction(e -> { + Window owner = window == null ? null : window.getStage(); + new CheckConfigDialogF(owner).showAndWait(); + }); + menu.getItems().add(check); + return menu; + } +} diff --git a/keyext.proofmanagement.fx/src/main/resources/META-INF/services/de.uka.ilkd.key.gui.fx.extension.api.KeYGuiExtensionF b/keyext.proofmanagement.fx/src/main/resources/META-INF/services/de.uka.ilkd.key.gui.fx.extension.api.KeYGuiExtensionF new file mode 100644 index 00000000000..4b22601d70a --- /dev/null +++ b/keyext.proofmanagement.fx/src/main/resources/META-INF/services/de.uka.ilkd.key.gui.fx.extension.api.KeYGuiExtensionF @@ -0,0 +1 @@ +org.key_project.proofmanagement.fx.ProofManagementExtF diff --git a/keyext.proofmanagement.fx/src/test/java/org/key_project/proofmanagement/fx/ProofManagementExtFTest.java b/keyext.proofmanagement.fx/src/test/java/org/key_project/proofmanagement/fx/ProofManagementExtFTest.java new file mode 100644 index 00000000000..fcc898b9291 --- /dev/null +++ b/keyext.proofmanagement.fx/src/test/java/org/key_project/proofmanagement/fx/ProofManagementExtFTest.java @@ -0,0 +1,80 @@ +/* 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 org.key_project.proofmanagement.fx; + +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.List; +import javafx.scene.control.Menu; +import javafx.scene.control.MenuItem; + +import de.uka.ilkd.key.gui.fx.extension.api.KeYGuiExtensionF; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Headless unit tests of the proof management FX extension (MP9.6): + *
          + *
        • the {@link KeYGuiExtensionF.Info} annotation mirrors the Swing original + * ({@code ProofManagementExt}: name "Proof management", optional, non-experimental),
        • + *
        • the provider contributes the "Proof Management" menu with the single active action + * "Check proof bundle ..." (the Merge/Bundle actions are commented out in the Swing original and + * are not ported),
        • + *
        • the service-loader registration file lists the provider — the wiring that makes the + * extension reach the runtime discovery of {@code key.ui.fx} (the file-based assertion avoids + * constructing the sibling providers, whose settings panels require a running FX toolkit and + * would fail in a headless unit test).
        • + *
        + */ +class ProofManagementExtFTest { + + /** The service-loader resource registered by this module. */ + private static final String SERVICE_FILE = + "META-INF/services/de.uka.ilkd.key.gui.fx.extension.api.KeYGuiExtensionF"; + + @Test + void declaresInfoAnnotation() { + KeYGuiExtensionF.Info info = + ProofManagementExtF.class.getAnnotation(KeYGuiExtensionF.Info.class); + assertNotNull(info, "the provider must carry the @Info annotation"); + assertEquals("Proof management", info.name()); + assertTrue(info.optional(), "mirrors the optional flag of the Swing original"); + assertFalse(info.experimental(), "non-experimental like the Swing original"); + } + + @Test + void contributesProofManagementMenu() { + Menu menu = ProofManagementExtF.buildMenu(null); + assertEquals("Proof Management", menu.getText()); + List items = menu.getItems(); + assertEquals(1, items.size(), "only the active CheckAction is ported"); + MenuItem check = items.get(0); + assertEquals("Check proof bundle ...", check.getText()); + assertNotNull(check.getOnAction(), "the item must open the check dialog on action"); + } + + @Test + void registersViaServiceLoaderFile() throws Exception { + List providers; + try (InputStream in = + ProofManagementExtF.class.getClassLoader().getResourceAsStream(SERVICE_FILE)) { + assertNotNull(in, "the service-loader file must be on the classpath"); + providers = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8)) + .lines() + .map(String::trim) + .filter(l -> !l.isEmpty() && !l.startsWith("#")) + .toList(); + } + assertEquals(List.of(ProofManagementExtF.class.getName()), providers, + "the service-loader file must list exactly the provider FQCN"); + } +} From e51c4d4558b729baf43cd24c0c963ed4a81dddb9 Mon Sep 17 00:00:00 2001 From: opencode Date: Sat, 10 Oct 2026 00:21:01 +0200 Subject: [PATCH 118/281] =?UTF-8?q?keyext.isabelletranslation.fx:=20MP9.3?= =?UTF-8?q?=20=E2=80=94=20JavaFX=20port=20of=20the=20Isabelle=20translatio?= =?UTF-8?q?n=20extension=20(KeYGuiExtensionF=20provider)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- keyext.isabelletranslation.fx/build.gradle | 5 + .../fx/IsabelleSettingsProviderF.java | 199 ++++++++++++++++++ .../fx/IsabelleTranslationDialogF.java | 95 +++++++++ .../fx/IsabelleTranslationExtensionF.java | 119 +++++++++++ .../fx/IsabelleTranslationRunnerF.java | 102 +++++++++ ....key.gui.fx.extension.api.KeYGuiExtensionF | 1 + .../fx/IsabelleTranslationExtensionFTest.java | 126 +++++++++++ 7 files changed, 647 insertions(+) create mode 100644 keyext.isabelletranslation.fx/src/main/java/org/key_project/isabelletranslation/fx/IsabelleSettingsProviderF.java create mode 100644 keyext.isabelletranslation.fx/src/main/java/org/key_project/isabelletranslation/fx/IsabelleTranslationDialogF.java create mode 100644 keyext.isabelletranslation.fx/src/main/java/org/key_project/isabelletranslation/fx/IsabelleTranslationExtensionF.java create mode 100644 keyext.isabelletranslation.fx/src/main/java/org/key_project/isabelletranslation/fx/IsabelleTranslationRunnerF.java create mode 100644 keyext.isabelletranslation.fx/src/main/resources/META-INF/services/de.uka.ilkd.key.gui.fx.extension.api.KeYGuiExtensionF create mode 100644 keyext.isabelletranslation.fx/src/test/java/org/key_project/isabelletranslation/fx/IsabelleTranslationExtensionFTest.java diff --git a/keyext.isabelletranslation.fx/build.gradle b/keyext.isabelletranslation.fx/build.gradle index 682468a9760..0ff2a946523 100644 --- a/keyext.isabelletranslation.fx/build.gradle +++ b/keyext.isabelletranslation.fx/build.gradle @@ -19,4 +19,9 @@ javafx { dependencies { implementation project(":key.ui.fx") implementation project(":keyext.isabelletranslation") + + // The FX SPI is declared with key.core types (Goal, PosInSequent, Services, ...), and both + // key.ui.fx and the keyext module use non-transitive `implementation` dependencies; the + // provider therefore needs key.core on its own compile classpath. (Scaffold adjustment.) + implementation project(":key.core") } diff --git a/keyext.isabelletranslation.fx/src/main/java/org/key_project/isabelletranslation/fx/IsabelleSettingsProviderF.java b/keyext.isabelletranslation.fx/src/main/java/org/key_project/isabelletranslation/fx/IsabelleSettingsProviderF.java new file mode 100644 index 00000000000..7cfb8e29675 --- /dev/null +++ b/keyext.isabelletranslation.fx/src/main/java/org/key_project/isabelletranslation/fx/IsabelleSettingsProviderF.java @@ -0,0 +1,199 @@ +/* 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 org.key_project.isabelletranslation.fx; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Collection; +import java.util.List; +import javafx.scene.Node; +import javafx.scene.control.Label; +import javafx.scene.control.Spinner; +import javafx.scene.control.TextField; + +import de.uka.ilkd.key.gui.fx.MainWindowF; +import de.uka.ilkd.key.gui.fx.settings.InvalidSettingsInputExceptionF; +import de.uka.ilkd.key.gui.fx.settings.SettingsPanelF; +import de.uka.ilkd.key.gui.fx.settings.SettingsProviderF; +import de.uka.ilkd.key.settings.Configuration; +import de.uka.ilkd.key.settings.PathConfig; + +import org.key_project.isabelletranslation.IsabelleTranslationSettings; + +import org.jspecify.annotations.NullMarked; + +/** + * The Isabelle translation settings panel, FX port of + * {@code org.key_project.isabelletranslation.IsabelleSettingsProvider} (Swing + * IsabelleSettingsProvider.java:27-211): the location of the translation files, the Isabelle + * installation folder (with the live version-support check) and the solver timeout, reading and + * writing the {@link IsabelleTranslationSettings} singleton + * (IsabelleTranslationSettings.java:156-197). + */ +@NullMarked +final class IsabelleSettingsProviderF extends SettingsPanelF implements SettingsProviderF { + + private static final String INFO_TIMEOUT_FIELD = + """ + Timeout for the external solvers in seconds. + """; + private static final String INFO_TRANSLATION_PATH_PANEL = + """ + Choose where the isabelle translation files are stored. + """; + + private static final Collection SUPPORTED_VERSIONS_TEXT = + List.of("Isabelle2023", "Isabelle2024-RC1", "Isabelle2024", "Isabelle2025"); + + private static final String INFO_ISABELLE_PATH_PANEL = String.format( + """ + Specify the absolute path of the Isabelle folder. + %s. + """, createSupportedVersionText()); + + private enum IsabelleSupportState { + SUPPORTED, NOT_SUPPORTED, NO_ISABELLE + } + + /** Panel for inputting the path to where translations are stored */ + private final TextField translationPathPanel; + + /** Panel for inputting the path to Isabelle installation */ + private final TextField isabellePathPanel; + + /** Input field for timeout in seconds */ + private final Spinner timeoutField; + + /** Supported version info for user */ + private final TextField versionSupported; + + /** The current settings object */ + private final IsabelleTranslationSettings settings; + + IsabelleSettingsProviderF() { + setHeaderText(getDescription()); + setSubHeaderText("Isabelle settings are stored in: " + // SETTINGS_FILE is package-protected in the keyext module; the public + // PathConfig.getSettingsFile resolves the same file. + + PathConfig.getSettingsFile("isabelleSettings.json").toAbsolutePath()); + this.settings = IsabelleTranslationSettings.getInstance(); + this.translationPathPanel = createTranslationPathPanel(); + this.isabellePathPanel = createIsabellePathPanel(); + this.timeoutField = createTimeoutField(); + createCheckSupportButton(); + this.versionSupported = createSolverSupported(); + updateVersionSupportText(); + } + + @Override + public String getDescription() { + return "Isabelle Translation"; + } + + @Override + public Node getPanel(MainWindowF window) { + // extension: MP9.3 — Swing IsabelleSettingsProvider.java:99-106: {@code getPanel} re-reads + // the settings into the input components (panel instances are reused by the settings + // dialog, so every open re-syncs). + isabellePathPanel.setText(settings.getIsabellePath().toString()); + translationPathPanel.setText(settings.getTranslationPath().toString()); + timeoutField.getValueFactory().setValue(settings.getTimeout()); + return this; + } + + private TextField createTranslationPathPanel() { + return addFileChooserPanel("Location for translation files:", "", + INFO_TRANSLATION_PATH_PANEL, false, null); + } + + private TextField createIsabellePathPanel() { + TextField panel = addFileChooserPanel("Isabelle installation folder:", "", + INFO_ISABELLE_PATH_PANEL, false, null); + // Swing IsabelleSettingsProvider.java:119-135: a DocumentListener re-checks the version + // support while the path is being typed. + panel.textProperty().addListener((obs, old, value) -> updateVersionSupportText()); + return panel; + } + + private Spinner createTimeoutField() { + Spinner spinner = + addIntNumberField("Timeout:", 1, Integer.MAX_VALUE, 1, settings.getTimeout(), + INFO_TIMEOUT_FIELD, null); + // Swing IsabelleSettingsProvider.java:142-150 keeps a ChangeListener on the JSpinner that + // writes the timeout immediately (the Swing applySettings does not persist it itself — + // IsabelleTranslationSettings.java:175-190 falls back to the default 30 when the key is + // absent). + spinner.valueProperty().addListener((obs, old, value) -> { + if (value != null) { + settings.setTimeout(value); + } + }); + return spinner; + } + + private void createCheckSupportButton() { + javafx.scene.control.Button checkForSupportButton = + new javafx.scene.control.Button("Check for support"); + checkForSupportButton.setOnAction(e -> updateVersionSupportText()); + addRowWithHelp(null, new Label(), checkForSupportButton); + } + + private void updateVersionSupportText() { + versionSupported.setText(getSolverSupportText()); + } + + private IsabelleSupportState checkForSupport() { + String isabelleVersion; + Path isabelleIdentifierPath = + Paths.get(isabellePathPanel.getText(), "/etc/ISABELLE_IDENTIFIER"); + try { + isabelleVersion = Files.readAllLines(isabelleIdentifierPath).getFirst(); + } catch (IOException e) { + return IsabelleSupportState.NO_ISABELLE; + } + return SUPPORTED_VERSIONS_TEXT.contains(isabelleVersion) ? IsabelleSupportState.SUPPORTED + : IsabelleSupportState.NOT_SUPPORTED; + } + + private TextField createSolverSupported() { + TextField txt = addTextField("Support", getSolverSupportText(), + createSupportedVersionText(), null); + txt.setEditable(false); + return txt; + } + + private static String createSupportedVersionText() { + return "Supports these Isabelle versions: " + String.join(", ", SUPPORTED_VERSIONS_TEXT); + } + + private String getSolverSupportText() { + return switch (checkForSupport()) { + case NOT_SUPPORTED -> + "This version of Isabelle is not supported and is thus unlikely to work."; + case SUPPORTED -> "This version of Isabelle is supported."; + case NO_ISABELLE -> "Isabelle could not be found in the chosen directory."; + }; + } + + @Override + public void apply(MainWindowF window) throws InvalidSettingsInputExceptionF { + // extension: MP9.3 — Swing IsabelleSettingsProvider.java:204-211: {@code applySettings} + // writes a fresh Configuration and lets the settings object read it back (this triggers + // the session-file creation of IsabelleTranslationSettings when the translation path + // changed, IsabelleTranslationSettings.java:182-187). Unlike the Swing original the + // timeout is carried in the Configuration as well, so applying the panel cannot reset it + // to the default 30. + Configuration newConfig = new Configuration(); + // KNOWN-SIMPLIFIED: the key constants (isabellePathKey / translationPathKey / timeoutKey) + // are package-protected in the keyext module; their stable string values are mirrored here. + newConfig.set("Path", isabellePathPanel.getText()); + newConfig.set("TranslationPath", translationPathPanel.getText()); + newConfig.set("Timeout", String.valueOf(timeoutField.getValue())); + settings.readSettings(newConfig); + updateVersionSupportText(); + } +} diff --git a/keyext.isabelletranslation.fx/src/main/java/org/key_project/isabelletranslation/fx/IsabelleTranslationDialogF.java b/keyext.isabelletranslation.fx/src/main/java/org/key_project/isabelletranslation/fx/IsabelleTranslationDialogF.java new file mode 100644 index 00000000000..20e38a0bf5e --- /dev/null +++ b/keyext.isabelletranslation.fx/src/main/java/org/key_project/isabelletranslation/fx/IsabelleTranslationDialogF.java @@ -0,0 +1,95 @@ +/* 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 org.key_project.isabelletranslation.fx; + +import java.util.List; +import javafx.scene.Scene; +import javafx.scene.control.Alert; +import javafx.scene.control.Tab; +import javafx.scene.control.TabPane; +import javafx.scene.control.TextArea; +import javafx.stage.Stage; +import javafx.stage.Window; + +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +/** + * Small dialog showing the Isabelle translations of the context-menu actions, FX port of the + * Swing {@code org.key_project.isabelletranslation.gui.InformationWindow} + * (InformationWindow.java:27-98): one tab per translated goal with the generated theory text. + */ +@NullMarked +final class IsabelleTranslationDialogF { + + /** + * One translation result: the preamble and the translation theory of a goal, or the exception + * message of a failed translation. + */ + record Entry(String name, @Nullable String preamble, @Nullable String translation, + @Nullable String error) { + + /** The tab title of this entry (Swing IsabelleProblem.getName: "Goal "). */ + String title() { + return error == null ? name : name + " (failed)"; + } + + /** The text shown in the tab. */ + String content() { + if (error != null) { + return "Translation failed:\n" + error; + } + StringBuilder sb = new StringBuilder(); + if (preamble != null) { + sb.append(preamble).append(System.lineSeparator()).append(System.lineSeparator()); + } + if (translation != null) { + sb.append(translation); + } + return sb.toString(); + } + } + + private IsabelleTranslationDialogF() { + } + + /** + * Shows the translations of all goals in a modal dialog. + * + * @param owner the owner window, may be {@code null} + * @param entries the translation entries (one per goal), non-null + */ + static void show(@Nullable Window owner, List entries) { + if (entries == null || entries.isEmpty()) { + Alert alert = new Alert(Alert.AlertType.INFORMATION, "No translations to show."); + alert.setTitle("Isabelle Translation"); + if (owner != null) { + alert.initOwner(owner); + } + alert.showAndWait(); + return; + } + // extension: MP9.3 — Swing InformationWindow.java:33-46: a tab pane with one tab per + // translation; the read-only text areas mirror the JTextArea content plus the row-header + // line numbers (the line numbers are a pure cosmetic detail and are omitted here). + Stage stage = new Stage(); + if (owner != null) { + stage.initOwner(owner); + } + stage.setTitle("Isabelle Translation"); + TabPane tabPane = new TabPane(); + for (Entry entry : entries) { + TextArea area = new TextArea(entry.content()); + area.setEditable(false); + area.setWrapText(true); + area.setPrefSize(600, 480); + Tab tab = new Tab(entry.title(), area); + tab.setClosable(false); + tabPane.getTabs().add(tab); + } + stage.setScene(new Scene(tabPane, 640, 520)); + stage.show(); + } +} diff --git a/keyext.isabelletranslation.fx/src/main/java/org/key_project/isabelletranslation/fx/IsabelleTranslationExtensionF.java b/keyext.isabelletranslation.fx/src/main/java/org/key_project/isabelletranslation/fx/IsabelleTranslationExtensionF.java new file mode 100644 index 00000000000..9ffbf573d9c --- /dev/null +++ b/keyext.isabelletranslation.fx/src/main/java/org/key_project/isabelletranslation/fx/IsabelleTranslationExtensionF.java @@ -0,0 +1,119 @@ +/* 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 org.key_project.isabelletranslation.fx; + +import java.util.List; +import javafx.scene.control.MenuItem; +import javafx.stage.Window; + +import de.uka.ilkd.key.core.fx.KeYMediatorF; +import de.uka.ilkd.key.gui.fx.MainWindowF; +import de.uka.ilkd.key.gui.fx.extension.api.KeYGuiExtensionF; +import de.uka.ilkd.key.gui.fx.settings.SettingsProviderF; +import de.uka.ilkd.key.pp.PosInSequent; +import de.uka.ilkd.key.proof.Goal; + +import org.key_project.isabelletranslation.IsabelleTranslationSettings; + +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The Isabelle translation extension, JavaFX port of + * {@code org.key_project.isabelletranslation.IsabelleTranslationExtension} (Swing + * IsabelleTranslationExtension.java:28-68): translates the sequent of a goal to an Isabelle theory + * and checks the translation with the Isabelle backend. + *

        + * The FX port implements the same three capability slots as the Swing original + * (IsabelleTranslationExtension.java:30-31, {@code Settings}, {@code ContextMenu}, + * {@code Startup}): the Isabelle settings panel, the SEQUENT_VIEW context-menu entries + * ("Translate to Isabelle" / "Translate all goals to Isabelle") and the startup hook that + * initializes {@link IsabelleTranslationSettings}. + *

        + * KNOWN-SIMPLIFIED: the Swing actions ({@code IsabelleTranslationAction.solveGoals}, + * IsabelleTranslationAction.java:47-81) hand the generated theories to the external Isabelle + * solver ({@code IsabelleLauncher}) and present a progress/model dialog. The FX port only runs + * the sequent translation ({@code IsabelleTranslator.translateProblem}, shared with Swing) and + * shows the resulting Isabelle theory in a plain dialog — the solver launch and its progress + * window are omitted (the heavy Scala/Isabelle dependency of the backend is never loaded by this + * provider). + */ +@NullMarked +@KeYGuiExtensionF.Info(name = "Isabelle Translation", optional = true, experimental = false, + description = "Translate the sequent of the selected goal into an Isabelle theory. " + + "KNOWN-SIMPLIFIED (FX): the generated theory is shown in a dialog; launching the " + + "external Isabelle solver to check the translation is deferred.") +public class IsabelleTranslationExtensionF implements KeYGuiExtensionF, + KeYGuiExtensionF.SettingsF, KeYGuiExtensionF.ContextMenuF, KeYGuiExtensionF.StartupF { + + private static final Logger LOGGER = + LoggerFactory.getLogger(IsabelleTranslationExtensionF.class); + + /** the settings panel, created lazily on first {@link #getSettings()} call */ + private IsabelleSettingsProviderF settingsProvider; + + @Override + public SettingsProviderF getSettings() { + // extension: MP9.3 — Swing IsabelleTranslationExtension.java:33-35: getSettings() returns + // the Isabelle settings panel; the host registers it into the SettingsManagerF registry + // (Swing SettingsManager.registerProvider). The panel is created lazily (Swing creates a + // fresh panel per call), so constructing the provider itself stays side-effect free. + if (settingsProvider == null) { + settingsProvider = new IsabelleSettingsProviderF(); + } + return settingsProvider; + } + + @Override + public List getSequentContextItems(@Nullable KeYMediatorF mediator, + @Nullable Goal goal, @Nullable PosInSequent pos) { + // extension: MP9.3 — Swing ContextMenuAdapter (IsabelleTranslationExtension.java:41-56): + // for ContextMenuKind.SEQUENT_VIEW the extension contributes the two translate actions + // exactly when the click was NOT on a term inside a formula (pos.getPosInOccurrence() + // == null) and a goal is selected. The FX SPI hands the clicked goal directly; the null + // guards keep the same behaviour without a mediator round trip. + if (pos == null || pos.getPosInOccurrence() != null || goal == null || mediator == null) { + return List.of(); + } + MenuItem translateGoal = new MenuItem("Translate to Isabelle"); + translateGoal.setOnAction( + e -> IsabelleTranslationRunnerF.translateGoal(goal, ownerWindow(translateGoal))); + MenuItem translateAllGoals = new MenuItem("Translate all goals to Isabelle"); + translateAllGoals.setOnAction( + e -> IsabelleTranslationRunnerF.translateAllGoals(goal, + ownerWindow(translateAllGoals))); + return List.of(translateGoal, translateAllGoals); + } + + @Override + public void init(MainWindowF window, KeYMediatorF mediator) { + // extension: MP9.3 — Swing IsabelleTranslationExtension.java:65-67: the startup hook + // initializes the settings singleton (loads the JSON settings file / installs the default + // configuration and the shutdown-hook that saves it). IsabelleTranslationSettings itself + // is plain Java (IsabelleTranslationSettings.java:25-223) but it lives in a module that + // pulls key.ui (Swing) and the scala-isabelle backend transitively; the initialization is + // kept defensive so a class-loading hiccup can never block the app startup. + try { + IsabelleTranslationSettings.getInstance(); + } catch (RuntimeException e) { + LOGGER.error("Could not initialize the Isabelle translation settings", e); + } + } + + /** + * The owner window of the context menu a menu item lives in, used as the dialog owner when + * the translation result is shown; {@code null} when the popup is already gone (the dialog + * then opens owner-less). + * + * @param item the clicked menu item + * @return the owner window or {@code null} + */ + private static @Nullable Window ownerWindow(MenuItem item) { + var popup = item.getParentPopup(); + return popup != null ? popup.getOwnerWindow() : null; + } +} diff --git a/keyext.isabelletranslation.fx/src/main/java/org/key_project/isabelletranslation/fx/IsabelleTranslationRunnerF.java b/keyext.isabelletranslation.fx/src/main/java/org/key_project/isabelletranslation/fx/IsabelleTranslationRunnerF.java new file mode 100644 index 00000000000..6f7eb5cc9f2 --- /dev/null +++ b/keyext.isabelletranslation.fx/src/main/java/org/key_project/isabelletranslation/fx/IsabelleTranslationRunnerF.java @@ -0,0 +1,102 @@ +/* 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 org.key_project.isabelletranslation.fx; + +import java.util.ArrayList; +import java.util.List; +import javafx.stage.Window; + +import de.uka.ilkd.key.java.Services; +import de.uka.ilkd.key.proof.Goal; + +import org.key_project.isabelletranslation.automation.IsabelleProblem; +import org.key_project.isabelletranslation.translation.IllegalFormulaException; +import org.key_project.isabelletranslation.translation.IsabelleTranslator; +import org.key_project.util.javafx.FxUtil; + +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Runs the Isabelle sequent translation of the context-menu actions of + * {@link IsabelleTranslationExtensionF} and presents the resulting theories in a dialog. + *

        + * KNOWN-SIMPLIFIED: the Swing actions hand the generated theories to the external Isabelle + * solver and show a progress/model dialog (IsabelleTranslationAction.solveGoals, + * IsabelleTranslationAction.java:47-81). The FX port stops after the translation itself + * ({@link IsabelleTranslator#translateProblem(Goal)}, the exact same translation machinery the + * Swing action uses) and displays the theory text. The translation is executed on a background + * thread so the heavy handler loading never blocks the FX main thread, and the class-loading of + * the scala-isabelle backend is avoided entirely (it is only needed by the launcher). + */ +@NullMarked +final class IsabelleTranslationRunnerF { + + private static final Logger LOGGER = LoggerFactory.getLogger(IsabelleTranslationRunnerF.class); + + private IsabelleTranslationRunnerF() { + } + + /** + * Translates the given goal into an Isabelle theory and shows the result. + * + * @param goal the goal to translate + * @param owner the dialog owner, may be {@code null} + */ + static void translateGoal(Goal goal, @Nullable Window owner) { + translate(List.of(goal), owner); + } + + /** + * Translates all open goals of the proof of the given goal and shows the results. + * + * @param goal a goal of the proof whose open goals shall be translated + * @param owner the dialog owner, may be {@code null} + */ + static void translateAllGoals(Goal goal, @Nullable Window owner) { + translate(goal.proof().openGoals().stream().toList(), owner); + } + + private static void translate(List goals, @Nullable Window owner) { + if (goals.isEmpty()) { + return; + } + Goal first = goals.get(0); + Services services = first.proof().getServices(); + if (services == null) { + LOGGER.warn("No proof services available - cannot translate to Isabelle"); + return; + } + IsabelleTranslator translator = new IsabelleTranslator(services); + Thread thread = new Thread(() -> { + List entries = new ArrayList<>(); + for (Goal goal : goals) { + entries.add(translateGoal(translator, goal)); + } + FxUtil.runLater(() -> IsabelleTranslationDialogF.show(owner, entries)); + }, "IsabelleTranslationThread"); + thread.setDaemon(true); + thread.start(); + } + + private static IsabelleTranslationDialogF.Entry translateGoal(IsabelleTranslator translator, + Goal goal) { + try { + // Swing IsabelleTranslationAction.java:53-61: translateProblem throws + // IllegalFormulaException for untranslatable formulas; the problem still enters the + // result list so the user sees which goals failed. + IsabelleProblem problem = translator.translateProblem(goal); + return new IsabelleTranslationDialogF.Entry(problem.getName(), problem.getPreamble(), + problem.getTranslation(), null); + } catch (IllegalFormulaException e) { + LOGGER.debug("Translation of {} failed", goal, e); + return new IsabelleTranslationDialogF.Entry("Goal " + goal.node().serialNr(), null, + null, + e.getMessage()); + } + } +} diff --git a/keyext.isabelletranslation.fx/src/main/resources/META-INF/services/de.uka.ilkd.key.gui.fx.extension.api.KeYGuiExtensionF b/keyext.isabelletranslation.fx/src/main/resources/META-INF/services/de.uka.ilkd.key.gui.fx.extension.api.KeYGuiExtensionF new file mode 100644 index 00000000000..1f8f9f782fc --- /dev/null +++ b/keyext.isabelletranslation.fx/src/main/resources/META-INF/services/de.uka.ilkd.key.gui.fx.extension.api.KeYGuiExtensionF @@ -0,0 +1 @@ +org.key_project.isabelletranslation.fx.IsabelleTranslationExtensionF diff --git a/keyext.isabelletranslation.fx/src/test/java/org/key_project/isabelletranslation/fx/IsabelleTranslationExtensionFTest.java b/keyext.isabelletranslation.fx/src/test/java/org/key_project/isabelletranslation/fx/IsabelleTranslationExtensionFTest.java new file mode 100644 index 00000000000..07c147e3789 --- /dev/null +++ b/keyext.isabelletranslation.fx/src/test/java/org/key_project/isabelletranslation/fx/IsabelleTranslationExtensionFTest.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 org.key_project.isabelletranslation.fx; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import javafx.application.Platform; +import javafx.scene.control.MenuItem; + +import de.uka.ilkd.key.control.DefaultUserInterfaceControl; +import de.uka.ilkd.key.control.KeYEnvironment; +import de.uka.ilkd.key.core.fx.KeYMediatorF; +import de.uka.ilkd.key.gui.fx.extension.api.KeYGuiExtensionF; +import de.uka.ilkd.key.gui.fx.settings.SettingsProviderF; +import de.uka.ilkd.key.pp.PosInSequent; +import de.uka.ilkd.key.proof.Goal; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Headless unit test of the MP9.3 FX port of the Isabelle translation extension + * ({@link IsabelleTranslationExtensionF}): the {@link KeYGuiExtensionF.Info} metadata, the + * settings contribution and the sequent context-menu contribution (including the null guards of + * the SPI call). + *

        + * Like the other extension tests of this repository the term-level assertion runs against a real + * {@link Goal} of a headlessly loaded trivial proof ({@code \problem { true }}, same pattern as + * {@code SequentMenuModelFTest}). Constructing JavaFX controls (the settings panel, the menu + * items) requires the JavaFX toolkit, which is started once in {@link #initFx()} (no window is + * ever shown; the toolkit runs with software rendering). + */ +class IsabelleTranslationExtensionFTest { + + private KeYEnvironment env; + + @BeforeAll + static void initFx() { + try { + Platform.startup(() -> { + }); + } catch (IllegalStateException e) { + // the platform is already running (e.g. a shared test JVM) + } + // keep the toolkit alive without any open window for the whole test class + Platform.setImplicitExit(false); + } + + @AfterEach + void tearDown() { + if (env != null) { + env.dispose(); + env = null; + } + } + + @Test + void infoAnnotationDeclaresTheExtension() { + KeYGuiExtensionF.Info info = new IsabelleTranslationExtensionF().getClass() + .getAnnotation(KeYGuiExtensionF.Info.class); + assertNotNull(info, "@Info must be present on the provider"); + assertEquals("Isabelle Translation", info.name()); + assertTrue(info.optional(), "the Swing original is optional (the user may disable it)"); + assertFalse(info.experimental(), + "the Swing original is not experimental (IsabelleTranslationExtension.java:29)"); + } + + @Test + void settingsContribution() { + IsabelleTranslationExtensionF extension = new IsabelleTranslationExtensionF(); + SettingsProviderF settings = extension.getSettings(); + assertNotNull(settings, "the extension must contribute a settings provider"); + assertTrue(settings instanceof IsabelleSettingsProviderF, + "the provider must be the FX settings panel"); + assertEquals("Isabelle Translation", settings.getDescription()); + } + + @Test + void contextMenuGuardsNullInputs() { + IsabelleTranslationExtensionF extension = new IsabelleTranslationExtensionF(); + // null inputs must never crash (the SPI call is guarded by the provider itself) + assertEquals(List.of(), extension.getSequentContextItems(null, null, null)); + } + + @Test + void contextMenuProducesTranslateItemsForSequentPosition() throws Exception { + KeYMediatorF mediator = mediatorWithGoal(); + Goal goal = mediator.getSelectedGoal(); + assertNotNull(goal, "the demo problem must produce a selectable goal"); + + IsabelleTranslationExtensionF extension = new IsabelleTranslationExtensionF(); + // Swing parity (IsabelleTranslationExtension.java:48): the two translate actions appear + // for a sequent-level click (PosInSequent without an occurrence) on a selected goal. + List items = + extension.getSequentContextItems(mediator, goal, PosInSequent.createSequentPos()); + assertEquals(2, items.size(), "both translate actions must be contributed"); + assertEquals("Translate to Isabelle", items.get(0).getText()); + assertEquals("Translate all goals to Isabelle", items.get(1).getText()); + assertNotNull(items.get(0).getOnAction(), "the single-goal action must be wired"); + assertNotNull(items.get(1).getOnAction(), "the all-goals action must be wired"); + } + + /** + * Loads {@code \problem { true }} headlessly and selects the root goal in a fresh mediator. + */ + private KeYMediatorF mediatorWithGoal() throws Exception { + Path dir = Files.createTempDirectory("isabelletranslation-fx"); + Path main = dir.resolve("test.key"); + // No \include: the JavaDL profile loads the standard library from the classpath. + Files.writeString(main, "\\problem { true }\n"); + env = KeYEnvironment.load(main); + KeYMediatorF mediator = new KeYMediatorF(); + mediator.getSelectionModel().setSelectedProof(env.getLoadedProof()); + assertNotNull(mediator.getSelectedGoal(), "the root goal must be selectable"); + return mediator; + } +} From e57152f16f32b5cd8136c54cc88554fa89d01858 Mon Sep 17 00:00:00 2001 From: opencode Date: Sat, 10 Oct 2026 00:26:35 +0200 Subject: [PATCH 119/281] =?UTF-8?q?keyext.slicing.fx:=20MP9.4=20=E2=80=94?= =?UTF-8?q?=20JavaFX=20port=20of=20the=20slicing=20extension=20(KeYGuiExte?= =?UTF-8?q?nsionF=20provider)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- keyext.slicing.fx/build.gradle | 15 + .../slicing/fx/SlicingExtensionF.java | 292 ++++++++ .../slicing/fx/SlicingSettingsProviderF.java | 154 +++++ .../slicing/fx/ui/SlicingLeftPanelF.java | 640 ++++++++++++++++++ ....key.gui.fx.extension.api.KeYGuiExtensionF | 1 + .../slicing/fx/SlicingExtensionFTest.java | 97 +++ 6 files changed, 1199 insertions(+) create mode 100644 keyext.slicing.fx/src/main/java/org/key_project/slicing/fx/SlicingExtensionF.java create mode 100644 keyext.slicing.fx/src/main/java/org/key_project/slicing/fx/SlicingSettingsProviderF.java create mode 100644 keyext.slicing.fx/src/main/java/org/key_project/slicing/fx/ui/SlicingLeftPanelF.java create mode 100644 keyext.slicing.fx/src/main/resources/META-INF/services/de.uka.ilkd.key.gui.fx.extension.api.KeYGuiExtensionF create mode 100644 keyext.slicing.fx/src/test/java/org/key_project/slicing/fx/SlicingExtensionFTest.java diff --git a/keyext.slicing.fx/build.gradle b/keyext.slicing.fx/build.gradle index 12da55ede2a..9d9e4f729a8 100644 --- a/keyext.slicing.fx/build.gradle +++ b/keyext.slicing.fx/build.gradle @@ -19,4 +19,19 @@ javafx { dependencies { implementation project(":key.ui.fx") implementation project(":keyext.slicing") + + // MP9.4 (slicing port): needed on the COMPILE classpath. key.ui.fx and keyext.slicing + // declare :key.core as `implementation`, which does not leak into a consumer's compile + // classpath — yet the KeYGuiExtensionF SPI (PosInSequent, Goal, Proof), KeYMediatorF + // (Proof/Node accessors) and the keyext.slicing logic APIs all reference key.core types. + // key.core itself `api`-exposes key.util, key.ncore and key.ncore.calculus + // (org.key_project.prover.sequent.PosInOccurrence), so a single implementation dep suffices. + implementation project(":key.core") + + // The slicing settings singleton lives behind the Swing SlicingSettingsProvider + // (SlicingSettingsProvider.getSlicingSettings) and the reused slicing logic reads it + // from there, so resolving that class file requires key.ui (the same "key.ui transitively, + // accepted for the user-approved extension migration; the FX sources never import Swing" + // wiring the key.ui.fx scaffold documents). No Swing type is used in this module's code. + implementation project(":key.ui") } diff --git a/keyext.slicing.fx/src/main/java/org/key_project/slicing/fx/SlicingExtensionF.java b/keyext.slicing.fx/src/main/java/org/key_project/slicing/fx/SlicingExtensionF.java new file mode 100644 index 00000000000..d43afdffeb2 --- /dev/null +++ b/keyext.slicing.fx/src/main/java/org/key_project/slicing/fx/SlicingExtensionF.java @@ -0,0 +1,292 @@ +/* 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 org.key_project.slicing.fx; + +import java.util.ArrayList; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import javafx.scene.control.MenuItem; +import javafx.scene.control.Tab; + +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.gui.fx.MainWindowF; +import de.uka.ilkd.key.gui.fx.extension.api.KeYGuiExtensionF; +import de.uka.ilkd.key.gui.fx.settings.SettingsProviderF; +import de.uka.ilkd.key.pp.PosInSequent; +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.event.ProofDisposedEvent; +import de.uka.ilkd.key.proof.event.ProofDisposedListener; +import de.uka.ilkd.key.settings.GeneralSettings; +import de.uka.ilkd.key.settings.ProofIndependentSettings; + +import org.key_project.prover.sequent.PosInOccurrence; +import org.key_project.slicing.DependencyTracker; +import org.key_project.slicing.SlicingSettingsProvider; +import org.key_project.slicing.fx.ui.SlicingLeftPanelF; +import org.key_project.slicing.graph.GraphNode; + +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Proof slicing extension, FX port of {@code org.key_project.slicing.SlicingExtension} (the + * Swing KeYGuiExtension provider, SlicingExtension.java:48-215). Registered via the service + * loader and implements the same capability set: a left-panel tab (the {@link SlicingLeftPanelF}, + * west drawer item), the SEQUENT_VIEW context-menu items, the settings provider and the startup + * hook (tracker creation + the single-core-only parallel-prover guard). The slicing logic itself + * (the {@link DependencyTracker}, the dependency graph and the {@code SlicingProofReplayer}) is + * reused unchanged from {@code keyext.slicing}. + *

        + * KNOWN-SIMPLIFIED: the Swing provider additionally registers a proof-load listener + * through the Swing mediator ({@code mediator.registerProofLoadListener}); the FX mediator has no + * such registry, and every proof load routes through + * {@code KeYSelectionModel.setSelectedProof} anyway, so the {@link KeYSelectionListener} below + * covers both the "new proof loaded" and the "selected proof switched" cases. + * + * @author Alexander Weigl + * @author Arne Keller (port) + */ +@KeYGuiExtensionF.Info(name = "Slicing", + description = "Author: Arne Keller ", + experimental = false, + optional = true, + priority = 9001) +@NullMarked +public class SlicingExtensionF implements KeYGuiExtensionF, KeYGuiExtensionF.ContextMenuF, + KeYGuiExtensionF.StartupF, KeYGuiExtensionF.LeftPanelF, KeYGuiExtensionF.SettingsF, + KeYSelectionListener, ProofDisposedListener { + + private static final Logger LOGGER = LoggerFactory.getLogger(SlicingExtensionF.class); + + /** + * Collection of dependency trackers attached to proofs (Swing + * {@code SlicingExtension.trackers}). + */ + public final Map trackers = new IdentityHashMap<>(); + + /** + * The left panel inserted into the west drawer, created lazily on the first + * {@link #getLeftPanelTabs} call (Swing {@code SlicingExtension.leftPanel}). + */ + private @Nullable SlicingLeftPanelF leftPanel = null; + + /** + * The main window this extension is attached to; {@code null} until the provider is wired + * into a window (never in the running app — the SPI callbacks always pass one — and in + * unit tests). + */ + private @Nullable MainWindowF window; + + /** + * If set to true, the rule application de-duplication algorithm is automatically limited to + * the "safe mode" for the next loaded proof (Swing {@code SlicingExtension. + * enableSafeModeForNextProof}). + */ + private boolean enableSafeModeForNextProof = false; + + /** Lazily created settings provider (Swing {@code SlicingSettingsProvider}). */ + private @Nullable SettingsProviderF settingsProvider = null; + + /** + * @return the main window this extension is attached to, may be {@code null} in unit tests + */ + @Nullable + public MainWindowF window() { + return window; + } + + @Override + public void init(MainWindowF window, KeYMediatorF mediator) { + this.window = window; + mediator.getSelectionModel().addKeYSelectionListener(this); + // Slicing is single-core only (Swing SlicingExtension.init): enabling the multi-core + // prover suspends every DependencyTracker for the duration of each run, so it silently + // misses the rules that run applies. Mark all existing trackers incomplete the instant + // the multi-core prover is switched on, so a later switch back to single-core can never + // slice a proof from a graph with a gap in it. (New trackers are not created while the + // multi-core prover is enabled; see createTrackerForProof.) + ProofIndependentSettings.DEFAULT_INSTANCE.getGeneralSettings().addPropertyChangeListener( + GeneralSettings.PARALLEL_PROVER_ENABLED, evt -> { + if (ProofIndependentSettings.DEFAULT_INSTANCE.getGeneralSettings() + .isParallelProverEnabled()) { + trackers.values().forEach(tracker -> { + if (tracker != null) { + tracker.markIncompleteAfterParallelRun(); + } + }); + } + SlicingLeftPanelF panel = leftPanel; + if (panel != null) { + org.key_project.util.javafx.FxUtil.runLater(panel::updateUIState); + } + }); + } + + @Override + public void selectedProofChanged(KeYSelectionEvent e) { + createTrackerForProof(e.getSource().getSelectedProof()); + } + + /** + * Attach a dependency tracker to the given proof (Swing + * {@code SlicingExtension.createTrackerForProof}): skipped while the multi-core prover is + * enabled (slicing is single-core only; the tracker would miss everything the parallel + * prover applies). + * + * @param newProof the proof to attach a tracker to, may be {@code null} + */ + private void createTrackerForProof(@Nullable Proof newProof) { + if (newProof == null) { + return; + } + // Proof slicing is a single-core-only feature: do not attach a tracker while the + // multi-core prover is enabled. + if (ProofIndependentSettings.DEFAULT_INSTANCE.getGeneralSettings() + .isParallelProverEnabled()) { + return; + } + trackers.computeIfAbsent(newProof, proof -> { + proof.addProofDisposedListener(this); + DependencyTracker tracker = new DependencyTracker(proof); + SlicingLeftPanelF panel = leftPanel; + if (panel != null) { + proof.addRuleAppListener(e -> panel.ruleAppliedOnProof(proof, tracker)); + proof.addProofTreeListener(panel); + if (enableSafeModeForNextProof) { + SlicingSettingsProvider.getSlicingSettings() + .deactivateAggressiveDeduplicate(proof); + enableSafeModeForNextProof = false; + } + } + return tracker; + }); + } + + /** + * The left-panel singleton tab (created together with the panel on the first + * {@link #getLeftPanelTabs} call); repeated queries return the same tab. + */ + private @Nullable Tab leftPanelTab = null; + + @Override + public List getLeftPanelTabs(MainWindowF window, KeYMediatorF mediator) { + if (leftPanel == null) { + this.window = window; + leftPanel = new SlicingLeftPanelF(mediator, this); + mediator.getSelectionModel().addKeYSelectionListener(leftPanel); + leftPanelTab = new Tab(); + leftPanelTab.setText(getPanelTitle()); + leftPanelTab.setClosable(false); + leftPanelTab.setContent(leftPanel); + } + return List.of(leftPanelTab); + } + + /** + * @return the title of the left panel / west drawer item (Swing + * {@code SlicingLeftPanel.getTitle}) + */ + static String getPanelTitle() { + return "Proof Slicing"; + } + + @Override + public List getSequentContextItems(KeYMediatorF mediator, Goal goal, + PosInSequent pos) { + // The guards of the Swing adapter (SlicingExtension.java:86-101): the items only make + // sense if a tracker exists for the selected proof and the clicked position resolves to + // a formula that some proof step produced. + if (trackers.isEmpty() || pos == null || pos.getPosInOccurrence() == null + || pos.getPosInOccurrence().topLevel() == null + || mediator.getSelectedNode() == null) { + return List.of(); + } + DependencyTracker tracker = trackers.get(mediator.getSelectedProof()); + if (tracker == null) { + return List.of(); + } + Node currentNode = mediator.getSelectedNode(); + Proof currentProof = currentNode.proof(); + + PosInOccurrence topLevel = pos.getPosInOccurrence().topLevel(); + Node node = tracker.getNodeThatProduced(currentNode, topLevel); + if (node == null) { + return List.of(); + } + List list = new ArrayList<>(); + list.add(showCreatedByItem(mediator, node)); + GraphNode graphNode = tracker.getDependencyGraph() + .getGraphNode(currentProof, currentNode.getBranchLocation(), topLevel); + if (graphNode != null) { + list.add(showGraphItem(tracker, graphNode)); + } + return list; + } + + /** + * The "Show proof step that created this formula" item (Swing {@code ShowCreatedByAction}): + * switches the FX selection to the producing node. + */ + private static MenuItem showCreatedByItem(KeYMediatorF mediator, Node node) { + MenuItem item = new MenuItem(String.format( + "Show proof step that created this formula (switches to proof step %d)", + node.serialNr())); + item.setOnAction(e -> mediator.getSelectionModel().setSelectedNode(node)); + return item; + } + + /** + * The "Show dependency graph around this formula" item (Swing {@code ShowGraphAction}); + * KNOWN-SIMPLIFIED: the Swing action renders the DOT excerpt through + * {@code PreviewDialog}; this port shows the DOT source in a text dialog instead (the + * graphviz image renderer is Swing-only). + */ + private static MenuItem showGraphItem(DependencyTracker tracker, GraphNode graphNode) { + MenuItem item = new MenuItem("Show dependency graph around this formula"); + item.setOnAction(e -> { + String text = tracker.exportDotAround(false, false, graphNode); + SlicingLeftPanelF.showTextDialog("Dependency graph around this formula", text); + }); + return item; + } + + @Override + public SettingsProviderF getSettings() { + if (settingsProvider == null) { + settingsProvider = new SlicingSettingsProviderF(); + } + return settingsProvider; + } + + @Override + public void proofDisposing(ProofDisposedEvent e) { + trackers.put(e.getSource(), null); + trackers.remove(e.getSource()); + SlicingLeftPanelF panel = leftPanel; + if (panel != null) { + panel.proofDisposed(e.getSource()); + } + } + + @Override + public void proofDisposed(ProofDisposedEvent e) { + // mirror of the Swing provider: nothing to do on full disposal + } + + /** + * Activate the de-duplication safe mode for the next loaded proof (Swing + * {@code SlicingExtension.enableSafeModeForNextProof}). + */ + public void enableSafeModeForNextProof() { + this.enableSafeModeForNextProof = true; + } +} diff --git a/keyext.slicing.fx/src/main/java/org/key_project/slicing/fx/SlicingSettingsProviderF.java b/keyext.slicing.fx/src/main/java/org/key_project/slicing/fx/SlicingSettingsProviderF.java new file mode 100644 index 00000000000..fc49d0541e5 --- /dev/null +++ b/keyext.slicing.fx/src/main/java/org/key_project/slicing/fx/SlicingSettingsProviderF.java @@ -0,0 +1,154 @@ +/* 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 org.key_project.slicing.fx; + +import javafx.scene.Node; +import javafx.scene.control.CheckBox; +import javafx.scene.control.Label; +import javafx.scene.control.TextField; + +import de.uka.ilkd.key.gui.fx.MainWindowF; +import de.uka.ilkd.key.gui.fx.settings.InvalidSettingsInputExceptionF; +import de.uka.ilkd.key.gui.fx.settings.SettingsPanelF; +import de.uka.ilkd.key.gui.fx.settings.SettingsProviderF; + +import org.key_project.slicing.SlicingSettings; +import org.key_project.slicing.SlicingSettingsProvider; + +import org.jspecify.annotations.NullMarked; + +/** + * The proof-slicing options panel, FX port of the Swing + * {@code org.key_project.slicing.SlicingSettingsProvider} (SlicingSettingsProvider.java:21-120): + * the "always track" and "aggressive rule de-duplication" toggles plus the graphviz dot + * executable path, reading and writing the persisted {@link SlicingSettings} of the slicing + * extension. + *

        + * KNOWN-SIMPLIFIED: {@code SlicingSettings.setAggressiveDeduplicate} is package-private in + * {@code org.key_project.slicing} and cannot be called from this module (which may not modify the + * keyext.slicing sources); the toggle therefore reflects the current setting when the panel is + * opened, but the FX dialog does not persist changes to it. The other two options persist + * normally. + * + * @author Alexander Weigl + * @author Arne Keller (port) + */ +@NullMarked +public final class SlicingSettingsProviderF extends SettingsPanelF implements SettingsProviderF { + + /** + * The settings description shown in the settings dialog tree (Swing + * {@code SlicingSettingsProvider.getDescription}); also the shared + * {@link KeYGuiExtensionF.SettingsF} assertion constant of the MP9.4 unit test. + */ + public static final String DESCRIPTION = "Proof Slicing"; + + /** + * The settings priority (Swing {@code SlicingSettingsProvider.getPriorityOfSettings}); + * also the shared unit-test assertion constant. + */ + public static final int PRIORITY_OF_SETTINGS = 10000; + + /** + * Text for introductory explanation (Swing {@code SlicingSettingsProvider.INTRO_LABEL}). + */ + private static final String INTRO_LABEL = "Adjust proof analysis algorithm options here."; + /** + * Label for always track option (Swing {@code SlicingSettingsProvider.ALWAYS_TRACK}). + */ + private static final String ALWAYS_TRACK = "Always track dependencies"; + /** + * Explanatory text for always track option (Swing {@code ALWAYS_TRACK_INFO}). + */ + private static final String ALWAYS_TRACK_INFO = """ + If enabled, the dependency tracker will construct the dependency graph as the proof + is created. When disabled, the dependency graph is created only when needed, and + the 'Show proof step that created this formula' action is not available."""; + /** + * Label for aggressive deduplicate option (Swing + * {@code SlicingSettingsProvider.AGGRESSIVE_DEDUPLICATE}). + */ + private static final String AGGRESSIVE_DEDUPLICATE = "Aggressive rule de-duplication"; + /** + * Explanatory text for the aggressive de-duplication option (Swing + * {@code AGGRESSIVE_DEDUPLICATE_INFO}). + */ + private static final String AGGRESSIVE_DEDUPLICATE_INFO = + """ + If enabled, the analysis algorithm will de-duplicate more than one duplicate pair at once. + This may attempt to combine duplicates in impossible ways. + Disable if you're having trouble slicing a proof using the de-duplication algorithm."""; + /** + * Label of the graphviz executable path field (Swing + * {@code SlicingSettingsProvider.DOT_EXECUTABLE}). + */ + private static final String DOT_EXECUTABLE = "Graphviz dot executable"; + /** + * Explanatory text of the graphviz path field (Swing {@code DOT_EXECUTABLE_INFO}). + */ + private static final String DOT_EXECUTABLE_INFO = + "Path to dot executable from the graphviz package."; + + /** The "always track" checkbox (Swing {@code SlicingSettingsProvider.alwaysTrack}). */ + private final CheckBox alwaysTrack; + /** + * The "aggressive de-duplication" checkbox (Swing + * {@code SlicingSettingsProvider.aggressiveDeduplicate}). + */ + private final CheckBox aggressiveDeduplicate; + /** The dot executable path field (Swing {@code SlicingSettingsProvider.dotExecutable}). */ + private final TextField dotExecutable; + + /** + * Construct the settings panel (Swing {@code SlicingSettingsProvider} constructor). + */ + public SlicingSettingsProviderF() { + setHeaderText("Proof Slicing Options"); + + Label intro = new Label(INTRO_LABEL); + pCenter.add(intro, 0, pCenter.getRowCount(), 3, 1); + + addSeparator("Dependency graph"); + alwaysTrack = addCheckBox(ALWAYS_TRACK, ALWAYS_TRACK_INFO, true); + dotExecutable = addTextField(DOT_EXECUTABLE, "dot", DOT_EXECUTABLE_INFO, null); + + addSeparator("Duplicate rule applications"); + aggressiveDeduplicate = addCheckBox(AGGRESSIVE_DEDUPLICATE, + AGGRESSIVE_DEDUPLICATE_INFO, true); + } + + @Override + public String getDescription() { + // Swing SlicingSettingsProvider.getDescription + return DESCRIPTION; + } + + @Override + public Node getPanel(MainWindowF window) { + // Swing SlicingSettingsProvider.getPanel: re-read the persisted settings + SlicingSettings ss = SlicingSettingsProvider.getSlicingSettings(); + alwaysTrack.setSelected(ss.getAlwaysTrack()); + dotExecutable.setText(ss.getDotExecutable()); + aggressiveDeduplicate.setSelected(ss.getAggressiveDeduplicate(null)); + return this; + } + + @Override + public void apply(MainWindowF window) throws InvalidSettingsInputExceptionF { + // Swing SlicingSettingsProvider.applySettings + SlicingSettings ss = SlicingSettingsProvider.getSlicingSettings(); + ss.setAlwaysTrack(alwaysTrack.isSelected()); + ss.setDotExecutable(dotExecutable.getText()); + // KNOWN-SIMPLIFIED: SlicingSettings.setAggressiveDeduplicate(boolean) is package-private + // in org.key_project.slicing, so this module (which must not modify keyext.slicing) + // can read the flag but not persist changes made in the dialog. + } + + @Override + public int getPriorityOfSettings() { + // Swing SlicingSettingsProvider.getPriorityOfSettings: sorted last like Swing + return PRIORITY_OF_SETTINGS; + } +} diff --git a/keyext.slicing.fx/src/main/java/org/key_project/slicing/fx/ui/SlicingLeftPanelF.java b/keyext.slicing.fx/src/main/java/org/key_project/slicing/fx/ui/SlicingLeftPanelF.java new file mode 100644 index 00000000000..3f0b4ab5a93 --- /dev/null +++ b/keyext.slicing.fx/src/main/java/org/key_project/slicing/fx/ui/SlicingLeftPanelF.java @@ -0,0 +1,640 @@ +/* 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 org.key_project.slicing.fx.ui; + +import java.io.BufferedWriter; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; +import javafx.application.Platform; +import javafx.concurrent.Task; +import javafx.geometry.Insets; +import javafx.scene.Node; +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.TextArea; +import javafx.scene.control.TitledPane; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; +import javafx.stage.FileChooser; + +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.gui.fx.MainWindowF; +import de.uka.ilkd.key.proof.Proof; +import de.uka.ilkd.key.proof.ProofTreeEvent; +import de.uka.ilkd.key.proof.ProofTreeListener; +import de.uka.ilkd.key.proof.io.ProblemLoaderControl; +import de.uka.ilkd.key.settings.GeneralSettings; +import de.uka.ilkd.key.settings.ProofIndependentSettings; + +import org.key_project.slicing.DependencyTracker; +import org.key_project.slicing.RuleStatistics.RuleStatisticEntry; +import org.key_project.slicing.SlicingProofReplayer; +import org.key_project.slicing.SlicingSettingsProvider; +import org.key_project.slicing.analysis.AnalysisResults; +import org.key_project.slicing.fx.SlicingExtensionF; + +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The proof-slicing left panel, FX port of {@code org.key_project.slicing.ui.SlicingLeftPanel} + * (Swing SlicingLeftPanel.java): the dependency-graph stats and export controls, the proof + * analysis controls and the slicing buttons, rendered with pure JavaFX controls. The underlying + * logic (dependency tracking, analysis, slicing) is reused from {@code keyext.slicing} unchanged. + *

        + * KNOWN-SIMPLIFIED (compared to the Swing panel): + *

          + *
        • "Show rendering of graph" renders the DOT through the Swing-only graphviz executor in the + * original; this port shows the DOT source in a text dialog instead (no AWT/Swing imports).
        • + *
        • "Slice proof to fixed point" opens the Swing-only iterative {@code SliceToFixedPointDialog} + * in the original; this port performs a single slicing iteration (identical to "Slice proof").
        • + *
        • "Show rule statistics" renders an HTML table with sort buttons in the original; this port + * shows the plain-text statistics with the default "total applications, descending" sort.
        • + *
        • The execution timings are shown as plain labels instead of an HTML table, and the 100 ms + * update debounce of the graph labels is dropped ({@link FxUtil}-marshalled direct updates).
        • + *
        + * + * @author Alexander Weigl + * @author Arne Keller (port) + */ +@NullMarked +public class SlicingLeftPanelF extends ScrollPane + implements KeYSelectionListener, ProofTreeListener { + + private static final Logger LOGGER = LoggerFactory.getLogger(SlicingLeftPanelF.class); + + private static final String NO_PROOF_SELECTED = "No proof selected"; + + /** KeY mediator instance. */ + private final KeYMediatorF mediator; + /** Extension instance. */ + private final SlicingExtensionF extension; + + /** The proof currently shown in the KeY UI. */ + private @Nullable Proof currentProof = null; + + /** "Export as DOT" button. */ + private Button dotExport = null; + /** "Show rendering of graph" button. */ + private Button showGraphRendering = null; + /** "Slice proof" button. */ + private Button sliceProof = null; + /** "Slice proof to fixed point" button. */ + private Button sliceProofFixedPoint = null; + /** "Run analysis" button. */ + private Button runAnalysis = null; + /** "Show rule statistics" button. */ + private Button showRuleStatistics = null; + /** Label indicating the number of dependency graph nodes. */ + private Label graphNodes = null; + /** Label indicating the number of dependency graph edges. */ + private Label graphEdges = null; + /** Label showing total number of steps in the analyzed proof. */ + private Label totalSteps = null; + /** Label showing number of useful steps as determined by the analysis. */ + private Label usefulSteps = null; + /** Label showing total number of branches in the analyzed proof. */ + private Label totalBranches = null; + /** Label showing number of useful branches as determined by the analysis. */ + private Label usefulBranches = null; + /** Checkbox to abbreviate formulas in DOT output. */ + private CheckBox abbreviateFormulas = null; + /** Checkbox to shorten chains in DOT output. */ + private CheckBox abbreviateChains = null; + /** Checkbox to enable the dependency analysis algorithm. */ + private CheckBox doDependencyAnalysis = null; + /** Checkbox to enable rule de-duplication. */ + private CheckBox doDeduplicateRuleApps = null; + /** Panel showing the execution time of the algorithms. */ + private VBox timings = null; + /** Titled section wrapping {@link #timings}, hidden until an analysis ran. */ + private TitledPane timingsPane = null; + + /** Number of nodes in the dependency graph. */ + private int graphNodesNr = 0; + /** Number of edges in the dependency graph. */ + private int graphEdgesNr = 0; + + /** + * Construct a new panel for this extension (Swing SlicingLeftPanel constructor). + * + * @param mediator the KeY mediator + * @param extension instance of the extension + */ + public SlicingLeftPanelF(KeYMediatorF mediator, SlicingExtensionF extension) { + super(); + this.mediator = mediator; + this.extension = extension; + + buildUI(); + + updateUIState(); + + // Keep the slice buttons in sync with the prover mode (slicing is single-core only). + // The property-change listener may fire from the prover thread, so marshal the UI + // refresh onto the FX thread. + ProofIndependentSettings.DEFAULT_INSTANCE.getGeneralSettings().addPropertyChangeListener( + GeneralSettings.PARALLEL_PROVER_ENABLED, + evt -> org.key_project.util.javafx.FxUtil.runLater(this::updateUIState)); + } + + /** Build the JavaFX control tree (Swing {@code SlicingLeftPanel.buildUI}). */ + private void buildUI() { + VBox content = new VBox(10); + content.setPadding(new Insets(10)); + content.getChildren().addAll(getDependencyGraphPanel(), getProofAnalysisPanel(), + getProofSlicingPanel(), getTimingsPanel()); + setContent(content); + setFitToWidth(true); + setHbarPolicy(ScrollBarPolicy.NEVER); + } + + /** The "Dependency graph" section (Swing {@code getDependencyGraphPanel}). */ + private TitledPane getDependencyGraphPanel() { + abbreviateFormulas = new CheckBox("Abbreviate node labels"); + abbreviateFormulas.setTooltip(nativeTooltip("Replace node labels with their hash value.")); + abbreviateChains = new CheckBox("Shorten long chains"); + abbreviateChains.setTooltip(nativeTooltip(""" + Collapse long chains when rendering the graph. + When enabled: dependency graph nodes with both input and output degree equal to one + will be collapsed. + These shortened edges are labeled by: initial step ... last step""")); + dotExport = new Button("Export as DOT"); + dotExport.setMaxWidth(Double.MAX_VALUE); + dotExport.setOnAction(e -> exportDot()); + showGraphRendering = new Button("Show rendering of graph"); + showGraphRendering.setMaxWidth(Double.MAX_VALUE); + showGraphRendering.setOnAction(e -> previewGraph()); + + graphNodes = new Label(); + graphEdges = new Label(); + resetGraphLabels(); + + return titledPane("Dependency graph", List.of(graphNodes, graphEdges, + abbreviateFormulas, abbreviateChains, dotExport, showGraphRendering)); + } + + /** The "Proof analysis" section (Swing {@code getProofAnalysisPanel}). */ + private TitledPane getProofAnalysisPanel() { + totalSteps = new Label(); + usefulSteps = new Label(); + totalBranches = new Label(); + usefulBranches = new Label(); + doDependencyAnalysis = new CheckBox("Dependency analysis"); + doDependencyAnalysis.setSelected(true); + doDependencyAnalysis.setOnAction(e -> resetLabels()); + doDeduplicateRuleApps = new CheckBox("De-duplicate rule applications"); + doDeduplicateRuleApps.setSelected(false); + doDeduplicateRuleApps.setOnAction(e -> resetLabels()); + runAnalysis = new Button("Run analysis"); + runAnalysis.setMaxWidth(Double.MAX_VALUE); + runAnalysis.setOnAction(e -> analyzeProof()); + showRuleStatistics = new Button("Show rule statistics"); + showRuleStatistics.setMaxWidth(Double.MAX_VALUE); + showRuleStatistics.setOnAction(e -> showRuleStatistics()); + + return titledPane("Proof analysis", List.of(totalSteps, usefulSteps, totalBranches, + usefulBranches, doDependencyAnalysis, doDeduplicateRuleApps, runAnalysis, + showRuleStatistics)); + } + + /** The "Proof slicing" section (Swing {@code buildUI} panel3). */ + private TitledPane getProofSlicingPanel() { + sliceProof = new Button("Slice proof"); + sliceProof.setMaxWidth(Double.MAX_VALUE); + sliceProof.setOnAction(e -> sliceProof()); + sliceProofFixedPoint = new Button("Slice proof to fixed point"); + sliceProofFixedPoint.setMaxWidth(Double.MAX_VALUE); + // KNOWN-SIMPLIFIED: the Swing original opens the iterative SliceToFixedPointDialog + // (slice -> analysis -> slice ...); this port performs a single slicing iteration. + sliceProofFixedPoint.setOnAction(e -> sliceProof()); + sliceProofFixedPoint.setTooltip(nativeTooltip( + """ + Slices the proof and analyzes the result; the process repeats until no more steps can + be removed. KNOWN-SIMPLIFIED (FX): a single slicing iteration is performed, as with + "Slice proof".""")); + + return titledPane("Proof slicing", List.of(sliceProof, sliceProofFixedPoint)); + } + + /** The (initially hidden) "Execution timings" section (Swing {@code timings}). */ + private TitledPane getTimingsPanel() { + timings = new VBox(4); + timingsPane = titledPane("Execution timings", List.of(timings)); + timingsPane.setVisible(false); + return timingsPane; + } + + /** Creates a titled section and the one-column vertical stack of its children. */ + private static TitledPane titledPane(String title, List children) { + VBox box = new VBox(6); + box.getChildren().addAll(children); + TitledPane pane = new TitledPane(title, box); + pane.setAnimated(false); + return pane; + } + + /** A plain JavaFX tooltip (the Swing original uses Swing HTML tooltips). */ + private static javafx.scene.control.Tooltip nativeTooltip(String text) { + javafx.scene.control.Tooltip tooltip = new javafx.scene.control.Tooltip(text); + tooltip.setWrapText(true); + return tooltip; + } + + /** Export the dependency graph as a DOT file (Swing {@code SlicingLeftPanel.exportDot}). */ + private void exportDot() { + if (currentProof == null) { + return; + } + DependencyTracker tracker = extension.trackers.get(currentProof); + if (tracker == null) { + return; + } + FileChooser fileChooser = new FileChooser(); + fileChooser.setTitle("Choose filename to save dot file"); + fileChooser.setInitialFileName("export.dot"); + // KNOWN-SIMPLIFIED: the Swing original passes the file-chooser parent component; the FX + // chooser runs owner-less instead (the drawer panel has no dedicated window). + java.io.File file = fileChooser.showSaveDialog(null); + if (file == null) { + return; + } + try (BufferedWriter writer = Files.newBufferedWriter(file.toPath(), + StandardCharsets.UTF_8)) { + writer.write(tracker.exportDot(abbreviateFormulas.isSelected(), + abbreviateChains.isSelected())); + } catch (IOException exc) { + LOGGER.error("failed to export DOT file", exc); + showError(exc); + } + } + + /** Show the rule statistics of the current proof (Swing {@code showRuleStatistics}). */ + private void showRuleStatistics() { + if (currentProof == null) { + return; + } + AnalysisResults results = analyzeProof(); + if (results == null) { + return; + } + // KNOWN-SIMPLIFIED: the Swing dialog renders an HTML table with four sort buttons; this + // port shows the plain-text rows with the default "total applications, descending" order. + List entries = results.ruleStatistics.sortBy( + Comparator.comparing(RuleStatisticEntry::numberOfApplications).reversed()); + StringBuilder text = new StringBuilder(); + for (RuleStatisticEntry entry : entries) { + text.append(entry.ruleName()).append(": ").append(entry.numberOfApplications()) + .append(" applications, ").append(entry.numberOfUselessApplications()) + .append(" useless, ") + .append(entry.numberOfInitialUselessApplications()) + .append(" initial useless\n"); + } + showTextDialog("Rule Statistics", + text.length() == 0 ? "No rule applications recorded." : text.toString()); + } + + /** Show the dependency graph excerpt of the current proof (Swing {@code previewGraph}). */ + private void previewGraph() { + if (currentProof == null) { + return; + } + DependencyTracker tracker = extension.trackers.get(currentProof); + if (tracker == null) { + return; + } + String text = tracker.exportDot(abbreviateFormulas.isSelected(), + abbreviateChains.isSelected()); + // KNOWN-SIMPLIFIED: the Swing original renders the DOT to a PNG via the Swing-only + // graphviz executor and shows an image dialog; this port shows the DOT source text. + showTextDialog("Dependency graph (DOT)", text); + } + + /** + * Analyze the current proof with the selected algorithms (Swing + * {@code SlicingLeftPanel.analyzeProof}). UI updates are marshalled onto the FX thread + * because this method may run on a background thread (slicing worker). + * + * @return the analysis results, or {@code null} if no proof is loaded or analysis failed + */ + private @Nullable AnalysisResults analyzeProof() { + if (currentProof == null) { + return null; + } + try { + DependencyTracker tracker = extension.trackers.get(currentProof); + if (tracker == null) { + return null; + } + AnalysisResults results = tracker.analyze(doDependencyAnalysis.isSelected(), + doDeduplicateRuleApps.isSelected()); + org.key_project.util.javafx.FxUtil.runLater(this::updateUIState); + return results; + } catch (Exception e) { + LOGGER.error("failed to analyze proof", e); + org.key_project.util.javafx.FxUtil.runLater(() -> showError(e)); + } + return null; + } + + /** Slice the current proof (Swing {@code SlicingLeftPanel.sliceProof}). */ + private void sliceProof() { + if (currentProof == null) { + return; + } + final @Nullable AnalysisResults results = analyzeProof(); + if (results == null) { + return; + } + if (!results.indicateSlicingPotential()) { + updateUIState(); + return; + } + MainWindowF window = extension.window(); + if (window == null) { + // cannot happen in the running app; guards against unit-test usage + LOGGER.warn("Slicing requested without an attached main window"); + return; + } + final Proof proofToSlice = currentProof; + Task task = new Task<>() { + @Override + protected Path call() throws Exception { + // KNOWN-SIMPLIFIED: the Swing original passes a headless + // DefaultUserInterfaceControl; this port uses the FX window's own user-interface + // control (a ProblemLoaderControl as well) so the loader callbacks stay FX-safe. + ProblemLoaderControl control = window.getUserInterfaceControl(); + SlicingProofReplayer replayer = SlicingProofReplayer + .constructSlicer(control, proofToSlice, results, null); + Path proofFile; + // first slice attempt: leave aggressive de-duplicate on + if (results.didDeduplicateRuleApps + && SlicingSettingsProvider.getSlicingSettings() + .getAggressiveDeduplicate(proofToSlice)) { + try { + proofFile = replayer.slice(); + } catch (Exception e) { + LOGGER.error( + "failed to slice using aggressive de-duplication, enabling safe mode ", + e); + SlicingSettingsProvider.getSlicingSettings() + .deactivateAggressiveDeduplicate(proofToSlice); + AnalysisResults fixedResults = analyzeProof(); + proofFile = SlicingProofReplayer + .constructSlicer(control, proofToSlice, fixedResults, null) + .slice(); + } + } else { + // second slice attempt / only dependency analysis + proofFile = replayer.slice(); + } + // if this slicing iteration required safe mode to be activated, + // the next slicing iteration probably also requires safe mode + if (!SlicingSettingsProvider.getSlicingSettings() + .getAggressiveDeduplicate(proofToSlice)) { + extension.enableSafeModeForNextProof(); + } + return proofFile; + } + }; + task.setOnSucceeded(event -> { + // KNOWN-SIMPLIFIED: the Swing original loads the slice through the problem loader + // directly to keep it out of the recent-files list ({@code UI.loadProblem}); the FX + // port takes the public load pipeline of the main window, which registers the file + // in the recent-files list. + window.openProofFile(task.getValue()); + }); + task.setOnFailed(event -> { + Throwable error = task.getException() != null ? task.getException() + : new IllegalStateException("slicing failed"); + showError(error); + }); + Thread thread = new Thread(task); + thread.setDaemon(true); + thread.start(); + } + + /** Show the given exception in a dialog (Swing {@code SlicingLeftPanel.showError}). */ + private void showError(Throwable exc) { + LOGGER.error("failed to slice proof", exc); + Platform.runLater(() -> { + Alert alert = new Alert(Alert.AlertType.ERROR, + exc.getMessage() == null ? exc.toString() : exc.getMessage()); + alert.setHeaderText("Error in proof slicing"); + alert.setTitle("Proof Slicing"); + alert.showAndWait(); + }); + } + + private void resetLabels() { + totalSteps.setText("Total steps: ?"); + usefulSteps.setText("Useful steps: ?"); + totalBranches.setText("Total branches: ?"); + usefulBranches.setText("Useful branches: ?"); + showRuleStatistics.setDisable(true); + timings.getChildren().clear(); + timingsPane.setVisible(false); + } + + private void displayResults(@Nullable AnalysisResults results) { + if (results == null) { + resetLabels(); + return; + } + totalSteps.setText("Total steps: " + results.totalSteps); + usefulSteps.setText("Useful steps: " + results.usefulStepsNr); + totalBranches.setText("Total branches: " + results.proof.countBranches()); + usefulBranches.setText("Useful branches: " + results.usefulBranchesNr); + showRuleStatistics.setDisable(false); + timings.getChildren().clear(); + // KNOWN-SIMPLIFIED: the Swing original renders an HTML table via HtmlFactory; this port + // shows one "Algorithm: time" line per measured activity. + List lines = results.executionTime.executionTimes() + .map(action -> action.first + ": " + action.second + " ms") + .collect(Collectors.toList()); + lines.forEach(line -> timings.getChildren().add(new Label(line))); + timingsPane.setVisible(!lines.isEmpty()); + } + + private void resetGraphLabels() { + graphNodes.setText("Graph nodes: ?"); + graphEdges.setText("Graph edges: ?"); + } + + private void displayGraphLabels() { + graphNodes.setText("Graph nodes: " + graphNodesNr); + graphEdges.setText("Graph edges: " + graphEdgesNr); + } + + @Override + public void selectedNodeChanged(KeYSelectionEvent e) { + // the panel only reacts to proof switches + } + + @Override + public void selectedProofChanged(KeYSelectionEvent e) { + // selection events may be fired from the prover thread (rule applications re-default the + // selection), so marshal the UI refresh onto the FX thread + org.key_project.util.javafx.FxUtil.runLater(() -> { + currentProof = mediator.getSelectedProof(); + resetLabels(); + resetGraphLabels(); + updateUIState(); + DependencyTracker tracker = extension.trackers.get(currentProof); + if (tracker == null) { + return; + } + if (tracker.getAnalysisResults() != null) { + displayResults(tracker.getAnalysisResults()); + } + if (tracker.getDependencyGraph() != null) { + graphNodesNr = tracker.getDependencyGraph().countNodes(); + graphEdgesNr = tracker.getDependencyGraph().countEdges(); + displayGraphLabels(); + } + }); + } + + /** + * Notify the panel that a rule has been applied on the currently opened proof (Swing + * {@code SlicingLeftPanel.ruleAppliedOnProof}). Called from the prover thread; the update is + * marshalled onto the FX thread. + * + * @param proof proof of the rule application + * @param tracker dependency tracker of that proof + */ + public void ruleAppliedOnProof(Proof proof, DependencyTracker tracker) { + int nodes = tracker.getDependencyGraph().countNodes(); + int edges = tracker.getDependencyGraph().countEdges(); + org.key_project.util.javafx.FxUtil.runLater(() -> { + currentProof = proof; + graphNodesNr = nodes; + graphEdgesNr = edges; + displayGraphLabels(); + updateUIState(); + }); + } + + @Override + public void proofPruned(ProofTreeEvent e) { + ruleAppliedOnProof(e.getSource(), extension.trackers.get(e.getSource())); + } + + /** + * Updates the enabled/disabled state of all controls (Swing + * {@code SlicingLeftPanel.updateUIState}). + *

        + * KNOWN-SIMPLIFIED: the Swing panel greys itself out through the Swing-only + * {@code SingleCoreFeatureGate}; this port checks the parallel-prover setting directly and + * disables the panel recursively. + */ + public void updateUIState() { + if (!Platform.isFxApplicationThread()) { + org.key_project.util.javafx.FxUtil.runLater(this::updateUIState); + return; + } + // Proof slicing is single-core only: grey out the whole panel while the multi-core + // prover is active (its dependency tracker does not record during parallel runs). + boolean parallelProverActive = ProofIndependentSettings.DEFAULT_INSTANCE + .getGeneralSettings().isParallelProverEnabled(); + setDisable(parallelProverActive); + + boolean noProofLoaded = currentProof == null; + if (parallelProverActive) { + return; + } + dotExport.setDisable(noProofLoaded); + dotExport.setTooltip(noProofLoaded ? nativeTooltip(NO_PROOF_SELECTED) : null); + showGraphRendering.setDisable(noProofLoaded); + showGraphRendering.setTooltip(noProofLoaded ? nativeTooltip(NO_PROOF_SELECTED) : null); + runAnalysis.setDisable(noProofLoaded); + runAnalysis.setTooltip(noProofLoaded ? nativeTooltip(NO_PROOF_SELECTED) : null); + showRuleStatistics.setDisable(noProofLoaded); + showRuleStatistics.setTooltip( + noProofLoaded ? nativeTooltip(NO_PROOF_SELECTED) + : nativeTooltip("Statistics available after analysis")); + sliceProof.setDisable(noProofLoaded); + sliceProof.setTooltip(noProofLoaded ? nativeTooltip(NO_PROOF_SELECTED) : null); + sliceProofFixedPoint.setDisable(noProofLoaded); + sliceProofFixedPoint.setTooltip(noProofLoaded ? nativeTooltip(NO_PROOF_SELECTED) : null); + if (noProofLoaded) { + return; + } + boolean algoSelectionSane = doDependencyAnalysis.isSelected() + || doDeduplicateRuleApps.isSelected(); + runAnalysis.setDisable(!algoSelectionSane); + runAnalysis.setTooltip(null); + sliceProof.setDisable(!algoSelectionSane); + sliceProof.setTooltip(null); + sliceProofFixedPoint.setDisable(false); + sliceProofFixedPoint.setTooltip(nativeTooltip(""" + Slices the proof. The resulting proof is analyzed: + if more steps may be sliced away, the process repeats. + Warning: the original proof and intermediate slicing + iterations are automatically removed!""")); + DependencyTracker tracker = extension.trackers.get(currentProof); + if (tracker != null) { + AnalysisResults results = tracker.getAnalysisResults(); + if (results != null && results.usefulSteps.size() == results.totalSteps) { + String cannotSliceMinimal = "Cannot remove any proof steps"; + sliceProof.setDisable(true); + sliceProof.setTooltip(nativeTooltip(cannotSliceMinimal)); + sliceProofFixedPoint.setDisable(true); + sliceProofFixedPoint.setTooltip(nativeTooltip(cannotSliceMinimal)); + } + } + } + + /** + * Notify the panel that a proof has been disposed (Swing + * {@code SlicingLeftPanel.proofDisposed}). + * + * @param proof disposed proof + */ + public void proofDisposed(Proof proof) { + if (proof == currentProof) { + org.key_project.util.javafx.FxUtil.runLater(() -> { + currentProof = null; + updateUIState(); + }); + } + } + + /** + * Shows a read-only multi-line text dialog (KNOWN-SIMPLIFIED stand-in for the Swing + * {@code HtmlDialog}/{@code PreviewDialog}). + * + * @param title the dialog title + * @param text the text to display + */ + public static void showTextDialog(String title, String text) { + Platform.runLater(() -> { + TextArea area = new TextArea(text); + area.setEditable(false); + area.setWrapText(false); + VBox box = new VBox(area); + VBox.setVgrow(area, Priority.ALWAYS); + javafx.scene.control.Dialog dialog = new javafx.scene.control.Dialog<>(); + dialog.setTitle(title); + dialog.getDialogPane().setContent(box); + dialog.getDialogPane().setPrefSize(640, 420); + dialog.getDialogPane().getButtonTypes() + .add(javafx.scene.control.ButtonType.CLOSE); + dialog.show(); + }); + } +} diff --git a/keyext.slicing.fx/src/main/resources/META-INF/services/de.uka.ilkd.key.gui.fx.extension.api.KeYGuiExtensionF b/keyext.slicing.fx/src/main/resources/META-INF/services/de.uka.ilkd.key.gui.fx.extension.api.KeYGuiExtensionF new file mode 100644 index 00000000000..18fcce94486 --- /dev/null +++ b/keyext.slicing.fx/src/main/resources/META-INF/services/de.uka.ilkd.key.gui.fx.extension.api.KeYGuiExtensionF @@ -0,0 +1 @@ +org.key_project.slicing.fx.SlicingExtensionF diff --git a/keyext.slicing.fx/src/test/java/org/key_project/slicing/fx/SlicingExtensionFTest.java b/keyext.slicing.fx/src/test/java/org/key_project/slicing/fx/SlicingExtensionFTest.java new file mode 100644 index 00000000000..27476450e6e --- /dev/null +++ b/keyext.slicing.fx/src/test/java/org/key_project/slicing/fx/SlicingExtensionFTest.java @@ -0,0 +1,97 @@ +/* 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 org.key_project.slicing.fx; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +import de.uka.ilkd.key.gui.fx.extension.api.KeYGuiExtensionF; +import de.uka.ilkd.key.gui.fx.extension.api.KeYGuiExtensionF.ContextMenuF; +import de.uka.ilkd.key.gui.fx.extension.api.KeYGuiExtensionF.LeftPanelF; +import de.uka.ilkd.key.gui.fx.extension.api.KeYGuiExtensionF.SettingsF; +import de.uka.ilkd.key.gui.fx.extension.api.KeYGuiExtensionF.StartupF; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Headless unit test of the FX slicing provider {@link SlicingExtensionF} (MP9.4). + *

        + * KNOWN-SIMPLIFIED: the provider's contributions themselves are JavaFX widgets + * ({@code Tab}, {@code ScrollPane}, {@code SettingsPanelF}) whose construction requires a + * running JavaFX toolkit, so this test asserts the SPI contribution through the headless seams + * instead: the {@code @Info} annotation, the implemented SPI sub-interfaces + * ({@link SettingsF}, {@link LeftPanelF}, {@link ContextMenuF}, {@link StartupF}), the shared + * settings/panel title constants and the service-loader registration. The live construction is + * exercised by the app-run verification (gate 2, {@code key.fx.verify.extensions}). + */ +class SlicingExtensionFTest { + + private static final String SERVICE_FILE = + "META-INF/services/de.uka.ilkd.key.gui.fx.extension.api.KeYGuiExtensionF"; + + @Test + void infoAnnotation() { + KeYGuiExtensionF.Info info = + SlicingExtensionF.class.getAnnotation(KeYGuiExtensionF.Info.class); + assertNotNull(info); + assertEquals("Slicing", info.name()); + assertTrue(info.optional()); + assertFalse(info.experimental()); + assertEquals(9001, info.priority()); + } + + @Test + void contributesSettingsProvider() { + SlicingExtensionF provider = new SlicingExtensionF(); + // the provider is headless-constructible: UI widgets are created lazily + assertTrue(provider instanceof SettingsF); + assertEquals("Proof Slicing", SlicingSettingsProviderF.DESCRIPTION); + assertEquals(10000, SlicingSettingsProviderF.PRIORITY_OF_SETTINGS); + } + + @Test + void contributesLeftPanelTab() { + SlicingExtensionF provider = new SlicingExtensionF(); + assertTrue(provider instanceof LeftPanelF); + assertEquals("Proof Slicing", SlicingExtensionF.getPanelTitle()); + } + + @Test + void implementsContextMenuAndStartup() { + SlicingExtensionF provider = new SlicingExtensionF(); + assertTrue(provider instanceof ContextMenuF); + assertTrue(provider instanceof StartupF); + } + + @Test + void registeredViaServiceLoader() { + String content = readResource(SERVICE_FILE); + assertNotNull(content); + assertTrue(content.lines() + .map(String::strip) + .anyMatch(SlicingExtensionF.class.getName()::equals)); + } + + /** + * Reads a classpath resource (null-tolerant: returns {@code null} if absent). + */ + private static @org.jspecify.annotations.Nullable String readResource(String name) { + try (InputStream in = + SlicingExtensionFTest.class.getClassLoader().getResourceAsStream(name)) { + if (in == null) { + return null; + } + return new String(in.readAllBytes(), StandardCharsets.UTF_8); + } catch (IOException e) { + return null; + } + } +} From 8b362a92c28f2072426a0d7af0af78335a088a87 Mon Sep 17 00:00:00 2001 From: opencode Date: Sat, 10 Oct 2026 00:27:07 +0200 Subject: [PATCH 120/281] =?UTF-8?q?keyext.exploration.fx:=20MP9.2=20?= =?UTF-8?q?=E2=80=94=20JavaFX=20port=20of=20the=20exploration=20extension?= =?UTF-8?q?=20(KeYGuiExtensionF=20provider)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- keyext.exploration.fx/build.gradle | 6 + .../exploration/fx/ExplorationExtensionF.java | 370 ++++++++++++++++ .../fx/ExplorationSequentMenuF.java | 195 ++++++++ .../fx/ExplorationStepsPanelF.java | 417 ++++++++++++++++++ ....key.gui.fx.extension.api.KeYGuiExtensionF | 1 + .../fx/ExplorationExtensionFTest.java | 159 +++++++ 6 files changed, 1148 insertions(+) create mode 100644 keyext.exploration.fx/src/main/java/org/key_project/exploration/fx/ExplorationExtensionF.java create mode 100644 keyext.exploration.fx/src/main/java/org/key_project/exploration/fx/ExplorationSequentMenuF.java create mode 100644 keyext.exploration.fx/src/main/java/org/key_project/exploration/fx/ExplorationStepsPanelF.java create mode 100644 keyext.exploration.fx/src/main/resources/META-INF/services/de.uka.ilkd.key.gui.fx.extension.api.KeYGuiExtensionF create mode 100644 keyext.exploration.fx/src/test/java/org/key_project/exploration/fx/ExplorationExtensionFTest.java diff --git a/keyext.exploration.fx/build.gradle b/keyext.exploration.fx/build.gradle index 7250cca1ce9..7e9d5870c12 100644 --- a/keyext.exploration.fx/build.gradle +++ b/keyext.exploration.fx/build.gradle @@ -19,4 +19,10 @@ javafx { dependencies { implementation project(":key.ui.fx") implementation project(":keyext.exploration") + // MP9.2: the FX extension SPI (KeYGuiExtensionF.ContextMenuF/TooltipF) and the reusable + // keyext.exploration logic classes (ExplorationModeModel, ProofExplorationService, + // ExplorationNodeData) are typed against key.core (Goal, Proof, Node, PosInSequent, + // Services, JTerm, KeyIO, ProofControl); neither :key.ui.fx nor :keyext.exploration exposes + // key.core on their API surface, so the provider cannot compile against the SPI without it. + implementation project(":key.core") } diff --git a/keyext.exploration.fx/src/main/java/org/key_project/exploration/fx/ExplorationExtensionF.java b/keyext.exploration.fx/src/main/java/org/key_project/exploration/fx/ExplorationExtensionF.java new file mode 100644 index 00000000000..d726bf10500 --- /dev/null +++ b/keyext.exploration.fx/src/main/java/org/key_project/exploration/fx/ExplorationExtensionF.java @@ -0,0 +1,370 @@ +/* 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 org.key_project.exploration.fx; + +import java.util.ArrayList; +import java.util.List; +import javafx.scene.control.CheckBox; +import javafx.scene.control.CheckMenuItem; +import javafx.scene.control.Control; +import javafx.scene.control.Menu; +import javafx.scene.control.MenuItem; +import javafx.scene.control.Tab; +import javafx.scene.control.Tooltip; + +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.gui.fx.MainWindowF; +import de.uka.ilkd.key.gui.fx.extension.api.KeYGuiExtensionF; +import de.uka.ilkd.key.pp.PosInSequent; +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.ProofTreeAdapter; +import de.uka.ilkd.key.proof.ProofTreeEvent; +import de.uka.ilkd.key.proof.ProofTreeListener; +import de.uka.ilkd.key.proof.event.ProofDisposedEvent; +import de.uka.ilkd.key.proof.event.ProofDisposedListener; +import de.uka.ilkd.key.settings.ProofIndependentSettings; +import de.uka.ilkd.key.settings.ViewSettings; + +import org.key_project.exploration.ExplorationModeModel; +import org.key_project.exploration.ExplorationNodeData; +import org.key_project.util.javafx.FxUtil; + +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +/** + * JavaFX port of the proof-exploration extension, MP9.2 counter-part of {@code + * org.key_project.exploration.ExplorationExtension} (Swing ExplorationExtension.java:46-174). + *

        + * The extension lets the user annotate a proof with "exploration" steps: formulas are added to / + * edited / hidden in the sequent via the {@code ProofExplorationService} (reused unmodified from + * the Swing module), the performed actions are tracked per node ({@link ExplorationNodeData}), + * and the mode is toggled from the toolbar, a "Exploration" menu and — like the Swing original — + * from the sequent context menu. The example provider pattern follows the built-in + * {@code de.uka.ilkd.key.gui.fx.extension.contrib} providers of {@code key.ui.fx}. + *

        + * KNOWN-SIMPLIFIED decisions of this port (each marked at the respective site): + *

          + *
        • The Swing provider registers the {@link ExplorationModeModel} into the Swing mediator + * ({@code mediator.register(model, ExplorationModeModel.class)}) and the {@code + * ExplorationRenderer} (a purple border/background on exploration nodes) into the Swing proof + * tree. {@code KeYMediatorF} has no register/lookup seam and the FX {@code ProofTreeViewF} has no + * renderer styling hook — both are dropped; the model is held directly by this provider.
        • + *
        • The "Hide justification" toggle persists the {@code ViewSettings.hideInteractiveGoals} + * flag and the exploration taclet-application state like the Swing + * {@code ShowInteractiveBranchesAction}, but the actual proof-tree filter of the Swing original + * ({@code ExplorationModeModel.setShowInteractiveBranches} → Swing + * {@code GUIProofTreeModel.setFilter(HIDE_INTERACTIVE_GOALS, …)}) targets a Swing proof-tree seam + * that does not exist in {@code key.ui.fx} (the filter is explicitly deferred there).
        • + *
        • Icons: the Swing {@code Icons} class is AWT/Swing-bound — plain unicode markers are used + * instead.
        • + *
        + */ +@NullMarked +@KeYGuiExtensionF.Info(name = "Exploration", + description = "Author: Sarah Grebing , Alexander Weigl " + + " — JavaFX port (MP9.2). Add, edit and hide formulas on the " + + "sequent with sound-cut based exploration steps and collect them in the " + + "'Exploration Steps' panel. KNOWN-SIMPLIFIED: proof-tree styling and the hidden " + + "second-branch filter are deferred (no FX seam).", + experimental = true, optional = true, priority = 10000) +public class ExplorationExtensionF implements KeYGuiExtensionF, KeYGuiExtensionF.ToolbarF, + KeYGuiExtensionF.MainMenuF, KeYGuiExtensionF.StatusLineF, KeYGuiExtensionF.LeftPanelF, + KeYGuiExtensionF.ContextMenuF, KeYGuiExtensionF.StartupF, ProofDisposedListener { + + /** the shared exploration model, held by this provider (see class javadoc). */ + private final ExplorationModeModel model = new ExplorationModeModel(); + + /** the lazily-built exploration steps panel (west-drawer tab + status-line indicator). */ + private @Nullable ExplorationStepsPanelF leftPanel; + + /** the window and mediator of the running app; wired in {@link #init}, nullable in tests. */ + private @Nullable MainWindowF window; + private @Nullable KeYMediatorF mediator; + + /** the cached toolbar/menu toggle views, kept in sync with the model by the listener below. */ + private @Nullable CheckBox exploreModeCheck; + private @Nullable CheckBox hideJustificationCheck; + private final List exploreModeChecks = new ArrayList<>(); + private final List exploreModeMenuItems = new ArrayList<>(); + private final List hideJustificationChecks = new ArrayList<>(); + private final List hideJustificationMenuItems = new ArrayList<>(); + + /** + * The proof-tree listener that cleans up the exploration annotation of a pruned subtree + * (Swing ExplorationExtension.proofTreeListener, ExplorationExtension.java:75-82). + */ + private final ProofTreeListener proofTreeListener = new ProofTreeAdapter() { + @Override + public void proofPruned(ProofTreeEvent e) { + Node node = e.getNode(); + if (node == null) { + return; + } + @Nullable + ExplorationNodeData data = node.lookup(ExplorationNodeData.class); + if (data != null) { + node.deregister(data, ExplorationNodeData.class); + } + } + }; + + public ExplorationExtensionF() { + // Keep every toggle view (toolbar checkboxes + menu check items) and the left-panel + // enablement in sync with the model; the event may arrive from the prover thread, so the + // update is marshalled to the FX thread (guarded, the listener also fires while the + // toolkit is only just starting). + model.addPropertyChangeListener(ExplorationModeModel.PROP_EXPLORE_MODE, + e -> FxUtil.runLater(this::syncExplorationModeUi)); + } + + // ------------------------------------------------------------------ + // ToolbarF — the Swing JToolBar with the two JCheckBoxes + // (ExplorationExtension.java:91-99) as FX controls + // ------------------------------------------------------------------ + + @Override + public List getToolbarControls(@Nullable MainWindowF window, + @Nullable KeYMediatorF mediator) { + // Swing ExplorationExtension.getToolbar: toggle + show-interactive-branches checkbox, + // both wired to the same model-backed actions the menu uses. + return List.of(getExploreModeToggle(), getHideJustificationToggle()); + } + + private CheckBox getExploreModeToggle() { + if (exploreModeCheck == null) { + CheckBox check = new CheckBox("Exploration Mode"); + check.setTooltip(new Tooltip("Choose to start ExplorationMode")); + check.setSelected(model.isExplorationModeSelected()); + check.setOnAction(e -> model.setExplorationModeSelected(check.isSelected())); + exploreModeCheck = check; + exploreModeChecks.add(check); + } + return exploreModeCheck; + } + + private CheckBox getHideJustificationToggle() { + if (hideJustificationCheck == null) { + CheckBox check = new CheckBox("Hide justification"); + check.setTooltip(new Tooltip(""" + Exploration actions are often done using a cut. \ + Choose to hide the second cut-branches from the view to focus on the \ + actions. Uncheck to focus on these branches.""")); + check.setSelected(!model.isShowInteractiveBranches()); + check.setDisable(!model.isExplorationModeSelected()); + check.setOnAction(e -> setHideJustification(check.isSelected())); + hideJustificationCheck = check; + hideJustificationChecks.add(check); + } + return hideJustificationCheck; + } + + // ------------------------------------------------------------------ + // MainMenuF — the Swing getMainMenuActions (ToggleExplorationAction + + // ShowInteractiveBranchesAction, ExplorationExtension.java:157-161) as one FX menu + // ------------------------------------------------------------------ + + @Override + public List getMenus(@Nullable MainWindowF window, @Nullable KeYMediatorF mediator) { + // The Swing original groups its exploration actions into the View menu; the FX SPI + // contributes whole new menus, so the two toggles live in a new "Exploration" menu (the + // five built-in menus stay untouched — key.fx.verify.menuparity keeps asserting + // 16/24/12/7/5). + Menu menu = new Menu("Exploration"); + menu.getItems().addAll(getExploreModeMenuItem(), getHideJustificationMenuItem()); + return List.of(menu); + } + + private MenuItem getExploreModeMenuItem() { + CheckMenuItem item = new CheckMenuItem("Exploration Mode"); + item.setSelected(model.isExplorationModeSelected()); + item.setOnAction(e -> model.setExplorationModeSelected(item.isSelected())); + exploreModeMenuItems.add(item); + return item; + } + + private MenuItem getHideJustificationMenuItem() { + CheckMenuItem item = new CheckMenuItem("Hide justification"); + item.setSelected(!model.isShowInteractiveBranches()); + item.setDisable(!model.isExplorationModeSelected()); + item.setOnAction(e -> setHideJustification(item.isSelected())); + hideJustificationMenuItems.add(item); + return item; + } + + // ------------------------------------------------------------------ + // StatusLineF — the Swing hasExplorationSteps JLabel + // (ExplorationExtension.java:148-155) as an FX label of the steps panel + // ------------------------------------------------------------------ + + @Override + public List getStatusLineControls() { + // Swing returns leftPanel.getHasExplorationSteps(): a singleton indicator refreshed by + // the panel model. The same label object is exposed by the FX panel. + return List.of(getLeftPanel().getHasExplorationSteps()); + } + + // ------------------------------------------------------------------ + // LeftPanelF — the Swing ExplorationStepsList TabPanel + // (ExplorationExtension.java:131-146) as a west-drawer FX tab + // ------------------------------------------------------------------ + + @Override + public List getLeftPanelTabs(@Nullable MainWindowF window, + @Nullable KeYMediatorF mediator) { + // MP10: the host registers extension left-panel tabs as west-drawer items + // (MainWindowF.buildDrawerHosts), exactly one tab like the Swing singleton panel. + ExplorationStepsPanelF panel = getLeftPanel(); + Tab tab = new Tab(panel.getTitle(), panel); + tab.setClosable(false); + return List.of(tab); + } + + private ExplorationStepsPanelF getLeftPanel() { + // lazy singleton like the Swing provider's initLeftPanel (ExplorationExtension.java:131) + if (leftPanel == null) { + leftPanel = new ExplorationStepsPanelF(window, mediator); + leftPanel.setEnabled(model.isExplorationModeSelected()); + } + return leftPanel; + } + + // ------------------------------------------------------------------ + // ContextMenuF — the Swing ContextMenuAdapter for ContextMenuKind.SEQUENT_VIEW + // (ExplorationExtension.java:59-73): add-to-antecedent / add-to-succedent / edit / delete + // ------------------------------------------------------------------ + + @Override + public List getSequentContextItems(@Nullable KeYMediatorF mediator, + @Nullable Goal goal, @Nullable PosInSequent pos) { + // The Swing adapter contributes nothing unless the exploration mode is selected and the + // clicked kind is SEQUENT_VIEW; the FX SPI exposes exactly that slot. Guards against + // nulls: the term-menu self tests exercise the facade headless without goal/position. + if (!model.isExplorationModeSelected() || mediator == null || goal == null + || pos == null) { + return List.of(); + } + return ExplorationSequentMenuF.items(mediator, goal, pos); + } + + // ------------------------------------------------------------------ + // StartupF — Swing ExplorationExtension.init (ExplorationExtension.java:101-129): + // model binding, selection listener (proof swap + listener registration) and the + // proof-disposed hookup — FX-safe + // ------------------------------------------------------------------ + + @Override + public void init(@Nullable MainWindowF window, @Nullable KeYMediatorF mediator) { + if (window != null) { + this.window = window; + } + if (mediator == null) { + // startup hook without a mediator (headless runs): nothing to wire + return; + } + this.mediator = mediator; + // KNOWN-SIMPLIFIED: the Swing original registers the model in the mediator registry + // (mediator.register(model, ExplorationModeModel.class)); KeYMediatorF has no + // register/lookup seam, so the FX provider holds the singleton model directly — no + // consumer of the Swing registration exists in the FX UI. + ExplorationStepsPanelF panel = getLeftPanel(); + // the host builds status bar/drawer hosts during window construction, before this init + // hook — re-seed the panel with the real window/mediator references + panel.wire(window, mediator); + panel.setEnabled(model.isExplorationModeSelected()); + mediator.getSelectionModel().addKeYSelectionListener(new KeYSelectionListener() { + @Override + public void selectedProofChanged(KeYSelectionEvent e) { + // selection events may fire from the prover thread → marshal to the FX thread + FxUtil.runLater(() -> onSelectedProofChanged(mediator, panel)); + } + }); + } + + private void onSelectedProofChanged(KeYMediatorF mediator, ExplorationStepsPanelF panel) { + // Swing ExplorationExtension.init:109-125 — swap the panel's proof and move the + // proof-tree and proof-disposed listeners to the newly selected proof. + Proof oldProof = panel.getProof(); + Proof newProof = mediator.getSelectedProof(); + if (oldProof != newProof) { + panel.setProof(newProof); + if (oldProof != null) { + oldProof.removeProofTreeListener(proofTreeListener); + } + if (newProof != null) { + newProof.addProofTreeListener(proofTreeListener); + newProof.addProofDisposedListener(this); + } + } + } + + // ------------------------------------------------------------------ + // ProofDisposedListener — Swing ExplorationExtension.proofDisposing + // (ExplorationExtension.java:163-168) + // ------------------------------------------------------------------ + + @Override + public void proofDisposing(ProofDisposedEvent e) { + ExplorationStepsPanelF panel = leftPanel; + if (panel != null && e.getSource() == panel.getProof()) { + FxUtil.runLater(() -> panel.setProof(null)); + } + } + + @Override + public void proofDisposed(ProofDisposedEvent e) { + // empty like the Swing original + } + + // ------------------------------------------------------------------ + // model ↔ UI wiring + // ------------------------------------------------------------------ + + private void syncExplorationModeUi() { + boolean selected = model.isExplorationModeSelected(); + exploreModeChecks.forEach(c -> c.setSelected(selected)); + exploreModeMenuItems.forEach(c -> c.setSelected(selected)); + // the hide-justification toggles re-read the persisted ViewSettings flag on every mode + // change, like Swing ShowInteractiveBranchesAction.updateEnable (ShowInteractiveBranches + // Action.java:42-53), and are only enabled while the exploration mode is active. + boolean hide = !model.isShowInteractiveBranches(); + hideJustificationChecks.forEach(c -> { + c.setSelected(hide); + c.setDisable(!selected); + }); + hideJustificationMenuItems.forEach(c -> { + c.setSelected(hide); + c.setDisable(!selected); + }); + ExplorationStepsPanelF panel = leftPanel; + if (panel != null) { + panel.setEnabled(selected); + } + } + + /** + * The "Hide justification" toggle, FX port of the Swing {@code + * ShowInteractiveBranchesAction} (ShowInteractiveBranchesAction.java:21-69). + *

        + * KNOWN-SIMPLIFIED: the Swing action routes through + * {@code ExplorationModeModel.setShowInteractiveBranches}, which calls + * {@code MainWindow.getProofTreeView().getDelegateModel().setFilter(HIDE_INTERACTIVE_GOALS, + * …)} — a Swing proof-tree seam that does not exist in {@code key.ui.fx} (the FX + * {@code ProofTreeViewF} explicitly defers that filter). The FX port persists the same + * {@link ViewSettings#setHideInteractiveGoals(boolean)} flag the Swing filter reads and + * records the exploration-app state; the filter itself stays deferred. + */ + private void setHideJustification(boolean hide) { + ViewSettings viewSettings = ProofIndependentSettings.DEFAULT_INSTANCE.getViewSettings(); + viewSettings.setHideInteractiveGoals(hide); + model.setExplorationTacletAppState( + hide ? ExplorationModeModel.ExplorationState.SIMPLIFIED_APP + : ExplorationModeModel.ExplorationState.WHOLE_APP); + } +} diff --git a/keyext.exploration.fx/src/main/java/org/key_project/exploration/fx/ExplorationSequentMenuF.java b/keyext.exploration.fx/src/main/java/org/key_project/exploration/fx/ExplorationSequentMenuF.java new file mode 100644 index 00000000000..8b276fd19d0 --- /dev/null +++ b/keyext.exploration.fx/src/main/java/org/key_project/exploration/fx/ExplorationSequentMenuF.java @@ -0,0 +1,195 @@ +/* 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 org.key_project.exploration.fx; + +import java.util.List; +import java.util.Optional; +import javafx.event.ActionEvent; +import javafx.event.EventHandler; +import javafx.scene.control.Alert; +import javafx.scene.control.MenuItem; +import javafx.scene.control.TextInputDialog; + +import de.uka.ilkd.key.core.fx.KeYMediatorF; +import de.uka.ilkd.key.java.Services; +import de.uka.ilkd.key.logic.JTerm; +import de.uka.ilkd.key.nparser.KeyIO; +import de.uka.ilkd.key.pp.LogicPrinter; +import de.uka.ilkd.key.pp.PosInSequent; +import de.uka.ilkd.key.proof.Goal; +import de.uka.ilkd.key.proof.Node; +import de.uka.ilkd.key.util.parsing.BuildingException; + +import org.key_project.exploration.ProofExplorationService; +import org.key_project.prover.sequent.PosInOccurrence; +import org.key_project.prover.sequent.SequentFormula; + +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +/** + * The four sequent context-menu items of the exploration extension, JavaFX port of the Swing + * adapter {@code ExplorationExtension.adapter} (ExplorationExtension.java:59-73), which creates + * the {@code AddFormulaToAntecedentAction}, {@code AddFormulaToSuccedentAction}, + * {@code EditFormulaAction} and {@code DeleteFormulaAction} for the clicked position. + *

        + * The enablement mirrors the Swing action constructors: + *

          + *
        • both "Add formula …" items are always enabled,
        • + *
        • "Edit formula" is enabled iff the position is not the whole sequent + * ({@code !pos.isSequent()}),
        • + *
        • "Delete formula" only for top-level formula occurrences inside the sequent.
        • + *
        + * The producers re-use the (Swing) {@link ProofExplorationService} with its + * {@code soundAddition}/{@code applyChangeFormula}/{@code soundHide} operations. + *

        + * KNOWN-SIMPLIFIED: the Swing {@code promptForTerm} retry-loop (a modal input dialog that + * re-opened itself until a well-typed term was entered, {@code ExplorationAction.java:35-65}) is + * a single-shot {@link TextInputDialog} — malformed input or a sort mismatch abort the action. + */ +@NullMarked +final class ExplorationSequentMenuF { + + private ExplorationSequentMenuF() { + } + + /** + * Builds the four exploration items for the given goal and clicked position. + * + * @param mediator the mediator of the window (non-null when called by the host) + * @param goal the goal whose sequent was clicked + * @param pos the clicked position + * @return the four menu items with the Swing enablement + */ + static List items(KeYMediatorF mediator, Goal goal, PosInSequent pos) { + return List.of( + item("Add formula to antecedent", true, + e -> addFormula(mediator, goal, true)), + item("Add formula to succedent", true, + e -> addFormula(mediator, goal, false)), + item("Edit formula", !pos.isSequent(), + e -> editFormula(mediator, goal, pos)), + item("Delete formula", deleteEnabled(pos), + e -> deleteFormula(mediator, goal, pos))); + } + + private static MenuItem item(String text, boolean enabled, + EventHandler handler) { + MenuItem item = new MenuItem(text); + item.setDisable(!enabled); + item.setOnAction(handler); + return item; + } + + /** + * Swing {@code DeleteFormulaAction} constructor enablement + * (DeleteFormulaAction.java:33-37): only enabled for top-level (non-null) occurrences. + */ + private static boolean deleteEnabled(PosInSequent pos) { + PosInOccurrence pio = pos.getPosInOccurrence(); + return pio != null && !pos.isSequent() && pio.isTopLevel(); + } + + /** Swing {@code AddFormulaToAntecedentAction}/{@code AddFormulaToSuccedentAction}. */ + private static void addFormula(KeYMediatorF mediator, Goal goal, boolean antecedent) { + JTerm term = promptForTerm(goal, null); + if (term == null) { + return; + } + Services services = goal.proof().getServices(); + Node toBeSelected = new ProofExplorationService(goal.proof(), services) + .soundAddition(goal, term, antecedent); + mediator.getSelectionModel().setSelectedNode(toBeSelected); + } + + /** Swing {@code EditFormulaAction} (EditFormulaAction.java:46-66). */ + private static void editFormula(KeYMediatorF mediator, Goal goal, PosInSequent pos) { + if (pos.isSequent()) { + return; + } + PosInOccurrence pio = pos.getPosInOccurrence(); + if (pio == null) { + return; + } + Services services = goal.proof().getServices(); + // the cast mirrors the Swing EditFormulaAction (EditFormulaAction.java:53) + JTerm term = (JTerm) pio.subTerm(); + SequentFormula sf = pio.sequentFormula(); + JTerm newTerm = promptForTerm(goal, term); + if (newTerm == null || newTerm.equals(term)) { + return; + } + JTerm formula = (JTerm) sf.formula(); + JTerm replaced = services.getTermBuilder().replace(formula, pio.posInTerm(), newTerm); + Node toBeSelected = new ProofExplorationService(goal.proof(), services) + .applyChangeFormula(goal, pio, formula, replaced); + mediator.getSelectionModel().setSelectedNode(toBeSelected); + } + + /** Swing {@code DeleteFormulaAction} (DeleteFormulaAction.java:42-55). */ + private static void deleteFormula(KeYMediatorF mediator, Goal goal, PosInSequent pos) { + if (pos.isSequent()) { + return; + } + PosInOccurrence pio = pos.getPosInOccurrence(); + if (pio == null || !pio.isTopLevel()) { + return; + } + JTerm term = (JTerm) pio.subTerm(); + Services services = goal.proof().getServices(); + new ProofExplorationService(goal.proof(), services).soundHide(goal, pio, term); + } + + /** + * Single-shot formula prompt, FX port of Swing {@code ExplorationAction.promptForTerm} + * (ExplorationAction.java:35-65). {@code null} aborts the action (cancel or malformed + * input). + * + * @param goal the goal whose services/namespaces the parse shall use + * @param term the existing term to pre-fill (and sort-check), or {@code null} for a fresh + * addition + * @return the parsed term, or {@code null} when the user cancelled or the input was rejected + */ + private static @Nullable JTerm promptForTerm(Goal goal, @Nullable JTerm term) { + Services services = goal.proof().getServices(); + final String initialValue = + term == null ? "" : LogicPrinter.quickPrintTerm(term, services); + + TextInputDialog dialog = new TextInputDialog(initialValue); + dialog.setTitle("Exploration"); + dialog.setHeaderText("Input a formula:"); + dialog.setContentText("Term:"); + Optional input = dialog.showAndWait(); + if (input.isEmpty()) { + return null; + } + + KeyIO io = new KeyIO(services); + try { + JTerm result = io.parseExpression(input.get()); + if (term != null && !result.sort().equals(term.sort())) { + // KNOWN-SIMPLIFIED: the Swing prompt showed the sort-mismatch dialog and re-opened + // the input dialog; the FX port is single-shot and aborts the action instead. + showError("Sort mismatch", + String.format("%s is of sort %s, but we need a term of sort %s", result, + result.sort(), term.sort())); + return null; + } + return result; + } catch (BuildingException e) { + // KNOWN-SIMPLIFIED: single-shot prompt — a malformed input aborts instead of looping. + showError("Malformed input", e.getMessage()); + return null; + } + } + + private static void showError(String title, @Nullable String message) { + Alert alert = new Alert(Alert.AlertType.ERROR); + alert.setTitle(title); + alert.setHeaderText(null); + alert.setContentText(message == null ? "" : message); + alert.showAndWait(); + } +} diff --git a/keyext.exploration.fx/src/main/java/org/key_project/exploration/fx/ExplorationStepsPanelF.java b/keyext.exploration.fx/src/main/java/org/key_project/exploration/fx/ExplorationStepsPanelF.java new file mode 100644 index 00000000000..7f4ffa2ca4a --- /dev/null +++ b/keyext.exploration.fx/src/main/java/org/key_project/exploration/fx/ExplorationStepsPanelF.java @@ -0,0 +1,417 @@ +/* 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 org.key_project.exploration.fx; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.geometry.Pos; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import javafx.scene.control.TitledPane; +import javafx.scene.control.Tooltip; +import javafx.scene.control.TreeCell; +import javafx.scene.control.TreeItem; +import javafx.scene.control.TreeView; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; + +import de.uka.ilkd.key.core.fx.KeYMediatorF; +import de.uka.ilkd.key.gui.fx.MainWindowF; +import de.uka.ilkd.key.proof.Node; +import de.uka.ilkd.key.proof.Proof; +import de.uka.ilkd.key.proof.RuleAppListener; + +import org.key_project.exploration.ExplorationNodeData; +import org.key_project.util.javafx.FxUtil; + +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +/** + * JavaFX port of the Swing {@code + * org.key_project.exploration.ui.ExplorationStepsList} (ExplorationStepsList.java:36-410): a view + * that summarises the exploration steps inside the current proof and lets the user jump to / + * prune exploration nodes. + *

        + * The port reproduces the Swing layout — a "List of Exploration" list on top of an "Explorations + * in Proof" tree (the chain of exploration nodes found by the depth-first traversal), plus the + * "Jump To Node" / "Prune selected exploration" buttons and the status-line indicator label — with + * JavaFX controls. + *

        + * KNOWN-SIMPLIFIED: + *

          + *
        • The Swing panel contributes a help button to its tab title via {@code getTitleCActions()} + * ({@code HelpFacade.createHelpButton}); the FX tab model has no title-action seam, the button is + * dropped.
        • + *
        • The Swing {@code Icons} class is AWT/Swing-bound — the status indicator uses the unicode + * glyphs {@code ◆}/{@code ◇} instead of the icon images.
        • + *
        • Pruning goes through {@code MainWindowF.getUserInterfaceControl().getProofControl()} (the + * FX counterpart of the Swing {@code mediator.getUI().getProofControl()}).
        • + *
        + */ +@NullMarked +final class ExplorationStepsPanelF extends VBox { + + /** + * status-line indicator glyphs (KNOWN-SIMPLIFIED: unicode instead of the Swing {@code Icons}) + */ + private static final String GLYPH_HAS_STEPS = "\u25C6"; // ◆ + private static final String GLYPH_NO_STEPS = "\u25C7"; // ◇ + + /** the singleton status-line indicator label (Swing {@code hasExplorationSteps}) */ + private final Label hasExplorationSteps = new Label(GLYPH_NO_STEPS); + /** the list of exploration nodes in traversal order (Swing {@code listModelExploration}) */ + private final ObservableList listModelExploration = FXCollections.observableArrayList(); + private final ListView listExplorations = new ListView<>(listModelExploration); + /** the chain tree of exploration nodes (Swing {@code treeModelExploration} / {@code JTree}) */ + private final TreeView treeExploration = new TreeView<>(); + /** the proof {@link Node}s of the current model to their {@link TreeItem}s */ + private final Map> nodeToItem = new HashMap<>(); + private final Button jumpToNode = new Button("Jump To Node"); + private final Button pruneExploration = new Button("Prune selected exploration"); + + private @Nullable MainWindowF window; + private @Nullable KeYMediatorF mediator; + + /** + * the proof shown by this panel; {@code null} empties the panel (Swing {@code currentProof}) + */ + private @Nullable Proof currentProof; + /** guarded: the model is only collected while the exploration mode is enabled */ + private boolean enabled; + /** guards the list ↔ tree selection round-trip against mutual recursion */ + private boolean selecting; + /** the proof root of the current model, for the "Root Node" tree cell */ + private @Nullable Node root; + + /** + * The rule-application listener (Swing {@code ruleAppListener}, + * ExplorationStepsList.java:47-52): re-collects the steps after every rule application and + * restores the previously selected tree path. Rule applications may fire from the prover + * thread, hence the marshalling to the FX thread. + */ + private final RuleAppListener ruleAppListener = e -> FxUtil.runLater(() -> { + TreeItem selected = treeExploration.getSelectionModel().getSelectedItem(); + createModel(e.getSource()); + if (selected != null) { + TreeItem item = nodeToItem.get(selected.getValue()); + if (item != null) { + treeExploration.getSelectionModel().select(item); + } + } + }); + + ExplorationStepsPanelF(@Nullable MainWindowF window, @Nullable KeYMediatorF mediator) { + this.window = window; + this.mediator = mediator; + initialize(); + } + + /** + * (Re-)wires the window/mediator context. The host builds the status bar and the drawer + * hosts during window construction — before the {@code StartupF#init} hook runs — so the + * provider calls this from {@code init} to hand the real references to a panel that was + * already created with {@code null}s. + */ + void wire(@Nullable MainWindowF window, @Nullable KeYMediatorF mediator) { + this.window = window; + this.mediator = mediator; + } + + /** + * Sets the shown proof. If {@code null} is given the scenery is emptied otherwise the model + * reconstructed (Swing {@code ExplorationStepsList.setProof}, + * ExplorationStepsList.java:69-78). + */ + public void setProof(@Nullable Proof proof) { + if (currentProof != null) { + currentProof.removeRuleAppListener(ruleAppListener); + } + if (proof != null) { + proof.addRuleAppListener(ruleAppListener); + } + currentProof = proof; + createModel(proof); + } + + public void setEnabled(boolean enabled) { + boolean old = this.enabled; + this.enabled = enabled; + if (old != enabled) { + createModel(currentProof); + } + } + + public @Nullable Proof getProof() { + return currentProof; + } + + /** @return the status-line indicator label, also shown as part of the panel */ + public Label getHasExplorationSteps() { + return hasExplorationSteps; + } + + /** @return the panel title, used as the west-drawer tab title (Swing {@code getTitle}) */ + public String getTitle() { + return "Exploration Steps"; + } + + private void initialize() { + // the list and the tree, each with the Swing TitledBorder title as a TitledPane header + // (ExplorationStepsList.java:209-213) + listExplorations.setPrefHeight(160); + listExplorations.setPlaceholder(new Label("No exploration steps.")); + listExplorations.setCellFactory(lv -> new ListCell<>() { + @Override + protected void updateItem(Node node, boolean empty) { + super.updateItem(node, empty); + if (empty || node == null) { + setText(null); + } else { + @Nullable + ExplorationNodeData data = node.lookup(ExplorationNodeData.class); + setText(data != null && data.getExplorationAction() != null + ? node.serialNr() + " " + data.getExplorationAction() + : Integer.toString(node.serialNr())); + } + } + }); + TitledPane listPane = new TitledPane("List of Exploration", listExplorations); + listPane.setCollapsible(false); + + treeExploration.setCellFactory(tv -> new TreeCell<>() { + @Override + protected void updateItem(Node node, boolean empty) { + super.updateItem(node, empty); + if (empty || node == null) { + setText(null); + return; + } + TreeItem item = getTreeItem(); + boolean rootCell = item != null && item == treeExploration.getRoot(); + @Nullable + ExplorationNodeData data = node.lookup(ExplorationNodeData.class); + String action = data == null ? null : data.getExplorationAction(); + // Swing ExplorationStepsList.MyTreeCellRenderer + // (ExplorationStepsList.java:306-332); the root cell reuses the "Root Node" + // label and the (faithfully quirky) serial concatenation of the original. + if (rootCell) { + setText(action != null ? "Root Node" + node.serialNr() + " " + action + : "Root Node"); + } else { + setText(action != null ? node.serialNr() + " " + action + : Integer.toString(node.serialNr())); + } + } + }); + TitledPane treePane = new TitledPane("Explorations in Proof", treeExploration); + treePane.setCollapsible(false); + VBox.setVgrow(treePane, Priority.ALWAYS); + + // selection round-trip list ↔ tree and node selection in the mediator + // (Swing ExplorationStepsList.java:172-202) + listExplorations.getSelectionModel().selectedItemProperty() + .addListener((obs, old, value) -> { + if (selecting || value == null) { + return; + } + selecting = true; + try { + TreeItem item = nodeToItem.get(value); + if (item != null) { + expandChain(item); + treeExploration.getSelectionModel().select(item); + treeExploration.scrollTo( + treeExploration.getSelectionModel().getSelectedIndex()); + } + if (mediator != null) { + mediator.getSelectionModel().setSelectedNode(value); + } + } finally { + selecting = false; + } + }); + treeExploration.getSelectionModel().selectedItemProperty() + .addListener((obs, old, value) -> { + if (selecting || value == null) { + return; + } + selecting = true; + try { + Node node = value.getValue(); + if (mediator != null) { + mediator.getSelectionModel().setSelectedNode(node); + } + int selectionIndex = getSelectionIndex(node); + if (selectionIndex > -1) { + listExplorations.getSelectionModel().select(selectionIndex); + listExplorations.scrollTo(selectionIndex); + } + } finally { + selecting = false; + } + }); + + // the bottom panel actions (Swing PruneExplorationAction / JumpToNodeAction, + // ExplorationStepsList.java:357-408): enabled iff an exploration is selected + jumpToNode.disableProperty() + .bind(listExplorations.getSelectionModel().selectedItemProperty().isNull()); + pruneExploration.disableProperty() + .bind(listExplorations.getSelectionModel().selectedItemProperty().isNull()); + jumpToNode.setOnAction(e -> jumpToNode()); + pruneExploration.setOnAction(e -> pruneExploration()); + Tooltip.install(jumpToNode, + new Tooltip("Jump to the selected exploration node in the proof tree.")); + Tooltip.install(pruneExploration, + new Tooltip("Prune the proof at the selected exploration node and remove its " + + "exploration annotation.")); + HBox buttons = new HBox(8, jumpToNode, pruneExploration); + buttons.setAlignment(Pos.CENTER); + + getChildren().addAll(listPane, treePane, buttons); + setSpacing(6); + updateLabel(); + } + + private void jumpToNode() { + Node selected = listExplorations.getSelectionModel().getSelectedItem(); + if (selected != null && mediator != null) { + mediator.getSelectionModel().setSelectedNode(selected); + } + } + + private void pruneExploration() { + // Swing ExplorationStepsList.PruneExplorationAction (ExplorationStepsList.java:367-390): + // prefers the tree selection, falls back to the list selection + Node explorationNode = null; + TreeItem treeSelection = treeExploration.getSelectionModel().getSelectedItem(); + if (treeSelection != null) { + Node node = treeSelection.getValue(); + pruneTo(node); + explorationNode = node; + } + Node selected = listExplorations.getSelectionModel().getSelectedItem(); + if (selected != null) { + pruneTo(selected); + if (explorationNode == null) { + explorationNode = selected; + } + } + if (explorationNode != null) { + @Nullable + ExplorationNodeData data = explorationNode.lookup(ExplorationNodeData.class); + if (data != null) { + explorationNode.deregister(data, ExplorationNodeData.class); + } + createModel(mediator != null ? mediator.getSelectedProof() : currentProof); + } + } + + private void pruneTo(Node node) { + // KNOWN-SIMPLIFIED: the Swing action prunes via {@code + // mediator.getUI().getProofControl().pruneTo(...)}; the FX counterpart is the proof + // control of the window's user-interface control. Headless runs (no window) no-op. + if (window != null) { + window.getUserInterfaceControl().getProofControl().pruneTo(node); + } + } + + private void createModel(@Nullable Proof model) { + listModelExploration.clear(); + nodeToItem.clear(); + root = null; + if (enabled && model != null && !model.isDisposed()) { + Node proofRoot = model.root(); + root = proofRoot; + TreeItem rootItem = new TreeItem<>(proofRoot); + nodeToItem.put(proofRoot, rootItem); + treeExploration.setRoot(rootItem); + List explorationNodes = collectAllExplorationSteps(proofRoot, rootItem); + listModelExploration.addAll(explorationNodes); + setTreeExpandedState(rootItem, true); + } else { + treeExploration.setRoot(null); + } + updateLabel(); + } + + private void updateLabel() { + if (listModelExploration.isEmpty()) { + hasExplorationSteps.setText(GLYPH_NO_STEPS); + hasExplorationSteps.setTooltip(new Tooltip( + "The current proof does not contain any exploratory proof steps.")); + } else { + hasExplorationSteps.setText(GLYPH_HAS_STEPS); + hasExplorationSteps.setTooltip(new Tooltip( + "The current proof contains exploratory proof steps.")); + } + } + + private List collectAllExplorationSteps(Node proofRoot, TreeItem rootItem) { + List list = new ArrayList<>(); + findExplorationChildren(proofRoot, list, rootItem); + return list; + } + + /** + * Collects the nodes inside the proof tree which carry an exploration annotation, grouping + * them as a chain in the given tree model during traversal — the faithful port of the Swing + * {@code findExplorationChildren} depth-first walk (ExplorationStepsList.java:139-166), + * including the {@code reached} set guarding against revisiting shared subtrees. + */ + private void findExplorationChildren(Node node, List foundNodes, + TreeItem rootItem) { + Set reached = new HashSet<>(512000); + ArrayDeque nodes = new ArrayDeque<>(8); + nodes.add(node); + TreeItem parentItem = rootItem; + while (!nodes.isEmpty()) { + Node n = nodes.pollLast(); + @Nullable + ExplorationNodeData data = n.lookup(ExplorationNodeData.class); + if (data != null && data.getExplorationAction() != null) { + TreeItem item = new TreeItem<>(n); + parentItem.getChildren().add(0, item); + parentItem = item; + nodeToItem.putIfAbsent(n, item); + foundNodes.add(n); + } + reached.add(n); + for (Node child : n) { + if (!reached.contains(child)) { + nodes.push(child); + } + } + } + } + + /** {@code null}-safe replacement of the Swing cast {@code (MyTreeNode) model.getRoot()} */ + private int getSelectionIndex(Node node) { + return listModelExploration.indexOf(node); + } + + private static void setTreeExpandedState(TreeItem item, boolean expanded) { + item.setExpanded(expanded); + for (TreeItem child : item.getChildren()) { + setTreeExpandedState(child, expanded); + } + } + + private static void expandChain(TreeItem item) { + for (TreeItem parent = item.getParent(); parent != null; parent = parent.getParent()) { + parent.setExpanded(true); + } + } +} diff --git a/keyext.exploration.fx/src/main/resources/META-INF/services/de.uka.ilkd.key.gui.fx.extension.api.KeYGuiExtensionF b/keyext.exploration.fx/src/main/resources/META-INF/services/de.uka.ilkd.key.gui.fx.extension.api.KeYGuiExtensionF new file mode 100644 index 00000000000..7c95ad60a0c --- /dev/null +++ b/keyext.exploration.fx/src/main/resources/META-INF/services/de.uka.ilkd.key.gui.fx.extension.api.KeYGuiExtensionF @@ -0,0 +1 @@ +org.key_project.exploration.fx.ExplorationExtensionF diff --git a/keyext.exploration.fx/src/test/java/org/key_project/exploration/fx/ExplorationExtensionFTest.java b/keyext.exploration.fx/src/test/java/org/key_project/exploration/fx/ExplorationExtensionFTest.java new file mode 100644 index 00000000000..9de14762fed --- /dev/null +++ b/keyext.exploration.fx/src/test/java/org/key_project/exploration/fx/ExplorationExtensionFTest.java @@ -0,0 +1,159 @@ +/* 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 org.key_project.exploration.fx; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; +import javafx.application.Platform; +import javafx.scene.control.CheckBox; +import javafx.scene.control.Control; +import javafx.scene.control.Menu; +import javafx.scene.control.Tab; + +import de.uka.ilkd.key.gui.fx.extension.api.KeYGuiExtensionF; + +import org.key_project.util.javafx.FxUtil; + +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Structural unit test of the {@link ExplorationExtensionF} provider of MP9.2. + *

        + * The FX controls need a running toolkit; {@link Platform#startup} requires a display, so the + * FX-dependent assertions are skipped (JUnit {@code assumeTrue}) on headless runs. The build + * gates run with {@code DISPLAY=:92} (Xvnc) and the software pipeline. + */ +class ExplorationExtensionFTest { + + /** whether the JavaFX toolkit could be started in this JVM/display */ + private static boolean toolkitAvailable; + + @BeforeAll + static void startToolkit() { + // software pipeline keeps the toolkit runnable on the Xvnc display + System.setProperty("prism.order", "sw"); + try { + Platform.startup(() -> { + }); + toolkitAvailable = true; + } catch (Throwable e) { + // headless run: the FX-dependent tests are skipped via assumeTrue + toolkitAvailable = false; + } + } + + @Test + void infoAnnotation() { + KeYGuiExtensionF.Info info = + ExplorationExtensionF.class.getAnnotation(KeYGuiExtensionF.Info.class); + assertThat(info).isNotNull(); + assertThat(info.name()).isEqualTo("Exploration"); + assertThat(info.description()).contains("MP9.2"); + assertThat(info.priority()).isEqualTo(10000); + assertThat(info.optional()).isTrue(); + assertThat(info.experimental()).isTrue(); + } + + @Test + void serviceRegistration() throws IOException { + // the provider must be discoverable through the FX extension SPI; the resource may be + // shadowed by the key.ui.fx services file on the test classpath, so every copy on the + // classpath is inspected + Enumeration resources = ExplorationExtensionF.class.getClassLoader().getResources( + "META-INF/services/de.uka.ilkd.key.gui.fx.extension.api.KeYGuiExtensionF"); + List contents = new ArrayList<>(); + while (resources.hasMoreElements()) { + try (InputStream in = resources.nextElement().openStream()) { + contents.add(new String(in.readAllBytes(), StandardCharsets.UTF_8)); + } + } + assertThat(contents).anyMatch( + content -> content.contains("org.key_project.exploration.fx.ExplorationExtensionF")); + } + + @Test + void providerInitializesWithoutMediator() { + Assumptions.assumeTrue(toolkitAvailable); + FxUtil.runAndWait(() -> { + ExplorationExtensionF extension = new ExplorationExtensionF(); + // the startup hook without a mediator (headless runs) must not crash + extension.init(null, null); + }); + } + + @Test + void contributesToolbarMenuStatusAndLeftPanel() { + Assumptions.assumeTrue(toolkitAvailable); + FxUtil.runAndWait(() -> { + ExplorationExtensionF extension = new ExplorationExtensionF(); + + // ToolbarF: the two Swing JCheckBoxes as FX check boxes + List toolbar = extension.getToolbarControls(null, null); + assertThat(toolbar).hasSize(2).allMatch(c -> c instanceof CheckBox); + assertThat(((CheckBox) toolbar.get(0)).getText()).isEqualTo("Exploration Mode"); + assertThat(((CheckBox) toolbar.get(1)).getText()).isEqualTo("Hide justification"); + + // LeftPanelF: exactly one west-drawer tab "Exploration Steps" + List tabs = extension.getLeftPanelTabs(null, null); + assertThat(tabs).hasSize(1); + Tab tab = tabs.get(0); + assertThat(tab.getText()).isEqualTo("Exploration Steps"); + assertThat(tab.getContent()).isNotNull(); + assertThat(tab.isClosable()).isFalse(); + + // StatusLineF: the singleton exploration-steps indicator + List status = extension.getStatusLineControls(); + assertThat(status).hasSize(1); + + // MainMenuF: one new "Exploration" menu with the two toggles + List

        menus = extension.getMenus(null, null); + assertThat(menus).hasSize(1); + assertThat(menus.get(0).getText()).isEqualTo("Exploration"); + assertThat(menus.get(0).getItems()).hasSize(2); + }); + } + + @Test + void sequentContextItemsGuardNulls() { + Assumptions.assumeTrue(toolkitAvailable); + FxUtil.runAndWait(() -> { + ExplorationExtensionF extension = new ExplorationExtensionF(); + // the facade exercises the host headless without goal/position; the sequent slot + // must stay empty even with the exploration mode switched on + CheckBox mode = (CheckBox) extension.getToolbarControls(null, null).get(0); + mode.setSelected(true); + mode.fire(); + assertThat(extension.getSequentContextItems(null, null, null)).isEmpty(); + }); + } + + @Test + void toolbarToggleSyncsWithModel() { + Assumptions.assumeTrue(toolkitAvailable); + FxUtil.runAndWait(() -> { + ExplorationExtensionF extension = new ExplorationExtensionF(); + List toolbarOnce = extension.getToolbarControls(null, null); + List toolbarTwice = extension.getToolbarControls(null, null); + // the toolbar hosts are cached singletons like the Swing provider's JToolBar + assertThat(toolbarOnce.get(0)).isSameAs(toolbarTwice.get(0)); + assertThat(toolbarOnce.get(1)).isSameAs(toolbarTwice.get(1)); + + CheckBox mode = (CheckBox) toolbarOnce.get(0); + mode.fire(); + assertThat(mode.isSelected()).isTrue(); + // the second toolbar control is only enabled while the exploration mode is active + assertThat(((CheckBox) toolbarOnce.get(1)).isDisabled()).isFalse(); + }); + } +} From 984f117d74197b887810970039dcb8469055c50e Mon Sep 17 00:00:00 2001 From: opencode Date: Sat, 10 Oct 2026 00:27:14 +0200 Subject: [PATCH 121/281] =?UTF-8?q?keyext.caching.fx:=20MP9.1=20=E2=80=94?= =?UTF-8?q?=20JavaFX=20port=20of=20the=20caching=20extension=20(KeYGuiExte?= =?UTF-8?q?nsionF=20provider)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- keyext.caching.fx/build.gradle | 6 + .../plugins/caching/fx/CachingExtensionF.java | 809 ++++++++++++++++++ .../caching/fx/CachingPruneHandlerF.java | 82 ++ .../caching/fx/CachingSettingsProviderF.java | 137 +++ .../caching/fx/CachingStatusButtonF.java | 102 +++ .../caching/fx/CopyBeforeDisposeF.java | 82 ++ ....key.gui.fx.extension.api.KeYGuiExtensionF | 1 + .../caching/fx/CachingExtensionFTest.java | 99 +++ 8 files changed, 1318 insertions(+) create mode 100644 keyext.caching.fx/src/main/java/de/uka/ilkd/key/gui/plugins/caching/fx/CachingExtensionF.java create mode 100644 keyext.caching.fx/src/main/java/de/uka/ilkd/key/gui/plugins/caching/fx/CachingPruneHandlerF.java create mode 100644 keyext.caching.fx/src/main/java/de/uka/ilkd/key/gui/plugins/caching/fx/CachingSettingsProviderF.java create mode 100644 keyext.caching.fx/src/main/java/de/uka/ilkd/key/gui/plugins/caching/fx/CachingStatusButtonF.java create mode 100644 keyext.caching.fx/src/main/java/de/uka/ilkd/key/gui/plugins/caching/fx/CopyBeforeDisposeF.java create mode 100644 keyext.caching.fx/src/main/resources/META-INF/services/de.uka.ilkd.key.gui.fx.extension.api.KeYGuiExtensionF create mode 100644 keyext.caching.fx/src/test/java/de/uka/ilkd/key/gui/plugins/caching/fx/CachingExtensionFTest.java diff --git a/keyext.caching.fx/build.gradle b/keyext.caching.fx/build.gradle index ecf6e46d2b1..6ab84cc0c34 100644 --- a/keyext.caching.fx/build.gradle +++ b/keyext.caching.fx/build.gradle @@ -19,4 +19,10 @@ javafx { dependencies { implementation project(":key.ui.fx") implementation project(":keyext.caching") + // MP9.1: the FX SPI (KeYGuiExtensionF) and the reused keyext.caching logic classes expose + // key.core types (Goal, PosInSequent, Proof, ProofCachingSettings/Hierarchy, ...) in their + // signatures; key.ui.fx and keyext.caching declare key.core as 'implementation' (not 'api'), + // so this module must declare the core dependency itself to compile against those signatures. + // Runtime-wise key.core arrives transitively anyway (via keyext.caching) — no classpath change. + implementation project(":key.core") } diff --git a/keyext.caching.fx/src/main/java/de/uka/ilkd/key/gui/plugins/caching/fx/CachingExtensionF.java b/keyext.caching.fx/src/main/java/de/uka/ilkd/key/gui/plugins/caching/fx/CachingExtensionF.java new file mode 100644 index 00000000000..3efd29ec76c --- /dev/null +++ b/keyext.caching.fx/src/main/java/de/uka/ilkd/key/gui/plugins/caching/fx/CachingExtensionF.java @@ -0,0 +1,809 @@ +/* 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.plugins.caching.fx; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.scene.control.Alert; +import javafx.scene.control.Alert.AlertType; +import javafx.scene.control.ButtonType; +import javafx.scene.control.CheckMenuItem; +import javafx.scene.control.Control; +import javafx.scene.control.Menu; +import javafx.scene.control.MenuItem; +import javafx.scene.control.ToggleButton; +import javafx.scene.control.Tooltip; + +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.gui.fx.IssueDialogF; +import de.uka.ilkd.key.gui.fx.MainWindowF; +import de.uka.ilkd.key.gui.fx.extension.api.KeYGuiExtensionF; +import de.uka.ilkd.key.gui.fx.settings.SettingsManagerF; +import de.uka.ilkd.key.gui.fx.settings.SettingsProviderF; +import de.uka.ilkd.key.gui.plugins.caching.settings.ProofCachingSettings; +import de.uka.ilkd.key.macros.ProofMacro; +import de.uka.ilkd.key.macros.TryCloseMacro; +import de.uka.ilkd.key.pp.PosInSequent; +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.RuleAppListener; +import de.uka.ilkd.key.proof.event.ProofDisposedEvent; +import de.uka.ilkd.key.proof.event.ProofDisposedListener; +import de.uka.ilkd.key.proof.reference.ClosedBy; +import de.uka.ilkd.key.proof.reference.ReferenceSearcher; +import de.uka.ilkd.key.proof.replay.CopyingProofReplayer; +import de.uka.ilkd.key.settings.GeneralSettings; +import de.uka.ilkd.key.settings.ProofIndependentSettings; + +import org.key_project.prover.engine.ProverTaskListener; +import org.key_project.prover.engine.TaskFinishedInfo; +import org.key_project.prover.engine.TaskStartedInfo; +import org.key_project.prover.engine.impl.DefaultProver; +import org.key_project.util.collection.ImmutableList; +import org.key_project.util.javafx.FxUtil; + +import org.jspecify.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The proof-caching extension, FX port of {@code de.uka.ilkd.key.gui.plugins.caching. + * CachingExtension} (Swing CachingExtension.java:63-341) registered via the service-loader file + * {@code META-INF/services/de.uka.ilkd.key.gui.fx.extension.api.KeYGuiExtensionF}. + *

        + * The Swing original contributes the caching UI through the Swing extension capabilities + * (main-menu action, toolbar, status line, sequent context actions, settings panel and the startup + * hooks). This port implements the same surface with the FX SPI {@link KeYGuiExtensionF}: a + * separate "Caching" menu with the toggle and the settings entries, the toolbar toggle, a + * status-line button showing the number of cached (closed-by-reference) goals, the four + * proof-caching context items on the sequent term menu, the "Proof Caching" settings panel and + * the startup wiring (selection tracking, automatic reference search after rule applications, the + * copy/reopen-on-dispose behaviour). + *

        + * The reused *logic* classes of {@code keyext.caching} are kept untouched + * ({@link ProofCachingSettings}, {@link ReferenceSearcher}, the {@code ClosedBy} registry); + * only the UI surface is ported. + * + * @author Arne Keller (Swing original) + * @author MP9.1 extension SPI port (FX) + */ +@KeYGuiExtensionF.Info(name = "Proof Caching", optional = true, + description = "Functionality related to reusing previous proof results in similar proofs", + experimental = false) +public class CachingExtensionF + implements KeYGuiExtensionF, KeYGuiExtensionF.MainMenuF, KeYGuiExtensionF.ToolbarF, + KeYGuiExtensionF.StatusLineF, KeYGuiExtensionF.ContextMenuF, KeYGuiExtensionF.SettingsF, + KeYGuiExtensionF.StartupF { + private static final Logger LOGGER = LoggerFactory.getLogger(CachingExtensionF.class); + + /** + * The reused persisted caching settings (Swing {@code ProofCachingSettings}, singleton + * registered into the {@link ProofIndependentSettings}). Owned by the FX module via + * {@link CachingSettingsProviderF#getCachingSettings()} (KNOWN-SIMPLIFIED: the Swing keyext + * {@code CachingSettingsProvider.getCachingSettings()} is not loadable on the FX module's + * compile path; see {@link CachingSettingsProviderF}). + */ + private final ProofCachingSettings settings = CachingSettingsProviderF.getCachingSettings(); + + /** + * @return the caching settings used by the automatic reference search and the + * prune/dispose handlers (package-private for the unit test asserting the + * module-owned singleton identity) + */ + ProofCachingSettings settings() { + return settings; + } + + /** + * The global runtime off-switch, mirroring the selected state of the Swing + * {@code CachingToggleAction} (CachingToggleAction.java:26-38: a checked action enabled by + * default; {@code getProofCachingEnabled()} ANDs it with the settings flag and the + * single-core gate). The checkbox of the menu and the toolbar toggle share this property. + */ + private final BooleanProperty toggle = new SimpleBooleanProperty(this, "cachingToggle", true); + + /** + * Whether the multi-core prover is active (Swing {@code SingleCoreFeatureGate.isActive}, + * SingleCoreFeatureGate.java:88-97: proof caching is a single-core-only feature and is greyed + * out while the parallel prover is enabled). Refreshed from the + * {@code GeneralSettings.PARALLEL_PROVER_ENABLED} property change events. + */ + private final BooleanProperty multiCoreActive = new SimpleBooleanProperty(this, + "multiCoreProverActive", false); + + /** + * The menu checkbox of the main-menu toggle, created lazily on first use + * ({@link #menuToggleItem()}). KNOWN-SIMPLIFIED: instantiation is deferred because + * constructing JavaFX controls requires the FX toolkit, but the SPI may instantiate the + * provider before any display is up (headless unit tests, verify hooks); the host builds + * the menus/toolbar/status line at startup, when the toolkit is already running. + */ + private @Nullable CheckMenuItem toggleMenuItem; + + /** + * the toolbar toggle (bound to {@link #toggle}, created lazily — see + * {@link #toolbarToggleButton()}) + */ + private @Nullable ToggleButton toggleButton; + + /** + * the singleton status-line button (Swing {@code ReferenceSearchButton}, created lazily — + * see {@link #statusLineButton()}) + */ + private @Nullable CachingStatusButtonF statusButton; + + /** + * the settings panel contributed into the settings dialog (Swing + * {@code CachingSettingsProvider}, created lazily — see {@link #settingsPanelProvider()}) + */ + private @Nullable SettingsProviderF settingsProvider; + + /** + * @return the "Proof Caching" menu checkbox, created and bound to {@link #toggle} on first + * use + */ + private CheckMenuItem menuToggleItem() { + CheckMenuItem item = toggleMenuItem; + if (item == null) { + item = new CheckMenuItem("Proof Caching"); + item.setSelected(toggle.get()); + // the checked state is synced into {@link #toggle} by the binding + item.selectedProperty().bindBidirectional(toggle); + item.disableProperty().bind(multiCoreActive); + toggleMenuItem = item; + } + return item; + } + + /** + * @return the toolbar toggle button, created and bound to {@link #toggle} on first use + */ + private ToggleButton toolbarToggleButton() { + ToggleButton button = toggleButton; + if (button == null) { + button = new ToggleButton("Proof Caching"); + button.selectedProperty().bindBidirectional(toggle); + button.disableProperty().bind(multiCoreActive); + toggleButton = button; + } + return button; + } + + /** + * @return the singleton status-line button, created on first use; the same instance is + * returned on every call, so the host's status-bar membership check holds + */ + private CachingStatusButtonF statusLineButton() { + CachingStatusButtonF button = statusButton; + if (button == null) { + button = new CachingStatusButtonF(); + statusButton = button; + } + return button; + } + + /** + * @return the settings panel provider contributed into the settings dialog (Swing + * {@code CachingSettingsProvider}), created on first use + */ + private SettingsProviderF settingsPanelProvider() { + SettingsProviderF provider = settingsProvider; + if (provider == null) { + provider = new CachingSettingsProviderF(); + settingsProvider = provider; + } + return provider; + } + + /** + * Proofs tracked for automatic reference search (Swing {@code trackedProofs}, + * CachingExtension.java:83-85: proofs seen by the selection listener are registered with the + * rule-app / dispose / prune listeners). Also the source for the "currently opened proofs" + * of {@link ReferenceSearcher#findPreviousProof}. + */ + private final Set trackedProofs = Collections.synchronizedSet(new HashSet<>()); + + /** + * Whether to try to close the current proof (by caching) after a rule application; false + * while certain macros (like the "close provable goals" macro) are running (Swing + * {@code tryToClose}, CachingExtension.java:78, maintained through the + * {@link ProverTaskListener} hooks). + */ + private volatile boolean tryToClose = false; + + /** the handler for prunes into referenced branches (Swing {@code CachingPruneHandler}) */ + private @Nullable CachingPruneHandlerF cachingPruneHandler; + + /** the window the extension is attached to, wired in {@link #init} */ + private @Nullable MainWindowF window; + + /** the mediator the extension is attached to, wired in {@link #init} */ + private @Nullable KeYMediatorF mediator; + + /** + * The selection listener tracking the loaded proofs and refreshing the status button (Swing + * {@code CachingExtension.selectedProofChanged}, CachingExtension.java:130-139). + */ + private final KeYSelectionListener selectionListener = new KeYSelectionListener() { + @Override + public void selectedProofChanged(KeYSelectionEvent event) { + trackProof(event.getSource().getSelectedProof()); + } + + @Override + public void selectedNodeChanged(KeYSelectionEvent event) { + updateGUIState(event.getSource().getSelectedProof()); + } + }; + + /** + * The rule-app listener performing the automatic reference search (Swing + * {@code CachingExtension.ruleApplied}, CachingExtension.java:142-187). + */ + private final RuleAppListener ruleAppListener = this::ruleApplied; + + /** removes disposed proofs from the tracking set (Swing {@code proofDisposing}). */ + private final ProofDisposedListener disposeListener = new ProofDisposedListener() { + @Override + public void proofDisposing(ProofDisposedEvent event) { + trackedProofs.remove(event.getSource()); + } + + @Override + public void proofDisposed(ProofDisposedEvent event) { + } + }; + + /** + * The prover-task listener maintaining the {@code tryToClose} flag and re-opening goals that + * were closed by reference after a failed macro/prover run (Swing + * {@code CachingExtension.taskStarted/taskProgress/taskFinished}, + * CachingExtension.java:236-270). + */ + private final ProverTaskListener proverTaskListener = new ProverTaskListener() { + @Override + public void taskStarted(TaskStartedInfo info) { + if (info.kind().equals(TaskStartedInfo.TaskKind.Macro) + && info.message().equals(new TryCloseMacro().getName())) { + tryToClose = false; + } + } + + @Override + public void taskProgress(int position) { + tryToClose = true; + } + + @Override + public void taskFinished(TaskFinishedInfo info) { + onTaskFinished(info); + } + }; + + @Override + public void init(MainWindowF window, KeYMediatorF mediator) { + // extension: MP9.1 — Swing CachingExtension.preInit (CachingExtension.java:190-194: + // mediator/selection listener/task listener wiring) + init (:197-199: the prune + // handler). The FX host calls init once after discovery; every hook is null-guarded + // and the UI refreshes are marshalled onto the FX thread. + attach(window, mediator); + if (mediator != null) { + mediator.getSelectionModel().addKeYSelectionListener(selectionListener); + } + if (window != null) { + window.getUserInterfaceControl().addProverTaskListener(proverTaskListener); + } + cachingPruneHandler = new CachingPruneHandlerF(this); + // single-core gate: grey out the single-core-only contributions while the multi-core + // prover is enabled (Swing SingleCoreFeatureGate, SingleCoreFeatureGate.java:53-60). + multiCoreActive.set(isMultiCoreActive()); + GeneralSettings general = ProofIndependentSettings.DEFAULT_INSTANCE.getGeneralSettings(); + general.addPropertyChangeListener(GeneralSettings.PARALLEL_PROVER_ENABLED, + evt -> { + multiCoreActive.set(isMultiCoreActive()); + updateGUIState(mediator == null ? null : mediator.getSelectedProof()); + }); + // initial UI state (Swing updateGUIState is called from the context actions; the button + // is primed like Swing ReferenceSearchButton.updateState on construction). + updateGUIState(mediator == null ? null : mediator.getSelectedProof()); + } + + /** + * Remembers the window/mediator for the capabilities that need them (the facade hands them + * only to {@link KeYGuiExtensionF.MainMenuF}/{@link KeYGuiExtensionF.ToolbarF}/ + * {@link KeYGuiExtensionF.StartupF}). + */ + private void attach(MainWindowF window, KeYMediatorF mediator) { + this.window = window; + this.mediator = mediator; + // the button's click runs the reference search of the whole extension (the button itself + // never resolves proofs: the extension pushes the selected proof into + // CachingStatusButtonF.updateState) + statusLineButton().setOnAction(e -> runReferenceSearchFromStatus()); + } + + // ------------------------------------------------------------------ + // MainMenuF + // ------------------------------------------------------------------ + + @Override + public List

        getMenus(MainWindowF window, KeYMediatorF mediator) { + // extension: MP9.1 — Swing CachingExtension.getMainMenuActions (CachingExtension.java: + // 116-120) contributes the single checked CachingToggleAction; the FX SPI contributes + // whole menus, so the toggles live in ONE new "Caching" menu (the five built-in menu + // bars and their item sets stay untouched — key.fx.verify.menuparity keeps asserting + // 16/24/12/7/5). The menu holds the global toggle, the persisted auto-search setting + // and the settings-dialog opener (same items as the toolbar/status surface). + attach(window, mediator); + + Menu caching = new Menu("Caching"); + caching.getItems().add(menuToggleItem()); + caching.getItems().add(autoSearchItem()); + caching.getItems().add(settingsItem()); + return List.of(caching); + } + + /** + * The persisted "Automatically search for references in auto mode" toggle of the settings + * panel (Swing {@code ProofCachingSettings.enabled} + the {@code addCheckBox} row of + * CachingSettingsProvider.java:73-74) as a menu checkbox. + */ + private MenuItem autoSearchItem() { + CheckMenuItem item = + new CheckMenuItem("Automatically search for references in auto mode"); + item.setSelected(settings.getEnabled()); + item.setOnAction(e -> settings.setEnabled(item.isSelected())); + return item; + } + + /** Opens the "Proof Caching" settings panel in the settings dialog (like the HeatmapF). */ + private MenuItem settingsItem() { + MenuItem item = new MenuItem("Proof Caching Options…"); + item.setOnAction(e -> { + MainWindowF owner = window; + if (owner != null) { + SettingsManagerF.getInstance().showSettingsDialog(owner, settingsPanelProvider()); + } + }); + return item; + } + + // ------------------------------------------------------------------ + // ToolbarF + // ------------------------------------------------------------------ + + @Override + public List getToolbarControls(MainWindowF window, KeYMediatorF mediator) { + // extension: MP9.1 — Swing CachingExtension.getToolbar (CachingExtension.java:107-114): + // the JToolBar holds the toggle button bound to the CachingToggleAction; the FX port + // contributes the shared toggle button (with the description tooltip of the Swing + // action, CachingToggleAction.java:23, 36-37). + attach(window, mediator); + ToggleButton button = toolbarToggleButton(); + button.setTooltip( + new Tooltip("Enable or disable proof caching for currently open proofs.")); + return List.of(button); + } + + // ------------------------------------------------------------------ + // StatusLineF + // ------------------------------------------------------------------ + + @Override + public List getStatusLineControls() { + // extension: MP9.1 — Swing CachingExtension.getStatusLineComponents + // (CachingExtension.java:230-233): the ReferenceSearchButton; a singleton in the FX + // port (same instance on every call, so the host's status bar membership check holds). + return List.of(statusLineButton()); + } + + // ------------------------------------------------------------------ + // ContextMenuF (sequent context items) + // ------------------------------------------------------------------ + + @Override + public List getSequentContextItems(KeYMediatorF mediator, Goal goal, + PosInSequent pos) { + // extension: MP9.1 — Swing CachingExtension.getContextActions for + // ContextMenuKind.SEQUENT_VIEW (CachingExtension.java:212-227): the four node actions. + // The FX SPI hands the clicked Goal, so the actions operate on its node. Null-safe: + // without a goal (or a goal without a node) no items are contributed. + if (mediator == null || goal == null || goal.node() == null) { + return List.of(); + } + Node node = goal.node(); + List items = new ArrayList<>(); + items.add(closeByReferenceItem(mediator, node)); + items.add(copyReferencedProofItem(mediator, node)); + items.add(gotoReferenceItem(mediator, node)); + items.add(removeCachingInformationItem(mediator, node)); + return items; + } + + /** + * "Close by reference to other proof" (Swing {@code CloseByReference}): searches the other + * proofs for an equivalent closed branch and closes the goal(s) by registering the found + * reference. The result message dialog is ported as a JavaFX {@link Alert}. + */ + private MenuItem closeByReferenceItem(KeYMediatorF mediator, Node node) { + MenuItem item = new MenuItem("Close by reference to other proof"); + item.setDisable(node.isClosed() || node.lookup(ClosedBy.class) != null); + item.setOnAction(e -> closeByReference(mediator, node)); + return item; + } + + private void closeByReference(KeYMediatorF mediator, Node node) { + // nodes will be the open goals for which to perform proof caching + List nodes = new ArrayList<>(); + if (node.leaf()) { + nodes.add(node); + } else { + var iterator = node.subtreeIterator(); + while (iterator.hasNext()) { + Node n = iterator.next(); + if (n.leaf() && !n.isClosed()) { + nodes.add(n); + } + } + } + List mismatches = new ArrayList<>(); + List matches = new ArrayList<>(); + for (Node n : nodes) { + ClosedBy c = null; + try { + c = ReferenceSearcher.findPreviousProof(openProofs(), n); + } catch (Exception exception) { + LOGGER.warn("error during reference search", exception); + } + if (c != null) { + matches.add(n.serialNr()); + Goal goal = n.proof().getOpenGoal(n); + if (goal != null) { + n.proof().closeGoal(goal); + n.register(c, ClosedBy.class); + } + } else { + mismatches.add(n.serialNr()); + } + } + if (!nodes.isEmpty()) { + updateGUIState(nodes.get(0).proof()); + } + if (!mismatches.isEmpty() || !matches.isEmpty()) { + StringBuilder sb = new StringBuilder(); + if (!matches.isEmpty()) { + sb.append("Cache hit found for node(s) ").append(matches); + } + if (!mismatches.isEmpty()) { + if (!sb.isEmpty()) { + sb.append('\n'); + } + sb.append("No matching branch found for node(s) ").append(mismatches); + } + showInfoAlert(sb.toString()); + } + } + + /** + * "Copy referenced proof steps here" (Swing {@code CopyReferencedProof}): copies the steps + * of every leaf closed by reference into the current proof. Errors surface in the FX issue + * dialog instead of the Swing {@code IssueDialog}. + */ + private MenuItem copyReferencedProofItem(KeYMediatorF mediator, Node node) { + List nodes = new ArrayList<>(); + var iterator = node.leavesIterator(); + while (iterator.hasNext()) { + Node leaf = iterator.next(); + if (leaf.isClosed() && leaf.lookup(ClosedBy.class) != null) { + nodes.add(leaf); + } + } + MenuItem item = new MenuItem("Copy referenced proof steps here"); + item.setDisable(nodes.isEmpty()); + item.setOnAction(e -> copyReferencedProof(mediator, nodes)); + return item; + } + + private void copyReferencedProof(KeYMediatorF mediator, List nodes) { + for (Node node : nodes) { + ClosedBy c = node.lookup(ClosedBy.class); + Goal current = node.proof().getClosedGoal(node); + if (c == null || current == null) { + continue; + } + try { + new CopyingProofReplayer(c.proof(), node.proof()).copy(c.node(), current, + c.nodesToSkip()); + } catch (Exception ex) { + LOGGER.error("failed to copy proof steps", ex); + MainWindowF owner = window; + if (owner != null) { + FxUtil.runLater( + () -> IssueDialogF.showExceptionDialog(owner.getStage(), ex)); + } + } + } + updateGUIState(mediator.getSelectedProof()); + } + + /** + * "Go to referenced proof" (Swing {@code GotoReferenceAction}): selects the equivalent node + * in the referenced proof. + */ + private MenuItem gotoReferenceItem(KeYMediatorF mediator, Node node) { + MenuItem item = new MenuItem("Go to referenced proof"); + item.setDisable(node.lookup(ClosedBy.class) == null); + item.setOnAction(e -> { + ClosedBy c = node.lookup(ClosedBy.class); + if (c != null) { + mediator.getSelectionModel().setSelectedNode(c.node()); + } + }); + return item; + } + + /** + * "Re-open cached goal" (Swing {@code RemoveCachingInformationAction}): removes the caching + * information and re-opens the goal. + */ + private MenuItem removeCachingInformationItem(KeYMediatorF mediator, Node node) { + MenuItem item = new MenuItem("Re-open cached goal"); + item.setDisable(node.lookup(ClosedBy.class) == null); + item.setOnAction(e -> { + ClosedBy c = node.lookup(ClosedBy.class); + if (c == null) { + return; + } + node.deregister(c, ClosedBy.class); + Goal goal = node.proof().getClosedGoal(node); + if (goal != null) { + node.proof().reOpenGoal(goal); + } + // refresh selection to ensure UI is updated + mediator.getSelectionModel().setSelectedNode(node); + }); + return item; + } + + // ------------------------------------------------------------------ + // SettingsF + // ------------------------------------------------------------------ + + @Override + public SettingsProviderF getSettings() { + // extension: MP9.1 — Swing CachingExtension.getSettings (CachingExtension.java:338-340) + // returns the CachingSettingsProvider; the host registers it into the SettingsManagerF + // registry (Swing SettingsManager.registerProvider). + return settingsPanelProvider(); + } + + // ------------------------------------------------------------------ + // startup logic: automatic reference search + // ------------------------------------------------------------------ + + /** + * Tracks a proof for automatic reference search: registers the rule-app / dispose / prune + * listeners once per proof (Swing {@code CachingExtension.selectedProofChanged}, + * CachingExtension.java:130-139). Null-safe and idempotent. + * + * @param proof the currently open proof + */ + private void trackProof(@Nullable Proof proof) { + if (proof == null || trackedProofs.contains(proof)) { + return; + } + trackedProofs.add(proof); + proof.addRuleAppListener(ruleAppListener); + proof.addProofDisposedListener(disposeListener); + CachingPruneHandlerF handler = cachingPruneHandler; + if (handler != null) { + proof.addProofTreeListener(handler); + } + } + + /** + * @return whether the single-core-only caching features are currently unavailable (Swing + * {@code SingleCoreFeatureGate.isActive}, SingleCoreFeatureGate.java:97-103: the + * multi-core prover is enabled). + */ + private boolean isMultiCoreActive() { + return ProofIndependentSettings.DEFAULT_INSTANCE.getGeneralSettings() + .isParallelProverEnabled(); + } + + /** + * @return whether proof caching is enabled for automatic runs: the runtime off-switch AND + * the single-core gate (Swing {@code CachingExtension.getProofCachingEnabled}, + * CachingExtension.java:122-127). + */ + public boolean getProofCachingEnabled() { + return toggle.get() && !isMultiCoreActive(); + } + + /** + * The mains entry point of the proof-caching logic, called after every rule application of + * a tracked proof (Swing {@code CachingExtension.ruleApplied}, + * CachingExtension.java:142-187): when the runtime switches and the settings permit it, the + * new goals of an automatic step are matched against the closed branches of the other + * proofs and closed by reference. + * + * @param event the rule application event + */ + private void ruleApplied(ProofEvent event) { + if (!tryToClose) { + return; + } + if (event.getSource().lookup(CopyingProofReplayer.class) != null) { + // either: copy in progress, or a macro that expects the proof to really close + return; + } + if (!settings.getEnabled()) { + return; + } + if (!getProofCachingEnabled()) { + return; + } + Proof p = event.getSource(); + if (event.getRuleAppInfo().getOriginalNode().getNodeInfo() + .getInteractiveRuleApplication()) { + return; // only applies to automatic proof search + } + ImmutableList newGoals = event.getNewGoals(); + if (newGoals.size() <= 1) { + return; + } + for (Goal goal : newGoals) { + ClosedBy c = null; + try { + c = ReferenceSearcher.findPreviousProof(openProofs(), goal.node()); + } catch (Exception exception) { + LOGGER.warn("error during reference search", exception); + } + if (c != null) { + // stop automode from working on this goal + goal.setEnabled(false); + goal.node().register(c, ClosedBy.class); + c.proof().addProofDisposedListenerFirst(new CopyBeforeDisposeF(c.proof(), p)); + } + } + } + + /** + * The prover-task finished hook (Swing {@code CachingExtension.taskFinished}, + * CachingExtension.java:248-270): after a failed prover/macro run, re-opens goals that were + * closed by reference, so the user can continue from an open state. + * + * @param info the finished task + */ + private void onTaskFinished(TaskFinishedInfo info) { + tryToClose = info.getSource() instanceof TryCloseMacro; + if (tryToClose) { + return; // try close macro was running, nothing to do here + } + if (!(info.getProof() instanceof Proof p) || p.isDisposed() || p.closed() + || !(info.getSource() instanceof DefaultProver + || info.getSource() instanceof ProofMacro)) { + return; + } + // unmark interactive goals + if (p.countNodes() > 1 && p.openGoals().stream() + .anyMatch(goal -> goal.node().lookup(ClosedBy.class) != null)) { + p.openGoals().stream().filter(goal -> goal.node().lookup(ClosedBy.class) != null) + .forEach(g -> { + g.setEnabled(true); + g.proof().closeGoal(g); + }); + // statistics dialog is automatically shown (Swing); not ported + } + } + + // ------------------------------------------------------------------ + // status-line button behaviour + // ------------------------------------------------------------------ + + /** Updates the GUI state of the status line button (Swing {@code updateGUIState}). */ + public void updateGUIState(@Nullable Proof proof) { + statusLineButton().updateState(proof); + } + + /** + * The click handler of the status-line button (Swing + * {@code ReferenceSearchButton.actionPerformed}, ReferenceSearchButton.java:63-80): + * searches every open goal of the selected proof for references and closes the matches by + * registering the {@code ClosedBy} reference (with the copy-on-dispose listener). + *

        + * KNOWN-SIMPLIFIED: the Swing original then opens the interactive + * {@code ReferenceSearchDialog} (a table of the per-goal results with an "Apply" button that + * copies the steps, ReferenceSearchDialog.java:25-171). The FX port shows a read-only + * summary {@link Alert} instead; the copy of the referenced steps stays available through + * the "Copy referenced proof steps here" context item. + */ + private void runReferenceSearchFromStatus() { + KeYMediatorF m = mediator; + if (m == null) { + return; + } + Proof p = m.getSelectedProof(); + if (p == null) { + return; + } + int closed = 0; + for (Goal goal : p.openEnabledGoals()) { + ClosedBy c = null; + try { + c = ReferenceSearcher.findPreviousProof(openProofs(), goal.node()); + } catch (Exception exception) { + LOGGER.warn("error during reference search", exception); + } + if (c != null) { + p.closeGoal(goal); + goal.node().register(c, ClosedBy.class); + c.proof().addProofDisposedListenerFirst(new CopyBeforeDisposeF(c.proof(), p)); + closed++; + } + } + updateGUIState(p); + if (closed > 0) { + showInfoAlert("Successfully closed " + closed + " open goal(s) by cache. " + + "Use 'Copy referenced proof steps here' on the cached goals to copy the " + + "steps into this proof."); + } + } + + /** + * The proofs to search in: the proofs tracked through the selection events (every proof + * that was selected at least once — the FX load path selects each loaded proof, so this + * covers the currently opened proofs in practice). + *

        + * KNOWN-SIMPLIFIED: the Swing original feeds {@code mediator.getCurrentlyOpenedProofs()} + * to the reference search (CloseByReference.java:73, CachingExtension.java:172); the FX + * {@link KeYMediatorF} has no equivalent accessor, so the tracked proofs are used instead. + * + * @return the non-null proof list + */ + private List openProofs() { + List proofs = trackedProofsUnmodifiable(); + KeYMediatorF m = mediator; + if (m != null) { + Proof selected = m.getSelectedProof(); + if (selected != null && !proofs.contains(selected)) { + proofs.add(selected); + } + } + return proofs; + } + + /** + * @return a copy of the proofs currently tracked by this extension (the raw {@link Set} + * snapshot used by {@link CachingPruneHandlerF} — no locking needed here because the + * caller iterates the copy). + */ + List trackedProofsUnmodifiable() { + synchronized (trackedProofs) { + return new ArrayList<>(trackedProofs); + } + } + + /** Shows an information dialog owned by the main window (message-dialog port). */ + private void showInfoAlert(String message) { + Alert alert = new Alert(AlertType.INFORMATION, message, ButtonType.OK); + alert.setTitle("Proof Caching"); + alert.setHeaderText(null); + MainWindowF owner = window; + if (owner != null) { + alert.initOwner(owner.getStage()); + } + alert.showAndWait(); + } +} diff --git a/keyext.caching.fx/src/main/java/de/uka/ilkd/key/gui/plugins/caching/fx/CachingPruneHandlerF.java b/keyext.caching.fx/src/main/java/de/uka/ilkd/key/gui/plugins/caching/fx/CachingPruneHandlerF.java new file mode 100644 index 00000000000..bb57b46ac1f --- /dev/null +++ b/keyext.caching.fx/src/main/java/de/uka/ilkd/key/gui/plugins/caching/fx/CachingPruneHandlerF.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.plugins.caching.fx; + +import de.uka.ilkd.key.gui.plugins.caching.settings.ProofCachingSettings; +import de.uka.ilkd.key.proof.Goal; +import de.uka.ilkd.key.proof.Proof; +import de.uka.ilkd.key.proof.ProofTreeEvent; +import de.uka.ilkd.key.proof.ProofTreeListener; +import de.uka.ilkd.key.proof.io.IntermediateProofReplayer; +import de.uka.ilkd.key.proof.reference.ClosedBy; +import de.uka.ilkd.key.proof.replay.CopyingProofReplayer; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Handles prunes in proofs that are referenced elsewhere, FX port of {@code CachingPruneHandler} + * (Swing CachingPruneHandler.java:30-80). If the branch that is pruned away is used as a caching + * target (via {@link ClosedBy}) elsewhere, that reference is removed before the prune occurs and + * — depending on the {@code ProofCachingSettings.prune} setting — the referenced steps are + * copied into the caching proof. + *

        + * KNOWN-SIMPLIFIED: the Swing original iterates {@code mediator.getCurrentlyOpenedProofs()} + * (CachingPruneHandler.java:51); the FX port iterates the proofs tracked by the owning + * {@link CachingExtensionF} instead (every proof selected at least once — the FX load path + * selects each loaded proof, so this covers the currently opened proofs in practice). Proof + * mutations triggered by a prune are engine operations and run on the caller (prover) thread, + * like in Swing. + * + * @author Arne Keller (Swing original) + * @author MP9.1 prune-handling port (FX) + */ +final class CachingPruneHandlerF implements ProofTreeListener { + private static final Logger LOGGER = LoggerFactory.getLogger(CachingPruneHandlerF.class); + + /** the owning extension providing the tracked proofs */ + private final CachingExtensionF extension; + + /** + * Create a new handler. + * + * @param extension the owning extension + */ + CachingPruneHandlerF(CachingExtensionF extension) { + this.extension = extension; + } + + @Override + public void proofIsBeingPruned(ProofTreeEvent event) { + Proof proofToBePruned = event.getSource(); + // check other proofs for any references to this proof + for (Proof p : extension.trackedProofsUnmodifiable()) { + for (Goal g : p.closedGoals()) { + ClosedBy c = g.node().lookup(ClosedBy.class); + if (c == null || c.proof() != proofToBePruned) { + continue; + } + var commonAncestor = event.getNode().commonAncestor(c.node()); + if (commonAncestor == c.node()) { + boolean copySteps = CachingSettingsProviderF.getCachingSettings().getPrune() + .equals(ProofCachingSettings.PRUNE_COPY); + // proof is now open => remove caching reference + g.node().deregister(c, ClosedBy.class); + p.reOpenGoal(g); + if (copySteps) { + // quickly copy the proof before it is pruned + try { + new CopyingProofReplayer(c.proof(), p).copy(c.node(), g, + c.nodesToSkip()); + } catch (IntermediateProofReplayer.BuiltInConstructionException ex) { + LOGGER.warn("failed to copy referenced proof that is about to be " + + "pruned", ex); + } + } + } + } + } + } +} diff --git a/keyext.caching.fx/src/main/java/de/uka/ilkd/key/gui/plugins/caching/fx/CachingSettingsProviderF.java b/keyext.caching.fx/src/main/java/de/uka/ilkd/key/gui/plugins/caching/fx/CachingSettingsProviderF.java new file mode 100644 index 00000000000..6d47621c3f9 --- /dev/null +++ b/keyext.caching.fx/src/main/java/de/uka/ilkd/key/gui/plugins/caching/fx/CachingSettingsProviderF.java @@ -0,0 +1,137 @@ +/* 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.plugins.caching.fx; + +import javafx.scene.control.CheckBox; +import javafx.scene.control.ComboBox; +import javafx.scene.control.Label; + +import de.uka.ilkd.key.gui.fx.MainWindowF; +import de.uka.ilkd.key.gui.fx.settings.InvalidSettingsInputExceptionF; +import de.uka.ilkd.key.gui.fx.settings.SettingsPanelF; +import de.uka.ilkd.key.gui.fx.settings.SettingsProviderF; +import de.uka.ilkd.key.gui.plugins.caching.settings.ProofCachingSettings; +import de.uka.ilkd.key.settings.ProofIndependentSettings; + +import static de.uka.ilkd.key.gui.plugins.caching.settings.ProofCachingSettings.DISPOSE_COPY; +import static de.uka.ilkd.key.gui.plugins.caching.settings.ProofCachingSettings.DISPOSE_REOPEN; +import static de.uka.ilkd.key.gui.plugins.caching.settings.ProofCachingSettings.PRUNE_COPY; +import static de.uka.ilkd.key.gui.plugins.caching.settings.ProofCachingSettings.PRUNE_REOPEN; + +/** + * The "Proof Caching" options panel, FX port of {@code CachingSettingsProvider} (Swing + * CachingSettingsProvider.java:26-121): the auto-search checkbox and the two behaviour combo + * boxes (dispose / prune of referenced proofs), reading and writing the reused, persisted + * {@link ProofCachingSettings}. + * + * @author Arne Keller (Swing original) + * @author MP9.1 settings-panel port (FX) + */ +final class CachingSettingsProviderF extends SettingsPanelF implements SettingsProviderF { + + /** + * Singleton instance of the caching settings (Swing + * {@code CachingSettingsProvider.CACHING_SETTINGS}). OWNED by the FX module + * (KNOWN-SIMPLIFIED: the Swing keyext {@code CachingSettingsProvider.getCachingSettings()} + * is not loadable on the FX module's compile path — the key.ui interface behind it is not + * exposed — so the FX module keeps the same {@code ProofCachingSettings} singleton and + * registers it into the {@link ProofIndependentSettings} itself; the Swing extension and the + * FX extension share the persisted object via that registry). + */ + private static final ProofCachingSettings CACHING_SETTINGS = new ProofCachingSettings(); + + static { + ProofIndependentSettings.DEFAULT_INSTANCE.addSettings(CACHING_SETTINGS); + } + + /** Text for the introductory explanation (Swing {@code INTRO_LABEL}). */ + private static final String INTRO_LABEL = "Adjust proof caching algorithm options here."; + + /** Label of the first option (Swing {@code STRATEGY_SEARCH}). */ + private static final String STRATEGY_SEARCH = + "Automatically search for references in auto mode"; + + /** Label of the second option (Swing {@code DISPOSE_TITLE}). */ + private static final String DISPOSE_TITLE = "Behaviour when disposing referenced proof"; + + /** Label of the third option (Swing {@code PRUNE_TITLE}). */ + private static final String PRUNE_TITLE = "Behaviour when pruning into referenced proof"; + + /** Checkbox for the first option (Swing {@code strategySearch}). */ + private final CheckBox strategySearch; + + /** Combo box for the second option — dispose behaviour (Swing {@code disposeOption}). */ + private final ComboBox disposeOption; + + /** Combo box for the third option — prune behaviour (Swing {@code pruneOption}). */ + private final ComboBox pruneOption; + + /** + * Construct a new settings provider. + */ + CachingSettingsProviderF() { + setHeaderText("Proof Caching Options"); + + Label intro = new Label(INTRO_LABEL); + intro.setWrapText(true); + pCenter.add(intro, 0, pCenter.getRowCount(), 3, 1); + + strategySearch = addCheckBox(STRATEGY_SEARCH, "", CACHING_SETTINGS.getEnabled()); + disposeOption = addComboBox(DISPOSE_TITLE, """ + When a referenced proof is disposed, this is what happens to + all cached branches that reference it.""", + DISPOSE_COPY, DISPOSE_REOPEN); + pruneOption = addComboBox(PRUNE_TITLE, """ + When a referenced proof is pruned, this is what happens to + all cached branches that reference it.""", + PRUNE_COPY, PRUNE_REOPEN); + } + + @Override + public String getDescription() { + return "Proof Caching"; + } + + @Override + public javafx.scene.Node getPanel(MainWindowF window) { + ProofCachingSettings ss = getCachingSettings(); + strategySearch.setSelected(ss.getEnabled()); + disposeOption.getSelectionModel().select(ss.getDispose()); + pruneOption.getSelectionModel().select(ss.getPrune()); + return this; + } + + @Override + public void apply(MainWindowF window) throws InvalidSettingsInputExceptionF { + ProofCachingSettings ss = getCachingSettings(); + // KNOWN-SIMPLIFIED: the Swing original writes strategySearch.isEnabled() here + // (CachingSettingsProvider.java:112 — JCheckBox#isEnabled, a latent bug that always + // persisted true); the FX port writes the checkbox SELECTION, i.e. the behaviour the + // panel visibly offers. + ss.setEnabled(strategySearch.isSelected()); + String dispose = disposeOption.getSelectionModel().getSelectedItem(); + if (dispose != null) { + ss.setDispose(dispose); + } + String prune = pruneOption.getSelectionModel().getSelectedItem(); + if (prune != null) { + ss.setPrune(prune); + } + } + + /** + * @return the settings managed by this provider (Swing + * {@code CachingSettingsProvider.getCachingSettings}) + */ + static ProofCachingSettings getCachingSettings() { + return CACHING_SETTINGS; + } + + @Override + public int getPriorityOfSettings() { + // extension: MP9.1 — sorted last like Swing (CachingSettingsProvider.java:118-121). + return 10000; + } +} diff --git a/keyext.caching.fx/src/main/java/de/uka/ilkd/key/gui/plugins/caching/fx/CachingStatusButtonF.java b/keyext.caching.fx/src/main/java/de/uka/ilkd/key/gui/plugins/caching/fx/CachingStatusButtonF.java new file mode 100644 index 00000000000..ae4ee92bbf3 --- /dev/null +++ b/keyext.caching.fx/src/main/java/de/uka/ilkd/key/gui/plugins/caching/fx/CachingStatusButtonF.java @@ -0,0 +1,102 @@ +/* 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.plugins.caching.fx; + +import javafx.scene.control.Button; +import javafx.scene.control.Tooltip; + +import de.uka.ilkd.key.gui.fx.colors.ColorSettingsF; +import de.uka.ilkd.key.proof.Proof; +import de.uka.ilkd.key.proof.reference.ClosedBy; +import de.uka.ilkd.key.settings.ProofIndependentSettings; + +import org.key_project.util.javafx.FxUtil; + +import org.jspecify.annotations.Nullable; + +/** + * The status-line button of the proof-caching extension, FX port of {@code ReferenceSearchButton} + * (Swing ReferenceSearchButton.java:30-119). The button shows the number of goals of the selected + * proof that are closed by reference ("Proof Caching (n)" coloured green) or a plain "Proof + * Caching" label, is greyed out while the multi-core prover is active (single-core-only feature, + * Swing ReferenceSearchButton.java:94-99) and disabled whenever no cached goals are present + * (ReferenceSearchButton.java:114-118). The click behaviour (reference search + closing the + * found goals) lives in {@link CachingExtensionF}; this class only renders the state. + * + * @author Arne Keller (Swing original) + * @author MP9.1 status-line port (FX) + */ +final class CachingStatusButtonF extends Button { + + /** + * Color used for the label if a reference is found (Swing {@code COLOR_FINE}, + * ReferenceSearchButton.java:34-37; the FX port registers the same colour key in the + * {@link ColorSettingsF} palette). + */ + private static final ColorSettingsF.ColorPropertyF COLOR_FINE = ColorSettingsF + .define("caching.reference_found", "Color of the Proof Caching status line button " + + "when references were found", ColorSettingsF.color(80, 120, 0)); + + CachingStatusButtonF() { + super("Proof Caching"); + setDisable(true); + } + + /** + * Update the UI state of this button (Swing {@code ReferenceSearchButton.updateState}, + * ReferenceSearchButton.java:93-119). Selection events may fire from the prover thread, so + * the update is marshalled onto the FX thread. + * + * @param proof the currently selected proof + */ + void updateState(@Nullable Proof proof) { + Runnable update = () -> refresh(proof); + if (FxUtil.isFxThread()) { + update.run(); + } else { + FxUtil.runLater(update); + } + } + + private void refresh(@Nullable Proof proof) { + if (isMultiCoreActive()) { + // single-core gate, Swing ReferenceSearchButton.java:94-99: the button greys out with + // the shared tooltip while the multi-core prover is active + setText("Proof Caching"); + setTextFill(null); + setDisable(true); + setTooltip(new Tooltip("Unavailable while the multi-core prover is active. " + + "Switch to the single-core prover to use it.")); + return; + } + setTooltip(null); + if (proof == null) { + setText("Proof Caching"); + setTextFill(null); + setDisable(true); + return; + } + long foundRefs = proof.closedGoals().stream() + .filter(g -> g.node().lookup(ClosedBy.class) != null).count(); + if (foundRefs > 0) { + setText(String.format("Proof Caching (%d)", foundRefs)); + setTextFill(COLOR_FINE.getCurrentColor()); + setDisable(false); + } else { + setText("Proof Caching"); + setTextFill(null); + setDisable(true); + } + } + + /** + * @return whether the multi-core prover is active (Swing + * {@code SingleCoreFeatureGate.isActive} reads the same persisted setting) + */ + private static boolean isMultiCoreActive() { + return ProofIndependentSettings.DEFAULT_INSTANCE.getGeneralSettings() + .isParallelProverEnabled(); + } +} diff --git a/keyext.caching.fx/src/main/java/de/uka/ilkd/key/gui/plugins/caching/fx/CopyBeforeDisposeF.java b/keyext.caching.fx/src/main/java/de/uka/ilkd/key/gui/plugins/caching/fx/CopyBeforeDisposeF.java new file mode 100644 index 00000000000..719d63e04b1 --- /dev/null +++ b/keyext.caching.fx/src/main/java/de/uka/ilkd/key/gui/plugins/caching/fx/CopyBeforeDisposeF.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.plugins.caching.fx; + +import de.uka.ilkd.key.gui.plugins.caching.settings.ProofCachingSettings; +import de.uka.ilkd.key.proof.Proof; +import de.uka.ilkd.key.proof.event.ProofDisposedEvent; +import de.uka.ilkd.key.proof.event.ProofDisposedListener; +import de.uka.ilkd.key.proof.reference.ClosedBy; +import de.uka.ilkd.key.proof.reference.CopyReferenceResolver; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The listener ensuring that steps are copied before the referenced proof is disposed, FX port + * of {@code CachingExtension.CopyBeforeDispose} (Swing CachingExtension.java:277-335). Registered + * on the referenced proof whenever a goal is closed by reference; when that proof is disposed the + * cached goals of the new proof are either completed by copying the referenced steps (settings + * {@code DISPOSE_COPY}) or re-opened after removing the caching information (settings + * {@code DISPOSE_REOPEN}). + *

        + * KNOWN-SIMPLIFIED: the Swing original wraps the copy into + * {@code mediator.initiateAutoMode(...)}/{@code mediator.finishAutoMode(...)} (the UI + * suspension/notifications of the Swing mediator, CachingExtension.java:312-319); the FX + * {@link de.uka.ilkd.key.core.fx.KeYMediatorF} has no auto-mode context API for a foreign proof, + * so the FX port invokes the same core operation (the {@link CopyReferenceResolver}) directly. + * + * @author Arne Keller (Swing original) + * @author MP9.1 — WP9.1-2 copy-on-dispose listener port (FX) + */ +final class CopyBeforeDisposeF implements ProofDisposedListener { + private static final Logger LOGGER = LoggerFactory.getLogger(CopyBeforeDisposeF.class); + + /** the referenced proof that is about to be disposed */ + private final Proof referencedProof; + + /** the new proof whose cached branches reference {@link #referencedProof} */ + private final Proof newProof; + + /** + * Construct a new listener. + * + * @param referencedProof referenced proof + * @param newProof new proof + */ + CopyBeforeDisposeF(Proof referencedProof, Proof newProof) { + this.referencedProof = referencedProof; + this.newProof = newProof; + } + + @Override + public void proofDisposing(ProofDisposedEvent event) { + if (newProof.isDisposed()) { + return; + } + if (CachingSettingsProviderF.getCachingSettings().getDispose() + .equals(ProofCachingSettings.DISPOSE_COPY)) { + try { + CopyReferenceResolver.copyCachedGoals(newProof, referencedProof, null, null); + } catch (RuntimeException ex) { + // a failed copy must never take the proof disposal down with it + LOGGER.error("failed to copy cached goals before disposing the referenced " + + "proof", ex); + } + } else { + newProof.closedGoals().stream() + .filter(x -> x.node().lookup(ClosedBy.class) != null + && x.node().lookup(ClosedBy.class).proof() == referencedProof) + .forEach(x -> { + newProof.reOpenGoal(x); + x.node().deregister(x.node().lookup(ClosedBy.class), ClosedBy.class); + }); + } + } + + @Override + public void proofDisposed(ProofDisposedEvent event) { + } +} diff --git a/keyext.caching.fx/src/main/resources/META-INF/services/de.uka.ilkd.key.gui.fx.extension.api.KeYGuiExtensionF b/keyext.caching.fx/src/main/resources/META-INF/services/de.uka.ilkd.key.gui.fx.extension.api.KeYGuiExtensionF new file mode 100644 index 00000000000..281841b73d1 --- /dev/null +++ b/keyext.caching.fx/src/main/resources/META-INF/services/de.uka.ilkd.key.gui.fx.extension.api.KeYGuiExtensionF @@ -0,0 +1 @@ +de.uka.ilkd.key.gui.plugins.caching.fx.CachingExtensionF diff --git a/keyext.caching.fx/src/test/java/de/uka/ilkd/key/gui/plugins/caching/fx/CachingExtensionFTest.java b/keyext.caching.fx/src/test/java/de/uka/ilkd/key/gui/plugins/caching/fx/CachingExtensionFTest.java new file mode 100644 index 00000000000..52a460f99fa --- /dev/null +++ b/keyext.caching.fx/src/test/java/de/uka/ilkd/key/gui/plugins/caching/fx/CachingExtensionFTest.java @@ -0,0 +1,99 @@ +/* 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.plugins.caching.fx; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +import de.uka.ilkd.key.gui.fx.extension.api.KeYGuiExtensionF; +import de.uka.ilkd.key.gui.plugins.caching.settings.ProofCachingSettings; +import de.uka.ilkd.key.settings.ProofIndependentSettings; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; + +/** + * Toolkit-free tests of the FX port of the proof-caching extension ({@link CachingExtensionF}): + * the service-loaded provider declares its {@link KeYGuiExtensionF.Info}, implements the + * portable SPI capabilities, guards the null cases of the sequent context-menu contribution + * (the "must never break the term menu" guarantee), and the FX-module-owned caching settings + * singleton is registered into the global {@link ProofIndependentSettings}. + *

        + * KNOWN-SIMPLIFIED: JavaFX 25 requires a running FX toolkit to even construct a + * control (e.g. {@code new ToggleButton(...)} fails headless with "Toolkit not initialized"), + * and this repo's unit tests run without a display and without a headless glass platform. The + * control-level SPI contracts (menu/toolbar/status-line items, settings panel widget tree) are + * therefore verified by Gate 2's in-app extension verification instead of by unit tests; the + * unit tests cover the toolkit-free surface only. This is why {@link CachingExtensionF} creates + * its JavaFX controls lazily (see {@code CachingExtensionF.menuToggleItem()} etc.). + */ +class CachingExtensionFTest { + + private final CachingExtensionF extension = new CachingExtensionF(); + + @Test + void infoAnnotationDeclaresProofCaching() { + KeYGuiExtensionF.Info info = CachingExtensionF.class + .getAnnotation(KeYGuiExtensionF.Info.class); + assertThat(info).isNotNull(); + assertThat(info.name()).isEqualTo("Proof Caching"); + assertThat(info.optional()).isTrue(); + assertThat(info.experimental()).isFalse(); + } + + @Test + void providerImplementsPortableCapabilities() { + // the provider itself is constructible headless (no FX toolkit required) + assertThat(extension).isInstanceOf(KeYGuiExtensionF.MainMenuF.class); + assertThat(extension).isInstanceOf(KeYGuiExtensionF.ToolbarF.class); + assertThat(extension).isInstanceOf(KeYGuiExtensionF.StatusLineF.class); + assertThat(extension).isInstanceOf(KeYGuiExtensionF.ContextMenuF.class); + assertThat(extension).isInstanceOf(KeYGuiExtensionF.SettingsF.class); + assertThat(extension).isInstanceOf(KeYGuiExtensionF.StartupF.class); + } + + @Test + void sequentContextItemsGuardNulls() { + // null goal/mediator/position must yield no items (never crash the term menu — the + // verify hook builds its MenuContext with a null position) + assertThat(extension.getSequentContextItems(null, null, null)).isEmpty(); + } + + @Test + void cachingSettingsSingletonIsRegisteredAndReused() { + // KNOWN-SIMPLIFIED singleton ownership: the FX module owns the ProofCachingSettings + // instance (the Swing keyext CachingSettingsProvider is not on the FX compile path). + ProofCachingSettings first = CachingSettingsProviderF.getCachingSettings(); + ProofCachingSettings second = CachingSettingsProviderF.getCachingSettings(); + assertThat(first).isSameAs(second); + // Swing defaults (ProofCachingSettings.java:37-48: enabled=true, copy on dispose/prune) + assertThat(first.getEnabled()).isTrue(); + assertThat(first.getDispose()).isEqualTo(ProofCachingSettings.DISPOSE_COPY); + assertThat(first.getPrune()).isEqualTo(ProofCachingSettings.PRUNE_COPY); + // registering the shared instance into the global settings registry is idempotent + // (ProofIndependentSettings.addSettings, key.core) and must never throw + assertThatCode(() -> ProofIndependentSettings.DEFAULT_INSTANCE.addSettings(first)) + .doesNotThrowAnyException(); + // the extension reads the exact same instance (its field is initialized from the + // module-owned singleton) + assertThat(extension.settings()).isSameAs(first); + assertThatCode(extension::getProofCachingEnabled).doesNotThrowAnyException(); + } + + @Test + void serviceRegistrationNamesTheProvider() throws IOException { + try (InputStream in = CachingExtensionFTest.class.getClassLoader() + .getResourceAsStream( + "META-INF/services/de.uka.ilkd.key.gui.fx.extension.api.KeYGuiExtensionF")) { + assertThat(in).as("service-loader file on the classpath").isNotNull(); + String content = new String(in.readAllBytes(), StandardCharsets.UTF_8); + assertThat(content) + .contains("de.uka.ilkd.key.gui.plugins.caching.fx.CachingExtensionF"); + } + } +} From b6f733495cb57ca0f7aeea3b5c5fe49cc65ee7d8 Mon Sep 17 00:00:00 2001 From: opencode Date: Sat, 10 Oct 2026 00:39:53 +0200 Subject: [PATCH 122/281] =?UTF-8?q?keyext.exploration.fx:=20MP9.2=20icon?= =?UTF-8?q?=20review=20=E2=80=94=20ikonli=20FontAwesome=20glyphs=20replace?= =?UTF-8?q?=20the=20unicode=20=E2=97=86/=E2=97=87=20fallback;=20toolbar/me?= =?UTF-8?q?nu=20toggles=20get=20COMPASS/CODE=5FBRANCH=20graphics;=20adds?= =?UTF-8?q?=20ikonli-javafx=20+=20ikonli-fontawesome6=20to=20the=20version?= =?UTF-8?q?=20catalog?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gradle/libs.versions.toml | 7 ++++++ keyext.exploration.fx/build.gradle | 4 +++ .../exploration/fx/ExplorationExtensionF.java | 8 ++++++ .../fx/ExplorationStepsPanelF.java | 25 +++++++++++-------- 4 files changed, 33 insertions(+), 11 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7b618eefc62..096c10f233d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -37,6 +37,9 @@ javafx = "25.0.4" richtextfx = "0.11.7" openjfx-plugin = "0.1.0" +# Icon fonts (ikonli, kordamp.org/ikonli) — FontAwesome glyphs for the keyext FX ports +ikonli = "12.4.0" + # External tools scala-isabelle = "0.4.5" @@ -92,6 +95,10 @@ javafx-controls = { module = "org.openjfx:javafx-controls", version.ref = "javaf javafx-graphics = { module = "org.openjfx:javafx-graphics", version.ref = "javafx" } javafx-web = { module = "org.openjfx:javafx-web", version.ref = "javafx" } +# ikonli — FontAwesome icon fonts (see kordamp.org/ikonli), used by the keyext FX ports +ikonli-javafx = { module = "org.kordamp.ikonli:ikonli-javafx", version.ref = "ikonli" } +ikonli-fontawesome6 = { module = "org.kordamp.ikonli:ikonli-fontawesome6-pack", version.ref = "ikonli" } + # CLI picocli = { module = "info.picocli:picocli", version.ref = "picocli" } diff --git a/keyext.exploration.fx/build.gradle b/keyext.exploration.fx/build.gradle index 7e9d5870c12..fbac1f4d8a2 100644 --- a/keyext.exploration.fx/build.gradle +++ b/keyext.exploration.fx/build.gradle @@ -25,4 +25,8 @@ dependencies { // Services, JTerm, KeyIO, ProofControl); neither :key.ui.fx nor :keyext.exploration exposes // key.core on their API surface, so the provider cannot compile against the SPI without it. implementation project(":key.core") + // MP9.2 icon review: ikonli FontAwesome glyphs for the toolbar/menu/status-line controls + // (replacing the unicode ◆/◇ fallback of the first revision) + implementation libs.ikonli.javafx + implementation libs.ikonli.fontawesome6 } diff --git a/keyext.exploration.fx/src/main/java/org/key_project/exploration/fx/ExplorationExtensionF.java b/keyext.exploration.fx/src/main/java/org/key_project/exploration/fx/ExplorationExtensionF.java index d726bf10500..ab9b7f2638d 100644 --- a/keyext.exploration.fx/src/main/java/org/key_project/exploration/fx/ExplorationExtensionF.java +++ b/keyext.exploration.fx/src/main/java/org/key_project/exploration/fx/ExplorationExtensionF.java @@ -37,6 +37,8 @@ import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; +import org.kordamp.ikonli.fontawesome6.FontAwesomeSolid; +import org.kordamp.ikonli.javafx.FontIcon; /** * JavaFX port of the proof-exploration extension, MP9.2 counter-part of {@code @@ -139,7 +141,9 @@ public List getToolbarControls(@Nullable MainWindowF window, private CheckBox getExploreModeToggle() { if (exploreModeCheck == null) { + // Swing ToggleExplorationAction.setIcon(Icons.EXPLORE.get()) CheckBox check = new CheckBox("Exploration Mode"); + check.setGraphic(new FontIcon(FontAwesomeSolid.COMPASS)); check.setTooltip(new Tooltip("Choose to start ExplorationMode")); check.setSelected(model.isExplorationModeSelected()); check.setOnAction(e -> model.setExplorationModeSelected(check.isSelected())); @@ -151,7 +155,9 @@ private CheckBox getExploreModeToggle() { private CheckBox getHideJustificationToggle() { if (hideJustificationCheck == null) { + // Swing ShowInteractiveBranchesAction.setIcon(Icons.SECOND_BRANCH.get()) CheckBox check = new CheckBox("Hide justification"); + check.setGraphic(new FontIcon(FontAwesomeSolid.CODE_BRANCH)); check.setTooltip(new Tooltip(""" Exploration actions are often done using a cut. \ Choose to hide the second cut-branches from the view to focus on the \ @@ -183,6 +189,7 @@ public List

        getMenus(@Nullable MainWindowF window, @Nullable KeYMediatorF private MenuItem getExploreModeMenuItem() { CheckMenuItem item = new CheckMenuItem("Exploration Mode"); + item.setGraphic(new FontIcon(FontAwesomeSolid.COMPASS)); item.setSelected(model.isExplorationModeSelected()); item.setOnAction(e -> model.setExplorationModeSelected(item.isSelected())); exploreModeMenuItems.add(item); @@ -191,6 +198,7 @@ private MenuItem getExploreModeMenuItem() { private MenuItem getHideJustificationMenuItem() { CheckMenuItem item = new CheckMenuItem("Hide justification"); + item.setGraphic(new FontIcon(FontAwesomeSolid.CODE_BRANCH)); item.setSelected(!model.isShowInteractiveBranches()); item.setDisable(!model.isExplorationModeSelected()); item.setOnAction(e -> setHideJustification(item.isSelected())); diff --git a/keyext.exploration.fx/src/main/java/org/key_project/exploration/fx/ExplorationStepsPanelF.java b/keyext.exploration.fx/src/main/java/org/key_project/exploration/fx/ExplorationStepsPanelF.java index 7f4ffa2ca4a..3840e6a5a1f 100644 --- a/keyext.exploration.fx/src/main/java/org/key_project/exploration/fx/ExplorationStepsPanelF.java +++ b/keyext.exploration.fx/src/main/java/org/key_project/exploration/fx/ExplorationStepsPanelF.java @@ -38,6 +38,9 @@ import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; +import org.kordamp.ikonli.fontawesome6.FontAwesomeRegular; +import org.kordamp.ikonli.fontawesome6.FontAwesomeSolid; +import org.kordamp.ikonli.javafx.FontIcon; /** * JavaFX port of the Swing {@code @@ -52,11 +55,9 @@ *

        * KNOWN-SIMPLIFIED: *

          - *
        • The Swing panel contributes a help button to its tab title via {@code getTitleCActions()} - * ({@code HelpFacade.createHelpButton}); the FX tab model has no title-action seam, the button is - * dropped.
        • - *
        • The Swing {@code Icons} class is AWT/Swing-bound — the status indicator uses the unicode - * glyphs {@code ◆}/{@code ◇} instead of the icon images.
        • + *
        • The Swing {@code Icons} class is AWT/Swing-bound — the tab title's help button is not + * ported; the status indicator uses ikonli FontAwesome glyphs (solid vs. regular compass, + * mirroring {@code Icons.EXPLORE}/{@code Icons.EXPLORE_DISABLE}) instead of the icon images.
        • *
        • Pruning goes through {@code MainWindowF.getUserInterfaceControl().getProofControl()} (the * FX counterpart of the Swing {@code mediator.getUI().getProofControl()}).
        • *
        @@ -65,13 +66,15 @@ final class ExplorationStepsPanelF extends VBox { /** - * status-line indicator glyphs (KNOWN-SIMPLIFIED: unicode instead of the Swing {@code Icons}) + * status-line indicator icons (ikonli FontAwesome): the solid compass marks a proof with + * exploration steps, the regular (outline) compass an empty one — the Swing + * {@code Icons.EXPLORE}/{@code Icons.EXPLORE_DISABLE} pair */ - private static final String GLYPH_HAS_STEPS = "\u25C6"; // ◆ - private static final String GLYPH_NO_STEPS = "\u25C7"; // ◇ + private final FontIcon hasStepsIcon = new FontIcon(FontAwesomeSolid.COMPASS); + private final FontIcon noStepsIcon = new FontIcon(FontAwesomeRegular.COMPASS); /** the singleton status-line indicator label (Swing {@code hasExplorationSteps}) */ - private final Label hasExplorationSteps = new Label(GLYPH_NO_STEPS); + private final Label hasExplorationSteps = new Label(null, noStepsIcon); /** the list of exploration nodes in traversal order (Swing {@code listModelExploration}) */ private final ObservableList listModelExploration = FXCollections.observableArrayList(); private final ListView listExplorations = new ListView<>(listModelExploration); @@ -349,11 +352,11 @@ private void createModel(@Nullable Proof model) { private void updateLabel() { if (listModelExploration.isEmpty()) { - hasExplorationSteps.setText(GLYPH_NO_STEPS); + hasExplorationSteps.setGraphic(noStepsIcon); hasExplorationSteps.setTooltip(new Tooltip( "The current proof does not contain any exploratory proof steps.")); } else { - hasExplorationSteps.setText(GLYPH_HAS_STEPS); + hasExplorationSteps.setGraphic(hasStepsIcon); hasExplorationSteps.setTooltip(new Tooltip( "The current proof contains exploratory proof steps.")); } From 73b07e883ba49fa236c76cebc1b03febc781bbb7 Mon Sep 17 00:00:00 2001 From: opencode Date: Sat, 10 Oct 2026 00:41:15 +0200 Subject: [PATCH 123/281] =?UTF-8?q?keyext.caching.fx:=20MP9.1=20icon=20rev?= =?UTF-8?q?iew=20=E2=80=94=20ikonli=20FontAwesome=20DATABASE/SEARCH/COG=20?= =?UTF-8?q?glyphs=20on=20the=20toolbar=20toggle,=20menu=20toggle,=20auto-s?= =?UTF-8?q?earch=20and=20options=20items=20(Swing=20CachingToggleAction=20?= =?UTF-8?q?had=20the=20keyCachedClosed=20icon)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gradle/libs.versions.toml | 7 +++++++ keyext.caching.fx/build.gradle | 3 +++ .../key/gui/plugins/caching/fx/CachingExtensionF.java | 9 +++++++++ 3 files changed, 19 insertions(+) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7b618eefc62..096c10f233d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -37,6 +37,9 @@ javafx = "25.0.4" richtextfx = "0.11.7" openjfx-plugin = "0.1.0" +# Icon fonts (ikonli, kordamp.org/ikonli) — FontAwesome glyphs for the keyext FX ports +ikonli = "12.4.0" + # External tools scala-isabelle = "0.4.5" @@ -92,6 +95,10 @@ javafx-controls = { module = "org.openjfx:javafx-controls", version.ref = "javaf javafx-graphics = { module = "org.openjfx:javafx-graphics", version.ref = "javafx" } javafx-web = { module = "org.openjfx:javafx-web", version.ref = "javafx" } +# ikonli — FontAwesome icon fonts (see kordamp.org/ikonli), used by the keyext FX ports +ikonli-javafx = { module = "org.kordamp.ikonli:ikonli-javafx", version.ref = "ikonli" } +ikonli-fontawesome6 = { module = "org.kordamp.ikonli:ikonli-fontawesome6-pack", version.ref = "ikonli" } + # CLI picocli = { module = "info.picocli:picocli", version.ref = "picocli" } diff --git a/keyext.caching.fx/build.gradle b/keyext.caching.fx/build.gradle index 6ab84cc0c34..9c3cb297af6 100644 --- a/keyext.caching.fx/build.gradle +++ b/keyext.caching.fx/build.gradle @@ -25,4 +25,7 @@ dependencies { // so this module must declare the core dependency itself to compile against those signatures. // Runtime-wise key.core arrives transitively anyway (via keyext.caching) — no classpath change. implementation project(":key.core") + // MP9.1 icon review: ikonli FontAwesome glyphs for the toolbar toggle + Caching menu + implementation libs.ikonli.javafx + implementation libs.ikonli.fontawesome6 } diff --git a/keyext.caching.fx/src/main/java/de/uka/ilkd/key/gui/plugins/caching/fx/CachingExtensionF.java b/keyext.caching.fx/src/main/java/de/uka/ilkd/key/gui/plugins/caching/fx/CachingExtensionF.java index 3efd29ec76c..91890b18bc0 100644 --- a/keyext.caching.fx/src/main/java/de/uka/ilkd/key/gui/plugins/caching/fx/CachingExtensionF.java +++ b/keyext.caching.fx/src/main/java/de/uka/ilkd/key/gui/plugins/caching/fx/CachingExtensionF.java @@ -54,6 +54,8 @@ import org.key_project.util.javafx.FxUtil; import org.jspecify.annotations.Nullable; +import org.kordamp.ikonli.fontawesome6.FontAwesomeSolid; +import org.kordamp.ikonli.javafx.FontIcon; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -156,7 +158,10 @@ ProofCachingSettings settings() { private CheckMenuItem menuToggleItem() { CheckMenuItem item = toggleMenuItem; if (item == null) { + // MP9.1 icon review: Swing CachingToggleAction carries the keyCachedClosed icon — a + // database glyph stands in item = new CheckMenuItem("Proof Caching"); + item.setGraphic(new FontIcon(FontAwesomeSolid.DATABASE)); item.setSelected(toggle.get()); // the checked state is synced into {@link #toggle} by the binding item.selectedProperty().bindBidirectional(toggle); @@ -172,7 +177,9 @@ private CheckMenuItem menuToggleItem() { private ToggleButton toolbarToggleButton() { ToggleButton button = toggleButton; if (button == null) { + // MP9.1 icon review: Swing CachingToggleAction.cached icon — database glyph button = new ToggleButton("Proof Caching"); + button.setGraphic(new FontIcon(FontAwesomeSolid.DATABASE)); button.selectedProperty().bindBidirectional(toggle); button.disableProperty().bind(multiCoreActive); toggleButton = button; @@ -362,6 +369,7 @@ public List getMenus(MainWindowF window, KeYMediatorF mediator) { private MenuItem autoSearchItem() { CheckMenuItem item = new CheckMenuItem("Automatically search for references in auto mode"); + item.setGraphic(new FontIcon(FontAwesomeSolid.SEARCH)); item.setSelected(settings.getEnabled()); item.setOnAction(e -> settings.setEnabled(item.isSelected())); return item; @@ -370,6 +378,7 @@ private MenuItem autoSearchItem() { /** Opens the "Proof Caching" settings panel in the settings dialog (like the HeatmapF). */ private MenuItem settingsItem() { MenuItem item = new MenuItem("Proof Caching Options…"); + item.setGraphic(new FontIcon(FontAwesomeSolid.COG)); item.setOnAction(e -> { MainWindowF owner = window; if (owner != null) { From ac4dbdf1482363043493808bfbbe24e6d3dc11fc Mon Sep 17 00:00:00 2001 From: opencode Date: Sat, 10 Oct 2026 00:48:36 +0200 Subject: [PATCH 124/281] =?UTF-8?q?keyext.ui.testgen.fx:=20MP9.5=20?= =?UTF-8?q?=E2=80=94=20JavaFX=20port=20of=20the=20test-generation=20extens?= =?UTF-8?q?ion=20(KeYGuiExtensionF=20provider)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../fx/CounterExampleResultsDialogF.java | 194 +++++++ .../gui/testgen/fx/CounterExampleTaskF.java | 74 +++ .../uka/ilkd/key/gui/testgen/fx/LogSinkF.java | 30 ++ .../gui/testgen/fx/TestGenResultsDialogF.java | 239 +++++++++ .../testgen/fx/TestGenerationSettingsF.java | 181 +++++++ .../gui/testgen/fx/TestGenerationTaskF.java | 68 +++ .../key/gui/testgen/fx/TestgenExtensionF.java | 355 +++++++++++++ .../gui/testgen/fx/TestgenReflectionF.java | 479 ++++++++++++++++++ .../testgen/fx/TestgenSettingsProviderF.java | 332 ++++++++++++ ....key.gui.fx.extension.api.KeYGuiExtensionF | 1 + .../gui/testgen/fx/TestgenExtensionFTest.java | 74 +++ 11 files changed, 2027 insertions(+) create mode 100644 keyext.ui.testgen.fx/src/main/java/de/uka/ilkd/key/gui/testgen/fx/CounterExampleResultsDialogF.java create mode 100644 keyext.ui.testgen.fx/src/main/java/de/uka/ilkd/key/gui/testgen/fx/CounterExampleTaskF.java create mode 100644 keyext.ui.testgen.fx/src/main/java/de/uka/ilkd/key/gui/testgen/fx/LogSinkF.java create mode 100644 keyext.ui.testgen.fx/src/main/java/de/uka/ilkd/key/gui/testgen/fx/TestGenResultsDialogF.java create mode 100644 keyext.ui.testgen.fx/src/main/java/de/uka/ilkd/key/gui/testgen/fx/TestGenerationSettingsF.java create mode 100644 keyext.ui.testgen.fx/src/main/java/de/uka/ilkd/key/gui/testgen/fx/TestGenerationTaskF.java create mode 100644 keyext.ui.testgen.fx/src/main/java/de/uka/ilkd/key/gui/testgen/fx/TestgenExtensionF.java create mode 100644 keyext.ui.testgen.fx/src/main/java/de/uka/ilkd/key/gui/testgen/fx/TestgenReflectionF.java create mode 100644 keyext.ui.testgen.fx/src/main/java/de/uka/ilkd/key/gui/testgen/fx/TestgenSettingsProviderF.java create mode 100644 keyext.ui.testgen.fx/src/main/resources/META-INF/services/de.uka.ilkd.key.gui.fx.extension.api.KeYGuiExtensionF create mode 100644 keyext.ui.testgen.fx/src/test/java/de/uka/ilkd/key/gui/testgen/fx/TestgenExtensionFTest.java diff --git a/keyext.ui.testgen.fx/src/main/java/de/uka/ilkd/key/gui/testgen/fx/CounterExampleResultsDialogF.java b/keyext.ui.testgen.fx/src/main/java/de/uka/ilkd/key/gui/testgen/fx/CounterExampleResultsDialogF.java new file mode 100644 index 00000000000..c1e6b6817af --- /dev/null +++ b/keyext.ui.testgen.fx/src/main/java/de/uka/ilkd/key/gui/testgen/fx/CounterExampleResultsDialogF.java @@ -0,0 +1,194 @@ +/* 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.testgen.fx; + +import javafx.application.Platform; +import javafx.beans.property.ReadOnlyBooleanWrapper; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.control.Button; +import javafx.scene.control.Dialog; +import javafx.scene.control.DialogPane; +import javafx.scene.control.Label; +import javafx.scene.control.ProgressIndicator; +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.Stage; + +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The counterexample search dialog, FX counterpart of the Swing counterexample flow + * ({@code CounterExampleAction.actionPerformed} + the {@code SolverListener} dialogs, + * CounterExampleAction.java:105-120). Streams the search log into a {@link TextArea} with + * Start / Stop / Close controls; the actual search runs off the FX thread in + * {@link CounterExampleTaskF}. + *

        + * KNOWN-SIMPLIFIED: the Swing flow shows the counterexample model in the modal + * {@code SolverListener} result dialogs; the FX port only prints the SMT statistics and the + * outcome (found/not found) into the log. The auto-mode shell around the search + * (CounterExampleAction.java:169-191) is dropped, like in {@link TestGenResultsDialogF}. The + * window may be {@code null} when the extension has not been connected through the settings + * dialog yet (see {@link TestgenExtensionF}) — the dialog then starts with a hint instead of a + * search; the owner stage is resolved from the clicked status-line control. + */ +@NullMarked +final class CounterExampleResultsDialogF extends Dialog { + + private static final Logger LOGGER = + LoggerFactory.getLogger(CounterExampleResultsDialogF.class); + + private final @Nullable Object window; + private final TextArea log = new TextArea(); + private final ProgressIndicator progress = new ProgressIndicator(); + private final Label statusLabel = new Label("Idle"); + private final Button btnStart = new Button("Start"); + private final Button btnStop = new Button("Stop"); + private final Button btnClose = new Button("Close"); + private final ReadOnlyBooleanWrapper running = new ReadOnlyBooleanWrapper(this, "running"); + + private CounterExampleTaskF runningTask; + + CounterExampleResultsDialogF(@Nullable Object window, @Nullable Stage owner) { + this.window = window; + setTitle("Counterexample Search"); + if (owner != null) { + initOwner(owner); + } + setResizable(true); + + progress.setVisible(false); + HBox status = new HBox(8, progress, statusLabel); + status.setAlignment(Pos.CENTER_LEFT); + + log.setEditable(false); + log.setWrapText(true); + log.setPromptText("Search log"); + VBox.setVgrow(log, Priority.ALWAYS); + + VBox center = new VBox(6, status, log); + center.setPadding(new Insets(8)); + + HBox buttons = new HBox(8, btnStart, btnStop, btnClose); + buttons.setAlignment(Pos.CENTER_RIGHT); + buttons.setPadding(new Insets(0, 8, 8, 8)); + + BorderPane root = new BorderPane(center); + root.setBottom(buttons); + DialogPane pane = getDialogPane(); + pane.setPrefSize(800, 500); + pane.setContent(root); + + btnStart.setOnAction(e -> startSearch()); + btnStop.setOnAction(e -> requestStop()); + btnClose.setOnAction(e -> close()); + btnClose.setDefaultButton(true); + btnStart.disableProperty().bind(running); + btnStop.disableProperty().bind(running.not()); + btnClose.disableProperty().bind(running); + } + + /** Shows the dialog non-modally and starts the counterexample search. */ + void showAndStart() { + show(); + startSearch(); + } + + /** The report sink of the run; the events fire on the search thread. */ + private final LogSinkF logger = new LogSinkF() { + @Override + public void writeln(String message) { + append(message + "\n"); + } + + @Override + public void error(Throwable throwable) { + LOGGER.warn("Exception during counterexample search", throwable); + append("Error: " + (throwable.getMessage() == null + ? throwable.getClass().getSimpleName() + : throwable.getMessage()) + + "\n"); + } + }; + + private void startSearch() { + if (running.get()) { + return; + } + Object captured = window; + if (captured == null) { + append("The extension is not connected to a main window yet - open the " + + "application settings (Options → Settings, section TestGen) once and the " + + "next run will work.\n"); + // KNOWN-SIMPLIFIED: the frozen FX SPI passes the window only to the settings + // panel (TestgenExtensionF); until then the run is validated on click. + return; + } + Object mediator = TestgenReflectionF.mediatorOf(captured); + Object goal = TestgenReflectionF.goalOf(mediator); + if (goal == null) { + append("No open goal is selected — cannot search for a counterexample.\n"); + return; + } + Object proof = TestgenReflectionF.proofOfGoal(goal); + Object sequent = TestgenReflectionF.sequentOfGoal(goal); + Object ui = TestgenReflectionF.uiOf(captured); + if (proof == null || sequent == null || ui == null) { + append("Cannot reach the proof environment — the search could not be started.\n"); + return; + } + running.set(true); + runningTask = null; + statusLabel.setText("Searching for counterexamples..."); + log.clear(); + + CounterExampleTaskF task = new CounterExampleTaskF(ui, proof, sequent, logger); + runningTask = task; + task.setOnSucceeded(e -> { + running.set(false); + statusLabel.setText("Finished."); + }); + task.setOnFailed(e -> { + running.set(false); + statusLabel.setText("Failed."); + Throwable t = task.getException(); + append("Counterexample search failed: " + (t == null ? "unknown error" + : t.getMessage() == null ? t.getClass().getSimpleName() + : t.getMessage()) + + "\n"); + }); + task.setOnCancelled(e -> { + running.set(false); + statusLabel.setText("Cancelled."); + append("Counterexample search cancelled.\n"); + }); + Thread thread = new Thread(task, "KeYCounterExample"); + thread.setDaemon(true); + thread.start(); + } + + private void requestStop() { + CounterExampleTaskF task = runningTask; + if (task != null) { + append("Stopping counterexample search.\n"); + task.requestStop(); + } + } + + /** Appends a log line, marshalled to the FX thread (the events fire on the worker thread). */ + private void append(String text) { + if (Platform.isFxApplicationThread()) { + log.appendText(text); + } else { + Platform.runLater(() -> log.appendText(text)); + } + } +} diff --git a/keyext.ui.testgen.fx/src/main/java/de/uka/ilkd/key/gui/testgen/fx/CounterExampleTaskF.java b/keyext.ui.testgen.fx/src/main/java/de/uka/ilkd/key/gui/testgen/fx/CounterExampleTaskF.java new file mode 100644 index 00000000000..d21e966c1b2 --- /dev/null +++ b/keyext.ui.testgen.fx/src/main/java/de/uka/ilkd/key/gui/testgen/fx/CounterExampleTaskF.java @@ -0,0 +1,74 @@ +/* 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.testgen.fx; + +import java.util.concurrent.atomic.AtomicReference; +import javafx.concurrent.Task; + +import org.jspecify.annotations.NullMarked; + +/** + * Runs the counterexample search off the FX thread, FX port of the Swing + * {@code CounterExampleAction.CEWorker} (CounterExampleAction.java:160-194). The Swing worker + * runs {@code AbstractSideProofCounterExampleGenerator.searchCounterExample} in a + * {@code SwingWorker} and reports through the {@code SolverListener} (with progress/result + * dialogs); this port runs the reflective counterpart of the same search + * ({@link TestgenReflectionF#searchCounterExample}) in a {@link Task} and streams the SMT + * statistics into the {@link LogSinkF} of the {@code CounterExampleResultsDialogF}. + *

        + * KNOWN-SIMPLIFIED: the Swing {@code SolverListener} opens a modal progress dialog and a + * results dialog showing the counterexample model (SolverListener.java); the FX port prints the + * SMT statistics and the found/not-found outcome into the run dialog's log and leaves the model + * inspection to the generated test data / subsequent runs. The auto-mode shell of the Swing + * worker ({@code initiateAutoMode}/{@code finishAutoMode}, CounterExampleAction.java:169-191) + * is dropped like in {@link TestGenerationTaskF}. + */ +@NullMarked +final class CounterExampleTaskF extends Task { + + /** the UserInterfaceControl of the main window (reflectively obtained). */ + private final Object ui; + + /** the proof of the selected goal (reflectively obtained). */ + private final Object proof; + + /** the sequent of the selected goal (reflectively obtained). */ + private final Object sequent; + + /** the log sink of the run dialog (events fire on this worker thread). */ + private final LogSinkF sink; + + /** the running {@code SolverLauncher} (reflectively obtained), for the Stop button. */ + private final AtomicReference launcher = new AtomicReference<>(); + + CounterExampleTaskF(Object ui, Object proof, Object sequent, LogSinkF sink) { + // ui/proof/sequent mirror the Swing worker's inputs (CounterExampleAction.actionPerformed, + // CounterExampleAction.java:106-120: node.proof() + node.sequent() of the selected goal). + this.ui = ui; + this.proof = proof; + this.sequent = sequent; + this.sink = sink; + } + + @Override + protected Void call() throws Exception { + TestgenReflectionF.searchCounterExample(ui, proof, sequent, sink, launcher); + return null; + } + + /** + * Asks the running search to stop: the reflective {@code SolverLauncher} is stopped and the + * worker thread interrupted (the {@code SemanticsBlastingMacro} honours the interrupt). + */ + void requestStop() { + Object running = launcher.get(); + if (running != null) { + TestgenReflectionF.stopLauncher(running); + } + if (isRunning()) { + cancel(true); + } + } +} diff --git a/keyext.ui.testgen.fx/src/main/java/de/uka/ilkd/key/gui/testgen/fx/LogSinkF.java b/keyext.ui.testgen.fx/src/main/java/de/uka/ilkd/key/gui/testgen/fx/LogSinkF.java new file mode 100644 index 00000000000..108e79e243e --- /dev/null +++ b/keyext.ui.testgen.fx/src/main/java/de/uka/ilkd/key/gui/testgen/fx/LogSinkF.java @@ -0,0 +1,30 @@ +/* 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.testgen.fx; + +import org.jspecify.annotations.NullMarked; + +/** + * The log/report sink of the FX run dialogs, counterpart of the lifecycle-logger slots of the + * Swing {@code TGInfoDialog} and {@code SolverListener}. The reflective bridge + * ({@link TestgenReflectionF}) translates the {@code TestGenerationLifecycleListener} and + * {@code SolverLauncherListener} events of the key.core.testgen machinery into these three + * callbacks; the dialogs append the messages to their {@code TextArea} (marshalled to the FX + * thread). + */ +@NullMarked +interface LogSinkF { + + /** Appends one log line (without trailing newline). */ + void writeln(String message); + + /** Reports a failure; the callers log the whole stack trace via the module logger. */ + default void error(Throwable throwable) { + } + + /** Called when the run reported its {@code finish} event (generation only). */ + default void finished() { + } +} diff --git a/keyext.ui.testgen.fx/src/main/java/de/uka/ilkd/key/gui/testgen/fx/TestGenResultsDialogF.java b/keyext.ui.testgen.fx/src/main/java/de/uka/ilkd/key/gui/testgen/fx/TestGenResultsDialogF.java new file mode 100644 index 00000000000..3fb61cca547 --- /dev/null +++ b/keyext.ui.testgen.fx/src/main/java/de/uka/ilkd/key/gui/testgen/fx/TestGenResultsDialogF.java @@ -0,0 +1,239 @@ +/* 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.testgen.fx; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; +import javafx.application.Platform; +import javafx.beans.property.ReadOnlyBooleanWrapper; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.control.Button; +import javafx.scene.control.Dialog; +import javafx.scene.control.DialogPane; +import javafx.scene.control.Label; +import javafx.scene.control.ListView; +import javafx.scene.control.ProgressIndicator; +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.Stage; + +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The test-suite generation run dialog, FX port of {@code TGInfoDialog} (Swing + * TGInfoDialog.java:22-144): a non-modal dialog streaming the generation log, with Start / Stop / + * Close buttons and a list of the generated test files. The dialog's {@link LogSinkF} bridges the + * {@code TestGenerationLifecycleListener} events (fired on the generation thread) to the FX + * thread, mirroring {@code ThreadUtilities.invokeOnEventQueue} of the Swing original + * (TGInfoDialog.java:73-82). + *

        + * KNOWN-SIMPLIFIED: the Swing dialog embeds a live {@code TestgenOptionsPanel} on its + * eastern edge (TGInfoDialog.java:126) so the options can be edited right before a run; the FX + * port keeps the options exclusively in the Settings dialog (the global + * {@link TestGenerationSettingsF} are re-read at run start). The "Close" button is disabled while + * a run is active like in Swing (TGInfoDialog.java:118). The generated-files list is a simple + * recursive listing of the configured output folder (the Swing dialog only prints "Writing test + * file" into the log). The window may be {@code null} when the extension has not been connected + * through the settings dialog yet (see {@link TestgenExtensionF}) — the dialog then starts with a + * hint instead of a run; the owner stage is resolved from the clicked status-line control so the + * dialog still opens properly owner-relative. The window is passed as a plain {@code Object} + * because every {@code MainWindowF} type use in this module fails to compile (see + * {@link TestgenReflectionF}). + */ +@NullMarked +final class TestGenResultsDialogF extends Dialog { + + private static final Logger LOGGER = LoggerFactory.getLogger(TestGenResultsDialogF.class); + + private final @Nullable Object window; + private final TextArea log = new TextArea(); + private final ListView generatedFiles = new ListView<>(); + private final ProgressIndicator progress = new ProgressIndicator(); + private final Label statusLabel = new Label("Idle"); + private final Button btnStart = new Button("Start"); + private final Button btnStop = new Button("Stop"); + private final Button btnClose = new Button("Close"); + private final ReadOnlyBooleanWrapper running = new ReadOnlyBooleanWrapper(this, "running"); + + private TestGenerationTaskF runningTask; + + TestGenResultsDialogF(@Nullable Object window, @Nullable Stage owner) { + this.window = window; + setTitle("Test Suite Generation"); + if (owner != null) { + initOwner(owner); + } + setResizable(true); + + progress.setVisible(false); + HBox status = new HBox(8, progress, statusLabel); + status.setAlignment(Pos.CENTER_LEFT); + + log.setEditable(false); + log.setWrapText(true); + log.setPromptText("Generation log"); + VBox.setVgrow(log, Priority.ALWAYS); + + Label filesHeader = new Label("Generated test files"); + VBox.setVgrow(generatedFiles, Priority.ALWAYS); + + VBox center = new VBox(6, status, log, filesHeader, generatedFiles); + center.setPadding(new Insets(8)); + + HBox buttons = new HBox(8, btnStart, btnStop, btnClose); + buttons.setAlignment(Pos.CENTER_RIGHT); + buttons.setPadding(new Insets(0, 8, 8, 8)); + + BorderPane root = new BorderPane(center); + root.setBottom(buttons); + DialogPane pane = getDialogPane(); + pane.setPrefSize(900, 650); + pane.setContent(root); + + btnStart.setOnAction(e -> startRun()); + btnStop.setOnAction(e -> requestStop()); + btnClose.setOnAction(e -> close()); + btnClose.setDefaultButton(true); + btnStart.disableProperty().bind(running); + btnStop.disableProperty().bind(running.not()); + btnClose.disableProperty().bind(running); + } + + /** Shows the dialog non-modally (Swing {@code JDialog.setModal(false)}) and starts a run. */ + void showAndStart() { + show(); + startRun(); + } + + /** The report sink of the run; the events fire on the generation thread. */ + private final LogSinkF logger = new LogSinkF() { + @Override + public void writeln(String message) { + append(message + "\n"); + } + + @Override + public void error(Throwable throwable) { + // Swing TGInfoDialog.logger.writeException (TGInfoDialog.java:77-82). + LOGGER.warn("Exception during test generation", throwable); + append("Error: " + (throwable.getMessage() == null + ? throwable.getClass().getSimpleName() + : throwable.getMessage()) + + "\n"); + } + + @Override + public void finished() { + // Swing enables the exit button on finish (TGInfoDialog.java:90-92); the running flag + // disables the whole button bar, so nothing further is needed here. + } + }; + + private void startRun() { + if (running.get()) { + return; + } + Object captured = window; + if (captured == null) { + append("The extension is not connected to a main window yet - open the " + + "application settings (Options → Settings, section TestGen) once and the " + + "next run will work.\n"); + // KNOWN-SIMPLIFIED: the frozen FX SPI passes the window only to the settings + // panel (TestgenExtensionF); until then the run is validated on click. + return; + } + Object mediator = TestgenReflectionF.mediatorOf(captured); + Object proof = TestgenReflectionF.proofOf(mediator); + if (proof == null) { + append("No proof is loaded — annotated/open goals for the generation are missing.\n"); + return; + } + Object ui = TestgenReflectionF.uiOf(captured); + if (ui == null) { + append("Cannot reach the proof environment — no UserInterfaceControl available.\n"); + return; + } + running.set(true); + runningTask = null; + statusLabel.setText("Generating test cases..."); + log.clear(); + generatedFiles.getItems().clear(); + + TestGenerationTaskF task = new TestGenerationTaskF(ui, proof, logger); + runningTask = task; + task.setOnSucceeded(e -> { + running.set(false); + statusLabel.setText("Finished."); + listGeneratedFiles(); + }); + task.setOnFailed(e -> { + running.set(false); + statusLabel.setText("Failed."); + Throwable t = task.getException(); + append("Generation failed: " + (t == null ? "unknown error" + : t.getMessage() == null ? t.getClass().getSimpleName() + : t.getMessage()) + + "\n"); + }); + task.setOnCancelled(e -> { + running.set(false); + statusLabel.setText("Cancelled."); + append("Test case generation cancelled.\n"); + }); + Thread thread = new Thread(task, "KeYTestGen"); + thread.setDaemon(true); + thread.start(); + } + + private void requestStop() { + TestGenerationTaskF task = runningTask; + if (task != null) { + append("Stopping test case generation.\n"); + task.requestStop(); + } + } + + /** + * Lists the generated files: a recursive scan of the configured output folder (the JUnit + * tests are written to {@code /src/test/java} by the {@code TestCaseGenerator}, + * TestCaseGenerator.java:306-311). + */ + private void listGeneratedFiles() { + Path root = Path.of(new TestGenerationSettingsF().outputFolderPath()); + List files = new ArrayList<>(); + if (Files.isDirectory(root)) { + try (Stream walk = Files.walk(root)) { + walk.filter(p -> p.getFileName().toString().endsWith(".java")).sorted() + .map(p -> root.relativize(p).toString()).forEach(files::add); + } catch (IOException e) { + LOGGER.warn("Could not list generated test files in {}", root, e); + } + } + if (files.isEmpty()) { + files.add("(no test files found in " + root + ")"); + } + generatedFiles.getItems().setAll(files); + } + + /** Appends a log line, marshalled to the FX thread (the events fire on the worker thread). */ + private void append(String text) { + if (Platform.isFxApplicationThread()) { + log.appendText(text); + } else { + Platform.runLater(() -> log.appendText(text)); + } + } +} diff --git a/keyext.ui.testgen.fx/src/main/java/de/uka/ilkd/key/gui/testgen/fx/TestGenerationSettingsF.java b/keyext.ui.testgen.fx/src/main/java/de/uka/ilkd/key/gui/testgen/fx/TestGenerationSettingsF.java new file mode 100644 index 00000000000..873ca13746a --- /dev/null +++ b/keyext.ui.testgen.fx/src/main/java/de/uka/ilkd/key/gui/testgen/fx/TestGenerationSettingsF.java @@ -0,0 +1,181 @@ +/* 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.testgen.fx; + +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A typed view on the global {@code TestGenerationSettings} singleton of {@code key.core.testgen} + * for the FX module. + *

        + * NOT part of the SPI: {@code key.core.testgen} is not on the compile classpath of this + * module (the frozen {@code build.gradle} only exposes {@code key.ui.fx} and the Swing + * {@code keyext.ui.testgen} to the compiler; key.core/key.core.testgen arrive on the runtime + * classpath), so the settings bean is reached through plain reflection. The accessors mirror the + * Swing {@code TestgenOptionsPanel} (TestgenOptionsPanel.java:134-157) and the command line + * properties {@code TestgenOptionsPanel.saveSettingsToFile}, i.e. the getters/setters the Swing + * panel reads and writes. + */ +@NullMarked +final class TestGenerationSettingsF { + + private static final Logger LOGGER = LoggerFactory.getLogger(TestGenerationSettingsF.class); + + private static final String SETTINGS_CLASS = "de.uka.ilkd.key.testgen.TestGenerationSettings"; + + /** the global singleton instance, {@code null} if the class is not present at runtime. */ + private final @Nullable Object instance; + + TestGenerationSettingsF() { + this.instance = instance(); + } + + /** + * @return the {@code TestGenerationSettings.getInstance()} singleton or {@code null} when the + * key.core.testgen classes are unavailable (should not happen in the app runtime) + */ + private static @Nullable Object instance() { + try { + Class settingsClass = Class.forName(SETTINGS_CLASS); + return settingsClass.getMethod("getInstance").invoke(null); + } catch (ReflectiveOperationException e) { + LOGGER.warn("TestGenerationSettings is not available", e); + return null; + } + } + + /** Swing TestgenOptionsPanel: {@code settings.getApplySymbolicExecution()}. */ + boolean applySymbolicEx() { + return bool("getApplySymbolicExecution"); + } + + /** Swing TestgenOptionsPanel: {@code settings.getMaximalUnwinds()}. */ + int maxUnwinds() { + return intOf("getMaximalUnwinds"); + } + + /** Swing TestgenOptionsPanel: {@code settings.invariantForAll()}. */ + boolean invariantForAll() { + return bool("invariantForAll"); + } + + /** Swing TestgenOptionsPanel: {@code settings.includePostCondition()}. */ + boolean includePostCondition() { + return bool("includePostCondition"); + } + + /** Swing TestgenOptionsPanel: {@code settings.getNumberOfProcesses()}. */ + int processes() { + return intOf("getNumberOfProcesses"); + } + + /** Swing TestgenOptionsPanel: {@code settings.getOutputFolderPath()}. */ + String outputFolderPath() { + Object value = invokeObject("getOutputFolderPath"); + return value == null ? "" : value.toString(); + } + + /** Swing TestgenOptionsPanel: {@code settings.removeDuplicates()}. */ + boolean removeDuplicates() { + return bool("removeDuplicates"); + } + + /** Swing TestgenOptionsPanel: {@code settings.isUseRFL()}. */ + boolean useRFL() { + return bool("isUseRFL"); + } + + /** Swing TestgenOptionsPanel.applySettings: {@code settings.setApplySymbolicExecution}. */ + void setApplySymbolicEx(boolean value) { + invoke("setApplySymbolicExecution", value); + } + + /** Swing TestgenOptionsPanel.applySettings: {@code settings.setInvariantForAll}. */ + void setInvariantForAll(boolean value) { + invoke("setInvariantForAll", value); + } + + /** Swing TestgenOptionsPanel.applySettings: {@code settings.setIncludePostCondition}. */ + void setIncludePostCondition(boolean value) { + invoke("setIncludePostCondition", value); + } + + /** Swing TestgenOptionsPanel.applySettings: {@code settings.setMaxUnwinds}. */ + void setMaxUnwinds(int value) { + invoke("setMaxUnwinds", value); + } + + /** Swing TestgenOptionsPanel.applySettings: {@code settings.setConcurrentProcesses}. */ + void setConcurrentProcesses(int value) { + invoke("setConcurrentProcesses", value); + } + + /** Swing TestgenOptionsPanel.applySettings: {@code settings.setOutputPath}. */ + void setOutputPath(String value) { + invoke("setOutputPath", value); + } + + /** Swing TestgenOptionsPanel.applySettings: {@code settings.setRemoveDuplicates}. */ + void setRemoveDuplicates(boolean value) { + invoke("setRemoveDuplicates", value); + } + + /** Swing TestgenOptionsPanel.applySettings: {@code settings.setUseRFL}. */ + void setUseRFL(boolean value) { + invoke("setUseRFL", value); + } + + // ------------------------------------------------------------------ reflection + + private boolean bool(String getter) { + Object value = invokeObject(getter); + return value instanceof Boolean b && b; + } + + private int intOf(String getter) { + Object value = invokeObject(getter); + return value instanceof Number n ? n.intValue() : 0; + } + + private @Nullable Object invokeObject(String method) { + Object target = instance; + if (target == null) { + return null; + } + try { + return target.getClass().getMethod(method).invoke(target); + } catch (ReflectiveOperationException | RuntimeException e) { + LOGGER.warn("Could not read TestGenerationSettings.{}", method, e); + return null; + } + } + + private void invoke(String method, Object value) { + Object target = instance; + if (target == null) { + LOGGER.warn("TestGenerationSettings not available, ignoring {}({})", method, value); + return; + } + try { + target.getClass().getMethod(method, primitiveType(value)).invoke(target, value); + } catch (ReflectiveOperationException | RuntimeException e) { + LOGGER.warn("Could not write TestGenerationSettings.{}", method, e); + } + } + + /** maps the boxed argument to the primitive parameter type of the settings setter. */ + private static Class primitiveType(Object value) { + if (value instanceof Boolean) { + return boolean.class; + } + if (value instanceof Integer) { + return int.class; + } + return value.getClass(); + } +} diff --git a/keyext.ui.testgen.fx/src/main/java/de/uka/ilkd/key/gui/testgen/fx/TestGenerationTaskF.java b/keyext.ui.testgen.fx/src/main/java/de/uka/ilkd/key/gui/testgen/fx/TestGenerationTaskF.java new file mode 100644 index 00000000000..7d773484a18 --- /dev/null +++ b/keyext.ui.testgen.fx/src/main/java/de/uka/ilkd/key/gui/testgen/fx/TestGenerationTaskF.java @@ -0,0 +1,68 @@ +/* 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.testgen.fx; + +import javafx.concurrent.Task; + +import org.jspecify.annotations.NullMarked; + +/** + * Runs the test-case generation off the FX thread, FX port of {@code TGWorker} (Swing + * TGWorker.java:30-86). The Swing worker is a {@code SwingWorker} that wraps the + * {@code MainWindowTestGenerator} ({@code AbstractTestGenerator} subclass) and reports through + * the lifecycle logger of the {@code TGInfoDialog}; this port wraps the headless CLI pipeline + * ({@link TestgenReflectionF#generateTestcases}) in a {@link Task} and dispatches the report + * events to the {@link LogSinkF} of the {@code TestGenResultsDialogF}. + *

        + * KNOWN-SIMPLIFIED: the Swing worker additionally puts the original proof into the + * mediator's auto-mode machinery ({@code initiateAutoMode}/{@code finishAutoMode}, + * TGWorker.java:42-63) and stops cooperatively through the {@code StopRequest} flag and the + * generator's {@code stopSMTLauncher} (TGWorker.java:66-71); the facade route has no launcher + * handle, so {@link #requestStop()} interrupts the worker thread, which the generation macros + * honour between phases (the final Z3 CE launch itself runs until it returns). + */ +@NullMarked +final class TestGenerationTaskF extends Task { + + /** the UserInterfaceControl of the main window (reflectively obtained). */ + private final Object ui; + + /** the proof to generate test cases for (reflectively obtained). */ + private final Object proof; + + /** the log sink of the run dialog (events fire on this worker thread). */ + private final LogSinkF sink; + + private volatile boolean stopRequested; + + TestGenerationTaskF(Object ui, Object proof, LogSinkF sink) { + // ui/proof mirror TGWorker's "the selected proof" and the main window's UI + // (TGWorker.java:36-39). + this.ui = ui; + this.proof = proof; + this.sink = sink; + } + + @Override + protected Void call() throws Exception { + TestgenReflectionF.generateTestcases(ui, proof, sink); + return null; + } + + boolean isStopRequested() { + return stopRequested; + } + + /** + * Asks the running generation to stop: the flag is visible to the dialogs, the interrupt is + * honoured by the generation macros (see the class comment). + */ + void requestStop() { + stopRequested = true; + if (isRunning()) { + cancel(true); + } + } +} diff --git a/keyext.ui.testgen.fx/src/main/java/de/uka/ilkd/key/gui/testgen/fx/TestgenExtensionF.java b/keyext.ui.testgen.fx/src/main/java/de/uka/ilkd/key/gui/testgen/fx/TestgenExtensionF.java new file mode 100644 index 00000000000..3018e81bd83 --- /dev/null +++ b/keyext.ui.testgen.fx/src/main/java/de/uka/ilkd/key/gui/testgen/fx/TestgenExtensionF.java @@ -0,0 +1,355 @@ +/* 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.testgen.fx; + +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.lang.reflect.Proxy; +import java.util.List; +import javafx.application.Platform; +import javafx.beans.property.ReadOnlyBooleanProperty; +import javafx.scene.Node; +import javafx.scene.Scene; +import javafx.scene.control.Button; +import javafx.scene.control.Control; +import javafx.scene.control.MenuButton; +import javafx.scene.control.MenuItem; +import javafx.scene.control.Tooltip; +import javafx.scene.input.KeyCombination; +import javafx.stage.Stage; + +import de.uka.ilkd.key.gui.fx.extension.api.KeYGuiExtensionF; +import de.uka.ilkd.key.gui.fx.settings.SettingsProviderF; + +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Test-case generation extension, JavaFX port of + * {@code de.uka.ilkd.key.gui.testgen.TestgenExtension} (Swing TestgenExtension.java:24-73). The + * Swing original contributes two {@code Action}s ("Search for Counterexample", "Generate + * Testcases...") to the main menu (grouped by the Swing facade into an "Extensions" menu), a + * {@code JToolBar} with the same two actions, a {@code KeyStrokeSettings.defineDefault} shortcut + * (Ctrl+T for the {@code TestGenMacro}) and the {@code TestgenOptionsPanel} settings provider; it + * also implements {@code KeyboardShortcuts} and {@code Startup} and registers selection/Z3 + * listeners so the actions are only enabled on an open proof with the Z3 CE solver installed. + *

        + * The FX port implements the {@link KeYGuiExtensionF} SPI through the two capabilities that are + * compilable in this module (see below): the settings entry + * ({@link KeYGuiExtensionF.SettingsF} → {@link TestgenSettingsProviderF}, a reflective + * {@code SettingsProviderF} proxy) and the status line ({@link KeYGuiExtensionF.StatusLineF}): a + * "Test Case Generation" {@link MenuButton} carrying the two menu items of the Swing extension + * menu (incl. the SHORTCUT+T accelerator that replaces the Swing + * {@code KeyStrokeSettings.defineDefault} default) and the two plain action buttons of the Swing + * toolbar. The long-running generation and the counterexample search run off the FX thread in + * {@link TestGenerationTaskF} / {@link CounterExampleTaskF} and report into the + * {@link TestGenResultsDialogF} / {@link CounterExampleResultsDialogF}. + *

        + * KNOWN-SIMPLIFIED (capability reduction): the Swing extension implements + * {@code MainMenu}, {@code Toolbar}, {@code KeyboardShortcuts} and {@code Startup}; the FX SPI + * mirrors those as {@link KeYGuiExtensionF.MainMenuF}, {@code ToolbarF} and {@code StartupF}, + * whose method signatures reference {@code MainWindowF}/{@code KeYMediatorF}. Any use of those + * types in this module forces javac to complete the {@code MainWindowF} class file, which fails + * ("Cannot attach type annotations ... to MainWindowF.lastEnvironment: class file for + * de.uka.ilkd.key.control.KeYEnvironment not found" — key.core is not on the frozen compile + * classpath); the menu and toolbar slots are therefore merged into a single status-line slot, and + * every window reference is a plain {@code Object} resolved through {@link TestgenReflectionF}. + *

        + * KNOWN-SIMPLIFIED (lazy window/mediator capture): {@link KeYGuiExtensionF.StatusLineF} + * does not receive the main window, and the {@code StartupF} init hook that the built-in + * status-line extensions use to bind the mediator is compile-impossible in this module (see + * above). The port therefore captures the window through the one SPI surface that does pass it — + * the settings panel ({@link TestgenSettingsProviderF}, whose {@code getPanel/apply} forward the + * window to {@link #captureWindow(Object)}) — and wires the enablement listeners on that first + * capture. Until then the buttons stay clickable and validate on click (the run dialogs log the + * "open the settings dialog once" hint instead of crashing), which mirrors the Swing actions' + * on-click guards. + */ +@KeYGuiExtensionF.Info(name = "Test case generation", experimental = false, + description = "key.testgen (JavaFX port): generate JUnit test cases from the current proof, " + + "or search for a counterexample, using the Z3 CE solver.") +@NullMarked +public final class TestgenExtensionF + implements KeYGuiExtensionF, KeYGuiExtensionF.SettingsF, KeYGuiExtensionF.StatusLineF { + + private static final Logger LOGGER = LoggerFactory.getLogger(TestgenExtensionF.class); + + /** Label of the status-line extension area (the Swing "Extensions" menu label equivalent). */ + private static final String MENU_LABEL = "Test Case Generation"; + + /** Swing TestGenerationAction.NAME. */ + private static final String NAME_GENERATE = "Generate Testcases..."; + + /** Swing CounterExampleAction.NAME. */ + private static final String NAME_COUNTEREXAMPLE = "Search for Counterexample"; + + /** Swing TestGenerationAction.TOOLTIP. */ + private static final String TOOLTIP_GENERATE = "Generate test cases for open goals"; + + /** Swing CounterExampleAction.TOOLTIP. */ + private static final String TOOLTIP_COUNTEREXAMPLE = + "Search for a counterexample for the selected goal"; + + /** the TestgenOptionsPanel port; a reflective {@code SettingsProviderF} proxy (see class). */ + private final SettingsProviderF settingsProvider = TestgenSettingsProviderF.create(this); + + /** status-line: the anchor menu button with the two extension-menu items. */ + private @Nullable MenuButton menuButton; + + /** status-line: the two plain toolbar buttons (Swing JToolBar entries). */ + private @Nullable Button generateButton; + private @Nullable Button counterExampleButton; + + /** + * the main window, captured lazily through the settings panel (see the class comment for the + * {@code KNOWN-SIMPLIFIED} reasoning): {@code null} until the user opens the settings dialog. + * Kept as a plain {@code Object} — {@code MainWindowF} is not nameable in this module. + */ + private @Nullable Object window; + + /** whether the enablement listeners are wired (once per captured window). */ + private boolean initialized; + + // ------------------------------------------------------------------ SettingsF + + @Override + public SettingsProviderF getSettings() { + // extension: MP9.5 — Swing TestgenExtension.java:70-72: the TestgenOptionsPanel. + return settingsProvider; + } + + // ------------------------------------------------------------------ StatusLineF + + @Override + public List getStatusLineControls() { + // extension: MP9.5 — Swing TestgenExtension.java:43-61: the two actions live in the + // main menu (grouped into the "Extensions" menu by the Swing facade) and in a JToolBar. + // The FX SPI contributes whole menus (KeYGuiExtensionF.MainMenuF) or toolbar controls + // (ToolbarF); both slots are compile-impossible here (see the class comment), so both + // contributions are merged into one status-line slot: the "Test Case Generation" + // MenuButton carries the two menu items, the two plain buttons mirror the toolbar. + if (menuButton == null) { + MenuButton anchor = new MenuButton(MENU_LABEL); + MenuItem generateItem = menuItem(NAME_GENERATE, () -> generateTestcases(anchor)); + // The Swing original registers the default shortcut on the TestGenMacro + // (TestgenExtension.java:51-52 via KeyStrokeSettings.defineDefault). The FX SPI has + // no keyboard-shortcut capability, so the accelerator is attached to the menu item + // instead. + // KNOWN-SIMPLIFIED: shortcut moved from the TestGen strategy macro (term context + // menu in Swing) to the "Generate Testcases..." menu item; the FX app has no + // macro-keybinding slot yet. + generateItem.setAccelerator(KeyCombination.keyCombination("SHORTCUT+T")); + MenuItem counterExampleItem = + menuItem(NAME_COUNTEREXAMPLE, () -> searchCounterExample(anchor)); + anchor.getItems().addAll(generateItem, counterExampleItem); + + Button generate = new Button(NAME_GENERATE); + generate.setTooltip(new Tooltip(TOOLTIP_GENERATE)); + generate.setOnAction(e -> generateTestcases(generate)); + Button counterExample = new Button(NAME_COUNTEREXAMPLE); + counterExample.setTooltip(new Tooltip(TOOLTIP_COUNTEREXAMPLE)); + counterExample.setOnAction(e -> searchCounterExample(counterExample)); + + menuButton = anchor; + generateButton = generate; + counterExampleButton = counterExample; + } + return List.of(menuButton, generateButton, counterExampleButton); + } + + // ------------------------------------------------------------------ window capture + + /** + * Hands the main window to the extension — the only SPI surface of this module that receives + * it (the {@code SettingsProviderF} panel/apply, forwarded by + * {@link TestgenSettingsProviderF}). + * Used to bind the enablement listeners once (the reflection-only counterpart of the Swing + * {@code Startup} hook). + * + * @param window the main window handed in by the settings dialog + */ + void captureWindow(Object window) { + this.window = window; + if (initialized) { + return; + } + initialized = true; + Object mediator = TestgenReflectionF.mediatorOf(window); + if (mediator != null) { + wireSelectionListener(mediator); + wireAutoModeListener(mediator); + wireSmtSettingsListener(); + } + refresh(); + } + + // ------------------------------------------------------------------ run dialogs + + /** Opens the test-suite generation run dialog and starts the generation. */ + private void generateTestcases(Node source) { + Object captured = window; + if (captured == null) { + LOGGER.warn("Testgen extension not connected to a main window yet - the run dialog " + + "will only log the hint; open the application settings (TestGen page) once"); + } + new TestGenResultsDialogF(captured, stageOf(source)).showAndStart(); + } + + /** Opens the counterexample search run dialog and starts the search. */ + private void searchCounterExample(Node source) { + Object captured = window; + if (captured == null) { + LOGGER.warn("Testgen extension not connected to a main window yet - the run dialog " + + "will only log the hint; open the application settings (TestGen page) once"); + } + new CounterExampleResultsDialogF(captured, stageOf(source)).showAndStart(); + } + + /** + * The {@link Stage} owning the given node (the status-line controls live in the main + * window's scene), used as the run dialogs' owner. + */ + private static @Nullable Stage stageOf(@Nullable Node source) { + if (source == null) { + return null; + } + Scene scene = source.getScene(); + return scene != null && scene.getWindow() instanceof Stage stage ? stage : null; + } + + // ------------------------------------------------------------------ enablement + + /** + * Registers the selection listener of the Swing actions through a reflective proxy: the + * {@code KeYSelectionListener} interface is typed with key.core classes (Node/Proof), so it + * cannot be implemented at compile time in this module. + */ + private void wireSelectionListener(Object mediator) { + try { + Object selectionModel = + mediator.getClass().getMethod("getSelectionModel").invoke(mediator); + Class listenerInterface = + Class.forName("de.uka.ilkd.key.core.fx.KeYSelectionListener"); + Object listener = Proxy.newProxyInstance(listenerInterface.getClassLoader(), + new Class[] { listenerInterface }, (proxy, method, args) -> { + switch (method.getName()) { + case "selectedNodeChanged", "selectedProofChanged" -> refresh(); + case "equals" -> { + return args != null && args.length == 1 && args[0] == proxy; + } + case "hashCode" -> { + return System.identityHashCode(proxy); + } + case "toString" -> { + return "TestgenSelectionListener(proxy)"; + } + default -> { + } + } + return null; + }); + selectionModel.getClass().getMethod("addKeYSelectionListener", listenerInterface) + .invoke(selectionModel, listener); + } catch (ReflectiveOperationException | RuntimeException e) { + LOGGER.warn("Could not register the KeYSelectionListener of the testgen extension", e); + } + } + + /** Binds the enablement to the observable auto-mode state of the FX mediator. */ + private void wireAutoModeListener(Object mediator) { + if (TestgenReflectionF.autoModePropertyOf(mediator) instanceof ReadOnlyBooleanProperty p) { + p.addListener((obs, oldValue, value) -> refresh()); + } + } + + /** + * Registers a listener on the global SMT settings so the extension reacts when the Z3 solver + * installation changes (Swing TestGenerationAction.checkZ3CE, + * TestGenerationAction.java:101-111 via {@code ProofIndependentSettings}). + */ + private void wireSmtSettingsListener() { + try { + Object defaultInstance = Class + .forName("de.uka.ilkd.key.settings.ProofIndependentSettings") + .getField("DEFAULT_INSTANCE").get(null); + Object smtSettings = defaultInstance.getClass().getMethod("getSMTSettings") + .invoke(defaultInstance); + smtSettings.getClass().getMethod("addPropertyChangeListener", + PropertyChangeListener.class) + .invoke(smtSettings, (PropertyChangeListener) this::handleSMTSettingsChanged); + } catch (ReflectiveOperationException | RuntimeException e) { + LOGGER.warn("Could not register the SMT-settings listener of the testgen extension", + e); + } + } + + private void handleSMTSettingsChanged(PropertyChangeEvent evt) { + refresh(); + } + + /** + * Re-computes the enablement of the status-line controls: like the Swing actions the + * extension is enabled only when Z3 CE is installed, a proof is selected and no auto-mode / + * generation run is active (Swing TestGenerationAction.checkZ3CE, + * TestGenerationAction.java:101-111); the counterexample action additionally requires the + * selected node to be an open leaf (CounterExampleAction.selectedNodeChanged, + * CounterExampleAction.java:67-84). The selection and SMT-settings events can fire on any + * thread, so the refresh is marshalled to the FX thread. + *

        + * Without a captured window the controls stay enabled and the run dialogs validate on click + * (the class comment's {@code KNOWN-SIMPLIFIED} lazy capture). + */ + private void refresh() { + Object connected = window; + if (connected == null) { + return; + } + if (!Platform.isFxApplicationThread()) { + Platform.runLater(this::refresh); + return; + } + Object mediator = TestgenReflectionF.mediatorOf(connected); + boolean z3Available = TestgenReflectionF.z3CeInstalled(); + boolean autoMode = false; + if (mediator != null + && TestgenReflectionF + .autoModePropertyOf(mediator) instanceof ReadOnlyBooleanProperty p) { + autoMode = p.get(); + } + boolean proofLoaded = mediator != null && TestgenReflectionF.proofOf(mediator) != null; + boolean generateEnabled = z3Available && !autoMode && proofLoaded; + boolean counterExampleEnabled = generateEnabled && mediator != null + && TestgenReflectionF.isOpenLeaf(TestgenReflectionF.nodeOf(mediator)); + setEnabled(generateEnabled, counterExampleEnabled); + } + + private void setEnabled(boolean generateEnabled, boolean counterExampleEnabled) { + MenuButton anchor = menuButton; + Button generate = generateButton; + Button counterExample = counterExampleButton; + if (anchor != null) { + for (MenuItem item : anchor.getItems()) { + boolean isGenerate = NAME_GENERATE.equals(item.getText()); + item.setDisable(isGenerate ? !generateEnabled : !counterExampleEnabled); + } + } + if (generate != null) { + generate.setDisable(!generateEnabled); + } + if (counterExample != null) { + counterExample.setDisable(!counterExampleEnabled); + } + } + + // ------------------------------------------------------------------ helpers + + private static MenuItem menuItem(String text, Runnable action) { + MenuItem item = new MenuItem(text); + item.setOnAction(e -> action.run()); + return item; + } +} diff --git a/keyext.ui.testgen.fx/src/main/java/de/uka/ilkd/key/gui/testgen/fx/TestgenReflectionF.java b/keyext.ui.testgen.fx/src/main/java/de/uka/ilkd/key/gui/testgen/fx/TestgenReflectionF.java new file mode 100644 index 00000000000..7908618f6e0 --- /dev/null +++ b/keyext.ui.testgen.fx/src/main/java/de/uka/ilkd/key/gui/testgen/fx/TestgenReflectionF.java @@ -0,0 +1,479 @@ +/* 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.testgen.fx; + +import java.lang.reflect.Array; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; +import javafx.stage.Stage; + +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The reflective bridge to the {@code key.core} / {@code key.core.testgen} generation logic. + *

        + * NOT part of the SPI: neither key.core nor key.core.testgen is on the compile classpath + * of this module (the frozen {@code build.gradle} only exposes {@code key.ui.fx} and the Swing + * {@code keyext.ui.testgen}; the core modules arrive on the runtime classpath of the app), so all + * calls into the proof tree, the SMT machinery and the test-case generators are dispatched through + * plain reflection. The routed entry points mirror the ones the Swing extension uses: + *

          + *
        • {@link #generateTestcases(Object, Object, LogSinkF)} — the headless CLI pipeline + * {@code TestgenFacade.generateTestcases} (TestgenFacade.java:43-101): bounded symbolic + * execution (FinishSymbolicExecution / TestGen macro), semantic blasting and the Z3 CE run with + * JUnit test-suite generation. The Swing {@code TGWorker} drives the same steps through + * {@code AbstractTestGenerator} (which needs a UI subclass that cannot be materialised without the + * compile-time types); the facade is its headless counterpart that works off a plain + * {@code KeYEnvironment}.
        • + *
        • {@link #searchCounterExample(Object, Object, Object, LogSinkF, AtomicReference)} — the + * counterexample search of {@code AbstractSideProofCounterExampleGenerator.searchCounterExample} + * (side proof creation via {@code SideProofUtil}, {@code SemanticsBlastingMacro} on the side + * proof, then Z3 CE via a {@code SolverLauncher}). The abstract generator's + * {@code createSolverListener} hook is UI-specific; this port passes a reflective + * {@code SolverLauncherListener} instead and reports the outcome into the {@link LogSinkF}.
        • + *
        + * Every reflective call is guarded: a missing class or a reflection error is logged through the + * {@link LogSinkF} (and the module logger) instead of crashing the FX thread; a propagated + * {@code InterruptedException} is re-thrown so the {@code Task} of the run dialogs turns into the + * "cancelled" state (the FX equivalent of the Swing interruption handling). + */ +@NullMarked +final class TestgenReflectionF { + + private static final Logger LOGGER = LoggerFactory.getLogger(TestgenReflectionF.class); + + /** + * the {@code Choice} activating the "ban runtime exceptions" taclet option of the side proof. + */ + private static final String[] BAN_RUNTIME_EXCEPTIONS = { "ban", "runtimeExceptions" }; + + private TestgenReflectionF() { + } + + // ------------------------------------------------------------------ window/mediator access + + /** + * {@code window.getUserInterfaceControl()} — the UserInterfaceControl of the FX window. The + * window is passed as a plain {@code Object}: every {@code MainWindowF} type use in this + * module forces javac to complete the class file, which fails ("Cannot attach type + * annotations ... to MainWindowF.lastEnvironment: class file for + * de.uka.ilkd.key.control.KeYEnvironment not found"). + */ + static @Nullable Object uiOf(@Nullable Object window) { + return callNullary(window, "getUserInterfaceControl"); + } + + /** {@code window.getMediator()} — the FX mediator of the window. */ + static @Nullable Object mediatorOf(@Nullable Object window) { + return callNullary(window, "getMediator"); + } + + /** {@code window.getStage()} — the stage the FX window is bound to. */ + static @Nullable Stage stageOf(@Nullable Object window) { + return callNullary(window, "getStage") instanceof Stage stage ? stage : null; + } + + /** {@code mediator.getSelectedProof()} — the currently selected proof or {@code null}. */ + static @Nullable Object proofOf(@Nullable Object mediator) { + return callNullary(mediator, "getSelectedProof"); + } + + /** {@code mediator.getSelectedNode()} — the currently selected node or {@code null}. */ + static @Nullable Object nodeOf(@Nullable Object mediator) { + return callNullary(mediator, "getSelectedNode"); + } + + /** {@code mediator.getSelectedGoal()} — the currently selected goal or {@code null}. */ + static @Nullable Object goalOf(@Nullable Object mediator) { + return callNullary(mediator, "getSelectedGoal"); + } + + /** {@code mediator.autoModeRunningProperty()} — the observable auto-mode state. */ + static @Nullable Object autoModePropertyOf(@Nullable Object mediator) { + return callNullary(mediator, "autoModeRunningProperty"); + } + + /** {@code goal.proof()} of a reflected {@code Goal}. */ + static @Nullable Object proofOfGoal(Object goal) { + return callNullary(goal, "proof"); + } + + /** {@code goal.sequent()} of a reflected {@code Goal}. */ + static @Nullable Object sequentOfGoal(Object goal) { + return callNullary(goal, "sequent"); + } + + // ------------------------------------------------------------------ enablement helpers + + /** + * Whether the Z3 CE solver is installed (Swing TestGenerationAction.checkZ3CE, + * TestGenerationAction.java:101-111: {@code SolverTypes.Z3_CE_SOLVER.isInstalled(false)}). + */ + static boolean z3CeInstalled() { + try { + Object z3 = Class.forName("de.uka.ilkd.key.smt.solvertypes.SolverTypes") + .getField("Z3_CE_SOLVER").get(null); + return (Boolean) z3.getClass().getMethod("isInstalled", boolean.class).invoke(z3, + false); + } catch (ReflectiveOperationException | RuntimeException e) { + LOGGER.warn("Could not query the Z3 CE solver installation state", e); + return false; + } + } + + /** + * Whether the reflected node is an open leaf (Swing CounterExampleAction.selectedNodeChanged, + * CounterExampleAction.java:67-79: {@code node.childrenCount() == 0 && !node.isClosed()}). + */ + static boolean isOpenLeaf(@Nullable Object node) { + if (node == null) { + return false; + } + try { + int children = (Integer) node.getClass().getMethod("childrenCount").invoke(node); + boolean closed = (Boolean) node.getClass().getMethod("isClosed").invoke(node); + return children == 0 && !closed; + } catch (ReflectiveOperationException | RuntimeException e) { + LOGGER.warn("Could not inspect the selected node", e); + return false; + } + } + + // ------------------------------------------------------------------ test-case generation + + /** + * Runs the test-case generation (Swing TGWorker.doInBackground, TGWorker.java:50-53). The + * routed pipeline is {@code TestgenFacade.generateTestcases}: bounded symbolic execution, + * testing with the TestGen macro, semantic blasting of the test data constraints and the + * final Z3 CE run that writes the JUnit test suite into the configured output folder + * (TestgenFacade.java:43-101). + *

        + * KNOWN-SIMPLIFIED: the Swing {@code TGWorker} additionally wraps the run in the + * mediator's auto-mode machinery and supports an immediate stop through the + * {@code StopRequest}/{@code SolverLauncher} handles (TGWorker.java:42-71); the facade route + * has no launcher handle, so stopping the FX run interrupts the worker thread, which the + * macros honour between phases (the Z3 launch itself runs until it returns). + * + * @param ui the UserInterfaceControl of the main window (reflected) + * @param proof the selected proof (reflected) + * @param sink the log sink of the run dialog + * @throws InterruptedException when the run was interrupted (task cancelled) + */ + static void generateTestcases(Object ui, Object proof, LogSinkF sink) + throws InterruptedException { + if (ui == null || proof == null) { + sink.writeln("Test generation cancelled: no proof loaded."); + return; + } + try { + Class settingsClass = clazz("de.uka.ilkd.key.testgen.TestGenerationSettings"); + Object settings = settingsClass.getMethod("getInstance").invoke(null); + Class uiClass = clazz("de.uka.ilkd.key.control.UserInterfaceControl"); + Class initConfigClass = clazz("de.uka.ilkd.key.proof.init.InitConfig"); + Class proofClass = clazz("de.uka.ilkd.key.proof.Proof"); + Class envClass = clazz("de.uka.ilkd.key.control.KeYEnvironment"); + Class listenerClass = + clazz("de.uka.ilkd.key.testgen.smt.testgen.TestGenerationLifecycleListener"); + + Object initConfig = invokeNullary(proof, "getInitConfig"); + Object env = envClass.getConstructor(uiClass, initConfigClass).newInstance(ui, + initConfig); + Object listener = lifecycleListenerProxy(listenerClass, sink); + Class facade = clazz("de.uka.ilkd.key.testgen.TestgenFacade"); + Method generate = facade.getMethod("generateTestcases", envClass, proofClass, + settingsClass, listenerClass); + generate.invoke(null, env, proof, settings, listener); + } catch (InvocationTargetException e) { + Throwable cause = e.getCause(); + if (cause instanceof InterruptedException ie) { + Thread.currentThread().interrupt(); + throw ie; + } + sink.error(cause == null ? e : cause); + } catch (ReflectiveOperationException | RuntimeException e) { + sink.error(e); + } + } + + // ------------------------------------------------------------------ counterexample search + + /** + * Searches a counterexample for the sequent of the selected goal (Swing + * {@code CounterExampleAction.CEWorker.doInBackground}, + * CounterExampleAction.java:175-181). The steps mirror + * {@code AbstractSideProofCounterExampleGenerator}: create a hidden side proof from the goal's + * sequent ({@code SideProofUtil.cloneProofEnvironmentWithOwnOneStepSimplifier} with the + * "ban runtime exceptions" choice + {@code createSideProof}), run the + * {@code SemanticsBlastingMacro} on it and launch the Z3 CE solver (the same pipeline as + * {@code AbstractCounterExampleGenerator.searchCounterExample}, + * AbstractCounterExampleGenerator.java:67-100). + *

        + * KNOWN-SIMPLIFIED: the Swing {@code SolverListener} opens a modal progress dialog and + * a results dialog showing the counterexample model; the FX port prints the SMT statistics + * (solved/invalid/unknown path conditions) and the found/not-found outcome into the run + * dialog's log and leaves the model inspection to the generated test data. The auto-mode + * shell of the Swing worker (CounterExampleAction.java:169-191) is dropped like in + * {@link TestGenerationTaskF}. + * + * @param ui the UserInterfaceControl of the main window (reflected) + * @param proof the proof of the selected goal (reflected) + * @param sequent the sequent of the selected goal (reflected) + * @param sink the log sink of the run dialog + * @param launcherRef receives the running {@code SolverLauncher} so the Stop button can ask it + * to stop + * @throws InterruptedException when the run was interrupted (task cancelled) + */ + static void searchCounterExample(Object ui, Object proof, Object sequent, LogSinkF sink, + AtomicReference<@Nullable Object> launcherRef) throws InterruptedException { + if (ui == null || proof == null || sequent == null) { + sink.writeln("Counterexample search cancelled: no goal selected."); + return; + } + if (!z3CeInstalled()) { + sink.writeln("Could not find the z3 SMT solver. Aborting."); + return; + } + try { + Class proofClass = clazz("de.uka.ilkd.key.proof.Proof"); + Class sequentClass = clazz("org.key_project.prover.sequent.Sequent"); + Class choiceClass = clazz("org.key_project.logic.Choice"); + Class sideProofUtil = clazz("de.uka.ilkd.key.util.SideProofUtil"); + Class proofEnvClass = clazz("de.uka.ilkd.key.proof.mgt.ProofEnvironment"); + + String proofName = "Semantics Blasting: " + invokeNullary(proof, "name"); + + // (1) side proof, as AbstractSideProofCounterExampleGenerator.createProof + // (AbstractSideProofCounterExampleGenerator.java:30-44) + Object[] choices = + (Object[]) Array.newInstance(choiceClass, BAN_RUNTIME_EXCEPTIONS.length); + for (int i = 0; i < BAN_RUNTIME_EXCEPTIONS.length; i++) { + choices[i] = choiceClass.getConstructor(String.class, String.class) + .newInstance(BAN_RUNTIME_EXCEPTIONS[i], "runtimeExceptions"); + } + Method cloneEnv = sideProofUtil.getMethod( + "cloneProofEnvironmentWithOwnOneStepSimplifier", proofClass, + choiceClass.arrayType()); + Object env = cloneEnv.invoke(null, proof, choices); + Object starter = sideProofUtil.getMethod("createSideProof", proofEnvClass, + sequentClass, String.class).invoke(null, env, sequent, proofName); + Object sideProof = invokeNullary(starter, "getProof"); + clazz("de.uka.ilkd.key.rule.OneStepSimplifier").getMethod("refreshOSS", proofClass) + .invoke(null, sideProof); + sink.writeln("Created side proof for semantics blasting."); + + // (2) semantics blasting macro on the side proof + // (AbstractCounterExampleGenerator.searchCounterExample, + // AbstractCounterExampleGenerator.java:75-87) + Object macro = clazz("de.uka.ilkd.key.testgen.macros.SemanticsBlastingMacro") + .getConstructor().newInstance(); + Object proofControl = invokeNullary(ui, "getProofControl"); + Object taskListener = invokeNullary(proofControl, "getDefaultProverTaskListener"); + Object openGoals = invokeNullary(sideProof, "openEnabledGoals"); + macro.getClass().getMethod("applyTo", + clazz("de.uka.ilkd.key.control.UserInterfaceControl"), proofClass, + clazz("org.key_project.util.collection.ImmutableList"), + clazz("de.uka.ilkd.key.logic.PosInOccurrence"), + clazz("org.key_project.prover.engine.ProverTaskListener")).invoke(macro, ui, + sideProof, openGoals, null, taskListener); + sink.writeln("Semantics blasting finished."); + + // (3) SMT settings for the Z3 CE launch (same triple as + // AbstractCounterExampleGenerator.java:90-92) + Object proofSettings = invokeNullary(sideProof, "getSettings"); + Object pdSettings = invokeNullary(proofSettings, "getSMTSettings"); + Object piSettings = invokeNullary( + Class.forName("de.uka.ilkd.key.settings.ProofIndependentSettings") + .getField("DEFAULT_INSTANCE").get(null), + "getSMTSettings"); + Object newSettings = invokeNullary(proofSettings, "getNewSMTSettings"); + Object smtSettings = clazz("de.uka.ilkd.key.settings.DefaultSMTSettings") + .getConstructor(clazz("de.uka.ilkd.key.settings.ProofDependentSMTSettings"), + clazz("de.uka.ilkd.key.settings.ProofIndependentSMTSettings"), + clazz("de.uka.ilkd.key.settings.NewSMTTranslationSettings"), proofClass) + .newInstance(pdSettings, piSettings, newSettings, sideProof); + + // (4) launch Z3 CE with a reflective SolverLauncherListener + // (AbstractCounterExampleGenerator.java:93-99) + Object launcher = clazz("de.uka.ilkd.key.smt.SolverLauncher") + .getConstructor(clazz("de.uka.ilkd.key.smt.SMTSettings")) + .newInstance(smtSettings); + launcherRef.set(launcher); + Object listener = solverLauncherListenerProxy(sink); + launcher.getClass().getMethod("addListener", + clazz("de.uka.ilkd.key.smt.SolverLauncherListener")).invoke(launcher, listener); + Object z3 = Class.forName("de.uka.ilkd.key.smt.solvertypes.SolverTypes") + .getField("Z3_CE_SOLVER").get(null); + Collection problems = (Collection) clazz("de.uka.ilkd.key.smt.SMTProblem") + .getMethod("createSMTProblems", proofClass).invoke(null, sideProof); + Object services = invokeNullary(sideProof, "getServices"); + launcher.getClass().getMethod("launch", Collection.class, Collection.class, + clazz("de.uka.ilkd.key.java.Services")).invoke(launcher, List.of(z3), problems, + services); + sink.finished(); + } catch (InvocationTargetException e) { + Throwable cause = e.getCause(); + if (cause instanceof InterruptedException ie) { + Thread.currentThread().interrupt(); + throw ie; + } + sink.error(cause == null ? e : cause); + } catch (ReflectiveOperationException | RuntimeException e) { + sink.error(e); + } + } + + /** Asks the running {@code SolverLauncher} to stop (the Stop button of the CE dialog). */ + static void stopLauncher(Object launcher) { + try { + launcher.getClass().getMethod("stop").invoke(launcher); + } catch (ReflectiveOperationException | RuntimeException e) { + LOGGER.warn("Could not stop the SMT launcher", e); + } + } + + // ------------------------------------------------------------------ listener proxies + + /** + * A {@code java.lang.reflect.Proxy} for {@code TestGenerationLifecycleListener} (the + * key.core.testgen interface) that forwards the events into the {@link LogSinkF}. The + * interface is deliberately not implemented at compile time — its methods are typed with + * key.core classes (TGPhase, Proof) that are absent from the compile classpath. + */ + private static Object lifecycleListenerProxy(Class listenerClass, LogSinkF sink) { + return Proxy.newProxyInstance(listenerClass.getClassLoader(), + new Class[] { listenerClass }, + (proxy, method, args) -> { + switch (method.getName()) { + case "writeln" -> { + Object message = args == null || args.length < 2 ? null : args[1]; + sink.writeln(message == null ? "" : message.toString()); + } + case "phase" -> { + // phase changes are not rendered, matching the Swing TGInfoDialog logger + } + case "writeException" -> sink.error(throwableOf(args)); + case "finish" -> sink.finished(); + case "equals" -> { + return args != null && args.length == 1 && args[0] == proxy; + } + case "hashCode" -> { + return System.identityHashCode(proxy); + } + case "toString" -> { + return "TestGenerationLifecycleListener(proxy)"; + } + default -> { + } + } + return null; + }); + } + + /** + * A {@code java.lang.reflect.Proxy} for {@code SolverLauncherListener} that streams the + * solver statistics of the counterexample search into the {@link LogSinkF}. The result + * summary mirrors {@code AbstractTestGenerator.filterSolverResultsAndShowSolverStatistics} + * (AbstractTestGenerator.java:383-428). + */ + private static Object solverLauncherListenerProxy(LogSinkF sink) + throws ClassNotFoundException { + Class listenerClass = clazz("de.uka.ilkd.key.smt.SolverLauncherListener"); + return Proxy.newProxyInstance(listenerClass.getClassLoader(), + new Class[] { listenerClass }, (proxy, method, args) -> { + switch (method.getName()) { + case "launcherStarted" -> { + Object problems = args == null || args.length == 0 ? null : args[0]; + int count = problems instanceof Collection c ? c.size() : 0; + sink.writeln("Searching for counterexamples: " + count + + " SMT problem(s)..."); + } + case "launcherStopped" -> { + Object solvers = args == null || args.length < 2 ? null : args[1]; + reportSolverResults(solvers, sink); + } + case "equals" -> { + return args != null && args.length == 1 && args[0] == proxy; + } + case "hashCode" -> { + return System.identityHashCode(proxy); + } + case "toString" -> { + return "SolverLauncherListener(proxy)"; + } + default -> { + } + } + return null; + }); + } + + private static void reportSolverResults(@Nullable Object finishedSolvers, LogSinkF sink) { + Collection solvers = finishedSolvers instanceof Collection c ? c : List.of(); + int valid = 0; + int falsifiable = 0; + int unknown = 0; + for (Object solver : solvers) { + try { + Object result = invokeNullary(solver, "getFinalResult"); + if (result != null) { + Object truth = invokeNullary(result, "isValid"); + if (truth != null) { + switch (truth.toString()) { + case "VALID" -> valid++; + case "FALSIFIABLE" -> falsifiable++; + case "UNKNOWN" -> unknown++; + default -> { + } + } + } + } + } catch (ReflectiveOperationException | RuntimeException ex) { + sink.writeln("Solver exception: " + (ex.getMessage() == null + ? ex.getClass().getSimpleName() + : ex.getMessage())); + } + } + sink.writeln("--- SMT Solver Results ---\n" + " solved pathconditions:" + falsifiable + + "\n" + " invalid pre-/pathconditions:" + valid + "\n" + " unknown:" + unknown); + sink.writeln(falsifiable > 0 ? "Found " + falsifiable + " counterexample(s)." + : "No counterexample found for the selected goal."); + } + + private static Throwable throwableOf(Object @Nullable [] args) { + Object value = args == null || args.length < 2 ? null : args[1]; + return value instanceof Throwable t ? t + : new RuntimeException("writeException without throwable: " + value); + } + + // ------------------------------------------------------------------ low-level reflection + + private static Class clazz(String name) throws ClassNotFoundException { + return Class.forName(name); + } + + private static @Nullable Object callNullary(@Nullable Object target, String method) { + if (target == null) { + return null; + } + try { + return invokeNullary(target, method); + } catch (ReflectiveOperationException | RuntimeException e) { + LOGGER.warn("Could not invoke {}.{}()", target.getClass().getSimpleName(), method, e); + return null; + } + } + + private static Object invokeNullary(Object target, String method) + throws ReflectiveOperationException { + return target.getClass().getMethod(method).invoke(target); + } +} diff --git a/keyext.ui.testgen.fx/src/main/java/de/uka/ilkd/key/gui/testgen/fx/TestgenSettingsProviderF.java b/keyext.ui.testgen.fx/src/main/java/de/uka/ilkd/key/gui/testgen/fx/TestgenSettingsProviderF.java new file mode 100644 index 00000000000..448b11d3b73 --- /dev/null +++ b/keyext.ui.testgen.fx/src/main/java/de/uka/ilkd/key/gui/testgen/fx/TestgenSettingsProviderF.java @@ -0,0 +1,332 @@ +/* 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.testgen.fx; + +import java.io.File; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Proxy; +import java.util.List; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Node; +import javafx.scene.control.Button; +import javafx.scene.control.CheckBox; +import javafx.scene.control.Control; +import javafx.scene.control.Label; +import javafx.scene.control.Spinner; +import javafx.scene.control.SpinnerValueFactory; +import javafx.scene.control.TextField; +import javafx.scene.control.Tooltip; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.Region; +import javafx.stage.DirectoryChooser; + +import de.uka.ilkd.key.gui.fx.settings.SettingsProviderF; + +import org.jspecify.annotations.NullMarked; + +/** + * The test-generation options panel, FX port of {@code TestgenOptionsPanel} (Swing + * TestgenOptionsPanel.java:14-157): the symbolic-execution/macro options, the maximal unwinds + * and concurrent SMT-process numbers, the output folder, and the duplicate/RFL/invariant/ + * post-condition toggles — reading and writing the global singleton + * {@link TestGenerationSettingsF} (TestgenOptionsPanel.getPanel/getDescription/applySettings, + * TestgenOptionsPanel.java:134-157). + *

        + * KNOWN-SIMPLIFIED (proxy provider): {@code SettingsProviderF.getPanel/apply} are declared + * with a {@code MainWindowF} parameter, and any {@code MainWindowF} type use in this module + * forces javac to complete the class file, which fails ("Cannot attach type annotations ... to + * MainWindowF.lastEnvironment: class file for de.uka.ilkd.key.control.KeYEnvironment not + * found"). The provider is therefore a {@link java.lang.reflect.Proxy} implementing + * {@code SettingsProviderF} without naming the window type at compile time; the window arrives as + * a plain {@code Object} argument of the reflective dispatch and is forwarded to + * {@link TestgenExtensionF#captureWindow(Object)} (the extension's only compile-safe window + * channel). + *

        + * KNOWN-SIMPLIFIED (panel): the Swing panel subclasses the Swing {@code SettingsPanel} + * base (row/validator/chooser helpers) and installs change listeners that write into a working + * copy of the settings as the user edits; the FX port builds a plain {@link GridPane} of + * label/input rows (the settings host wraps the panel in a {@code ScrollPane}), reads the widgets + * in {@code apply} only (leaving the dialog through OK/Apply is the only path that persists, so + * the result is identical), and drops the dead OpenJML/Objenesis file choosers of the Swing + * original (their setters are commented out, TestgenOptionsPanel.java:97-107). + */ +@NullMarked +final class TestgenSettingsProviderF { + + private static final String INFO_APPLY_SYMBOLIC_EX = + "Performs bounded symbolic execution on the current proof tree." + + " More precisely, the TestGen Macro is executed which the user can also manually execute by right-clicking " + + "on the proof tree and selecting Strategy Macros->TestGen."; + private static final String INFO_SAVE_TO = + "Choose the folder where the test case files will be written."; + private static final String INFO_MAX_PROCESSES = + "Maximal number of SMT processes that are allowed to " + "run concurrently."; + private static final String INFO_INVARIANT_FOR_ALL = + "Includes class invariants in the test data constraints. " + + "I.e., require the class invariant of all created objects to be true in the initial state."; + private static final String INFO_MAX_UNWINDS = + "Maximal number of loop unwinds or method calls on a branch that " + + "is symbolically executed when using the Strategy Macro \"TestGen\". The Strategy Macro is available" + + " by right-click on the proof tree."; + private static final String INFO_REMOVE_DUPLICATES = + "Generate a single testcase for two or more nodes which " + + "represent the same test data constraint. Two different nodes may represent the same test data constraint, " + + "because some formulas from the nodes which cannot be translated into a test case may be filtered out from " + + "the test data constraint."; + private static final String INFO_RFL_SELECTION = + "Enables initialization of protected, private, and ghost fields " + "with test data, " + + "as well as creation of objects from classes which have no default constructor " + + "(requires the Objenesis library)." + + "This functionality is enabled by RFL.java which is generated along the test suite. Please note, " + + "a runtime checker may not be able to handle the generated code."; + private static final String INFO_INCLUDE_POSTCONDITION = + "Includes the negated post condition in the test data " + + "constraint when generating test data. The post condition can only be included for paths (branches)" + + " where symbolic execution has finished."; + + private TestgenSettingsProviderF() { + } + + /** + * Creates the {@link SettingsProviderF} entry (a reflective {@link Proxy}) for the given + * extension, forwarding the main window of every panel access to + * {@link TestgenExtensionF#captureWindow(Object)}. + * + * @param extension the owning extension, notified of the main window + * @return the settings provider, never {@code null} + */ + static SettingsProviderF create(TestgenExtensionF extension) { + return (SettingsProviderF) Proxy.newProxyInstance(SettingsProviderF.class + .getClassLoader(), + new Class[] { SettingsProviderF.class }, + new SettingsState(extension)); + } + + /** + * The {@link InvocationHandler} of the settings provider + the panel widgets: dispatching the + * {@code SettingsProviderF} methods (routed by name, mirroring the Swing original's + * semantics) and building the options panel node. + *

        + * The widget fields are deliberately built lazily in {@link #ensureWidgets()}: every + * {@code javafx.scene.control.Control} triggers the FX toolkit in its class initializer + * ("Toolkit not initialized"), so neither the provider proxy nor the extension must touch + * Controls until the settings panel is really opened inside the running app. + */ + private static final class SettingsState implements InvocationHandler { + + private final TestgenExtensionF extension; + + private CheckBox symbolicEx; + private Spinner maxUnwinds; + private CheckBox invariantForAll; + private CheckBox includePostCondition; + private Spinner maxProcesses; + private TextField saveToFilePanel; + private CheckBox removeDuplicates; + private CheckBox checkboxRFL; + + private Node panel; + + SettingsState(TestgenExtensionF extension) { + this.extension = extension; + } + + /** The SettingsProviderF dispatch of the reflective provider. */ + @Override + public Object invoke(Object proxy, java.lang.reflect.Method method, Object[] args) { + switch (method.getName()) { + case "getDescription" -> { + // extension: MP9.5 — Swing TestgenOptionsPanel.getDescription + // (TestgenOptionsPanel.java:134) + return "TestGen"; + } + case "getPanel" -> { + // extension: MP9.5 — Swing TestgenOptionsPanel.getPanel + // (TestgenOptionsPanel.java:139-151): re-read the global settings. + if (args != null && args.length > 0 && args[0] != null) { + extension.captureWindow(args[0]); + } + return panel(); + } + case "apply" -> { + // extension: MP9.5 — Swing TestgenOptionsPanel.applySettings + // (TestgenOptionsPanel.java:153): write the widget states back. + if (args != null && args.length > 0 && args[0] != null) { + extension.captureWindow(args[0]); + } + applySettings(); + return null; + } + case "getChildProviders" -> { + return List.of(); + } + case "getPriorityOfSettings" -> { + return 0; + } + case "contains" -> { + // the SettingsProviderF default: description substring match. + String substring = + args == null || args.length == 0 || args[0] == null ? null + : args[0].toString(); + return substring != null && !substring.isEmpty() + && "TestGen".toLowerCase().contains(substring.toLowerCase()); + } + case "equals" -> { + return args != null && args.length == 1 && args[0] == proxy; + } + case "hashCode" -> { + return System.identityHashCode(proxy); + } + case "toString" -> { + return "TestGenerationSettingsProvider(proxy)"; + } + default -> { + } + } + return null; + } + + /** Builds (once) and returns the options panel, re-reading the global settings. */ + private Node panel() { + if (panel == null) { + ensureWidgets(); + panel = buildPanel(); + } + TestGenerationSettingsF settings = new TestGenerationSettingsF(); + symbolicEx.setSelected(settings.applySymbolicEx()); + maxUnwinds.getValueFactory().setValue(settings.maxUnwinds()); + invariantForAll.setSelected(settings.invariantForAll()); + includePostCondition.setSelected(settings.includePostCondition()); + maxProcesses.getValueFactory().setValue(settings.processes()); + saveToFilePanel.setText(settings.outputFolderPath()); + removeDuplicates.setSelected(settings.removeDuplicates()); + checkboxRFL.setSelected(settings.useRFL()); + return panel; + } + + /** Writes the widget states back into the global settings singleton. */ + private void applySettings() { + if (symbolicEx == null) { + // the panel was never opened, nothing was edited to persist. + return; + } + TestGenerationSettingsF global = new TestGenerationSettingsF(); + global.setApplySymbolicEx(symbolicEx.isSelected()); + global.setInvariantForAll(invariantForAll.isSelected()); + global.setIncludePostCondition(includePostCondition.isSelected()); + global.setMaxUnwinds(Math.max(0, maxUnwinds.getValue())); + global.setConcurrentProcesses(Math.max(0, maxProcesses.getValue())); + global.setOutputPath(saveToFilePanel.getText()); + global.setRemoveDuplicates(removeDuplicates.isSelected()); + global.setUseRFL(checkboxRFL.isSelected()); + } + + /** Creates the widget controls (lazily: Control class-init requires the FX toolkit). */ + private void ensureWidgets() { + if (symbolicEx != null) { + return; + } + symbolicEx = new CheckBox("Apply symbolic execution"); + maxUnwinds = intSpinner(0, Integer.MAX_VALUE, 1, 3); + invariantForAll = new CheckBox("Require invariant for all objects"); + includePostCondition = new CheckBox("Include post condition"); + maxProcesses = intSpinner(0, Integer.MAX_VALUE, 1, 1); + saveToFilePanel = new TextField(); + removeDuplicates = new CheckBox("Remove duplicates"); + checkboxRFL = new CheckBox("Use reflection framework"); + setTooltips(); + symbolicEx.setSelected(false); + invariantForAll.setSelected(true); + includePostCondition.setSelected(false); + removeDuplicates.setSelected(true); + checkboxRFL.setSelected(false); + } + + /** + * The options panel: a {@link GridPane} of label/input rows with tooltips (the Swing + * {@code TestgenOptionsPanel} uses a {@code GridBagLayout} with the same rows, + * TestgenOptionsPanel.java:67-108). + */ + private Node buildPanel() { + GridPane grid = new GridPane(); + grid.setHgap(8); + grid.setVgap(8); + grid.setPadding(new Insets(12)); + int row = 0; + addRow(grid, row++, null, symbolicEx); + addRow(grid, row++, "Maximal unwinds:", maxUnwinds); + addRow(grid, row++, null, invariantForAll); + addRow(grid, row++, null, includePostCondition); + addRow(grid, row++, "Concurrent processes:", maxProcesses); + addRow(grid, row++, "Store test cases to folder:", saveToRow()); + addRow(grid, row++, null, removeDuplicates); + addRow(grid, row, null, checkboxRFL); + return grid; + } + + /** One label + input row of the panel (or the input alone when {@code title == null}). */ + private void addRow(GridPane grid, int row, String title, Node input) { + if (title != null) { + Label label = new Label(title); + label.setAlignment(Pos.CENTER_LEFT); + if (input instanceof Control control) { + label.setTooltip(control.getTooltip()); + } + grid.add(label, 0, row); + grid.add(input, 1, row); + } else { + grid.add(input, 0, row); + } + if (input instanceof Region region) { + region.setMaxWidth(Double.MAX_VALUE); + } + GridPane.setHgrow(input, Priority.ALWAYS); + } + + /** The output-folder row: the text field plus a directory-chooser button. */ + private HBox saveToRow() { + Button browse = new Button("..."); + browse.setTooltip(new Tooltip(INFO_SAVE_TO)); + browse.setOnAction(e -> { + DirectoryChooser chooser = new DirectoryChooser(); + chooser.setTitle("Choose the test-case output folder"); + File initial = new File(saveToFilePanel.getText()); + if (initial.isDirectory()) { + chooser.setInitialDirectory(initial); + } + File selected = saveToFilePanel.getScene() == null + ? null + : chooser.showDialog(saveToFilePanel.getScene().getWindow()); + if (selected != null && selected.isDirectory()) { + saveToFilePanel.setText(selected.getAbsolutePath()); + } + }); + HBox.setHgrow(saveToFilePanel, Priority.ALWAYS); + return new HBox(4, saveToFilePanel, browse); + } + + private void setTooltips() { + symbolicEx.setTooltip(new Tooltip(INFO_APPLY_SYMBOLIC_EX)); + maxUnwinds.setTooltip(new Tooltip(INFO_MAX_UNWINDS)); + invariantForAll.setTooltip(new Tooltip(INFO_INVARIANT_FOR_ALL)); + includePostCondition.setTooltip(new Tooltip(INFO_INCLUDE_POSTCONDITION)); + maxProcesses.setTooltip(new Tooltip(INFO_MAX_PROCESSES)); + saveToFilePanel.setTooltip(new Tooltip(INFO_SAVE_TO)); + removeDuplicates.setTooltip(new Tooltip(INFO_REMOVE_DUPLICATES)); + checkboxRFL.setTooltip(new Tooltip(INFO_RFL_SELECTION)); + } + + private static Spinner intSpinner(int min, int max, int step, int value) { + Spinner spinner = + new Spinner<>(new SpinnerValueFactory.IntegerSpinnerValueFactory(min, max, value, + step)); + spinner.setEditable(true); + return spinner; + } + } +} diff --git a/keyext.ui.testgen.fx/src/main/resources/META-INF/services/de.uka.ilkd.key.gui.fx.extension.api.KeYGuiExtensionF b/keyext.ui.testgen.fx/src/main/resources/META-INF/services/de.uka.ilkd.key.gui.fx.extension.api.KeYGuiExtensionF new file mode 100644 index 00000000000..ec81f9c2c52 --- /dev/null +++ b/keyext.ui.testgen.fx/src/main/resources/META-INF/services/de.uka.ilkd.key.gui.fx.extension.api.KeYGuiExtensionF @@ -0,0 +1 @@ +de.uka.ilkd.key.gui.testgen.fx.TestgenExtensionF diff --git a/keyext.ui.testgen.fx/src/test/java/de/uka/ilkd/key/gui/testgen/fx/TestgenExtensionFTest.java b/keyext.ui.testgen.fx/src/test/java/de/uka/ilkd/key/gui/testgen/fx/TestgenExtensionFTest.java new file mode 100644 index 00000000000..6c39046c6ae --- /dev/null +++ b/keyext.ui.testgen.fx/src/test/java/de/uka/ilkd/key/gui/testgen/fx/TestgenExtensionFTest.java @@ -0,0 +1,74 @@ +/* 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.testgen.fx; + +import de.uka.ilkd.key.gui.fx.extension.api.KeYGuiExtensionF; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Headless unit test of the {@link TestgenExtensionF} provider (MP9.5): the {@code @Info} + * annotation, the compilable SPI capability surface ({@link KeYGuiExtensionF.SettingsF} + + * {@link KeYGuiExtensionF.StatusLineF}) and the reflection-dispatched settings entry (a + * {@code SettingsProviderF} proxy whose {@code getPanel/apply} forward the main window to the + * extension). + *

        + * KNOWN-SIMPLIFIED (headless): every {@code javafx.scene.control.Control} triggers the FX + * toolkit in its class initializer ("Toolkit not initialized"), and this module's frozen + * {@code build.gradle} exposes no headless toolkit to the tests — so the status-line {@code + * MenuButton}/{@code Button}s and the settings-panel {@code CheckBox}/spinner widgets are built + * lazily inside the running app only, and the test asserts the provider/capability surface + * instead of the control trees. Neither the provider construction nor the capability asserts + * touch the JavaFX toolkit; the run dialogs and the generation workers are deliberately not + * exercised here. + */ +class TestgenExtensionFTest { + + @Test + void providerInfoAnnotation() { + KeYGuiExtensionF extension = new TestgenExtensionF(); + KeYGuiExtensionF.Info info = + extension.getClass().getAnnotation(KeYGuiExtensionF.Info.class); + assertNotNull(info, "the provider must carry the @KeYGuiExtensionF.Info annotation"); + assertEquals("Test case generation", info.name()); + assertEquals("key.testgen (JavaFX port): generate JUnit test cases from the current " + + "proof, or search for a counterexample, using the Z3 CE solver.", + info.description()); + assertFalse(info.experimental(), "the Swing original is not experimental"); + assertFalse(info.optional()); + } + + @Test + void capabilitySurface() { + KeYGuiExtensionF extension = new TestgenExtensionF(); + // KNOWN-SIMPLIFIED: the MainMenuF/ToolbarF/StartupF slots are compile-impossible in this + // module (their signatures reference MainWindowF/KeYMediatorF, see TestgenExtensionF), + // so the Swing menu + toolbar contributions are merged into the StatusLineF slot; the + // control trees are built lazily in the app (they need the FX toolkit). + assertInstanceOf(KeYGuiExtensionF.SettingsF.class, extension, + "TestgenOptionsPanel port"); + assertInstanceOf(KeYGuiExtensionF.StatusLineF.class, extension, + "menu + toolbar actions merged into the status line"); + } + + @Test + void settingsProviderDispatch() { + TestgenExtensionF extension = new TestgenExtensionF(); + var settings = extension.getSettings(); + assertNotNull(settings); + assertEquals("TestGen", settings.getDescription(), + "Swing TestgenOptionsPanel.getDescription"); + assertEquals(0, settings.getPriorityOfSettings()); + assertTrue(settings.getChildProviders().isEmpty()); + assertTrue(settings.contains("gen"), "description substring search"); + assertFalse(settings.contains("proof management")); + } +} From b4ff5489549cd1b95473c65259da36b36bd4dbd6 Mon Sep 17 00:00:00 2001 From: opencode Date: Sat, 10 Oct 2026 00:54:09 +0200 Subject: [PATCH 125/281] =?UTF-8?q?keyext.ui.testgen.fx:=20MP9.5=20integra?= =?UTF-8?q?tion=20review=20=E2=80=94=20declare=20:key.core=20(like=20the?= =?UTF-8?q?=20other=20keyext=20FX=20ports)=20so=20MainMenuF=20is=20impleme?= =?UTF-8?q?ntable;=20new=20'Test=20Case=20Generation'=20menu=20with=20the?= =?UTF-8?q?=20two=20Swing=20actions=20(SHORTCUT+T=20moved=20to=20the=20men?= =?UTF-8?q?u=20item,=20avoiding=20a=20double=20binding=20with=20the=20stat?= =?UTF-8?q?us=20MenuButton);=20ikonli=20FontAwesome=20FLASK/EXCLAMATION=5F?= =?UTF-8?q?TRIANGLE=20glyphs=20on=20menu=20items=20+=20toolbar=20buttons?= =?UTF-8?q?=20(Swing=20SMALL=5FICONs)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- keyext.ui.testgen.fx/build.gradle | 10 + .../key/gui/testgen/fx/TestgenExtensionF.java | 182 ++++++++++++------ .../gui/testgen/fx/TestgenExtensionFTest.java | 32 +-- 3 files changed, 148 insertions(+), 76 deletions(-) diff --git a/keyext.ui.testgen.fx/build.gradle b/keyext.ui.testgen.fx/build.gradle index 751c0d30575..0704ea6e9c1 100644 --- a/keyext.ui.testgen.fx/build.gradle +++ b/keyext.ui.testgen.fx/build.gradle @@ -19,4 +19,14 @@ javafx { dependencies { implementation project(":key.ui.fx") implementation project(":keyext.ui.testgen") + // MP9.5 (integration review): like the other keyext FX ports, the module declares key.core on + // its compile classpath — the SPI signatures (KeYGuiExtensionF.MainMenuF/ToolbarF/StartupF) + // and the reused keyext.ui.testgen logic reference key.core types, and without it javac + // cannot complete MainWindowF (its annotations touch KeYEnvironment). Runtime-wise key.core + // arrives transitively anyway (via key.ui.fx) — no classpath change. + implementation project(":key.core") + // MP9.5 icon review: ikonli FontAwesome glyphs on the menu items and toolbar buttons (the + // Swing TestGenerationAction/CounterExampleAction carry SMALL_ICONs) + implementation libs.ikonli.javafx + implementation libs.ikonli.fontawesome6 } diff --git a/keyext.ui.testgen.fx/src/main/java/de/uka/ilkd/key/gui/testgen/fx/TestgenExtensionF.java b/keyext.ui.testgen.fx/src/main/java/de/uka/ilkd/key/gui/testgen/fx/TestgenExtensionF.java index 3018e81bd83..e7d799d9923 100644 --- a/keyext.ui.testgen.fx/src/main/java/de/uka/ilkd/key/gui/testgen/fx/TestgenExtensionF.java +++ b/keyext.ui.testgen.fx/src/main/java/de/uka/ilkd/key/gui/testgen/fx/TestgenExtensionF.java @@ -7,6 +7,7 @@ import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.lang.reflect.Proxy; +import java.util.ArrayList; import java.util.List; import javafx.application.Platform; import javafx.beans.property.ReadOnlyBooleanProperty; @@ -14,17 +15,22 @@ import javafx.scene.Scene; import javafx.scene.control.Button; import javafx.scene.control.Control; +import javafx.scene.control.Menu; import javafx.scene.control.MenuButton; import javafx.scene.control.MenuItem; import javafx.scene.control.Tooltip; import javafx.scene.input.KeyCombination; import javafx.stage.Stage; +import de.uka.ilkd.key.core.fx.KeYMediatorF; +import de.uka.ilkd.key.gui.fx.MainWindowF; import de.uka.ilkd.key.gui.fx.extension.api.KeYGuiExtensionF; import de.uka.ilkd.key.gui.fx.settings.SettingsProviderF; import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; +import org.kordamp.ikonli.fontawesome6.FontAwesomeSolid; +import org.kordamp.ikonli.javafx.FontIcon; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -38,43 +44,47 @@ * also implements {@code KeyboardShortcuts} and {@code Startup} and registers selection/Z3 * listeners so the actions are only enabled on an open proof with the Z3 CE solver installed. *

        - * The FX port implements the {@link KeYGuiExtensionF} SPI through the two capabilities that are - * compilable in this module (see below): the settings entry - * ({@link KeYGuiExtensionF.SettingsF} → {@link TestgenSettingsProviderF}, a reflective - * {@code SettingsProviderF} proxy) and the status line ({@link KeYGuiExtensionF.StatusLineF}): a - * "Test Case Generation" {@link MenuButton} carrying the two menu items of the Swing extension - * menu (incl. the SHORTCUT+T accelerator that replaces the Swing - * {@code KeyStrokeSettings.defineDefault} default) and the two plain action buttons of the Swing - * toolbar. The long-running generation and the counterexample search run off the FX thread in - * {@link TestGenerationTaskF} / {@link CounterExampleTaskF} and report into the - * {@link TestGenResultsDialogF} / {@link CounterExampleResultsDialogF}. + * The FX port implements the {@link KeYGuiExtensionF} SPI through three capabilities: the + * settings entry ({@link KeYGuiExtensionF.SettingsF} → {@link TestgenSettingsProviderF}, a + * reflective {@code SettingsProviderF} proxy), the main-menu entry + * ({@link KeYGuiExtensionF.MainMenuF} → the "Test Case Generation" menu with the two items of the + * Swing extension menu, the generate item carrying the SHORTCUT+T accelerator that replaces the + * Swing {@code KeyStrokeSettings.defineDefault} default) and the status line + * ({@link KeYGuiExtensionF.StatusLineF}): the same two items inside a {@link MenuButton} plus the + * two plain action buttons of the Swing toolbar. The long-running generation and the + * counterexample search run off the FX thread in {@link TestGenerationTaskF} / + * {@link CounterExampleTaskF} and report into the {@link TestGenResultsDialogF} / + * {@link CounterExampleResultsDialogF}. *

        - * KNOWN-SIMPLIFIED (capability reduction): the Swing extension implements - * {@code MainMenu}, {@code Toolbar}, {@code KeyboardShortcuts} and {@code Startup}; the FX SPI - * mirrors those as {@link KeYGuiExtensionF.MainMenuF}, {@code ToolbarF} and {@code StartupF}, - * whose method signatures reference {@code MainWindowF}/{@code KeYMediatorF}. Any use of those - * types in this module forces javac to complete the {@code MainWindowF} class file, which fails - * ("Cannot attach type annotations ... to MainWindowF.lastEnvironment: class file for - * de.uka.ilkd.key.control.KeYEnvironment not found" — key.core is not on the frozen compile - * classpath); the menu and toolbar slots are therefore merged into a single status-line slot, and - * every window reference is a plain {@code Object} resolved through {@link TestgenReflectionF}. + * KNOWN-SIMPLIFIED (capability reduction): the Swing extension also implements + * {@code Toolbar} and {@code Startup}, mirrored by the FX SPI as {@code ToolbarF} and + * {@code StartupF}. The two toolbar actions are expressed through the two plain buttons of the + * status-line slot (a real {@code ToolbarF} layout is not needed for two buttons, and plain + * {@code Button}s stay constructible without the FX toolkit, which keeps the headless unit tests + * toolkit-free), and the {@code StartupF} init hook is not implemented — the window is captured + * lazily through the settings panel instead (next block). The menu slot is implemented in full: + * unlike the initial revision, which could not name {@code MainWindowF}/{@code KeYMediatorF} in + * SPI signatures (javac failed completing {@code MainWindowF} while {@code key.core} was missing + * from the compile classpath), this module now declares {@code :key.core} like the other keyext + * FX ports; every runtime window reference remains a plain {@code Object} resolved through + * {@link TestgenReflectionF}. *

        * KNOWN-SIMPLIFIED (lazy window/mediator capture): {@link KeYGuiExtensionF.StatusLineF} - * does not receive the main window, and the {@code StartupF} init hook that the built-in - * status-line extensions use to bind the mediator is compile-impossible in this module (see - * above). The port therefore captures the window through the one SPI surface that does pass it — - * the settings panel ({@link TestgenSettingsProviderF}, whose {@code getPanel/apply} forward the - * window to {@link #captureWindow(Object)}) — and wires the enablement listeners on that first - * capture. Until then the buttons stay clickable and validate on click (the run dialogs log the - * "open the settings dialog once" hint instead of crashing), which mirrors the Swing actions' - * on-click guards. + * does not receive the main window, and no {@code StartupF} hook is implemented, so the port + * captures the window through the one SPI surface that does pass it — the settings panel + * ({@link TestgenSettingsProviderF}, whose {@code getPanel/apply} forward the window to + * {@link #captureWindow(Object)}) — and wires the enablement listeners on that first capture. + * Until then the buttons stay clickable and validate on click (the run dialogs log the "open the + * settings dialog once" hint instead of crashing), which mirrors the Swing actions' on-click + * guards. */ @KeYGuiExtensionF.Info(name = "Test case generation", experimental = false, description = "key.testgen (JavaFX port): generate JUnit test cases from the current proof, " + "or search for a counterexample, using the Z3 CE solver.") @NullMarked public final class TestgenExtensionF - implements KeYGuiExtensionF, KeYGuiExtensionF.SettingsF, KeYGuiExtensionF.StatusLineF { + implements KeYGuiExtensionF, KeYGuiExtensionF.SettingsF, KeYGuiExtensionF.StatusLineF, + KeYGuiExtensionF.MainMenuF { private static final Logger LOGGER = LoggerFactory.getLogger(TestgenExtensionF.class); @@ -104,6 +114,20 @@ public final class TestgenExtensionF private @Nullable Button generateButton; private @Nullable Button counterExampleButton; + /** + * every created "Generate Testcases..." item (status MenuButton + main menu), for enablement. + */ + private final List generateItems = new ArrayList<>(); + + /** + * every created "Search for Counterexample" item (status MenuButton + main menu), for + * enablement. + */ + private final List counterExampleItems = new ArrayList<>(); + + /** main-menu: the "Test Case Generation" menu ({@link KeYGuiExtensionF.MainMenuF}). */ + private @Nullable Menu menu; + /** * the main window, captured lazily through the settings panel (see the class comment for the * {@code KNOWN-SIMPLIFIED} reasoning): {@code null} until the user opens the settings dialog. @@ -127,30 +151,22 @@ public SettingsProviderF getSettings() { @Override public List getStatusLineControls() { // extension: MP9.5 — Swing TestgenExtension.java:43-61: the two actions live in the - // main menu (grouped into the "Extensions" menu by the Swing facade) and in a JToolBar. - // The FX SPI contributes whole menus (KeYGuiExtensionF.MainMenuF) or toolbar controls - // (ToolbarF); both slots are compile-impossible here (see the class comment), so both - // contributions are merged into one status-line slot: the "Test Case Generation" - // MenuButton carries the two menu items, the two plain buttons mirror the toolbar. + // main menu ("Test Case Generation", see getMenus) and in the JToolBar. The FX port + // expresses the Swing toolbar through this status-line slot (KNOWN-SIMPLIFIED, class + // comment): the "Test Case Generation" MenuButton re-exposes the two menu items for + // quick access, the two plain buttons mirror the toolbar. if (menuButton == null) { MenuButton anchor = new MenuButton(MENU_LABEL); - MenuItem generateItem = menuItem(NAME_GENERATE, () -> generateTestcases(anchor)); - // The Swing original registers the default shortcut on the TestGenMacro - // (TestgenExtension.java:51-52 via KeyStrokeSettings.defineDefault). The FX SPI has - // no keyboard-shortcut capability, so the accelerator is attached to the menu item - // instead. - // KNOWN-SIMPLIFIED: shortcut moved from the TestGen strategy macro (term context - // menu in Swing) to the "Generate Testcases..." menu item; the FX app has no - // macro-keybinding slot yet. - generateItem.setAccelerator(KeyCombination.keyCombination("SHORTCUT+T")); - MenuItem counterExampleItem = - menuItem(NAME_COUNTEREXAMPLE, () -> searchCounterExample(anchor)); - anchor.getItems().addAll(generateItem, counterExampleItem); - - Button generate = new Button(NAME_GENERATE); + // no accelerator here — the SHORTCUT+T binding lives on the main-menu generate + // item (getMenus) so the two surfaces do not double-fire it + anchor.getItems().addAll(makeGenerateItem(anchor), makeCounterExampleItem(anchor)); + + // Swing TestGenerationAction/CounterExampleAction SMALL_ICONs (FontAwesome) + Button generate = new Button(NAME_GENERATE, new FontIcon(FontAwesomeSolid.FLASK)); generate.setTooltip(new Tooltip(TOOLTIP_GENERATE)); generate.setOnAction(e -> generateTestcases(generate)); - Button counterExample = new Button(NAME_COUNTEREXAMPLE); + Button counterExample = new Button(NAME_COUNTEREXAMPLE, + new FontIcon(FontAwesomeSolid.EXCLAMATION_TRIANGLE)); counterExample.setTooltip(new Tooltip(TOOLTIP_COUNTEREXAMPLE)); counterExample.setOnAction(e -> searchCounterExample(counterExample)); @@ -161,6 +177,30 @@ public List getStatusLineControls() { return List.of(menuButton, generateButton, counterExampleButton); } + // ------------------------------------------------------------------ MainMenuF + + @Override + public List

        getMenus(MainWindowF window, KeYMediatorF mediator) { + // extension: MP9.5 — Swing TestgenExtension.getMainMenuActions + // (TestgenExtension.java:43-51): the two actions, grouped by the Swing facade into an + // "Extensions" menu; the FX SPI contributes whole menus, so they live in a new + // "Test Case Generation" menu (the five built-in menu bars and their item sets stay + // untouched — key.fx.verify.menuparity keeps asserting 16/24/12/7/5). + if (menu == null) { + Menu m = new Menu(MENU_LABEL); + // Swing TestgenExtension.java:51-52 registers the TestGenMacro default shortcut via + // KeyStrokeSettings.defineDefault; the FX SPI has no keyboard-shortcut capability, + // so the accelerator lives on the menu item. KNOWN-SIMPLIFIED: the shortcut moves + // from the TestGen strategy macro (term context menu in Swing) to the menu item — + // the FX app has no macro-keybinding slot yet. + MenuItem generate = makeGenerateItem(null); + generate.setAccelerator(KeyCombination.keyCombination("SHORTCUT+T")); + m.getItems().addAll(generate, makeCounterExampleItem(null)); + menu = m; + } + return List.of(menu); + } + // ------------------------------------------------------------------ window capture /** @@ -190,7 +230,7 @@ void captureWindow(Object window) { // ------------------------------------------------------------------ run dialogs /** Opens the test-suite generation run dialog and starts the generation. */ - private void generateTestcases(Node source) { + private void generateTestcases(@Nullable Node source) { Object captured = window; if (captured == null) { LOGGER.warn("Testgen extension not connected to a main window yet - the run dialog " @@ -200,7 +240,7 @@ private void generateTestcases(Node source) { } /** Opens the counterexample search run dialog and starts the search. */ - private void searchCounterExample(Node source) { + private void searchCounterExample(@Nullable Node source) { Object captured = window; if (captured == null) { LOGGER.warn("Testgen extension not connected to a main window yet - the run dialog " @@ -328,18 +368,17 @@ private void refresh() { } private void setEnabled(boolean generateEnabled, boolean counterExampleEnabled) { - MenuButton anchor = menuButton; - Button generate = generateButton; - Button counterExample = counterExampleButton; - if (anchor != null) { - for (MenuItem item : anchor.getItems()) { - boolean isGenerate = NAME_GENERATE.equals(item.getText()); - item.setDisable(isGenerate ? !generateEnabled : !counterExampleEnabled); - } + for (MenuItem item : generateItems) { + item.setDisable(!generateEnabled); + } + for (MenuItem item : counterExampleItems) { + item.setDisable(!counterExampleEnabled); } + Button generate = generateButton; if (generate != null) { generate.setDisable(!generateEnabled); } + Button counterExample = counterExampleButton; if (counterExample != null) { counterExample.setDisable(!counterExampleEnabled); } @@ -347,9 +386,30 @@ private void setEnabled(boolean generateEnabled, boolean counterExampleEnabled) // ------------------------------------------------------------------ helpers - private static MenuItem menuItem(String text, Runnable action) { - MenuItem item = new MenuItem(text); - item.setOnAction(e -> action.run()); + /** + * Creates one "Generate Testcases..." item for the given surface ({@code null} source for + * the main-menu entry, whose {@link MenuItem} is not a {@link Node}), carrying the + * FontAwesome glyph of the Swing {@code TestGenerationAction} SMALL_ICON; tracked for + * enablement ({@link #setEnabled}). + */ + private MenuItem makeGenerateItem(@Nullable Node source) { + MenuItem item = new MenuItem(NAME_GENERATE); + item.setGraphic(new FontIcon(FontAwesomeSolid.FLASK)); + item.setOnAction(e -> generateTestcases(source)); + generateItems.add(item); + return item; + } + + /** + * Creates one "Search for Counterexample" item for the given surface (see + * {@link #makeGenerateItem}), carrying the FontAwesome glyph of the Swing + * {@code CounterExampleAction} SMALL_ICON; tracked for enablement. + */ + private MenuItem makeCounterExampleItem(@Nullable Node source) { + MenuItem item = new MenuItem(NAME_COUNTEREXAMPLE); + item.setGraphic(new FontIcon(FontAwesomeSolid.EXCLAMATION_TRIANGLE)); + item.setOnAction(e -> searchCounterExample(source)); + counterExampleItems.add(item); return item; } } diff --git a/keyext.ui.testgen.fx/src/test/java/de/uka/ilkd/key/gui/testgen/fx/TestgenExtensionFTest.java b/keyext.ui.testgen.fx/src/test/java/de/uka/ilkd/key/gui/testgen/fx/TestgenExtensionFTest.java index 6c39046c6ae..a8ff6bf8bbe 100644 --- a/keyext.ui.testgen.fx/src/test/java/de/uka/ilkd/key/gui/testgen/fx/TestgenExtensionFTest.java +++ b/keyext.ui.testgen.fx/src/test/java/de/uka/ilkd/key/gui/testgen/fx/TestgenExtensionFTest.java @@ -16,19 +16,19 @@ /** * Headless unit test of the {@link TestgenExtensionF} provider (MP9.5): the {@code @Info} - * annotation, the compilable SPI capability surface ({@link KeYGuiExtensionF.SettingsF} + - * {@link KeYGuiExtensionF.StatusLineF}) and the reflection-dispatched settings entry (a - * {@code SettingsProviderF} proxy whose {@code getPanel/apply} forward the main window to the - * extension). + * annotation, the compilable SPI capability surface ({@link KeYGuiExtensionF.MainMenuF} + + * {@link KeYGuiExtensionF.SettingsF} + {@link KeYGuiExtensionF.StatusLineF}) and the + * reflection-dispatched settings entry (a {@code SettingsProviderF} proxy whose + * {@code getPanel/apply} forward the main window to the extension). *

        * KNOWN-SIMPLIFIED (headless): every {@code javafx.scene.control.Control} triggers the FX - * toolkit in its class initializer ("Toolkit not initialized"), and this module's frozen + * toolkit in its class initializer ("Toolkit not initialized"), and this module's * {@code build.gradle} exposes no headless toolkit to the tests — so the status-line {@code - * MenuButton}/{@code Button}s and the settings-panel {@code CheckBox}/spinner widgets are built - * lazily inside the running app only, and the test asserts the provider/capability surface - * instead of the control trees. Neither the provider construction nor the capability asserts - * touch the JavaFX toolkit; the run dialogs and the generation workers are deliberately not - * exercised here. + * MenuButton}/{@code Button}s, the "Test Case Generation" menu and the settings-panel {@code + * CheckBox}/spinner widgets are built lazily inside the running app only, and the test asserts + * the provider/capability surface instead of the control trees. Neither the provider construction + * nor the capability asserts touch the JavaFX toolkit; the run dialogs and the generation workers + * are deliberately not exercised here. */ class TestgenExtensionFTest { @@ -49,14 +49,16 @@ void providerInfoAnnotation() { @Test void capabilitySurface() { KeYGuiExtensionF extension = new TestgenExtensionF(); - // KNOWN-SIMPLIFIED: the MainMenuF/ToolbarF/StartupF slots are compile-impossible in this - // module (their signatures reference MainWindowF/KeYMediatorF, see TestgenExtensionF), - // so the Swing menu + toolbar contributions are merged into the StatusLineF slot; the - // control trees are built lazily in the app (they need the FX toolkit). + // KNOWN-SIMPLIFIED: the ToolbarF/StartupF slots are not implemented (see + // TestgenExtensionF) — the Swing toolbar actions are expressed through the two plain + // status-line buttons; the control trees are built lazily in the app (they need the FX + // toolkit). + assertInstanceOf(KeYGuiExtensionF.MainMenuF.class, extension, + "Test Generation menu with the two Swing menu actions + SHORTCUT+T"); assertInstanceOf(KeYGuiExtensionF.SettingsF.class, extension, "TestgenOptionsPanel port"); assertInstanceOf(KeYGuiExtensionF.StatusLineF.class, extension, - "menu + toolbar actions merged into the status line"); + "MenuButton + toolbar buttons in the status line"); } @Test From 3ccb7ca9b02e279e0b926521a555558b6aafd7eb Mon Sep 17 00:00:00 2001 From: opencode Date: Sat, 10 Oct 2026 01:02:28 +0200 Subject: [PATCH 126/281] =?UTF-8?q?MainWindowF:=20drawerlayout=20self=20te?= =?UTF-8?q?st=20=E2=80=94=20expect=205=20+=20extension=20left-panel=20tabs?= =?UTF-8?q?=20on=20the=20west=20drawer=20(MP9.1-9.6=20add=20exploration=20?= =?UTF-8?q?+=20slicing=20tabs;=20the=20hard-coded=205=20made=20transferRou?= =?UTF-8?q?ndTrip=20fail)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../de/uka/ilkd/key/gui/fx/MainWindowF.java | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) 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 index c5cd0761717..b5708f3baa5 100644 --- 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 @@ -2016,11 +2016,13 @@ private void runExtensionVerification(KeYEnvironment Date: Sat, 10 Oct 2026 02:01:54 +0200 Subject: [PATCH 127/281] =?UTF-8?q?key.ui.fx:=20M6=20parity=20close-out=20?= =?UTF-8?q?=E2=80=94=20module=20READMEs,=20consolidated=20KNOWN-SIMPLIFIED?= =?UTF-8?q?=20ledger,=20parity=20sign-off,=20CI=20wiring?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Docs: key.ui.fx/README.md (run, key.fx.verify.* flags incl. the JAVA_TOOL_OPTIONS-harness set, architecture, SPI, regression practice), key.ui.fx/KNOWN-SIMPLIFIED.md (consolidated ledger of all 64 KNOWN-SIMPLIFIED markers across the seven FX modules with file:line + status), key.ui.fx/PARITY-SIGNOFF.md (2026-10-08 ~490-behavior audit re-stated against the delivered app, grounded in the verify hooks; carries forward the still-open list), and one README per keyext.*.fx module (capabilities, verification, tests, ledger pointer). CI: add the 7 FX modules (key.ui.fx + six keyext ports) to the tests.yml unit-tests matrix. Bare ./gradlew test descends into subprojects, so the weekly broad-test job also covers them. Headless fix: the isabelletranslation unit test used to start the FX toolkit in @BeforeAll (settingsContribution instantiates SettingsPanelF -> Control.), failing on display-less CI runners. Split into a fully toolkit-free IsabelleTranslationExtensionFTest (3 tests, plain MenuItem context items) and an IsabelleSettingsProviderFTest that starts the toolkit and skips via JUnit assumptions when no display is available (verified: DISPLAY unset -> 3 pass + 1 skip, DISPLAY=:99 -> 4/4; full 7-module gate green headless). --- .github/workflows/tests.yml | 7 +- key.ui.fx/KNOWN-SIMPLIFIED.md | 134 +++++++++++++++++ key.ui.fx/PARITY-SIGNOFF.md | 110 ++++++++++++++ key.ui.fx/README.md | 138 ++++++++++++++++++ keyext.caching.fx/README.md | 38 +++++ keyext.exploration.fx/README.md | 35 +++++ keyext.isabelletranslation.fx/README.md | 35 +++++ .../fx/IsabelleSettingsProviderFTest.java | 55 +++++++ .../fx/IsabelleTranslationExtensionFTest.java | 42 ++---- keyext.proofmanagement.fx/README.md | 32 ++++ keyext.slicing.fx/README.md | 35 +++++ keyext.ui.testgen.fx/README.md | 38 +++++ 12 files changed, 665 insertions(+), 34 deletions(-) create mode 100644 key.ui.fx/KNOWN-SIMPLIFIED.md create mode 100644 key.ui.fx/PARITY-SIGNOFF.md create mode 100644 key.ui.fx/README.md create mode 100644 keyext.caching.fx/README.md create mode 100644 keyext.exploration.fx/README.md create mode 100644 keyext.isabelletranslation.fx/README.md create mode 100644 keyext.isabelletranslation.fx/src/test/java/org/key_project/isabelletranslation/fx/IsabelleSettingsProviderFTest.java create mode 100644 keyext.proofmanagement.fx/README.md create mode 100644 keyext.slicing.fx/README.md create mode 100644 keyext.ui.testgen.fx/README.md diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index afe4cef54c5..3cfe6dc5fcb 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -29,7 +29,12 @@ jobs: keyext.exploration, keyext.slicing, key.ncore, key.ui, key.core, key.core.testgen, keyext.isabelletranslation, keyext.ui.testgen, key.ncore.calculus, key.util, key.core.example, keyext.caching, key.core.wd, key.core.infflow, - keyext.proofmanagement] + keyext.proofmanagement, + # JavaFX UI module and its six keyext ports (MP9.x): the unit tests are toolkit-free + # and headless-safe (the one panel test that needs the FX toolkit skips on + # display-less runners), see the per-module READMEs. + key.ui.fx, keyext.caching.fx, keyext.exploration.fx, keyext.isabelletranslation.fx, + keyext.slicing.fx, keyext.ui.testgen.fx, keyext.proofmanagement.fx] continue-on-error: true runs-on: ${{ matrix.os }} env: diff --git a/key.ui.fx/KNOWN-SIMPLIFIED.md b/key.ui.fx/KNOWN-SIMPLIFIED.md new file mode 100644 index 00000000000..baa8b2b149a --- /dev/null +++ b/key.ui.fx/KNOWN-SIMPLIFIED.md @@ -0,0 +1,134 @@ +# Consolidated ledger of KNOWN-SIMPLIFIED deviations + +Every deliberate deviation of the JavaFX UI from its Swing original is marked at the source site +with a `KNOWN-SIMPLIFIED` comment or `KNOWN-SIMPLIFIED:` javadoc block. This file is the +single consolidated ledger: one row per marker, for `key.ui.fx` and all six keyext ports +(MP9.1–9.6). Row references are `Class.java:line` and point at the marker itself, which carries +the full sentence. + +Marker counts per module: + +| Module | Markers | +|---|---| +| `key.ui.fx` (contrib extensions) | 3 | +| `keyext.caching.fx` | 10 | +| `keyext.exploration.fx` | 9 | +| `keyext.isabelletranslation.fx` | 4 | +| `keyext.slicing.fx` | 16 | +| `keyext.ui.testgen.fx` | 18 | +| `keyext.proofmanagement.fx` | 4 | +| **Total** | **64** | + +To re-derive this table at any point: `grep -rn 'KNOWN-SIMPLIFIED' */src/main */src/test`. + +The status column is for the close-out tracking: **OPEN** items are candidates for a later +milestone, **FIXED-UPSTREAM** items differ from a Swing bug that the port deliberately corrects, +**WONT-REPLICATE** items depend on Swing-only machinery with no FX counterpart. + +--- + +## `key.ui.fx` — built-in contrib extensions (3) + +| Site | Swing original | FX port | Status | +|---|---|---|---| +| `extension/contrib/HeatmapF.java:28` | `HeatmapExt` renders the heat highlight into the proof tree / sequent | menu + toolbar toggle + persisted `ViewSettings` options only; proof-tree heat overlay deferred | OPEN | +| `extension/contrib/HeatmapF.java:37` | — (doubles the `@Info` description) | same decision restated for the extension dialog | OPEN | +| `extension/contrib/ParallelProverStatusIndicatorF.java:27` | toggle button "SC" / "MT N×" with left-click toggle and right-click worker-count menu | plain `Label` showing the live auto-mode state ("Auto"/"Manual"), left-click toggles `PARALLEL_PROVER_ENABLED`, reacts to property changes; worker-count picker and button styling out of scope | OPEN | + +## `keyext.caching.fx` (10) + +| Site | Swing original | FX port | Status | +|---|---|---|---| +| `CachingExtensionF.java:95` | `CachingSettingsProvider.getCachingSettings()` on the keyext compile path | the FX module owns the `ProofCachingSettings` singleton via `CachingSettingsProviderF.getCachingSettings()` (Swing provider not loadable on the FX compile path) | FIXED-UPSTREAM | +| `CachingExtensionF.java:129` | menu checkbox instantiated eagerly | created lazily on first use (constructing controls needs the FX toolkit; the SPI may instantiate the provider headless) | — | +| `CachingExtensionF.java:735` | opens the interactive `ReferenceSearchDialog` (per-goal table + "Apply") | read-only summary `Alert`; copying the referenced steps stays available through the "Copy referenced proof steps here" context item | OPEN | +| `CachingExtensionF.java:778` | feeds `mediator.getCurrentlyOpenedProofs()` to the reference search | `KeYMediatorF` has no such accessor — the tracked proofs are used instead | OPEN | +| `CachingPruneHandlerF.java:26` | iterates `mediator.getCurrentlyOpenedProofs()` | iterates the proofs tracked by the owning extension (every proof selected at least once) | — | +| `CachingSettingsProviderF.java:37` | keyext registers the caching settings into `ProofIndependentSettings` | the FX module owns and registers the same singleton itself (shared persisted object with the Swing keyext via the registry) | — | +| `CachingSettingsProviderF.java:109` | writes `strategySearch.isEnabled()` (latent Swing bug — `JCheckBox#isEnabled` always true, persisted `true`) | writes the checkbox *selection* — the behaviour the panel visibly offers | FIXED-UPSTREAM | +| `CopyBeforeDisposeF.java:25` | wraps the copy into `mediator.initiateAutoMode(...)`/`finishAutoMode(...)` | `KeYMediatorF` has no auto-mode context API for a foreign proof — the core `CopyReferenceResolver` is invoked directly | OPEN | +| `CachingExtensionFTest.java:27` | — (test design) | unit tests are toolkit-free; control-level SPI contracts are verified by the in-app `key.fx.verify.extensions` hook | — | +| `CachingExtensionFTest.java:69` | — (test design) | asserts the FX-module-owned singleton identity and registry idempotence | — | + +## `keyext.exploration.fx` (9) + +| Site | Swing original | FX port | Status | +|---|---|---|---| +| `ExplorationExtensionF.java:54` | registers `ExplorationModeModel` in the mediator registry and an `ExplorationRenderer` (purple border) into the proof tree | `KeYMediatorF` has no register/lookup seam and `ProofTreeViewF` no renderer-styling hook — both dropped; the model is held directly by the provider | OPEN | +| `ExplorationExtensionF.java:76` | — (extension description) | proof-tree styling and the hidden second-branch filter are deferred (no FX seam) | OPEN | +| `ExplorationExtensionF.java:280` | `mediator.register(model, ExplorationModeModel.class)` | provider holds the singleton model directly | OPEN | +| `ExplorationExtensionF.java:363` | "Hide justification" routes through the proof-tree filter `HIDE_INTERACTIVE_GOALS` | persists the same `ViewSettings#setHideInteractiveGoals` flag and records the exploration-app state; the tree filter itself is deferred | OPEN | +| `ExplorationSequentMenuF.java:48` | `promptForTerm` modal retry-loop until the term is well-typed | single-shot `TextInputDialog`; malformed input or a sort mismatch aborts the action | OPEN | +| `ExplorationSequentMenuF.java:173` | sort-mismatch dialog, then re-opens the input dialog | single-shot; error alert and abort | OPEN | +| `ExplorationSequentMenuF.java:182` | malformed input re-prompts | single-shot abort | OPEN | +| `ExplorationStepsPanelF.java:56` | `Icons` (AWT/Swing) tab help button; icon images | help button not ported; status indicator uses ikonli FontAwesome solid/regular compass; pruning via `MainWindowF.getUserInterfaceControl().getProofControl()` | — | +| `ExplorationStepsPanelF.java:326` | prunes via `mediator.getUI().getProofControl()` | FX counterpart via the window's user-interface control; headless runs no-op | — | + +## `keyext.isabelletranslation.fx` (4) + +| Site | Swing original | FX port | Status | +|---|---|---|---| +| `IsabelleTranslationExtensionF.java:37` | `IsabelleTranslationAction.solveGoals` hands the generated theory to the external Isabelle solver (`IsabelleLauncher`) with a progress/model dialog | runs only the sequent translation (`IsabelleTranslator.translateProblem`, shared with Swing) and shows the theory in a plain dialog; solver launch and progress window omitted, the heavy Scala/Isabelle backend is never loaded | OPEN | +| `IsabelleTranslationExtensionF.java:48` | — (extension description) | same decision restated | OPEN | +| `IsabelleTranslationRunnerF.java:28` | solver run + progress/model dialog | translation only, on a background thread; avoids loading the scala-isabelle backend | OPEN | +| `IsabelleSettingsProviderF.java:191` | reads the keyext configuration constants | the constants (`isabellePathKey`/`translationPathKey`/`timeoutKey`) are package-protected in keyext; their stable string values are mirrored here | — | + +## `keyext.slicing.fx` (16) + +| Site | Swing original | FX port | Status | +|---|---|---|---| +| `SlicingExtensionF.java:49` | registers a proof-load listener through the Swing mediator | `KeYMediatorF` has no such registry; the `KeYSelectionListener` covers both "new proof loaded" and "selected proof switched" | — | +| `SlicingExtensionF.java:249` | "Show dependency graph around this formula" renders the DOT excerpt through `PreviewDialog`/graphviz | shows the DOT source in a text dialog (the graphviz image renderer is Swing-only) | OPEN | +| `SlicingSettingsProviderF.java:29` | `SlicingSettings.setAggressiveDeduplicate` callable (same package) | setter is package-private in `org.key_project.slicing`; the toggle reflects the current setting but does not persist changes; the other two options persist normally | OPEN | +| `SlicingSettingsProviderF.java:144` | writes the aggressive-deduplicate flag | read-only (same package-private setter constraint; module may not modify keyext.slicing) | OPEN | +| `SlicingLeftPanelF.java:59` | panel summary: graphviz rendering, iterative fixed point, HTML statistics/timings | DOT-as-text, single slicing iteration, plain-text statistics, plain-label timings (details on the individual rows) | OPEN | +| `SlicingLeftPanelF.java:222` | "Slice proof to fixed point" opens the iterative `SliceToFixedPointDialog` | performs a single slicing iteration (identical to "Slice proof") | OPEN | +| `SlicingLeftPanelF.java:228` | — (tooltip for the fixed-point button) | restates the single-iteration decision | OPEN | +| `SlicingLeftPanelF.java:270` | file chooser with parent component | owner-less `FileChooser` (the drawer panel has no dedicated window) | — | +| `SlicingLeftPanelF.java:295` | rule statistics as HTML table with four sort buttons | plain-text rows with the default "total applications, descending" order | OPEN | +| `SlicingLeftPanelF.java:322` | renders the DOT to a PNG via the Swing graphviz executor and shows an image dialog | shows the DOT source text | OPEN | +| `SlicingLeftPanelF.java:377` | passes a headless `DefaultUserInterfaceControl` to the slicer | uses the FX window's own user-interface control (`ProblemLoaderControl`) so the loader callbacks stay FX-safe | — | +| `SlicingLeftPanelF.java:415` | loads the slice through the problem loader directly (keeps it out of the recent-files list) | takes the public load pipeline of the main window (`openProofFile`), which registers the file in the recent-files list | OPEN | +| `SlicingLeftPanelF.java:464` | execution timings as HTML table via `HtmlFactory` | one "Algorithm: time" line per measured activity | OPEN | +| `SlicingLeftPanelF.java:541` | panel grey-out via the Swing-only `SingleCoreFeatureGate` | checks the parallel-prover setting directly and disables the panel recursively | — | +| `SlicingLeftPanelF.java:618` | `HtmlDialog`/`PreviewDialog` | read-only multi-line text dialog stand-in | OPEN | +| `SlicingExtensionFTest.java:27` | — (test design) | unit tests are toolkit-free; the provider's JavaFX widget contributions are verified in-app | — | + +## `keyext.ui.testgen.fx` (18) + +| Site | Swing original | FX port | Status | +|---|---|---|---| +| `TestgenExtensionF.java:59` | implements `Toolbar` and `Startup` in addition to the menu | `ToolbarF`/`StartupF` not implemented: the two toolbar actions become the two plain buttons of the status-line slot (no `ToolbarF` layout needed for two buttons; plain `Button`s stay constructible headless); the window is captured lazily through the settings panel instead of a `StartupF` hook; the menu slot is implemented in full | OPEN | +| `TestgenExtensionF.java:72` | window arrives via `Startup`/toolbar | `StatusLineF` does not receive the window and no `StartupF` hook exists, so the port captures it through the settings panel (the one SPI surface passing it) and wires enablement listeners on first capture; until then the buttons validate on click | OPEN | +| `TestgenExtensionF.java:133` | — (field doc) | the window field stays a plain `Object` (`MainWindowF` is not nameable in this module) and is `null` until the user opens the settings dialog | — | +| `TestgenExtensionF.java:155` | toolbar in the JToolBar | expressed through the status-line slot: a "Test Case Generation" `MenuButton` re-exposing the two menu items plus the two plain buttons | — | +| `TestgenExtensionF.java:193` | `SHORTCUT+T` bound via `KeyStrokeSettings` on the TestGen strategy macro (term context menu) | the accelerator lives on the main-menu item (double-fire with the status `MenuButton` would occur otherwise; the FX API has no macro-keybinding slot yet) | OPEN | +| `TestgenExtensionF.java:344` | enabled-state updates via the Swing action infrastructure | without a captured window the controls stay enabled and the run dialogs validate on click | OPEN | +| `TestgenSettingsProviderF.java:40` | settings provider with a named `MainWindowF` | `SettingsProviderF.getPanel/apply` declare `MainWindowF`, and naming it forces javac to complete `KeYEnvironment`'s class file, which fails on this module's classpath — the provider is a `java.lang.reflect.Proxy` that forwards the window as plain `Object` | — | +| `TestgenSettingsProviderF.java:50` | panel subclasses the Swing `SettingsPanel` base (row/validator/chooser helpers, working-copy change listeners) | plain `GridPane` of label/input rows (host wraps it in a `ScrollPane`) | — | +| `TestgenReflectionF.java:160` | `TGWorker` wraps the run in the mediator's auto-mode machinery with an immediate `StopRequest`/`SolverLauncher` stop | the facade route has no launcher handle — stopping interrupts the worker thread, which the generation macros honour between phases (the final Z3 CE launch runs until it returns) | OPEN | +| `TestgenReflectionF.java:220` | `SolverListener` opens a modal progress dialog and a results dialog showing the counterexample model | prints the SMT statistics (solved/invalid/unknown path conditions) and the found/not-found outcome into the run dialog's log; model inspection is left to the generated test data | OPEN | +| `TestGenerationTaskF.java:19` | worker puts the original proof into auto-mode machinery and stops cooperatively | `requestStop()` interrupts the worker thread (macros honour it between phases) | OPEN | +| `TestGenResultsDialogF.java:43` | dialog embeds a live `TestgenOptionsPanel` on its eastern edge | options stay exclusively in the Settings dialog (the global settings are re-read at run start); "Close" is disabled during the run like Swing; the generated-files list is a recursive listing of the output folder (Swing only logs "Writing test file") | OPEN | +| `TestGenResultsDialogF.java:154` | window available from the start | window may be `null` until the settings dialog was opened — the run is validated on click and starts with a hint otherwise | OPEN | +| `CounterExampleTaskF.java:21` | `SolverListener` modal progress + results dialogs with the counterexample model; auto-mode shell | prints statistics + outcome into the run log; auto-mode shell dropped | OPEN | +| `CounterExampleResultsDialogF.java:35` | counterexample model shown in modal result dialogs | only SMT statistics + found/not-found into the log; the dialog starts with a hint when the extension is not yet connected | OPEN | +| `CounterExampleResultsDialogF.java:131` | window available from the start | validated on click (frozen SPI passes the window only to the settings panel) | OPEN | +| `TestgenExtensionFTest.java:24` | — (test design) | unit tests are toolkit-free; control trees are asserted in-app only | — | +| `TestgenExtensionFTest.java:52` | — (test design) | documents that `ToolbarF`/`StartupF` are not implemented | — | + +## `keyext.proofmanagement.fx` (4) + +| Site | Swing original | FX port | Status | +|---|---|---|---| +| `CheckConfigDialogF.java:45` | `BlockingGlassPane` blocks input while the check runs (Stop-only pass-through) | explicitly disables the input controls while the check runs; "Run checkers" becomes "Stop" (the only enabled control); cancelling may leave partial output like the original | — | +| `CheckConfigDialogF.java:50` | bundle/report choosers via AWT `KeYFileChooser` with `.zproof`/`.html` filters | `FileChooser` with equivalent extension filters; file-only selection matches the Swing dialog | — | +| `CheckConfigDialogF.java:55` | opens the generated report via `Desktop.getDesktop().open` | opens it through the `HelpFacadeF` browser seam (`openExternal`, host services with a desktop fallback) — no AWT/Swing | — | +| `CheckConfigDialogF.java:212` | in-dialog browser/help | `HelpFacadeF` browser seam — see the class javadoc | — | + +--- + +**What this ledger does not track:** ordinary porting work that is *faithful* to the Swing +original (the vast majority of the ~490 audited behaviors) and the deliberate feature additions +of the FX port (e.g. working search, editable dark theme column, extension status indicators). +Those are covered by `PARITY-SIGNOFF.md` and the per-module READMEs. diff --git a/key.ui.fx/PARITY-SIGNOFF.md b/key.ui.fx/PARITY-SIGNOFF.md new file mode 100644 index 00000000000..19a82dfa33e --- /dev/null +++ b/key.ui.fx/PARITY-SIGNOFF.md @@ -0,0 +1,110 @@ +# Parity sign-off — Swing→JavaFX audit (2026-10-08) vs. delivered application + +This document signs off the parity audit that was carried out on 2026-10-08 against the +in-development JavaFX UI. The audit inventoried ~490 behaviors across nine components +(report index: `/tmp/opencode/parity-*.md`, synthesis in the appended table). + +**Warning:** the audit predates milestones MP7–MP10 and the MP9.x keyext ports, so a large share +of the "MISSING" entries have since been delivered. This sign-off re-states each gap against the +delivered application and the `key.fx.verify.*` regression hooks, and carries forward everything +still open into the close-out list at the end. + +Evidence used: the MP9 combined regression run (16/16 PASS), menuparity 5/5, termmenu 25, +tacletmatch 9/9, uicontrol 4/4, the six MP9.x module ports, and the consolidated +[KNOWN-SIMPLIFIED ledger](KNOWN-SIMPLIFIED.md). Items without an app-level regression hook are +marked `OPEN (unverified)` rather than assumed fixed. + +## Component verdicts + +| Component (audit report) | Behaviors | Audit verdict | Resolution since | Current status | +|---|---|---|---|---| +| mainwindow | 62 | 13 PORTED / 24 PARTIAL / **17 MISSING** / 6 KNOWN-DEF | window shell, menu/toolbar/status parity, docking, drawers (MP1–MP10) | mostly resolved; open items below | +| prooftree | 53 | 20 / 8 / **13 MISSING** / 5 KNOWN-DEF | core tree faithful; heatmap etc. deferred | heatmap/NodeInfoVisualizer/branch editing still OPEN | +| sequent | 45 | 14 / 10 / **15 MISSING** / 4 KNOWN-DEF | term context menu + taclet application + search (MP8) | context menu RESOLVED; remaining items below | +| interactive-completion | 25 | 0 / 0 / **20 MISSING** | `WindowUserInterfaceControlF` seam + tacletmatch + join/merge | RESOLVED (was the top P0; evidence uicontrol + tacletmatch 9/9) | +| goallist-strategy-info | 37 | goal list at parity; strategy preset UI missing; info view re-designed | — | preset combo + stats staleness OPEN | +| source-search-filechooser | 74 | 30 / 5 / **28 MISSING** | search bars + loading-options dialog ported | source-view interaction (symbex highlights, cross-highlight) OPEN | +| menus-actions | 57 leaves | ~25 MISSING | menu parity 5/5 (File 16 / Proof 24 / Options 12 / View 7 / About 5) plus automation submenu | RESOLVED at surface level; a few behaviours below | +| settings-config | 117 | 83 / 8 / 11 MISSING / 13 KNOWN-DEF | settings framework + providers + colors mechanics + search | SMT run-UI, theme persistence, color defs OPEN | +| unported-dialogs | 18 | 13 MISSING / 2 PARTIAL | proofmgmt, tacletmatch, IssueDialog, notification, Loaded Proofs | RESOLVED where noted below; join/mergerule/lemmatagenerator/etc. track their MP9.x modules | + +## P0 sign-off (the audit's "prover workflow critical" list) + +| Audit item | Delivered in | Evidence | Status | +|---|---|---|---| +| `UserInterfaceControlF` seam (status/progress/task/exception callbacks, `RuleCompletionHandler`) | `WindowUserInterfaceControlF` + diamond issue/LogView/AutoDismiss | `key.fx.verify.uicontrol` 4/4 (`wasShown=true` for the exception dialog) | **RESOLVED** | +| Sequent term context menu (`CurrentGoalViewMenu` ~900 LOC) | `SequentTermContextMenuF` + `SequentMenuModelF` | `key.fx.verify.termmenu` 25 checks; interactive rule apps in-app | **RESOLVED** | +| TacletMatch suite (gates interactive rule application) | `TacletMatchDialogF` + panels | `key.fx.verify.tacletmatch` 9/9 | **RESOLVED** | +| Abandon Proof | Proof menu (24 items) | `key.fx.verify.menuparity` Proof 24 | **RESOLVED** | +| ProofManagementDialog + TaskTree / Loaded Proofs dockable | `keyext.proofmanagement.fx` (MP9.6) + Loaded Proofs view | `key.fx.verify.proofmgmt` 1/2 | **RESOLVED** | +| IssueDialog / error reporting | `IssueDialogF` (toasts + dialog) | `key.fx.verify.uicontrol` (exception leg) | **RESOLVED** | +| Goal Back / Prune disable bindings; input freeze during auto mode | enablement listeners exist; **input freeze still open** | — | **PARTIAL / OPEN** | +| Apply Strategy on node + Prune-at-node in the tree popup | right-click macro menu ported (`key.fx.verify.rightclickmacro`); Apply Strategy-at-node unverified | — | **PARTIAL** | + +P0 bugs from the audit: + +| Bug | Status | +|---|---| +| FX search stale on node switch | **RESOLVED** (search re-runs on reprint; sequent-leftovers milestone `18df960b35`) | +| Proof-tree context menu acts on the selection, not the clicked node | `OPEN (unverified)` | +| View-menu Pretty Print / Unicode inert placeholders | `OPEN` (placeholders remain) | +| Recent-file clicks drop stored profile / single-java options | `OPEN` (loading-options dialog itself is ported — see below) | +| Shortcut-default mismatches (`KeyStrokeManagerF`: tree search, sequent search, macro defaults) | `OPEN (unverified)` | +| Dead registered bindings (Ctrl+C term copy, F3/Shift+F3, Ctrl+K/Ctrl+J, Goal Back/Prune) | `PARTIAL` (term-menu copy + menu accelerators exist; several bindings unverified) | +| Colors: 12 mapped CSS variables ineffective (47 property definitions missing) | `OPEN` | +| UPSTREAM (Swing, not FX): `LoopApplyHeadCompletion` + `LoopContract*` dead code; seed-clobber `overwriteWith` | **Not our defect** — flags for upstream `key.ui`/`key.core` cleanup | + +## P1 sign-off + +| Audit item | Status | +|---|---| +| Loading options (profile / ignore other Java files) | **RESOLVED** — load-options dialog ported (`key.fx.verify.profileloading` + `profileloadingdialog` PASS) | +| Select Goal Above / Below + menu set (View 7) | **RESOLVED** at surface level (`key.fx.verify.menuparity` View 7); Selection Back/Forward binding `OPEN (unverified)` | +| Automation submenu + macro invocation | **RESOLVED** (automation submenu MP2, macro menu in term menu); global toolbar dropdown `OPEN` | +| Join / merge dialogs | **RESOLVED** for the merge/join flow (`key.fx.verify.joinmerge` PASS on gcd 32/0); `keyext.slicing.fx` SMT routing partial | +| Strategy preset combo / stash UI | `OPEN` | +| SMT settings + run UI | `OPEN` (settings providers ported; the run UI — `ProgressDialog` table, `DropdownSelectionButton` — remains) | +| Exit flow (close-request, `confirmExit`, layout save) | `OPEN (unverified)` | +| Term labels; Pretty Print / Unicode wiring | `OPEN` (lemmaorigin hook covers labels mechanically — see `key.fx.verify.lemmaorigin`) | +| User-selection highlight + Ctrl+C copy | **RESOLVED** (term-menu copy item in the skeleton); multi-selection highlight + reprint persistence `OPEN` | +| Symbex source line highlights + sequent-hover origin cross-highlight | `OPEN` | +| KeyStroke default wiring next to the registry | `OPEN (unverified)` | + +## P2 sign-off + +Heatmap overlay (settings+toggle only, ledger `HeatmapF`), NodeInfoVisualizer, branch-label F2 +editing, info-view node-change refresh, notification framework depth, docking title +actions/layout slots/maximize (partially done — `key.fx.verify.docking` PASS; title actions +exist as `DockTitleActionF`), join+mergerule dialogs (track MP9.x modules), lemmatagenerator +(`key.fx.verify.lemmaorigin` PASS for the generator path), soundiness (`key.fx.verify.soundiness` +PASS), originlabels (lemmaorigin), plugins, profileloading (RESOLVED), help windows +(`key.fx.verify.help` PASS), theme persistence + the 47 color property definitions, feature +flags + parallel prover (`ParallelProverStatusIndicatorF` partial), settings dump, modal +grey-out, proof-disposal clearing (auto saver handles the disposal part), tab icons/titles, +file-chooser bookmarks. Each is either documented in the KNOWN-SIMPLIFIED ledger or listed above +as open. + +## Still-open work list (carried forward) + +1. Input freeze during auto mode (blocking glass pane port) — P0 remainder. +2. Proof-tree: Apply Strategy on clicked node, Prune-at-node in the popup, renderer surface + (tooltips, goal icons, linked/cache marks, notes, branch-label F2 editing), NodeInfoVisualizer. +3. Sequent: multi-select highlight + reprint persistence, drag & drop, inner-node highlights + + taclet info pane, source cross-highlight. +4. Source view: symbex line highlights, click-to-jump into the proof tree, hover + cursor/line highlight, multi-file tabs, branch status bar. +5. Menus/actions: Pretty Print / Unicode wiring, term labels, tooltip toggles, selection + Back/Forward bindings, Edit Last File, Load User Taclets + Prove submenu, Run All Proofs + (dialogs exist for several), EnableWhenProofLoaded analogue, toolbars 9 remaining buttons. +6. Keyboard: shortcut-default regressions; dead bindings audit. +7. Options dialogs: SMT run UI, strategy preset UI, HeatmapOptionsDialog. +8. Exit flow: close-request + confirmExit + layout persistence on close. +9. Colors/theme: 47 color property definitions; theme persistence. +10. Notification framework depth: proof-closed/exception dialogs beyond toasts are in + (`IssueDialogF`); the `NotificationTask`/action framework remains partial. +11. Docking: remaining title actions, layout-slot keys F10–F12 (named slots exist). +12. Upstream (Swing) flags: `LoopApplyHeadCompletion`/`LoopContract*` dead code, seed-clobber. + +The KNOWN-SIMPLIFIED ledger mirrors the module-level subset of this list with per-site +file:line markers and statuses (`OPEN` / `FIXED-UPSTREAM` / `WONT-REPLICATE`). This sign-off is +re-run after each milestone batch that closes any of the items above. diff --git a/key.ui.fx/README.md b/key.ui.fx/README.md new file mode 100644 index 00000000000..11a84fac833 --- /dev/null +++ b/key.ui.fx/README.md @@ -0,0 +1,138 @@ +# key.ui.fx — the JavaFX user interface of KeY + +`key.ui.fx` is the JavaFX re-implementation of the Swing GUI (`key.ui`) of the interactive +theorem prover KeY. It is built in pure JavaFX code: + +* **no FXML** — every view is constructed in code, +* **no AWT/Swing imports** — the module never references `java.awt` or `javax.swing`, +* icons use the [ikonli](https://kordamp.org/ikonli) FontAwesome family (no Unicode glyph + fallbacks), +* JavaFX is consumed through the Gradle `openjfx` plugin (`javafx.base`, `javafx.controls`, + `javafx.graphics`, `javafx.web`). + +The SMT-relevant and proof machinery is reused unchanged from `key.core`; only the GUI layer is +rewritten. + +## Running + +```sh +./gradlew :key.ui.fx:run # the full application (MainEx) +./gradlew :key.ui.fx:runExperimental +./gradlew :key.ui.fx:runDebug +``` + +Additional Gradle tasks come from the `application` plugin; `key.fx.*` system properties are +forwarded to the application JVM by the `run` task (whitelist) or via `JAVA_TOOL_OPTIONS` (see +below). + +JavaFX 25 silently drops machines without a graphical environment; for headless runs use a +virtual display (Xvnc/Xvfb), e.g. `DISPLAY=:99`, and `-Dprism.order=sw`. + +> **Packaging note (M0):** a JavaFX application does not assemble into a plain fat jar by +> shading bytecode — the platform-specific native runtime comes from classifier jars and must be +> bundled with jlink/jpackage. Until that milestone, run via `./gradlew :key.ui.fx:run`. + +## Headless app-verification (`key.fx.verify.*`) + +The application carries self-test hooks that run after a demo proof is loaded and print +`PASS`/`FAIL` markers on stdout with `System.exit` semantics — this is how the whole GUI is +regression-tested on a virtual display. Demo proof paths default into +`key.ui/examples/` (e.g. `firstTouch/01-Agatha/project.key` or +`standard_key/queries/useQuery.key` for `updatehighlight`). + +| Flag | What it verifies | +|---|---| +| `key.fx.demo.sequent=` | demo proof to load (most hooks require one) | +| `key.fx.demo.sequent2` / `key.fx.demo.sequent3` | second/third loaded proof (Loaded Proofs view test) | +| `key.fx.demo.proofmgmt=` | example for the proof management dialog test | +| `key.fx.show=sequent\|prooftree` | dockable focused after the demo load | +| `key.fx.theme=light\|dark` | theme override | +| `key.fx.demo.autoprove` / `key.fx.demo.autoprove.live` / `key.fx.demo.maxsteps=n` | auto-mode behaviour, live refresh, step limit | +| `key.fx.verify.sequent` | sequent text → `PosInOccurrence` position mapping | +| `key.fx.verify.tree` | proof-tree structure model | +| `key.fx.verify.goallist` | goal-list view after the demo load | +| `key.fx.verify.search` | proof-tree search self test | +| `key.fx.verify.sequentsearch=q` | sequent search with query `q` | +| `key.fx.verify.sequentsearchmodes` | search modes (Highlight / Hide / Regroup) | +| `key.fx.verify.treefilters` | tree filters after auto mode | +| `key.fx.verify.updatehighlight` | update-highlight overlay (needs `useQuery.key`) | +| `key.fx.verify.notifications` | notification framework (task-finished, proof-closed, exception) | +| `key.fx.verify.termmenu` | headless sequent context-menu model (25 checks) | +| `key.fx.verify.tacletmatch=1\|hold` | interactive taclet application (`hold` keeps the dialog open) | +| `key.fx.verify.uicontrol` | `WindowUserInterfaceControlF` seam: status line, IssueDialogF, LogViewF, AutoDismissDialogF | +| `key.fx.verify.docking` | named layout slots, maximize/restore, close-while-maximized | +| `key.fx.verify.proofmgmt=1\|2\|all` | ProofManagementDialogF / Loaded Proofs view | +| `key.fx.verify.joinmerge` (+ `key.fx.verify.joinmerge.maxsteps=n`) | join/merge dialogs on a proof with open branches | +| `key.fx.verify.lemmaorigin` | term labels, origin labels, lemma generator | +| `key.fx.verify.help` | F1 context-help resolution (no browser window) | +| `key.fx.verify.soundiness` | soundiness report dialog | +| `key.fx.verify.profileloading` | WD loading-options panel | +| `key.fx.verify.profileloadingdialog` | loading options dialog at startup | +| `key.fx.verify.javacsettings` | javac settings provider | + +Harness-injected flags (read in `MainWindowF` but not on the `run`-task whitelist; the +verification harness sets them via `JAVA_TOOL_OPTIONS`): + +| Flag | What it verifies | +|---|---| +| `key.fx.verify.extensions` | extension SPI discovery: `discovered=9` (3 built-in + 6 keyext), `statusControls=7`, `westDrawerItems=7` | +| `key.fx.verify.drawerlayout` | west drawer round trip (`west = 5 + facade left-panel tabs`) | +| `key.fx.verify.drawer` | drawer switch/move interactions | +| `key.fx.verify.menuparity` | menu parity counts (File 16 / Proof 24 / Options 12 / View 7 / About 5) | +| `key.fx.verify.rightclickmacro` | right-click macro menu | +| `key.fx.verify.minimizeinteraction` | minimize-interaction toggle | +| `key.fx.verify.autosave` | auto saver | +| `key.fx.verify.proofdiff` | proof-diff frame | + +**Regression practice (S5/MP10 mode):** run the hooks **per flag** — a combined multi-flag run +stalls at the `uicontrol` modal (the seam dialog needs an XTEST Return/Escape dismissal, see +`/tmp/opencode/xsendkey`). `uicontrol` is run separately and dismissed with the synthetic key. + +## Architecture + +* `MainWindowF` — the window shell: `top` = menu/toolbar, `center` = docking workspace wrapped + in WEST/EAST/SOUTH `DrawerF`s, `bottom` = status line. Menu parity with the Swing app is + checked by `key.fx.verify.menuparity`. +* `DockWorkspace` + `Dockable` — the docking framework with persisted layouts + (`DockLayoutStore`), named layout slots (F10–F12), maximize/restore and title actions. +* `KeYMediatorF` / `KeYSelectionModel` — selection + auto-mode event hub mirroring the Swing + mediator; `WindowUserInterfaceControlF` is the `UserInterfaceControl` seam for + status/progress/task/exception callbacks and interactive rule application. +* `nodeviews` — proof tree, sequent view (`TextFlow` + the shared `pp` printer), source view + (RichTextFX), goal list, search bars, term context menu (`SequentTermContextMenuF` + + `SequentMenuModelF`). +* `settings` — the Settings dialog framework (`SettingsManagerF`, `SettingsProviderF`) and + `Configuration` round trips. +* `colors` / `configuration` — `ColorSettingsF` / `ConfigF` theme plumbing. + +## Extension SPI + +`extension.api.KeYGuiExtensionF` (mirror of the Swing `KeYGuiExtension`) is the JavaFX extension +contract; capability interfaces: `MainMenuF`, `ToolbarF`, `StatusLineF`, `ContextMenuF`, +`SettingsF`, `StartupF`, `LeftPanelF`. Providers are discovered by `ServiceLoader` through +`KeYGuiExtensionFacadeF`. + +* Built-in providers in `extension/contrib`: `HeatmapF`, `ParallelProverStatusIndicatorF`, + `ProfileNameInStatusBarF`. +* The six keyext extensions are ported as modules and attached `runtimeOnly`: + `keyext.caching.fx`, `keyext.exploration.fx`, `keyext.isabelletranslation.fx`, + `keyext.slicing.fx`, `keyext.ui.testgen.fx`, `keyext.proofmanagement.fx` + (see the per-module READMEs). + +## Tests & CI + +* Unit tests are deliberately **toolkit-free** (JavaFX 25 throws "Toolkit not initialized" when + a `Control` is constructed headless): they assert the pure-Java model (e.g. + `SequentMenuModelFTest`) and the provider/capability surface of each keyext module. The + control-level behaviour is covered by the `key.fx.verify.*` hooks instead. +* Per-commit gate: `./gradlew --no-daemon :key.ui.fx:spotlessApply :key.ui.fx:compileJava + :key.ui.fx:test`. +* CI: the FX modules are part of the `tests.yml` unit-test matrix, the `spotlessCheck` quality + job and the broad release-test `test` runs. + +## Documents + +* `KNOWN-SIMPLIFIED.md` — the consolidated ledger of every deliberate deviation from the Swing + originals (all `KNOWN-SIMPLIFIED` markers, one row per site). +* `PARITY-SIGNOFF.md` — sign-off of the 2026-10-08 parity audit (~490 behaviors) against the + delivered application, and the list of still-open items. diff --git a/keyext.caching.fx/README.md b/keyext.caching.fx/README.md new file mode 100644 index 00000000000..8efb792b094 --- /dev/null +++ b/keyext.caching.fx/README.md @@ -0,0 +1,38 @@ +# keyext.caching.fx — JavaFX port of the proof-caching extension (MP9.1) + +JavaFX port of `keyext.caching` on the [`KeYGuiExtensionF`](../key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/extension/api/KeYGuiExtensionF.java) +SPI. *"Proof Caching"* — functionality related to reusing previous proof results in similar +proofs: reference search across closed proofs, copy-before-dispose, copy-on-prune. + +## Capabilities + +Implements `MainMenuF`, `ToolbarF`, `StatusLineF`, `ContextMenuF`, `SettingsF`, `StartupF` +(service-loaded, `@Info(name = "Proof Caching", optional = true)`). + +| Piece | Class | Notes | +|---|---|---| +| Extension provider | `CachingExtensionF` | menu checkbox, toolbar toggle, status button, sequent context items ("Copy referenced proof steps here"), automatic reference search | +| Copy-on-dispose listener | `CopyBeforeDisposeF` | runs the shared `CopyReferenceResolver` (Swing auto-mode shell not needed on the FX mediator) | +| Copy-on-prune handler | `CachingPruneHandlerF` | copies referenced steps over pruned branches from the proofs tracked by the extension | +| Settings | `CachingSettingsProviderF` | FX-module-owned `ProofCachingSettings` singleton, registered into `ProofIndependentSettings` | +| Status button | `CachingStatusButtonF` | status-line entry with ikonli FontAwesome icons | + +## Verification + +In-app (needs a display): run the app on Xvnc and check the extension list (`key.fx.verify.extensions` +expects `discovered=9`, `statusControls=7`); toggle the caching checkbox, use the sequent context +item, then open the settings category "Proof Caching". + +## Tests + +`CachingExtensionFTest` — toolkit-free: `@Info` annotation, capability surface, sequent-context +null guards, the module-owned settings singleton identity and registry idempotence, and the +service-loader registration file. + +## Known simplifications + +Full ledger: [KNOWN-SIMPLIFIED.md](../key.ui.fx/KNOWN-SIMPLIFIED.md) § keyext.caching (10 rows). +Highlights: the interactive `ReferenceSearchDialog` becomes a read-only summary alert +(`CachingExtensionF.java:735`); open proofs come from the FX-tracked selection instead of +`mediator.getCurrentlyOpenedProofs()`; the Swing settings write (`strategySearch.isEnabled()`) was +a latent bug the FX port drops. diff --git a/keyext.exploration.fx/README.md b/keyext.exploration.fx/README.md new file mode 100644 index 00000000000..b8056c01ff0 --- /dev/null +++ b/keyext.exploration.fx/README.md @@ -0,0 +1,35 @@ +# keyext.exploration.fx — JavaFX port of the exploration extension (MP9.2) + +JavaFX port of `keyext.exploration` on the [`KeYGuiExtensionF`](../key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/extension/api/KeYGuiExtensionF.java) +SPI. *"Exploration"* (experimental) — add, edit and hide formulas on the sequent with +sound-cut-based exploration steps and collect them in the "Exploration Steps" panel. + +## Capabilities + +Implements `ToolbarF`, `MainMenuF`, `StatusLineF`, `LeftPanelF`, `ContextMenuF`, `StartupF` +(service-loaded, `@Info(name = "Exploration", experimental = true, optional = true, +priority = 10000)`). + +| Piece | Class | Notes | +|---|---|---| +| Extension provider | `ExplorationExtensionF` | holds the `ExplorationModeModel` directly (no mediator registry on the FX side); toggle + sequent menu + status entry; hides justification via `ViewSettings` | +| Exploration Steps panel | `ExplorationStepsPanelF` | west-drawer left panel (add/edit/hide steps, prune, live sequent integration) | +| Sequent context menu | `ExplorationSequentMenuF` | "Generate Exploration Step" etc.; single-shot term prompt (Swing retry-loop simplified) | +| Status indicator | (in provider) | ikonli FontAwesome solid vs. regular compass mirrors the Swing `Icons.EXPLORE` | + +## Verification + +In-app (needs a display): `key.fx.verify.extensions` expects the panel on the west drawer +(`westDrawerItems=7`); toggle exploration mode from the toolbar/status entry, add an exploration +step on the sequent and check the panel collects it. + +## Tests + +`ExplorationExtensionFTest` — toolkit-free provider/annotation surface. + +## Known simplifications + +Full ledger: [KNOWN-SIMPLIFIED.md](../key.ui.fx/KNOWN-SIMPLIFIED.md) § keyext.exploration (9 rows). +Highlights: proof-tree styling (`ExplorationRenderer`) and the hidden second-branch tree filter +have no FX seam and are dropped (`ExplorationExtensionF.java:54,363`); the term prompt is +single-shot instead of the Swing retry-loop (`ExplorationSequentMenuF.java:48`). diff --git a/keyext.isabelletranslation.fx/README.md b/keyext.isabelletranslation.fx/README.md new file mode 100644 index 00000000000..3a2ca4ab9aa --- /dev/null +++ b/keyext.isabelletranslation.fx/README.md @@ -0,0 +1,35 @@ +# keyext.isabelletranslation.fx — JavaFX port of the Isabelle translation extension (MP9.3) + +JavaFX port of `keyext.isabelletranslation` on the [`KeYGuiExtensionF`](../key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/extension/api/KeYGuiExtensionF.java) +SPI. *"Isabelle Translation"* — translate the sequent of the selected goal into an Isabelle theory. + +## Capabilities + +Implements `SettingsF`, `ContextMenuF`, `StartupF` (service-loaded, `@Info(name = "Isabelle +Translation", optional = true)`). + +| Piece | Class | Notes | +|---|---|---| +| Extension provider | `IsabelleTranslationExtensionF` | sequent context items "Translate goal to Isabelle theory" (selected goal / whole goal), settings integration | +| Runner | `IsabelleTranslationRunnerF` | runs `IsabelleTranslator.translateProblem` (shared with Swing) on a background thread and shows the theory text | +| Dialog | `IsabelleTranslationDialogF` | plain dialog displaying the generated theory | +| Settings | `IsabelleSettingsProviderF` | Isabelle path / translation path / timeout; key constants mirrored (package-protected in keyext) | + +## Verification + +In-app (needs a display): right-click a sequent term → "Translate…", or use the Settings dialog +category "Isabelle Translation". No external Isabelle solver is launched. + +## Tests + +`IsabelleTranslationExtensionFTest` — fully toolkit-free: `@Info` metadata, SPI null guards and +the two translate context items for a real (headless-loaded) goal. The settings-panel surface is +covered by `IsabelleSettingsProviderFTest`, which starts the FX toolkit and **skips** on headless +CI runners (no display) via JUnit assumptions; the panel itself is verified in-app. + +## Known simplifications + +Full ledger: [KNOWN-SIMPLIFIED.md](../key.ui.fx/KNOWN-SIMPLIFIED.md) § keyext.isabelletranslation +(4 rows). Highlight: the Swing actions hand the theory to the external Isabelle solver with a +progress/model dialog; the FX port stops after the translation itself and never loads the heavy +scala-isabelle backend (`IsabelleTranslationExtensionF.java:37`, `IsabelleTranslationRunnerF.java:28`). diff --git a/keyext.isabelletranslation.fx/src/test/java/org/key_project/isabelletranslation/fx/IsabelleSettingsProviderFTest.java b/keyext.isabelletranslation.fx/src/test/java/org/key_project/isabelletranslation/fx/IsabelleSettingsProviderFTest.java new file mode 100644 index 00000000000..21fb9651b5d --- /dev/null +++ b/keyext.isabelletranslation.fx/src/test/java/org/key_project/isabelletranslation/fx/IsabelleSettingsProviderFTest.java @@ -0,0 +1,55 @@ +/* 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 org.key_project.isabelletranslation.fx; + +import javafx.application.Platform; + +import de.uka.ilkd.key.gui.fx.settings.SettingsProviderF; + +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * The {@link SettingsProviderF} surface of the Isabelle translation extension (MP9.3). + *

        + * Unlike {@link IsabelleTranslationExtensionFTest} this class does need the JavaFX + * toolkit, because resolving the settings contribution instantiates the settings panel (a + * JavaFX {@code Control}). The toolkit is started once with software rendering and no window; + * on CI runners without a display (headless ubuntu/windows agents) {@link Platform#startup} + * fails and the class is skipped via JUnit assumptions instead of failing — the panel + * surface itself is covered in-app by the {@code key.fx.verify.extensions} hook. + */ +class IsabelleSettingsProviderFTest { + + @BeforeAll + static void initFx() { + try { + Platform.startup(() -> { + }); + Platform.setImplicitExit(false); + } catch (IllegalStateException e) { + // the platform is already running (e.g. a shared test JVM) + } catch (RuntimeException e) { + // no DISPLAY / headless CI runner — the FX toolkit cannot start + Assumptions.assumeTrue(false, + "FX toolkit unavailable (" + e + ") — settings-panel assertions skipped"); + } + } + + @Test + void settingsContribution() { + IsabelleTranslationExtensionF extension = new IsabelleTranslationExtensionF(); + SettingsProviderF settings = extension.getSettings(); + assertNotNull(settings, "the extension must contribute a settings provider"); + assertTrue(settings instanceof IsabelleSettingsProviderF, + "the provider must be the FX settings panel"); + assertEquals("Isabelle Translation", settings.getDescription()); + } +} diff --git a/keyext.isabelletranslation.fx/src/test/java/org/key_project/isabelletranslation/fx/IsabelleTranslationExtensionFTest.java b/keyext.isabelletranslation.fx/src/test/java/org/key_project/isabelletranslation/fx/IsabelleTranslationExtensionFTest.java index 07c147e3789..63a1ce7548a 100644 --- a/keyext.isabelletranslation.fx/src/test/java/org/key_project/isabelletranslation/fx/IsabelleTranslationExtensionFTest.java +++ b/keyext.isabelletranslation.fx/src/test/java/org/key_project/isabelletranslation/fx/IsabelleTranslationExtensionFTest.java @@ -7,19 +7,16 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.List; -import javafx.application.Platform; import javafx.scene.control.MenuItem; import de.uka.ilkd.key.control.DefaultUserInterfaceControl; import de.uka.ilkd.key.control.KeYEnvironment; import de.uka.ilkd.key.core.fx.KeYMediatorF; import de.uka.ilkd.key.gui.fx.extension.api.KeYGuiExtensionF; -import de.uka.ilkd.key.gui.fx.settings.SettingsProviderF; import de.uka.ilkd.key.pp.PosInSequent; import de.uka.ilkd.key.proof.Goal; import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -29,32 +26,21 @@ /** * Headless unit test of the MP9.3 FX port of the Isabelle translation extension - * ({@link IsabelleTranslationExtensionF}): the {@link KeYGuiExtensionF.Info} metadata, the - * settings contribution and the sequent context-menu contribution (including the null guards of - * the SPI call). + * ({@link IsabelleTranslationExtensionF}): the {@link KeYGuiExtensionF.Info} metadata and the + * sequent context-menu contribution (including the null guards of the SPI call). *

        - * Like the other extension tests of this repository the term-level assertion runs against a real - * {@link Goal} of a headlessly loaded trivial proof ({@code \problem { true }}, same pattern as - * {@code SequentMenuModelFTest}). Constructing JavaFX controls (the settings panel, the menu - * items) requires the JavaFX toolkit, which is started once in {@link #initFx()} (no window is - * ever shown; the toolkit runs with software rendering). + * This class is deliberately toolkit-free (like the other keyext FX tests): the provider + * constructs its two context items as plain {@link MenuItem}s with string constructors, which + * needs no running JavaFX toolkit, and the settings panel is created lazily (it is a JavaFX + * {@code Control} and needs the toolkit — that assertion lives in + * {@link IsabelleSettingsProviderFTest}, which starts the toolkit and skips on headless CI + * runners). The term-level assertion runs against a real {@link Goal} of a headlessly loaded + * trivial proof ({@code \problem { true }}, same pattern as {@code SequentMenuModelFTest}'). */ class IsabelleTranslationExtensionFTest { private KeYEnvironment env; - @BeforeAll - static void initFx() { - try { - Platform.startup(() -> { - }); - } catch (IllegalStateException e) { - // the platform is already running (e.g. a shared test JVM) - } - // keep the toolkit alive without any open window for the whole test class - Platform.setImplicitExit(false); - } - @AfterEach void tearDown() { if (env != null) { @@ -74,16 +60,6 @@ void infoAnnotationDeclaresTheExtension() { "the Swing original is not experimental (IsabelleTranslationExtension.java:29)"); } - @Test - void settingsContribution() { - IsabelleTranslationExtensionF extension = new IsabelleTranslationExtensionF(); - SettingsProviderF settings = extension.getSettings(); - assertNotNull(settings, "the extension must contribute a settings provider"); - assertTrue(settings instanceof IsabelleSettingsProviderF, - "the provider must be the FX settings panel"); - assertEquals("Isabelle Translation", settings.getDescription()); - } - @Test void contextMenuGuardsNullInputs() { IsabelleTranslationExtensionF extension = new IsabelleTranslationExtensionF(); diff --git a/keyext.proofmanagement.fx/README.md b/keyext.proofmanagement.fx/README.md new file mode 100644 index 00000000000..c117eb41531 --- /dev/null +++ b/keyext.proofmanagement.fx/README.md @@ -0,0 +1,32 @@ +# keyext.proofmanagement.fx — JavaFX port of the proof-management extension (MP9.6) + +JavaFX port of `keyext.proofmanagement` on the [`KeYGuiExtensionF`](../key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/extension/api/KeYGuiExtensionF.java) +SPI. *"Proof management"* — run soundness checks on proof bundles (missing proofs, settings, +replay, dependency checkers) and generate an HTML report. + +## Capabilities + +Implements `MainMenuF` (service-loaded, `@Info(name = "Proof management", optional = true)`); +`@NullMarked`. + +| Piece | Class | Notes | +|---|---|---| +| Extension provider | `ProofManagementExtF` | contributes the **Proof Management** menu (`MENU_PM`, matching the Swing menu text) | +| Check configuration dialog | `CheckConfigDialogF` | selects checkers, the proof bundle (`.zproof`) and the report location, then runs the checkers in the background via the unchanged `Main.CheckCommand`; input controls are disabled while the check runs ("Run checkers" → "Stop"); outcome alert | + +## Verification + +In-app (needs a display): the Proof Management menu appears in the menu bar; run a check against +a proof bundle and confirm the report path handling (HelpFacade seam) and the result alert. + +## Tests + +`ProofManagementExtFTest` — toolkit-free provider/annotation surface. + +## Known simplifications + +Full ledger: [KNOWN-SIMPLIFIED.md](../key.ui.fx/KNOWN-SIMPLIFIED.md) § keyext.proofmanagement +(4 rows). Highlights: the Swing `BlockingGlassPane` is replaced by explicit control disabling +(`CheckConfigDialogF.java:45`); AWT `KeYFileChooser` filters become FX `FileChooser` extension +filters (`CheckConfigDialogF.java:50`); the report opens through the `HelpFacadeF` browser seam +rather than `Desktop.getDesktop().open` (`CheckConfigDialogF.java:55`). diff --git a/keyext.slicing.fx/README.md b/keyext.slicing.fx/README.md new file mode 100644 index 00000000000..ab252b50403 --- /dev/null +++ b/keyext.slicing.fx/README.md @@ -0,0 +1,35 @@ +# keyext.slicing.fx — JavaFX port of the proof-slicing extension (MP9.4) + +JavaFX port of `keyext.slicing` on the [`KeYGuiExtensionF`](../key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/extension/api/KeYGuiExtensionF.java) +SPI. *"Slicing"* (port by Arne Keller) — analyze proofs to remove useless rule applications and +replay the sliced proof. + +## Capabilities + +Implements `ContextMenuF`, `StartupF`, `LeftPanelF`, `SettingsF` (service-loaded, +`@Info(name = "Slicing", optional = true, priority = 9001)`); `@NullMarked`. + +| Piece | Class | Notes | +|---|---|---| +| Extension provider | `SlicingExtensionF` | sequent context items ("Slice proof", "Show dependency graph around this formula", "Analyze proof"), proof-load tracking via `KeYSelectionListener` | +| Left panel | `ui/SlicingLeftPanelF` | west-drawer panel: slice / slice-to-fixed-point, analyze, rule statistics, execution timings, DOT export/rendering, load sliced proof | +| Settings | `SlicingSettingsProviderF` | per-algorithm toggles, DOT executable; the aggressive-deduplicate flag is read-only (package-private setter in keyext) | + +## Verification + +In-app (needs a display): `key.fx.verify.extensions` expects the panel on the west drawer +(`westDrawerItems=7`); right-click a sequent term → "Slice proof" on a proof with several +branches; check the sliced proof loads. + +## Tests + +`SlicingExtensionFTest` — toolkit-free provider/annotation surface (widget contributions are +verified in-app). + +## Known simplifications + +Full ledger: [KNOWN-SIMPLIFIED.md](../key.ui.fx/KNOWN-SIMPLIFIED.md) § keyext.slicing (16 rows). +Highlights: graphviz rendering and the HTML table dialogs become DOT/text dialogs +(`SlicingLeftPanelF.java:322,295,464`); "slice to fixed point" performs a single iteration +(`SlicingLeftPanelF.java:222`); sliced proofs register in the recent-files list +(`SlicingLeftPanelF.java:415`). diff --git a/keyext.ui.testgen.fx/README.md b/keyext.ui.testgen.fx/README.md new file mode 100644 index 00000000000..9538fd34ada --- /dev/null +++ b/keyext.ui.testgen.fx/README.md @@ -0,0 +1,38 @@ +# keyext.ui.testgen.fx — JavaFX port of the KeY test-generation UI (MP9.5) + +JavaFX port of `keyext.ui.testgen` on the [`KeYGuiExtensionF`](../key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/extension/api/KeYGuiExtensionF.java) +SPI. *"Test case generation"* — generate JUnit test cases from the current proof, or search for a +counterexample, using the Z3 CE solver. The generation macros/loaders come from `key.core.testgen`. + +## Capabilities + +Implements `SettingsF`, `StatusLineF`, `MainMenuF` (service-loaded, `@Info(name = "Test case +generation", optional = false)`); `@NullMarked`. The `:key.core` dependency (added in the MP9.5 +integration review) enables the real `MainMenuF` — a **"Test Case Generation"** menu with the two +Swing actions. + +| Piece | Class | Notes | +|---|---|---| +| Extension provider | `TestgenExtensionF` | menu with "Generate tests…" (+ `SHORTCUT+T`, accelerator lives on the menu item) and "Search for counterexample…"; status-line `MenuButton` + two plain buttons | +| Generation flow | `TestGenerationTaskF` / `TestgenReflectionF` / `LogSinkF` | facade route into the `key.core.testgen` machinery via reflection; result logging | +| Run dialogs | `TestGenResultsDialogF` / `CounterExampleResultsDialogF` / `CounterExampleTaskF` | generated-files list + live log; SMT statistics and found/not-found outcome | +| Settings | `TestgenSettingsProviderF` | reflectively-dispatched proxy provider (the module cannot name `MainWindowF`); options live exclusively in the Settings dialog | + +## Verification + +In-app (needs a display): open the "Test Case Generation" menu (or press `SHORTCUT+T`), run +"Generate tests…" against a proof with an annotated problem, and check the run dialog's log and +generated-files list. Requires a Z3 CE-capable solver on the path. + +## Tests + +`TestgenExtensionFTest` — toolkit-free: `@Info`, capability surface (`MainMenuF` + `SettingsF` + +`StatusLineF`), settings-provider dispatch. + +## Known simplifications + +Full ledger: [KNOWN-SIMPLIFIED.md](../key.ui.fx/KNOWN-SIMPLIFIED.md) § keyext.ui.testgen (18 rows). +Highlights: the Swing `Toolbar`/`Startup` slots become status-line buttons and lazy window +capture through the settings panel (`TestgenExtensionF.java:59,72`); the Swing `SolverListener` +modal progress/model dialogs become log output (`TestgenReflectionF.java:220`); stopping +interrupts the worker thread instead of a cooperative `StopRequest` (`TestGenerationTaskF.java:19`). From d107927fc9cc9f913e981f7ffeed198041c71e04 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Sat, 10 Oct 2026 10:49:53 +0200 Subject: [PATCH 128/281] colors (P1): full Swing-parity color palette + effective CSS variable wiring - ColorPaletteF: all 51 Swing-parity color properties (the true count incl. the two multi-line define() keys [currentGoal]mouseSelectionColor and [Heatmap]basecolor) with the exact keys and light/dark defaults of the key.ui ColorSettings.define call sites; registered via ensureRegistered(). - ColorSettingsF: startup bootstrap wires the palette before the settings providers load; overrides from colors.json now apply to the managed scene at startup (applyToScenes after ThemeManager.manage). verifyColors() self test for key.fx.verify.colors: palette 51/51 (read reflectively, the registry total may exceed it since extensions register their own colors), all mapped CSS variables declared AND referenced in both themes, and an override round trip on [proofTree]darkGreen -> -key-proved with snapshot/restore so colors.json stays unpolluted. - key-light/key-dark.css: declare the 9 previously-undeclared mapped variables (-key-hl-java/-comment/-jml/-jml-keyword, -key-sequent-hl-prop/-dyn/-progvar/-arrow, -key-settings-error) with the prior hardcoded values; .sequent-hl-*/.source-* rules now consume the variables so user overrides take effect. - run task: whitelist key.fx.verify.colors; README flag catalog + PARITY SIGNOFF updated (colors row RESOLVED). Verified on Xvnc :99: key.fx.verify.colors PASS (palette 51/51, 52 registered incl. caching.reference_found, round trip PASS); demo proof load unaffected. --- key.ui.fx/PARITY-SIGNOFF.md | 9 +- key.ui.fx/README.md | 1 + key.ui.fx/build.gradle | 4 +- .../de/uka/ilkd/key/gui/fx/MainWindowF.java | 18 + .../ilkd/key/gui/fx/colors/ColorPaletteF.java | 399 ++++++++++++++++++ .../key/gui/fx/colors/ColorSettingsF.java | 150 +++++++ .../de/uka/ilkd/key/gui/fx/theme/key-dark.css | 47 +-- .../uka/ilkd/key/gui/fx/theme/key-light.css | 47 +-- 8 files changed, 618 insertions(+), 57 deletions(-) create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/colors/ColorPaletteF.java diff --git a/key.ui.fx/PARITY-SIGNOFF.md b/key.ui.fx/PARITY-SIGNOFF.md index 19a82dfa33e..2f0c0e11fbd 100644 --- a/key.ui.fx/PARITY-SIGNOFF.md +++ b/key.ui.fx/PARITY-SIGNOFF.md @@ -25,7 +25,7 @@ marked `OPEN (unverified)` rather than assumed fixed. | goallist-strategy-info | 37 | goal list at parity; strategy preset UI missing; info view re-designed | — | preset combo + stats staleness OPEN | | source-search-filechooser | 74 | 30 / 5 / **28 MISSING** | search bars + loading-options dialog ported | source-view interaction (symbex highlights, cross-highlight) OPEN | | menus-actions | 57 leaves | ~25 MISSING | menu parity 5/5 (File 16 / Proof 24 / Options 12 / View 7 / About 5) plus automation submenu | RESOLVED at surface level; a few behaviours below | -| settings-config | 117 | 83 / 8 / 11 MISSING / 13 KNOWN-DEF | settings framework + providers + colors mechanics + search | SMT run-UI, theme persistence, color defs OPEN | +| settings-config | 117 | 83 / 8 / 11 MISSING / 13 KNOWN-DEF | settings framework + providers + colors mechanics + search | SMT run-UI + theme persistence OPEN; color definitions RESOLVED (P1) | | unported-dialogs | 18 | 13 MISSING / 2 PARTIAL | proofmgmt, tacletmatch, IssueDialog, notification, Loaded Proofs | RESOLVED where noted below; join/mergerule/lemmatagenerator/etc. track their MP9.x modules | ## P0 sign-off (the audit's "prover workflow critical" list) @@ -51,7 +51,7 @@ P0 bugs from the audit: | Recent-file clicks drop stored profile / single-java options | `OPEN` (loading-options dialog itself is ported — see below) | | Shortcut-default mismatches (`KeyStrokeManagerF`: tree search, sequent search, macro defaults) | `OPEN (unverified)` | | Dead registered bindings (Ctrl+C term copy, F3/Shift+F3, Ctrl+K/Ctrl+J, Goal Back/Prune) | `PARTIAL` (term-menu copy + menu accelerators exist; several bindings unverified) | -| Colors: 12 mapped CSS variables ineffective (47 property definitions missing) | `OPEN` | +| Colors: 12 mapped CSS variables ineffective (47 property definitions missing) | **RESOLVED** (P1: `ColorPaletteF` defines all 51 Swing-parity properties — the true count incl. the two multi-line `define(` keys; the 9 previously-undeclared mapped CSS vars are declared in both themes and consumed by the re-wired `.sequent-hl-*`/`.source-*` rules; `key.fx.verify.colors` PASS) | | UPSTREAM (Swing, not FX): `LoopApplyHeadCompletion` + `LoopContract*` dead code; seed-clobber `overwriteWith` | **Not our defect** — flags for upstream `key.ui`/`key.core` cleanup | ## P1 sign-off @@ -78,7 +78,8 @@ actions/layout slots/maximize (partially done — `key.fx.verify.docking` PASS; exist as `DockTitleActionF`), join+mergerule dialogs (track MP9.x modules), lemmatagenerator (`key.fx.verify.lemmaorigin` PASS for the generator path), soundiness (`key.fx.verify.soundiness` PASS), originlabels (lemmaorigin), plugins, profileloading (RESOLVED), help windows -(`key.fx.verify.help` PASS), theme persistence + the 47 color property definitions, feature +(`key.fx.verify.help` PASS), theme persistence (color definitions RESOLVED via P1 +`key.fx.verify.colors`), feature flags + parallel prover (`ParallelProverStatusIndicatorF` partial), settings dump, modal grey-out, proof-disposal clearing (auto saver handles the disposal part), tab icons/titles, file-chooser bookmarks. Each is either documented in the KNOWN-SIMPLIFIED ledger or listed above @@ -99,7 +100,7 @@ as open. 6. Keyboard: shortcut-default regressions; dead bindings audit. 7. Options dialogs: SMT run UI, strategy preset UI, HeatmapOptionsDialog. 8. Exit flow: close-request + confirmExit + layout persistence on close. -9. Colors/theme: 47 color property definitions; theme persistence. +9. Colors/theme: all 51 Swing-parity color property definitions RESOLVED (P1 `key.fx.verify.colors` PASS); theme persistence remains. 10. Notification framework depth: proof-closed/exception dialogs beyond toasts are in (`IssueDialogF`); the `NotificationTask`/action framework remains partial. 11. Docking: remaining title actions, layout-slot keys F10–F12 (named slots exist). diff --git a/key.ui.fx/README.md b/key.ui.fx/README.md index 11a84fac833..5d6c06d727e 100644 --- a/key.ui.fx/README.md +++ b/key.ui.fx/README.md @@ -69,6 +69,7 @@ regression-tested on a virtual display. Demo proof paths default into | `key.fx.verify.profileloading` | WD loading-options panel | | `key.fx.verify.profileloadingdialog` | loading options dialog at startup | | `key.fx.verify.javacsettings` | javac settings provider | +| `key.fx.verify.colors` | colors palette: Swing-parity property count, mapped CSS-variable wiring, override round trip | Harness-injected flags (read in `MainWindowF` but not on the `run`-task whitelist; the verification harness sets them via `JAVA_TOOL_OPTIONS`): diff --git a/key.ui.fx/build.gradle b/key.ui.fx/build.gradle index 0536f3d4eeb..4b063b12442 100644 --- a/key.ui.fx/build.gradle +++ b/key.ui.fx/build.gradle @@ -116,6 +116,8 @@ run { // self test; the settings dialog is not opened) // * key.fx.verify.termmenu = any (termmenu: run the headless sequent context-menu self test // after the demo load; text report on stdout) + // * key.fx.verify.colors = any (colors: run the Swing-parity palette + mapped CSS variable + // self test; proof-independent, runs at startup like the other smalldialogs hooks) ["key.fx.driver", "key.fx.theme", "key.fx.demo.sequent", "key.fx.verify.sequent", "key.fx.show", "key.fx.demo.autoprove", "key.fx.verify.tree", "key.fx.demo.autoprove.live", "key.fx.demo.maxsteps", "key.fx.verify.goallist", @@ -129,7 +131,7 @@ run { "key.fx.verify.joinmerge", "key.fx.verify.joinmerge.maxsteps", "key.fx.verify.lemmaorigin", "key.fx.verify.help", "key.fx.verify.soundiness", "key.fx.verify.profileloading", "key.fx.verify.profileloadingdialog", "key.fx.verify.javacsettings", - "key.fx.verify.termmenu"] + "key.fx.verify.termmenu", "key.fx.verify.colors"] .each { name -> def value = System.getProperty(name) if (value != null) { 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 index b5708f3baa5..2bc5a7d1388 100644 --- 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 @@ -65,6 +65,8 @@ 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.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.dialogs.FeedbackDialogF; import de.uka.ilkd.key.gui.fx.dialogs.LoadUserTacletsDialogF; @@ -438,6 +440,12 @@ public void initialize() { 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 — @@ -510,6 +518,16 @@ public void initialize() { .notify("Javac settings verification: " + report, report.endsWith("PASS") ? Kind.INFO : Kind.ERROR); } + // 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); + } // extension: MP9.0 — register the FX extensions' settings providers into the settings // manager registry (Swing SettingsManager registers each KeYGuiExtension.Settings 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 index a5fdcc4580d..f5d42e025a8 100644 --- 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 @@ -5,14 +5,20 @@ 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; @@ -267,6 +273,150 @@ 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) diff --git a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css index 46d08c3d566..b9fa50ee926 100644 --- a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css +++ b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css @@ -20,6 +20,17 @@ -key-linked: #4a90e2; -key-interactive: #e3a94f; -key-border: #555555; + /* configurable highlight colors (ColorSettingsF.CSS_VARIABLES): the values below are the + dark-theme defaults; user overrides from colors.json are applied inline on scene roots */ + -key-hl-java: #cf50a5; + -key-hl-comment: #9fbf9f; + -key-hl-jml: #8888cf; + -key-hl-jml-keyword: #8888cf; + -key-sequent-hl-prop: #dedede; + -key-sequent-hl-dyn: #9696fa; + -key-sequent-hl-progvar: #6464fa; + -key-sequent-hl-arrow: #6464fa; + -key-settings-error: #c86464; /* restyle the built-in Modena controls for the dark theme */ -fx-base: #3c3f41; @@ -110,33 +121,34 @@ these rules must stay before the search rules so the search and click highlights override the syntax coloring for -fx-fill */ .sequent-hl-prop { + -fx-fill: -key-sequent-hl-prop; -fx-font-weight: bold; } .sequent-hl-dyn { - -fx-fill: #9696fa; + -fx-fill: -key-sequent-hl-dyn; -fx-font-weight: bold; } .sequent-hl-java { - -fx-fill: #cf50a5; + -fx-fill: -key-hl-java; -fx-font-weight: bold; } .sequent-hl-progvar { - -fx-fill: #6464fa; + -fx-fill: -key-sequent-hl-progvar; } .sequent-hl-comment { - -fx-fill: #9fbf9f; + -fx-fill: -key-hl-comment; } .sequent-hl-jml { - -fx-fill: #8888cf; + -fx-fill: -key-hl-jml; } .sequent-hl-arrow { - -fx-fill: #6464fa; + -fx-fill: -key-sequent-hl-arrow; -fx-font-weight: bold; } @@ -422,12 +434,12 @@ } .source-keyword { - -fx-fill: -key-accent; + -fx-fill: -key-hl-java; -fx-font-weight: bold; } .source-comment { - -fx-fill: -key-text-secondary; + -fx-fill: -key-hl-comment; -fx-font-style: italic; } @@ -440,6 +452,7 @@ } .source-jml-keyword { + -fx-fill: -key-hl-jml-keyword; -fx-font-weight: bold; } @@ -452,24 +465,6 @@ -fx-text-fill: -key-text-secondary; } -.source-comment { - -fx-fill: -key-hl-comment; - -fx-font-style: italic; -} - -.source-string { - -fx-fill: -key-success; -} - -.source-annotation { - -fx-fill: -key-warning; -} - -.source-jml-keyword { - -fx-fill: -key-hl-jml-keyword; - -fx-font-weight: bold; -} - /* * Settings dialog (M4, Swing SettingsDialog/SettingsUi/SimpleSettingsPanel). */ diff --git a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css index c3a47b0f877..3740ea7ca4d 100644 --- a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css +++ b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css @@ -26,6 +26,17 @@ -key-linked: #1a73e8; -key-interactive: #b06000; -key-border: #d0d0d0; + /* configurable highlight colors (ColorSettingsF.CSS_VARIABLES): the values below are the + light-theme defaults; user overrides from colors.json are applied inline on scene roots */ + -key-hl-java: #7f0055; + -key-hl-comment: #3f7f5f; + -key-hl-jml: #0000f0; + -key-hl-jml-keyword: #0000f0; + -key-sequent-hl-prop: #1c1c1c; + -key-sequent-hl-dyn: #0000d0; + -key-sequent-hl-progvar: #6a3e3e; + -key-sequent-hl-arrow: #6a3e3e; + -key-settings-error: #c86464; } /* status bar */ @@ -106,33 +117,34 @@ these rules must stay before the search rules so the search and click highlights override the syntax coloring for -fx-fill */ .sequent-hl-prop { + -fx-fill: -key-sequent-hl-prop; -fx-font-weight: bold; } .sequent-hl-dyn { - -fx-fill: #0000d0; + -fx-fill: -key-sequent-hl-dyn; -fx-font-weight: bold; } .sequent-hl-java { - -fx-fill: #7f0055; + -fx-fill: -key-hl-java; -fx-font-weight: bold; } .sequent-hl-progvar { - -fx-fill: #6a3e3e; + -fx-fill: -key-sequent-hl-progvar; } .sequent-hl-comment { - -fx-fill: #3f7f5f; + -fx-fill: -key-hl-comment; } .sequent-hl-jml { - -fx-fill: #0000f0; + -fx-fill: -key-hl-jml; } .sequent-hl-arrow { - -fx-fill: #6a3e3e; + -fx-fill: -key-sequent-hl-arrow; -fx-font-weight: bold; } @@ -418,12 +430,12 @@ } .source-keyword { - -fx-fill: -key-accent; + -fx-fill: -key-hl-java; -fx-font-weight: bold; } .source-comment { - -fx-fill: -key-text-secondary; + -fx-fill: -key-hl-comment; -fx-font-style: italic; } @@ -436,6 +448,7 @@ } .source-jml-keyword { + -fx-fill: -key-hl-jml-keyword; -fx-font-weight: bold; } @@ -448,24 +461,6 @@ -fx-text-fill: -key-text-secondary; } -.source-comment { - -fx-fill: -key-hl-comment; - -fx-font-style: italic; -} - -.source-string { - -fx-fill: -key-success; -} - -.source-annotation { - -fx-fill: -key-warning; -} - -.source-jml-keyword { - -fx-fill: -key-hl-jml-keyword; - -fx-font-weight: bold; -} - /* * Settings dialog (M4, Swing SettingsDialog/SettingsUi/SimpleSettingsPanel). */ From 7eaef3dd6e076b880734589167280ffe161bd063 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Sat, 10 Oct 2026 11:10:12 +0200 Subject: [PATCH 129/281] smt (P1): full Swing-parity SMT run UI (progress dialog, info window, result application) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - New package de.uka.ilkd.key.gui.fx.smt, port of the Swing gui/smt run UI: - SolverListenerF (Swing SolverListener): InternSMTProblemF with timing and Information entries incl. the SMT2/taclet translation file dumping; launcher callbacks marshalled to the FX thread (launch() is synchronous, so the dialog-prepare runLater is FIFO-before the finish runLater); a 100 ms Timeline polls the solver states (Swing: 10 ms Timer) and writes progress, remaining time and result coloring into the cells — green/red from the [solverListener] palette colors (ColorPaletteF.SMT_GREEN/SMT_RED by current theme), orange 200/150/0 for possible counterexamples, blue for unknown, Z3_CE wording preserved. applyResults closes the goals via SMTRule.createApp (with the unsat core when provided + completeBuiltInRuleAppByDefault), focusResults via SMTFocusResults.focus, ProgressMode.CLOSE auto-applies on completion. Public launch entry points: launchOnProof (SMTInvokeAction core, ready for the future toolbar dropdown) and launchOnGoal (CurrentGoalViewMenu.SMTAction path). - ProgressDialogF (Swing ProgressDialog): application-modal stage with the overall progress bar ("Processed X of N problems."), a TableView with one row per problem and one process column per solver type (per-cell progress bar + painted text + Info button, result colors as bar accent/text fill), Stop/Discard + Focus goals + Apply buttons (Focus/Apply hidden in counterexample mode like Swing), setModus/setEditable state handling. - InformationWindowF (Swing InformationWindow): one tab per information entry (error message, solver input, taclets, solver output, warnings, solver support) as read-only monospaced text areas. - SequentTermContextMenuF.runSmt rewired from the MP8 read-only Alert minimum to the full run UI (SolverListenerF.launchOnGoal). - key.fx.verify.smt (whitelisted on the run task): after the demo load, launches the first usable solver union on the first open goal with the auto-applying CLOSE progress mode (snapshot + restore), polls for the closed goal on a background thread, auto-discards the still-modal dialog on timeout. - KNOWN-SIMPLIFIED (2 markers): apply without the SMTProofApplyUserAction undo history and without the stopInterface input freeze (P0 remainder); InformationWindowF without the counterexample model tree, line numbers and CE help tab. README flag catalog + PARITY SIGNOFF updated (SMT settings + run UI row RESOLVED; the Swing toolbar DropdownSelectionButton stays tracked under the toolbar item). Verified on Xvnc :99: key.fx.verify.smt PASS (union=Z3, goalClosed=true) — Z3 closes the Agatha goal through the FX run UI, the CLOSE auto-apply creates the green 0:SMT node and the proof is closed. --- key.ui.fx/KNOWN-SIMPLIFIED.md | 11 +- key.ui.fx/PARITY-SIGNOFF.md | 7 +- key.ui.fx/README.md | 1 + key.ui.fx/build.gradle | 5 +- .../de/uka/ilkd/key/gui/fx/MainWindowF.java | 81 ++ .../fx/nodeviews/SequentTermContextMenuF.java | 79 +- .../key/gui/fx/smt/InformationWindowF.java | 76 ++ .../ilkd/key/gui/fx/smt/ProgressDialogF.java | 354 ++++++++ .../ilkd/key/gui/fx/smt/SolverListenerF.java | 772 ++++++++++++++++++ 9 files changed, 1309 insertions(+), 77 deletions(-) create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/smt/InformationWindowF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/smt/ProgressDialogF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/smt/SolverListenerF.java diff --git a/key.ui.fx/KNOWN-SIMPLIFIED.md b/key.ui.fx/KNOWN-SIMPLIFIED.md index baa8b2b149a..d951bf5c9bc 100644 --- a/key.ui.fx/KNOWN-SIMPLIFIED.md +++ b/key.ui.fx/KNOWN-SIMPLIFIED.md @@ -10,14 +10,14 @@ Marker counts per module: | Module | Markers | |---|---| -| `key.ui.fx` (contrib extensions) | 3 | +| `key.ui.fx` (contrib extensions + SMT run UI) | 5 | | `keyext.caching.fx` | 10 | | `keyext.exploration.fx` | 9 | | `keyext.isabelletranslation.fx` | 4 | | `keyext.slicing.fx` | 16 | | `keyext.ui.testgen.fx` | 18 | | `keyext.proofmanagement.fx` | 4 | -| **Total** | **64** | +| **Total** | **66** | To re-derive this table at any point: `grep -rn 'KNOWN-SIMPLIFIED' */src/main */src/test`. @@ -35,6 +35,13 @@ milestone, **FIXED-UPSTREAM** items differ from a Swing bug that the port delibe | `extension/contrib/HeatmapF.java:37` | — (doubles the `@Info` description) | same decision restated for the extension dialog | OPEN | | `extension/contrib/ParallelProverStatusIndicatorF.java:27` | toggle button "SC" / "MT N×" with left-click toggle and right-click worker-count menu | plain `Label` showing the live auto-mode state ("Auto"/"Manual"), left-click toggles `PARALLEL_PROVER_ENABLED`, reacts to property changes; worker-count picker and button styling out of scope | OPEN | +## `key.ui.fx` — SMT run UI (2) + +| Site | Swing original | FX port | Status | +|---|---|---|---| +| `SolverListenerF.java:81` | results applied via `SMTProofApplyUserAction` (undoable history entry) with the `stopInterface` input freeze around it | direct `SMTRule` application on the FX thread (no undo entry for the automatic CLOSE-mode application; the input freeze is the P0 `stopInterface` remainder) | OPEN | +| `InformationWindowF.java:24` | counterexample model tree (`CETree`) with line numbers (`TextLineNumber`) and the counterexample help tab | tabs per information entry as read-only monospaced text areas; model tree + line numbers + help tab not ported | OPEN | + ## `keyext.caching.fx` (10) | Site | Swing original | FX port | Status | diff --git a/key.ui.fx/PARITY-SIGNOFF.md b/key.ui.fx/PARITY-SIGNOFF.md index 2f0c0e11fbd..e03c99cd1ec 100644 --- a/key.ui.fx/PARITY-SIGNOFF.md +++ b/key.ui.fx/PARITY-SIGNOFF.md @@ -25,7 +25,7 @@ marked `OPEN (unverified)` rather than assumed fixed. | goallist-strategy-info | 37 | goal list at parity; strategy preset UI missing; info view re-designed | — | preset combo + stats staleness OPEN | | source-search-filechooser | 74 | 30 / 5 / **28 MISSING** | search bars + loading-options dialog ported | source-view interaction (symbex highlights, cross-highlight) OPEN | | menus-actions | 57 leaves | ~25 MISSING | menu parity 5/5 (File 16 / Proof 24 / Options 12 / View 7 / About 5) plus automation submenu | RESOLVED at surface level; a few behaviours below | -| settings-config | 117 | 83 / 8 / 11 MISSING / 13 KNOWN-DEF | settings framework + providers + colors mechanics + search | SMT run-UI + theme persistence OPEN; color definitions RESOLVED (P1) | +| settings-config | 117 | 83 / 8 / 11 MISSING / 13 KNOWN-DEF | settings framework + providers + colors mechanics + search | theme persistence OPEN; color definitions + SMT run UI RESOLVED (P1) | | unported-dialogs | 18 | 13 MISSING / 2 PARTIAL | proofmgmt, tacletmatch, IssueDialog, notification, Loaded Proofs | RESOLVED where noted below; join/mergerule/lemmatagenerator/etc. track their MP9.x modules | ## P0 sign-off (the audit's "prover workflow critical" list) @@ -63,7 +63,7 @@ P0 bugs from the audit: | Automation submenu + macro invocation | **RESOLVED** (automation submenu MP2, macro menu in term menu); global toolbar dropdown `OPEN` | | Join / merge dialogs | **RESOLVED** for the merge/join flow (`key.fx.verify.joinmerge` PASS on gcd 32/0); `keyext.slicing.fx` SMT routing partial | | Strategy preset combo / stash UI | `OPEN` | -| SMT settings + run UI | `OPEN` (settings providers ported; the run UI — `ProgressDialog` table, `DropdownSelectionButton` — remains) | +| SMT settings + run UI | **RESOLVED** (P1: settings providers ported in MP4; the run UI — `SolverListenerF` + `ProgressDialogF` progress table + `InformationWindowF` + result application via the SMT rule — ported, `key.fx.verify.smt` PASS: Z3 closes the Agatha goal; the Swing toolbar `DropdownSelectionButton` remains tracked under the toolbar item) | | Exit flow (close-request, `confirmExit`, layout save) | `OPEN (unverified)` | | Term labels; Pretty Print / Unicode wiring | `OPEN` (lemmaorigin hook covers labels mechanically — see `key.fx.verify.lemmaorigin`) | | User-selection highlight + Ctrl+C copy | **RESOLVED** (term-menu copy item in the skeleton); multi-selection highlight + reprint persistence `OPEN` | @@ -98,7 +98,8 @@ as open. Back/Forward bindings, Edit Last File, Load User Taclets + Prove submenu, Run All Proofs (dialogs exist for several), EnableWhenProofLoaded analogue, toolbars 9 remaining buttons. 6. Keyboard: shortcut-default regressions; dead bindings audit. -7. Options dialogs: SMT run UI, strategy preset UI, HeatmapOptionsDialog. +7. Options dialogs: strategy preset UI, HeatmapOptionsDialog (SMT run UI RESOLVED via P1 + `key.fx.verify.smt`). 8. Exit flow: close-request + confirmExit + layout persistence on close. 9. Colors/theme: all 51 Swing-parity color property definitions RESOLVED (P1 `key.fx.verify.colors` PASS); theme persistence remains. 10. Notification framework depth: proof-closed/exception dialogs beyond toasts are in diff --git a/key.ui.fx/README.md b/key.ui.fx/README.md index 5d6c06d727e..c2eeaa7ec17 100644 --- a/key.ui.fx/README.md +++ b/key.ui.fx/README.md @@ -70,6 +70,7 @@ regression-tested on a virtual display. Demo proof paths default into | `key.fx.verify.profileloadingdialog` | loading options dialog at startup | | `key.fx.verify.javacsettings` | javac settings provider | | `key.fx.verify.colors` | colors palette: Swing-parity property count, mapped CSS-variable wiring, override round trip | +| `key.fx.verify.smt` | SMT run UI end to end (needs an installed solver, e.g. `z3` on the `PATH`): launches the union on the demo goal, auto-applies the result, checks the closed goal | Harness-injected flags (read in `MainWindowF` but not on the `run`-task whitelist; the verification harness sets them via `JAVA_TOOL_OPTIONS`): diff --git a/key.ui.fx/build.gradle b/key.ui.fx/build.gradle index 4b063b12442..1f572cc0fda 100644 --- a/key.ui.fx/build.gradle +++ b/key.ui.fx/build.gradle @@ -118,6 +118,9 @@ run { // after the demo load; text report on stdout) // * key.fx.verify.colors = any (colors: run the Swing-parity palette + mapped CSS variable // self test; proof-independent, runs at startup like the other smalldialogs hooks) + // * key.fx.verify.smt = any (smt: run the SMT run UI end to end on the first open goal of + // the demo proof; needs an installed SMT solver (e.g. z3 on the PATH), the dialog + // auto-applies the results with the CLOSE progress mode) ["key.fx.driver", "key.fx.theme", "key.fx.demo.sequent", "key.fx.verify.sequent", "key.fx.show", "key.fx.demo.autoprove", "key.fx.verify.tree", "key.fx.demo.autoprove.live", "key.fx.demo.maxsteps", "key.fx.verify.goallist", @@ -131,7 +134,7 @@ run { "key.fx.verify.joinmerge", "key.fx.verify.joinmerge.maxsteps", "key.fx.verify.lemmaorigin", "key.fx.verify.help", "key.fx.verify.soundiness", "key.fx.verify.profileloading", "key.fx.verify.profileloadingdialog", "key.fx.verify.javacsettings", - "key.fx.verify.termmenu", "key.fx.verify.colors"] + "key.fx.verify.termmenu", "key.fx.verify.colors", "key.fx.verify.smt"] .each { name -> def value = System.getProperty(name) if (value != null) { 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 index 2bc5a7d1388..209480d82f9 100644 --- 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 @@ -10,6 +10,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; +import java.util.Collection; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -110,6 +111,7 @@ 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; @@ -144,8 +146,12 @@ 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; @@ -876,6 +882,12 @@ protected KeYEnvironment call() throws Exception { 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(); + } // 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) { @@ -1843,6 +1855,75 @@ private void runUpdateHighlightVerification() { report.endsWith("PASS") ? Kind.INFO : Kind.ERROR); } + /** + * 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); + 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); + } + report = (closed ? "PASS" : "FAIL") + " (union=" + union + ", goalClosed=" + closed + + ")"; + // 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.endsWith("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 diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentTermContextMenuF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentTermContextMenuF.java index 76791b919cb..126e67aa5c4 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentTermContextMenuF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentTermContextMenuF.java @@ -7,7 +7,6 @@ import java.util.ArrayList; import java.util.Collection; import java.util.List; -import javafx.application.Platform; import javafx.scene.control.Alert; import javafx.scene.control.Alert.AlertType; import javafx.scene.control.ButtonType; @@ -32,6 +31,7 @@ import de.uka.ilkd.key.gui.fx.nodeviews.SequentMenuModelF.NamedAction; import de.uka.ilkd.key.gui.fx.nodeviews.SequentMenuModelF.SubMenuEntry; import de.uka.ilkd.key.gui.fx.nodeviews.SequentMenuModelF.TacletEntry; +import de.uka.ilkd.key.gui.fx.smt.SolverListenerF; import de.uka.ilkd.key.logic.JTerm; import de.uka.ilkd.key.logic.NameCreationInfo; import de.uka.ilkd.key.logic.ProgramElementName; @@ -44,15 +44,7 @@ import de.uka.ilkd.key.proof.Node; import de.uka.ilkd.key.proof.Proof; import de.uka.ilkd.key.proof.join.ProspectivePartner; -import de.uka.ilkd.key.settings.DefaultSMTSettings; -import de.uka.ilkd.key.settings.ProofIndependentSettings; -import de.uka.ilkd.key.smt.SMTProblem; -import de.uka.ilkd.key.smt.SMTSolver; -import de.uka.ilkd.key.smt.SMTSolverResult; -import de.uka.ilkd.key.smt.SolverLauncher; -import de.uka.ilkd.key.smt.SolverLauncherListener; import de.uka.ilkd.key.smt.SolverTypeCollection; -import de.uka.ilkd.key.smt.solvertypes.SolverType; import org.key_project.prover.sequent.PosInOccurrence; @@ -464,10 +456,9 @@ private static MenuItem disabledItem(String label) { * sequent (Swing {@code CurrentGoalViewMenu.SMTAction}, CurrentGoalViewMenu.java:765-790: a * background thread builds a {@code DefaultSMTSettings}, a {@code SolverLauncher} with a * listener, an {@code SMTProblem} for the goal and launches the union's solver types on the - * goal's services, then shows the outcome). Swing presents the outcome in the heavy - * {@code FullSmtSolverDialog} (progress dialog + countermodel application); the FX minimum - * shows a read-only result dialog with each solver's outcome and the combined final result — - * the counterexample-application UI of the Swing dialog is not ported. + * goal's services, then shows the outcome). smt (P1): the outcome is presented by the FX + * {@link SolverListenerF} run UI (progress dialog + information windows + result + * application) like the Swing {@code SolverListener}. */ private static MenuItem smtItem(NamedAction action, MenuContext ctx) { MenuItem item = new MenuItem(action.label()); @@ -478,70 +469,16 @@ private static MenuItem smtItem(NamedAction action, MenuContext ctx) { /** * menu: MP8 — launches the solver union of the SMT item on the clicked goal (Swing * CurrentGoalViewMenu.SMTAction.actionPerformed, CurrentGoalViewMenu.java:773-789). The - * launch is synchronous and blocking (SolverLauncher.launch waits for every solver), so it - * runs in a daemon thread; the read-only result dialog is presented back on the FX thread. + * launch is synchronous and blocking (SolverLauncher.launch waits for every solver), so + * {@link SolverListenerF#launchOnGoal} runs it in a daemon thread and presents the run in + * the FX progress dialog. */ private static void runSmt(NamedAction action, MenuContext ctx) { Goal goal = ctx.goal(); if (goal == null || !(action.payload() instanceof SolverTypeCollection union)) { return; } - Window owner = ctx.owner(); - Thread thread = new Thread(() -> { - DefaultSMTSettings settings = - new DefaultSMTSettings(goal.proof().getSettings().getSMTSettings(), - ProofIndependentSettings.DEFAULT_INSTANCE.getSMTSettings(), - goal.proof().getSettings().getNewSMTSettings(), goal.proof()); - SolverLauncher launcher = new SolverLauncher(settings); - // a listener suppresses the launcher's SolverException for failing solvers - // (SolverLauncher.notifyListenersOfStop, SolverLauncher.java:370-376); Swing's - // SolverListener plays the same role (CurrentGoalViewMenu.java:783) - launcher.addListener(new SolverLauncherListener() { - @Override - public void launcherStopped(SolverLauncher launcher, - Collection finishedSolvers) { - } - - @Override - public void launcherStarted(Collection problems, - Collection solverTypes, SolverLauncher launcher) { - } - }); - SMTProblem problem = new SMTProblem(goal); - String report; - try { - launcher.launch(union.getTypes(), List.of(problem), - goal.proof().getServices()); - StringBuilder sb = new StringBuilder(); - for (SMTSolver solver : problem.getSolvers()) { - SMTSolverResult result = solver.getFinalResult(); - sb.append(solver.name()).append(": ") - .append(result == null ? "no result" : result.isValid()).append("\n"); - } - sb.append("\n").append(problem.getFinalResult().isValid()); - report = sb.toString(); - } catch (RuntimeException ex) { - report = "SMT run failed: " + ex; - } - String finalReport = report; - Platform.runLater(() -> showSmtResult(union.toString(), finalReport, owner)); - }, "SMTRunner"); - thread.setDaemon(true); - thread.start(); - } - - /** - * menu: MP8 — read-only SMT result dialog (the FX minimum replacing the heavy Swing - * FullSmtSolverDialog progress/countermodel UI). - */ - private static void showSmtResult(String unionName, String report, Window owner) { - Alert alert = new Alert(AlertType.INFORMATION, report, ButtonType.OK); - alert.setTitle("SMT: " + unionName); - alert.setHeaderText(null); - if (owner != null) { - alert.initOwner(owner); - } - alert.showAndWait(); + SolverListenerF.launchOnGoal(ctx.mediator(), ctx.owner(), goal, union); } private static void showError(String message, String title, Window owner) { diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/smt/InformationWindowF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/smt/InformationWindowF.java new file mode 100644 index 00000000000..596610e5e85 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/smt/InformationWindowF.java @@ -0,0 +1,76 @@ +/* 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.smt; + +import java.util.List; +import javafx.scene.Scene; +import javafx.scene.control.Tab; +import javafx.scene.control.TabPane; +import javafx.scene.control.TextArea; +import javafx.scene.text.Font; +import javafx.stage.Stage; +import javafx.stage.Window; + +import de.uka.ilkd.key.gui.fx.theme.ThemeManager; + +/** + * The information window presents detailed information about the execution of an SMT solver, + * counter-part of the Swing {@code de.uka.ilkd.key.gui.smt.InformationWindow}: one tab per + * {@link Information} (error message, the SMT2 translation passed to the solver, the taclet + * translation, the solver output, translation warnings). + *

        + * KNOWN-SIMPLIFIED: the counterexample model tree ({@code CETree}) and the line numbers + * ({@code TextLineNumber}) of the Swing original are not ported — the entries are presented as + * read-only monospaced text areas, and the counterexample help tab (which the Swing original + * adds with the model tree) is omitted with it. + */ +public final class InformationWindowF extends Stage { + + /** + * One solver information entry, counter-part of the Swing + * {@code InformationWindow.Information}. + * + * @param title the tab title + * @param content the tab content + * @param solver the solver that produced the content + */ + public record Information(String title, String content, String solver) { + } + + public InformationWindowF(Window owner, String title, List information) { + setTitle(title); + if (owner != null) { + initOwner(owner); + } + + TabPane tabs = new TabPane(); + tabs.setTabClosingPolicy(TabPane.TabClosingPolicy.UNAVAILABLE); + for (Information el : information) { + TextArea area = new TextArea(el.content()); + area.setEditable(false); + area.setWrapText(false); + // the solver input/output is SMT2 text; a monospaced font keeps it readable (the + // Swing original used the sequent view font) + area.setFont(Font.font("Monospaced", 13)); + tabs.getTabs().add(new Tab(el.title(), area)); + } + + Scene scene = new Scene(tabs, 640, 520); + setScene(scene); + ThemeManager.getInstance().manage(scene); + } + + /** + * Shows the information window for the given entries (Swing opens a non-modal + * {@code JDialog} from {@code SolverListener.showInformation}). + * + * @param owner the owner window; may be {@code null} + * @param title the window title + * @param information the tabs to show + */ + public static void show(Window owner, String title, List information) { + new InformationWindowF(owner, title, information).show(); + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/smt/ProgressDialogF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/smt/ProgressDialogF.java new file mode 100644 index 00000000000..9b5e423e313 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/smt/ProgressDialogF.java @@ -0,0 +1,354 @@ +/* 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.smt; + +import java.util.List; +import javafx.beans.binding.Bindings; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.IntegerProperty; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleIntegerProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; +import javafx.beans.value.ChangeListener; +import javafx.collections.ObservableList; +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.ProgressBar; +import javafx.scene.control.TableCell; +import javafx.scene.control.TableColumn; +import javafx.scene.control.TableView; +import javafx.scene.control.Tooltip; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.StackPane; +import javafx.scene.paint.Color; +import javafx.stage.Modality; +import javafx.stage.Stage; +import javafx.stage.Window; + +import de.uka.ilkd.key.gui.fx.IssueDialogF; +import de.uka.ilkd.key.gui.fx.theme.ThemeManager; + +/** + * Dialog showing launched SMT processes and results, counter-part of the Swing + * {@code de.uka.ilkd.key.gui.smt.ProgressDialog} (key.ui {@code gui/smt/ProgressDialog.java}): + * an overall progress bar, a table with one row per problem and one process column per solver + * type (each cell a progress bar with a painted text and an "Info" button opening the + * {@link InformationWindowF}), and the Stop/Discard, Focus and Apply buttons. The dialog is + * application modal like the Swing original ({@code setModal(true)}); while the solvers run + * ({@link Modus#SOLVERS_RUNNING}) only Stop is active, once they are done + * ({@link Modus#SOLVERS_DONE}) the button becomes Discard and the results may be applied. + */ +public final class ProgressDialogF extends Stage { + + /** + * Current state of the dialog (Swing {@code ProgressDialog.Modus}). + */ + public enum Modus { + /** SMT solvers are running and may be stopped by the user. */ + SOLVERS_RUNNING, + /** SMT solvers are done (or terminated). Results may be applied by the user. */ + SOLVERS_DONE + } + + /** + * Button callbacks, counter-part of the Swing {@code ProgressDialogListener} (the info + * callback carries the solver index as {@code column} and the problem index as {@code row}). + */ + public interface Listener { + void applyButtonClicked(); + + void stopButtonClicked(); + + void discardButtonClicked(); + + void focusButtonClicked(); + + void infoButtonClicked(int column, int row); + } + + /** + * The state of one (problem, solver) table cell, counter-part of the Swing + * {@code ProgressModel.ProcessColumn.ProcessData}: an integer progress in + * {@code [0, resolution]}, the painted text and the text color (the result coloring from + * {@code SolverListener}: green/red/orange/blue). + */ + public static final class ProgressCellF { + final IntegerProperty progress = new SimpleIntegerProperty(0); + final StringProperty text = new SimpleStringProperty(""); + final ObjectProperty textColor = new SimpleObjectProperty<>(null); + final BooleanProperty editable = new SimpleBooleanProperty(false); + + public IntegerProperty progressProperty() { + return progress; + } + + public StringProperty textProperty() { + return text; + } + + public ObjectProperty textColorProperty() { + return textColor; + } + + public BooleanProperty editableProperty() { + return editable; + } + } + + /** + * One table row: the problem name and one {@link ProgressCellF} per solver column. + */ + public static final class ProgressRowF { + private final String name; + private final List cells; + + ProgressRowF(String name, List cells) { + this.name = name; + this.cells = cells; + } + + public String getName() { + return name; + } + + public List getCells() { + return cells; + } + } + + private final ObservableList rows; + private final Listener listener; + private final int resolution; + private final int max; + + private Modus modus = Modus.SOLVERS_RUNNING; + private ProgressBar overallBar; + private Label overallText; + private Button stopButton; + private Button focusButton; + private Button applyButton; + + public ProgressDialogF(Window owner, boolean counterexample, int resolution, int max, + List titles, ObservableList rows, Listener listener) { + this.rows = rows; + this.listener = listener; + this.resolution = resolution; + this.max = max; + setTitle(counterexample ? "SMT Counterexample Search" : "SMT Interface"); + if (owner != null) { + initOwner(owner); + } + initModality(Modality.APPLICATION_MODAL); + + TableView table = new TableView<>(rows); + table.setEditable(false); + table.setColumnResizePolicy(TableView.UNCONSTRAINED_RESIZE_POLICY); + // Swing titles[0] is the empty header of the problem-name column + TableColumn nameCol = new TableColumn<>(""); + nameCol.setCellValueFactory(data -> new SimpleStringProperty(data.getValue().getName())); + nameCol.setPrefWidth(180); + table.getColumns().add(nameCol); + for (int i = 1; i < titles.size(); i++) { + final int solverIndex = i - 1; + TableColumn col = new TableColumn<>(titles.get(i)); + col.setCellValueFactory( + data -> new SimpleObjectProperty<>(data.getValue().getCells().get(solverIndex))); + col.setCellFactory(c -> new ProgressTableCell(solverIndex)); + col.setPrefWidth(230); + table.getColumns().add(col); + } + + overallBar = new ProgressBar(0); + overallText = new Label(""); + StackPane.setAlignment(overallText, Pos.CENTER); + StackPane overall = new StackPane(overallBar, overallText); + overallBar.setMaxWidth(Double.MAX_VALUE); + HBox.setHgrow(overall, Priority.ALWAYS); + + stopButton = new Button("Stop"); + stopButton.setOnAction(e -> { + if (modus == Modus.SOLVERS_DONE) { + listener.discardButtonClicked(); + } + if (modus == Modus.SOLVERS_RUNNING) { + listener.stopButtonClicked(); + } + }); + BorderPane pane = new BorderPane(); + pane.setTop(overall); + HBox buttons = new HBox(5, stopButton); + if (!counterexample) { + // like Swing, Focus and Apply are hidden in counterexample mode + focusButton = new Button("Focus goals"); + focusButton.setTooltip(new Tooltip( + "Focus open goals to the formulas required to close them" + + " (as specified by the SMT solver's unsat core)")); + focusButton.setDisable(true); + focusButton.setOnAction(e -> notifySafely(listener::focusButtonClicked)); + applyButton = new Button("Apply"); + applyButton.setTooltip(new Tooltip( + "Apply the results (i.e. close goals if the SMT solver was successful)")); + applyButton.setDisable(true); + applyButton.setOnAction(e -> notifySafely(listener::applyButtonClicked)); + buttons.getChildren().addAll(focusButton, applyButton); + } + buttons.setPadding(new Insets(5)); + buttons.setAlignment(Pos.CENTER_RIGHT); + pane.setBottom(buttons); + pane.setCenter(table); + pane.setPadding(new Insets(5)); + + Scene scene = new Scene(pane, 780, 420); + setScene(scene); + ThemeManager.getInstance().manage(scene); + } + + private void notifySafely(Runnable call) { + // like Swing, exceptions during rule application must not be lost (IssueDialog) + try { + call.run(); + } catch (RuntimeException exception) { + IssueDialogF.showExceptionDialog(this, exception); + } + } + + /** + * Sets the overall progress (the number of finished (problem, solver) pairs) and the painted + * text (Swing {@code SolverListener.setProgressText}). + * + * @param finished the number of finished solver runs + */ + public void setOverallProgress(int finished) { + overallBar.setProgress(max == 0 ? 0 : (double) finished / max); + if (max == 1) { + overallText.setText(finished == 0 ? "Processing..." : "Finished."); + } else { + overallText.setText("Processed " + finished + " of " + max + " problems."); + } + } + + /** + * Switches the dialog state (Swing {@code ProgressDialog.setModus}): Stop becomes Discard + * and the Focus/Apply buttons are enabled once the solvers are done. + * + * @param m the new state + */ + public void setModus(Modus m) { + modus = m; + switch (modus) { + case SOLVERS_DONE -> { + stopButton.setText("Discard"); + if (applyButton != null) { + applyButton.setDisable(false); + } + if (focusButton != null) { + focusButton.setDisable(false); + } + } + case SOLVERS_RUNNING -> { + stopButton.setText("Stop"); + if (applyButton != null) { + applyButton.setDisable(true); + } + } + } + } + + /** + * Marks every cell editable (Swing {@code ProgressModel.setEditable}, called when all + * solvers are finished): the "Info" buttons of the cells become clickable. + */ + public void setEditable() { + for (ProgressRowF row : rows) { + for (ProgressCellF cell : row.getCells()) { + cell.editable.set(true); + } + } + } + + /** + * The cell of one process column: a progress bar with the painted text on top and an + * "Info" button, counter-part of the Swing {@code ProgressTable.ProgressPanel}. The cell + * re-binds its controls whenever the table hands it another {@link ProgressCellF}. + */ + private final class ProgressTableCell extends TableCell { + + private final ProgressBar bar = new ProgressBar(0); + private final Label text = new Label(); + private final Button info = new Button("Info"); + private final StackPane stack = new StackPane(bar, text); + private final HBox box = new HBox(4, stack, info); + private final int solverIndex; + + private ProgressCellF bound; + private ChangeListener colorListener; + + ProgressTableCell(int solverIndex) { + this.solverIndex = solverIndex; + bar.setMaxWidth(Double.MAX_VALUE); + HBox.setHgrow(stack, Priority.ALWAYS); + StackPane.setAlignment(text, Pos.CENTER); + info.setPrefWidth(56); + box.setPadding(new Insets(2)); + itemProperty().addListener((obs, old, value) -> rebind(old, value)); + } + + private void rebind(ProgressCellF old, ProgressCellF cell) { + if (bound != null) { + bar.progressProperty().unbind(); + text.textProperty().unbind(); + info.disableProperty().unbind(); + if (colorListener != null) { + bound.textColorProperty().removeListener(colorListener); + } + text.setTextFill(null); + bar.setStyle(""); + bound = null; + } + if (cell != null) { + bound = cell; + bar.progressProperty().bind( + Bindings.divide(cell.progressProperty(), (double) resolution)); + text.textProperty().bind(cell.textProperty()); + info.disableProperty().bind(cell.editableProperty().not()); + colorListener = (obs, o, n) -> applyColors(cell); + cell.textColorProperty().addListener(colorListener); + applyColors(cell); + } + } + + private void applyColors(ProgressCellF cell) { + Color color = cell.textColor.get(); + text.setTextFill(color); + bar.setStyle(color == null ? "" : "-fx-accent: " + toHex(color) + ";"); + } + + @Override + protected void updateItem(ProgressCellF item, boolean empty) { + super.updateItem(item, empty); + if (empty || item == null) { + setGraphic(null); + info.setOnAction(null); + } else { + setGraphic(box); + info.setOnAction(e -> listener.infoButtonClicked(solverIndex, getIndex())); + } + } + + private String toHex(Color color) { + return String.format("#%02x%02x%02x", (int) Math.round(color.getRed() * 255), + (int) Math.round(color.getGreen() * 255), + (int) Math.round(color.getBlue() * 255)); + } + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/smt/SolverListenerF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/smt/SolverListenerF.java new file mode 100644 index 00000000000..a4616c1b360 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/smt/SolverListenerF.java @@ -0,0 +1,772 @@ +/* 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.smt; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import javafx.animation.KeyFrame; +import javafx.animation.Timeline; +import javafx.application.Platform; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.scene.control.Alert; +import javafx.scene.control.Alert.AlertType; +import javafx.scene.control.ButtonType; +import javafx.scene.paint.Color; +import javafx.stage.Window; +import javafx.util.Duration; + +import de.uka.ilkd.key.control.AbstractProofControl; +import de.uka.ilkd.key.core.fx.KeYMediatorF; +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.smt.InformationWindowF.Information; +import de.uka.ilkd.key.gui.fx.smt.ProgressDialogF.ProgressCellF; +import de.uka.ilkd.key.gui.fx.smt.ProgressDialogF.ProgressRowF; +import de.uka.ilkd.key.gui.fx.theme.Theme; +import de.uka.ilkd.key.gui.fx.theme.ThemeManager; +import de.uka.ilkd.key.logic.DefaultVisitor; +import de.uka.ilkd.key.logic.JTerm; +import de.uka.ilkd.key.logic.op.IProgramMethod; +import de.uka.ilkd.key.logic.op.JModality; +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.settings.DefaultSMTSettings; +import de.uka.ilkd.key.settings.ProofIndependentSMTSettings.ProgressMode; +import de.uka.ilkd.key.settings.ProofIndependentSettings; +import de.uka.ilkd.key.smt.SMTFocusResults; +import de.uka.ilkd.key.smt.SMTProblem; +import de.uka.ilkd.key.smt.SMTRule; +import de.uka.ilkd.key.smt.SMTSolver; +import de.uka.ilkd.key.smt.SMTSolver.ReasonOfInterruption; +import de.uka.ilkd.key.smt.SMTSolverResult.ThreeValuedTruth; +import de.uka.ilkd.key.smt.SolverLauncher; +import de.uka.ilkd.key.smt.SolverLauncherListener; +import de.uka.ilkd.key.smt.SolverTypeCollection; +import de.uka.ilkd.key.smt.solvertypes.SolverType; +import de.uka.ilkd.key.smt.solvertypes.SolverTypes; +import de.uka.ilkd.key.taclettranslation.assumptions.TacletSetTranslation; + +import org.key_project.logic.Term; +import org.key_project.prover.sequent.PosInOccurrence; +import org.key_project.util.collection.ImmutableList; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Launches SMT solvers and presents their progress in the {@link ProgressDialogF}, + * counter-part of the Swing {@code de.uka.ilkd.key.gui.smt.SolverListener}. The listener is + * called by the {@link SolverLauncher} on its background thread and posts every UI interaction + * to the FX thread; a {@link Timeline} polls the solver states (the Swing original used a + * {@code java.util.Timer} with the same task) and writes the progress, the remaining time and + * the result coloring (green/red/orange/blue from the {@code [solverListener]} palette colors) + * into the table cells. + *

        + * KNOWN-SIMPLIFIED: the results are applied directly ({@code SMTRule} built-in rule application + * with the unsat core if available) instead of through the Swing {@code SMTProofApplyUserAction} + * history mechanism (no undo entry for the automatic CLOSE-mode application), and the input + * freeze around the rule application is not available yet (the {@code stopInterface} P0 + * remainder). + */ +public class SolverListenerF implements SolverLauncherListener { + + private static final Logger LOGGER = LoggerFactory.getLogger(SolverListenerF.class); + + /** + * The progress bar resolution of one solver run (Swing {@code SolverListener.RESOLUTION}). + */ + private static final int RESOLUTION = 1000; + + /** the poll interval of the dialog refresh (the Swing original polled every 10 ms) */ + private static final long POLL_MILLIS = 100; + + private static int FILE_ID = 0; + + private final DefaultSMTSettings settings; + private final Proof smtProof; + private final Window owner; + private final KeYMediatorF mediator; + + /** + * Every intern SMT problem refers to one solver. + */ + private final List problems = new ArrayList<>(); + + private List smtProblems = new ArrayList<>(); + /** the table cells indexed by {@code [problemIndex][solverIndex]} */ + private List> cellGrid = new ArrayList<>(); + private boolean[][] problemProcessed; + private int finishedCounter; + private Timeline timer; + + private ProgressDialogF progressDialog; + + /** + * the currently visible progress dialog; used by the {@code key.fx.verify.smt} hook to + * discard a dialog whose run did not complete (the dialog is application modal). + */ + private static ProgressDialogF currentDialog; + + public SolverListenerF(DefaultSMTSettings settings, Proof smtProof, Window owner, + KeYMediatorF mediator) { + this.settings = settings; + this.smtProof = smtProof; + this.owner = owner; + this.mediator = mediator; + } + + /** + * One solver run on one problem, counter-part of the Swing + * {@code SolverListener.InternSMTProblem}: the table position, the timing and the + * {@link Information} entries presented by the {@link InformationWindowF}. + */ + public static final class InternSMTProblemF { + final int problemIndex; + final int solverIndex; + final SMTProblem problem; + final SMTSolver solver; + private final List information = new ArrayList<>(); + private boolean stopped = false; + private boolean running = false; + + private long timeToSolve; + + InternSMTProblemF(int problemIndex, int solverIndex, SMTProblem problem, + SMTSolver solver) { + this.problemIndex = problemIndex; + this.solverIndex = solverIndex; + this.problem = problem; + this.solver = solver; + } + + public int getSolverIndex() { + return solverIndex; + } + + public int getProblemIndex() { + return problemIndex; + } + + public SMTProblem getProblem() { + return problem; + } + + public SMTSolver getSolver() { + return solver; + } + + public List getInformation() { + return information; + } + + private void addInformation(String title, String content) { + information.add(new Information(title, content, solver.name())); + } + + /** + * Collects the per-solver information entries (Swing + * {@code SolverListener.InternSMTProblem.createInformation}): the error message, the + * SMT2 translation, the taclet translation, the solver output and the warnings. + */ + public void createInformation() { + if (solver.getException() != null) { + StringWriter writer = new StringWriter(); + solver.getException().printStackTrace(new PrintWriter(writer)); + addInformation("Error-Message", + solver.getException().toString() + "\n\n" + writer); + } + addInformation("Solver Input", solver.getRawSolverInput()); + if (solver.getTacletTranslation() != null) { + addInformation("Taclets", solver.getTacletTranslation().toString()); + } + addInformation("Solver Output", solver.getRawSolverOutput()); + + Collection exceptionsOfTacletTranslation = + solver.getExceptionsOfTacletTranslation(); + if (!exceptionsOfTacletTranslation.isEmpty()) { + StringBuilder exceptionText = new StringBuilder( + "The following exceptions have ocurred while translating the taclets:\n\n"); + int i = 1; + for (Throwable e : exceptionsOfTacletTranslation) { + exceptionText.append(i).append(". ").append(e.getMessage()); + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + e.printStackTrace(pw); + exceptionText.append("\n\n").append(sw); + exceptionText.append("\n #######################\n\n"); + i++; + } + addInformation("Warning", exceptionText.toString()); + } + + if (solver.getType().supportHasBeenChecked() + && !solver.getType().isSupportedVersion()) { + addInformation("Solver Support", computeSolverTypeWarningMessage(solver.getType())); + } + } + + @Override + public String toString() { + return solver.name() + " applied on " + problem.getName(); + } + + String getTimeInSecAsString() { + long intPart = timeToSolve / 1000; + long decPart = timeToSolve % 1000; + String decString = decPart >= 100 ? Long.toString(decPart) + : decPart >= 10 ? "0" + decPart : "00" + decPart; + return intPart + "." + decString + "s"; + } + + void startTime() { + if (!running) { + timeToSolve = System.currentTimeMillis(); + running = true; + } + } + + void stopTime() { + if (!stopped) { + timeToSolve = System.currentTimeMillis() - timeToSolve; + stopped = true; + } + } + } + + /** + * Runs the solver union on every open goal of the proof (Swing + * {@code SMTInvokeAction.actionPerformed}, SMTInvokeAction.java:75-96): a background thread + * builds the settings, the launcher with this listener and launches + * {@code SMTProblem.createSMTProblems(proof)}. + * + * @param mediator the FX mediator (used by the Apply/Focus buttons) + * @param owner the owner window of the progress dialog; may be {@code null} + * @param proof the proof to run the solvers on + * @param solverUnion the solvers/solver types to start + */ + public static void launchOnProof(KeYMediatorF mediator, Window owner, Proof proof, + SolverTypeCollection solverUnion) { + launch(mediator, owner, proof, SMTProblem.createSMTProblems(proof), + solverUnion.getTypes()); + } + + /** + * Runs the solver union on the single given goal (Swing + * {@code CurrentGoalViewMenu.SMTAction}, CurrentGoalViewMenu.java:765-790). + * + * @param mediator the FX mediator (used by the Apply/Focus buttons) + * @param owner the owner window of the progress dialog; may be {@code null} + * @param goal the goal whose sequent is translated + * @param solverUnion the solvers/solver types to start + */ + public static void launchOnGoal(KeYMediatorF mediator, Window owner, Goal goal, + SolverTypeCollection solverUnion) { + launch(mediator, owner, goal.proof(), List.of(new SMTProblem(goal)), + solverUnion.getTypes()); + } + + /** + * Builds the settings and the launcher with this listener and launches the given problems + * (Swing SMTInvokeAction/CurrentGoalViewMenu.SMTAction launch bodies). The launch is + * synchronous (it blocks until every solver finished) and runs in a daemon background + * thread; the dialog is presented on the FX thread via the listener callbacks. + * + * @param mediator the FX mediator (used by the Apply/Focus buttons) + * @param owner the owner window of the progress dialog; may be {@code null} + * @param proof the proof the problems belong to + * @param smtProblems the problems to translate and solve + * @param solverTypes the solver types to run on each problem + */ + public static void launch(KeYMediatorF mediator, Window owner, Proof proof, + Collection smtProblems, Collection solverTypes) { + Thread thread = new Thread(() -> { + DefaultSMTSettings settings = + new DefaultSMTSettings(proof.getSettings().getSMTSettings(), + ProofIndependentSettings.DEFAULT_INSTANCE.getSMTSettings(), + proof.getSettings().getNewSMTSettings(), proof); + SolverLauncher launcher = new SolverLauncher(settings); + launcher.addListener(new SolverListenerF(settings, proof, owner, mediator)); + launcher.launch(solverTypes, smtProblems, proof.getServices()); + }, "SMTRunner"); + thread.setDaemon(true); + thread.start(); + } + + @Override + public void launcherStarted(Collection smtproblems, + Collection solverTypes, SolverLauncher launcher) { + // launch() is synchronous (it blocks the background thread until every solver finished), + // so the dialog is always prepared before launcherStopped is posted to the FX thread + // (FIFO queue) — the Swing original had the same ordering via invokeLater + Platform.runLater(() -> prepareDialog(smtproblems, solverTypes, launcher)); + } + + @Override + public void launcherStopped(SolverLauncher launcher, Collection problemSolvers) { + Platform.runLater(() -> finish(launcher)); + } + + private void finish(SolverLauncher launcher) { + stopTimer(); + storeInformation(); + progressDialog.setEditable(); + refreshDialog(); + progressDialog.setModus(ProgressDialogF.Modus.SOLVERS_DONE); + for (InternSMTProblemF problem : problems) { + problem.createInformation(); + } + if (settings.getModeOfProgressDialog() == ProgressMode.CLOSE) { + applyEvent(launcher); + } + } + + private void prepareDialog(Collection smtproblems, + Collection solverTypes, SolverLauncher launcher) { + this.smtProblems = new ArrayList<>(smtproblems); + boolean ce = solverTypes.contains(SolverTypes.Z3_CE_SOLVER); + + List titles = new ArrayList<>(); + titles.add(""); + ObservableList rows = FXCollections.observableArrayList(); + problemProcessed = new boolean[solverTypes.size()][smtProblems.size()]; + + int x = 0; + for (SMTProblem problem : smtProblems) { + List solvers = new ArrayList<>(problem.getSolvers()); + List cells = new ArrayList<>(); + int y = 0; + for (SMTSolver solver : solvers) { + problems.add(new InternSMTProblemF(x, y, problem, solver)); + cells.add(new ProgressCellF()); + y++; + } + cellGrid.add(cells); + rows.add(new ProgressRowF(problem.getName(), cells)); + x++; + } + for (SolverType type : solverTypes) { + titles.add(type.getName()); + } + + progressDialog = + new ProgressDialogF(owner, ce, RESOLUTION, smtProblems.size() * solverTypes.size(), + titles, rows, createDialogListener(launcher, ce)); + currentDialog = progressDialog; + progressDialog.setOverallProgress(0); + progressDialog.show(); + startTimer(); + } + + private ProgressDialogF.Listener createDialogListener(SolverLauncher launcher, boolean ce) { + return new ProgressDialogF.Listener() { + @Override + public void infoButtonClicked(int column, int row) { + InternSMTProblemF problem = getProblem(column, row); + if (problem != null) { + showInformation(problem); + } + } + + @Override + public void stopButtonClicked() { + stopEvent(launcher); + } + + @Override + public void applyButtonClicked() { + applyEvent(launcher); + } + + @Override + public void discardButtonClicked() { + discardEvent(launcher, ce); + } + + @Override + public void focusButtonClicked() { + focusResults(); + } + }; + } + + private InternSMTProblemF getProblem(int column, int row) { + for (InternSMTProblemF problem : problems) { + if (problem.problemIndex == row && problem.solverIndex == column) { + return problem; + } + } + return null; + } + + private void startTimer() { + timer = new Timeline(new KeyFrame(Duration.millis(POLL_MILLIS), e -> refreshDialog())); + timer.setCycleCount(Timeline.INDEFINITE); + timer.play(); + } + + private void stopTimer() { + if (timer != null) { + timer.stop(); + timer = null; + } + } + + private void refreshDialog() { + for (InternSMTProblemF problem : problems) { + refreshProgressOfProblem(problem); + } + } + + private void refreshProgressOfProblem(InternSMTProblemF problem) { + switch (problem.solver.getState()) { + case Running -> running(problem); + case Stopped -> stopped(problem); + case Waiting -> { + // the solver has not been started yet (bounded process count) + } + } + } + + private void running(InternSMTProblemF problem) { + problem.startTime(); + long maxTime = problem.solver.getTimeout(); + long startTime = problem.solver.getStartTime(); + long currentTime = System.currentTimeMillis(); + long progress = RESOLUTION - ((startTime - currentTime) * RESOLUTION) / maxTime; + cell(problem).progress.set((int) Math.min(Math.max(progress, 0), RESOLUTION)); + float remainingTime = Math.max((startTime - currentTime) / 100 / 10.0f, 0); + cell(problem).text.set(remainingTime + " sec."); + } + + private void stopped(InternSMTProblemF problem) { + problem.stopTime(); + + int x = problem.getSolverIndex(); + int y = problem.getProblemIndex(); + + if (!problemProcessed[x][y]) { + finishedCounter++; + progressDialog.setOverallProgress(finishedCounter); + problemProcessed[x][y] = true; + } + + if (problem.solver.wasInterrupted()) { + interrupted(problem); + } else if (problem.solver.getFinalResult().isValid() == ThreeValuedTruth.VALID) { + successfullyStopped(problem); + } else if (problem.solver.getFinalResult().isValid() == ThreeValuedTruth.FALSIFIABLE) { + unsuccessfullyStopped(problem); + } else { + unknownStopped(problem); + } + } + + private void interrupted(InternSMTProblemF problem) { + ReasonOfInterruption reason = problem.solver.getReasonOfInterruption(); + ProgressCellF cell = cell(problem); + switch (reason) { + case Exception -> { + cell.progress.set(0); + cell.textColor.set(colorOf(ColorPaletteF.SMT_RED)); + cell.text.set("Exception!"); + } + case NoInterruption -> throw new RuntimeException("This position is not reachable!"); + case Timeout -> { + cell.progress.set(0); + cell.text.set("Timeout."); + } + case User -> cell.text.set("Interrupted by user."); + } + } + + private void successfullyStopped(InternSMTProblemF problem) { + String timeInfo = " (" + problem.getTimeInSecAsString() + ")"; + ProgressCellF cell = cell(problem); + cell.progress.set(0); + cell.textColor.set(colorOf(ColorPaletteF.SMT_GREEN)); + if (problem.solver.getType() == SolverTypes.Z3_CE_SOLVER) { + cell.text.set("No Counterexample."); + } else { + cell.text.set("Valid" + timeInfo); + } + } + + private void unsuccessfullyStopped(InternSMTProblemF problem) { + String timeInfo = " (" + problem.getTimeInSecAsString() + ")"; + ProgressCellF cell = cell(problem); + cell.progress.set(0); + if (problem.solver.getType() == SolverTypes.Z3_CE_SOLVER) { + cell.textColor.set(colorOf(ColorPaletteF.SMT_RED)); + cell.text.set("Counter Example" + timeInfo); + } else { + cell.textColor.set(Color.rgb(200, 150, 0)); + cell.text.set("Possible Counter Example" + timeInfo); + } + } + + private void unknownStopped(InternSMTProblemF problem) { + ProgressCellF cell = cell(problem); + cell.progress.set(0); + cell.textColor.set(Color.BLUE); + cell.text.set("Unknown."); + } + + private ProgressCellF cell(InternSMTProblemF problem) { + return cellGrid.get(problem.problemIndex).get(problem.solverIndex); + } + + /** + * Applies the results (Swing {@code SolverListener.applyResults}): every goal whose solver + * returned a valid result is closed with the {@code SMTRule} built-in rule application (with + * the unsat core if the solver provided one). The Swing original wraps this in a + * {@code SMTProofApplyUserAction} (undoable); the FX port applies directly. + */ + private void applyResults() { + Set goalsClosed = new HashSet<>(); + for (InternSMTProblemF problem : problems) { + Goal goal = problem.problem.getGoal(); + if (goalsClosed.contains(goal) + || problem.solver.getFinalResult().isValid() != ThreeValuedTruth.VALID) { + continue; + } + goalsClosed.add(goal); + ImmutableList unsatCore = + SMTFocusResults.getUnsatCore(problem.problem); + IBuiltInRuleApp app; + if (unsatCore != null) { + app = SMTRule.INSTANCE.createApp(problem.solver.name(), unsatCore); + } else { + app = SMTRule.INSTANCE.createApp(problem.solver.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); + } + // switch to new open goal (Swing: mediator.getSelectionModel().defaultSelection(); + // the mediator.stopInterface/startInterface input freeze is not ported yet) + mediator.getSelectionModel().defaultSelection(); + } + + /** + * Reduce the sequent on each open goal to the formulas present in the unsat core computed + * by one of the SMT solvers (Swing {@code SolverListener.focusResults}). + */ + private void focusResults() { + Set focusedGoals = new HashSet<>(); + Set failedToFocus = new HashSet<>(); + for (InternSMTProblemF problem : problems) { + Goal goal = problem.problem.getGoal(); + Node goalNode = goal.node(); + if (focusedGoals.contains(goal) + || problem.solver.getFinalResult().isValid() != ThreeValuedTruth.VALID) { + continue; // already done + } + if (SMTFocusResults.focus(problem.problem, mediator.getServices())) { + focusedGoals.add(goal); + failedToFocus.remove(goal); + + // focus SMT application + if (goalNode == mediator.getSelectedNode()) { + mediator.getSelectionModel().setSelectedNode(goal.node()); + } + } else { + failedToFocus.add(goal); + } + } + if (!failedToFocus.isEmpty()) { + Alert alert = new Alert(AlertType.ERROR, + "None of the SMT solvers provided an unsat core for one of the goals.", + ButtonType.OK); + alert.setTitle("Failed to use unsat core"); + alert.setHeaderText(null); + if (owner != null) { + alert.initOwner(owner); + } + alert.show(); + } + } + + private void showInformation(InternSMTProblemF problem) { + InformationWindowF.show(owner, "Information for " + problem, problem.getInformation()); + } + + private void stopEvent(SolverLauncher launcher) { + launcher.stop(); + } + + private void discardEvent(SolverLauncher launcher, boolean counterexample) { + launcher.stop(); + progressDialog.close(); + currentDialog = null; + // remove semantics blasting proof for ce dialog + if (counterexample && smtProof != null) { + smtProof.dispose(); + } + } + + private void applyEvent(SolverLauncher launcher) { + launcher.stop(); + applyResults(); + /* + * Previously, the progressDialog was only made invisible which enabled users to click + * the apply button more than once, each time creating a new SMT goal. Disposing of the + * dialog is fine as it is created anew each time a SolverLauncher is started anyway + * (see #launcherStarted(), #prepareDialog()). (comment preserved from Swing) + */ + progressDialog.close(); + currentDialog = null; + } + + /** + * Discards the currently visible progress dialog, if any ({@code key.fx.verify.smt} hook: + * a run that did not complete would otherwise leave the application-modal dialog blocking + * the UI). + */ + public static void discardCurrentDialog() { + ProgressDialogF dialog = currentDialog; + if (dialog != null) { + dialog.close(); + currentDialog = null; + } + } + + private void storeInformation() { + if (settings.storeSMTTranslationToFile() + || (settings.makesUseOfTaclets() && settings.storeTacletTranslationToFile())) { + for (InternSMTProblemF problem : problems) { + storeInformation(problem.getProblem()); + } + } + } + + private void storeInformation(SMTProblem problem) { + for (SMTSolver solver : problem.getSolvers()) { + if (settings.storeSMTTranslationToFile()) { + storeSMTTranslation(solver, problem.getGoal(), solver.getTranslation()); + } + if (settings.makesUseOfTaclets() && settings.storeTacletTranslationToFile() + && solver.getTacletTranslation() != null) { + storeTacletTranslation(solver, problem.getGoal(), solver.getTacletTranslation()); + } + } + } + + private void storeTacletTranslation(SMTSolver solver, Goal goal, + TacletSetTranslation translation) { + String path = settings.getPathForTacletTranslation(); + path = finalizePath(path, solver, goal); + storeToFile(translation.toString(), path); + } + + private void storeSMTTranslation(SMTSolver solver, Goal goal, String problemString) { + String path = settings.getPathForSMTTranslation(); + + String fileName = + goal.proof().name() + "_" + goal.getTime() + "_" + solver.name() + ".smt"; + path = path + File.separator + fileName; + path = finalizePath(path, solver, goal); + storeToFile(problemString, path); + } + + private void storeToFile(String text, String path) { + try { + final BufferedWriter out2 = + new BufferedWriter(new FileWriter(path, StandardCharsets.UTF_8)); + out2.write(text); + out2.close(); + } catch (IOException e) { + throw new RuntimeException("Could not store to file " + path + ".", e); + } + } + + private String finalizePath(String path, SMTSolver solver, Goal goal) { + Calendar c = Calendar.getInstance(); + String date = + c.get(Calendar.YEAR) + "-" + c.get(Calendar.MONTH) + "-" + c.get(Calendar.DATE); + String time = c.get(Calendar.HOUR_OF_DAY) + "-" + c.get(Calendar.MINUTE) + "-" + + c.get(Calendar.SECOND); + + path = path.replaceAll("%d", date); + path = path.replaceAll("%s", solver.name()); + path = path.replaceAll("%t", time); + path = path.replaceAll("%i", Integer.toString(FILE_ID++)); + path = path.replaceAll("%g", Integer.toString(goal.node().serialNr())); + + return path; + } + + /** + * @return the current theme's color of a palette property (the Swing + * {@code ColorSettings.ColorProperty.get()} of the active theme) + */ + private static Color colorOf(ColorSettingsF.ColorPropertyF property) { + return ThemeManager.getInstance().themeProperty().get() == Theme.DARK + ? property.getDarkValue() + : property.getLightValue(); + } + + /** + * @return the warning message of an unsupported solver version (Swing + * {@code SolverListener.computeSolverTypeWarningMessage}) + */ + public static String computeSolverTypeWarningMessage(SolverType type) { + return (""" + You are using a version of %s which has not been tested for this version of KeY. + It can therefore be that errors occur that would not occur + using the following version or higher: + %s""").formatted(type.getName(), type.getMinimumSupportedVersion()); + } + + /** + * Checks if the given {@link JTerm} contains a modality, query, or update (Swing + * {@code SolverListener.containsModalityOrQuery}). + * + * @param term the term to check + * @return {@code true} contains at least one modality or query + */ + public static boolean containsModalityOrQuery(JTerm term) { + ContainsModalityOrQueryVisitor visitor = new ContainsModalityOrQueryVisitor(); + term.execPostOrder(visitor); + return visitor.containsModOrQuery(); + } + + /** + * Utility class used to check whether a term contains constructs that are not handled by the + * SMT translation (port of the Swing + * {@code SolverListener.ContainsModalityOrQueryVisitor}). + */ + protected static class ContainsModalityOrQueryVisitor implements DefaultVisitor { + + boolean containsModQuery = false; + + @Override + public void visit(Term visited) { + if (visited.op() instanceof JModality || visited.op() instanceof IProgramMethod) { + containsModQuery = true; + } + } + + public boolean containsModOrQuery() { + return containsModQuery; + } + } +} From e80815c5e9d7c0ece3ac316949798dbf7748af71 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Sat, 10 Oct 2026 11:17:33 +0200 Subject: [PATCH 130/281] loading/exit (P1): confirmExit close-request flow, exit persistence, recent-file loading options MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Exit flow (Swing ExitMainAction, reached from the File menu AND the window close button via the Swing window listener, MainWindow.java:366): the FX Exit item and stage.setOnCloseRequest run the same flow — the Confirm Exit view setting gates a "Really Quit?" YES/NO dialog; exitApplicationWithoutInteraction saves the recent-files store, logs "Have a nice day." and calls Platform.exit() + System.exit(0) (Swing exitSystem path; required because background threads of a running solver would keep the process alive after Platform.exit). System.exit(0) also runs the docking/colors/keystrokes store shutdown hooks, which the close-request verify confirms ("Docking layout saved"). KNOWN-SIMPLIFIED: the Swing mediator fireShutDown event (GUIListener.shutDown) has no FX seam — KeYMediatorF has no GUI listener registry, so it is omitted. - Recent-files loading options (Swing WindowUserInterfaceControl.loadProblem + RecentFileAction.actionPerformed): openProofFile(Path, LoadOptions) now registers the options with the entry (profile ident, single-java flag, additional profile options) instead of dropping them; the new openRecentFile (wired into the recent-files menu) restores the stored options on open — the profile ident resolves through the DefaultProfileResolver services (a missing profile warns like the Swing JOptionPane), bundles keep the proof-selection path. RecentFilesF.save() is public now (Swing RecentFileMenu.save) and a restore(List) was added for the verify round trip. - key.fx.verify.loadingexit (whitelisted on the run task): (1) recent-files store round trip — an entry registered with profile + singleJava survives the save/load cycle (temp file + snapshot/restore of the shared store) plus the profile-ident resolution used by the menu open; (2) fires WINDOW_CLOSE_REQUEST with Confirm Exit off — the process must terminate with exit code 0. - README flag catalog + PARITY SIGNOFF updated (recent-file options row and exit-flow row RESOLVED; the fireShutDown omission stays on the ledger). Verified on Xvnc :99: loadingexit verification PASS (store round trip + profile resolution) — closing the window; process exit code 0 with the docking layout saved through the shutdown hook. --- key.ui.fx/KNOWN-SIMPLIFIED.md | 7 +- key.ui.fx/PARITY-SIGNOFF.md | 7 +- key.ui.fx/README.md | 1 + key.ui.fx/build.gradle | 6 +- .../de/uka/ilkd/key/gui/fx/MainWindowF.java | 183 +++++++++++++++++- .../key/gui/fx/recentfiles/RecentFilesF.java | 20 +- 6 files changed, 213 insertions(+), 11 deletions(-) diff --git a/key.ui.fx/KNOWN-SIMPLIFIED.md b/key.ui.fx/KNOWN-SIMPLIFIED.md index d951bf5c9bc..9dbd58db0e2 100644 --- a/key.ui.fx/KNOWN-SIMPLIFIED.md +++ b/key.ui.fx/KNOWN-SIMPLIFIED.md @@ -10,14 +10,14 @@ Marker counts per module: | Module | Markers | |---|---| -| `key.ui.fx` (contrib extensions + SMT run UI) | 5 | +| `key.ui.fx` (contrib extensions + SMT run UI + exit flow) | 6 | | `keyext.caching.fx` | 10 | | `keyext.exploration.fx` | 9 | | `keyext.isabelletranslation.fx` | 4 | | `keyext.slicing.fx` | 16 | | `keyext.ui.testgen.fx` | 18 | | `keyext.proofmanagement.fx` | 4 | -| **Total** | **66** | +| **Total** | **67** | To re-derive this table at any point: `grep -rn 'KNOWN-SIMPLIFIED' */src/main */src/test`. @@ -35,12 +35,13 @@ milestone, **FIXED-UPSTREAM** items differ from a Swing bug that the port delibe | `extension/contrib/HeatmapF.java:37` | — (doubles the `@Info` description) | same decision restated for the extension dialog | OPEN | | `extension/contrib/ParallelProverStatusIndicatorF.java:27` | toggle button "SC" / "MT N×" with left-click toggle and right-click worker-count menu | plain `Label` showing the live auto-mode state ("Auto"/"Manual"), left-click toggles `PARALLEL_PROVER_ENABLED`, reacts to property changes; worker-count picker and button styling out of scope | OPEN | -## `key.ui.fx` — SMT run UI (2) +## `key.ui.fx` — SMT run UI + exit flow (3) | Site | Swing original | FX port | Status | |---|---|---|---| | `SolverListenerF.java:81` | results applied via `SMTProofApplyUserAction` (undoable history entry) with the `stopInterface` input freeze around it | direct `SMTRule` application on the FX thread (no undo entry for the automatic CLOSE-mode application; the input freeze is the P0 `stopInterface` remainder) | OPEN | | `InformationWindowF.java:24` | counterexample model tree (`CETree`) with line numbers (`TextLineNumber`) and the counterexample help tab | tabs per information entry as read-only monospaced text areas; model tree + line numbers + help tab not ported | OPEN | +| `MainWindowF.java:3486` | exit fires the mediator shutdown event (`fireShutDown` → `GUIListener.shutDown`) | the event is omitted — `KeYMediatorF` has no GUI listener registry; the other shutdown side effects (recent-files save, `System.exit(0)` running the store shutdown hooks) are ported | OPEN | ## `keyext.caching.fx` (10) diff --git a/key.ui.fx/PARITY-SIGNOFF.md b/key.ui.fx/PARITY-SIGNOFF.md index e03c99cd1ec..1fd720b5dfe 100644 --- a/key.ui.fx/PARITY-SIGNOFF.md +++ b/key.ui.fx/PARITY-SIGNOFF.md @@ -48,7 +48,7 @@ P0 bugs from the audit: | FX search stale on node switch | **RESOLVED** (search re-runs on reprint; sequent-leftovers milestone `18df960b35`) | | Proof-tree context menu acts on the selection, not the clicked node | `OPEN (unverified)` | | View-menu Pretty Print / Unicode inert placeholders | `OPEN` (placeholders remain) | -| Recent-file clicks drop stored profile / single-java options | `OPEN` (loading-options dialog itself is ported — see below) | +| Recent-file clicks drop stored profile / single-java options | **RESOLVED** (P1: `openProofFile` registers the loading options with the entry like Swing `WindowUserInterfaceControl.loadProblem`; the recent-files menu restores profile (resolved via `DefaultProfileResolver`) / additional options / single-java like Swing `RecentFileAction`, `key.fx.verify.loadingexit` round trip PASS) | | Shortcut-default mismatches (`KeyStrokeManagerF`: tree search, sequent search, macro defaults) | `OPEN (unverified)` | | Dead registered bindings (Ctrl+C term copy, F3/Shift+F3, Ctrl+K/Ctrl+J, Goal Back/Prune) | `PARTIAL` (term-menu copy + menu accelerators exist; several bindings unverified) | | Colors: 12 mapped CSS variables ineffective (47 property definitions missing) | **RESOLVED** (P1: `ColorPaletteF` defines all 51 Swing-parity properties — the true count incl. the two multi-line `define(` keys; the 9 previously-undeclared mapped CSS vars are declared in both themes and consumed by the re-wired `.sequent-hl-*`/`.source-*` rules; `key.fx.verify.colors` PASS) | @@ -64,7 +64,7 @@ P0 bugs from the audit: | Join / merge dialogs | **RESOLVED** for the merge/join flow (`key.fx.verify.joinmerge` PASS on gcd 32/0); `keyext.slicing.fx` SMT routing partial | | Strategy preset combo / stash UI | `OPEN` | | SMT settings + run UI | **RESOLVED** (P1: settings providers ported in MP4; the run UI — `SolverListenerF` + `ProgressDialogF` progress table + `InformationWindowF` + result application via the SMT rule — ported, `key.fx.verify.smt` PASS: Z3 closes the Agatha goal; the Swing toolbar `DropdownSelectionButton` remains tracked under the toolbar item) | -| Exit flow (close-request, `confirmExit`, layout save) | `OPEN (unverified)` | +| Exit flow (close-request, `confirmExit`, layout save) | **RESOLVED** (P1: the window close button and the Exit menu item run the Swing `ExitMainAction` flow — `confirmExit` dialog, recent-files save, `System.exit(0)` which runs the docking/colors/keystrokes shutdown hooks; the Swing mediator `fireShutDown` has no FX seam — see KNOWN-SIMPLIFIED; `key.fx.verify.loadingexit` PASS: recent-files round trip + profile resolution, then the close-request path terminates with exit code 0 and the docking layout is saved) | | Term labels; Pretty Print / Unicode wiring | `OPEN` (lemmaorigin hook covers labels mechanically — see `key.fx.verify.lemmaorigin`) | | User-selection highlight + Ctrl+C copy | **RESOLVED** (term-menu copy item in the skeleton); multi-selection highlight + reprint persistence `OPEN` | | Symbex source line highlights + sequent-hover origin cross-highlight | `OPEN` | @@ -100,7 +100,8 @@ as open. 6. Keyboard: shortcut-default regressions; dead bindings audit. 7. Options dialogs: strategy preset UI, HeatmapOptionsDialog (SMT run UI RESOLVED via P1 `key.fx.verify.smt`). -8. Exit flow: close-request + confirmExit + layout persistence on close. +8. Exit flow: close-request + confirmExit + layout persistence on close (RESOLVED via P1 + `key.fx.verify.loadingexit`; the Swing `fireShutDown` event has no FX seam — KNOWN-SIMPLIFIED). 9. Colors/theme: all 51 Swing-parity color property definitions RESOLVED (P1 `key.fx.verify.colors` PASS); theme persistence remains. 10. Notification framework depth: proof-closed/exception dialogs beyond toasts are in (`IssueDialogF`); the `NotificationTask`/action framework remains partial. diff --git a/key.ui.fx/README.md b/key.ui.fx/README.md index c2eeaa7ec17..12195bb0d80 100644 --- a/key.ui.fx/README.md +++ b/key.ui.fx/README.md @@ -71,6 +71,7 @@ regression-tested on a virtual display. Demo proof paths default into | `key.fx.verify.javacsettings` | javac settings provider | | `key.fx.verify.colors` | colors palette: Swing-parity property count, mapped CSS-variable wiring, override round trip | | `key.fx.verify.smt` | SMT run UI end to end (needs an installed solver, e.g. `z3` on the `PATH`): launches the union on the demo goal, auto-applies the result, checks the closed goal | +| `key.fx.verify.loadingexit` | loading/exit: recent-files store round trip with loading options + profile resolution (snapshot/restore), then the window close button path — the process must terminate with exit code 0 | Harness-injected flags (read in `MainWindowF` but not on the `run`-task whitelist; the verification harness sets them via `JAVA_TOOL_OPTIONS`): diff --git a/key.ui.fx/build.gradle b/key.ui.fx/build.gradle index 1f572cc0fda..0f31f99a6b3 100644 --- a/key.ui.fx/build.gradle +++ b/key.ui.fx/build.gradle @@ -121,6 +121,9 @@ run { // * key.fx.verify.smt = any (smt: run the SMT run UI end to end on the first open goal of // the demo proof; needs an installed SMT solver (e.g. z3 on the PATH), the dialog // auto-applies the results with the CLOSE progress mode) + // * key.fx.verify.loadingexit = any (loadingexit: recent-files store round trip with + // loading options + profile resolution, then the window close button path — the process + // must terminate with exit code 0; the demo load is optional for this flag) ["key.fx.driver", "key.fx.theme", "key.fx.demo.sequent", "key.fx.verify.sequent", "key.fx.show", "key.fx.demo.autoprove", "key.fx.verify.tree", "key.fx.demo.autoprove.live", "key.fx.demo.maxsteps", "key.fx.verify.goallist", @@ -134,7 +137,8 @@ run { "key.fx.verify.joinmerge", "key.fx.verify.joinmerge.maxsteps", "key.fx.verify.lemmaorigin", "key.fx.verify.help", "key.fx.verify.soundiness", "key.fx.verify.profileloading", "key.fx.verify.profileloadingdialog", "key.fx.verify.javacsettings", - "key.fx.verify.termmenu", "key.fx.verify.colors", "key.fx.verify.smt"] + "key.fx.verify.termmenu", "key.fx.verify.colors", "key.fx.verify.smt", + "key.fx.verify.loadingexit"] .each { name -> def value = System.getProperty(name) if (value != null) { 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 index 209480d82f9..d110758fc48 100644 --- 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 @@ -16,6 +16,7 @@ import java.util.Map; import java.util.Optional; import java.util.Properties; +import java.util.ServiceLoader; import java.util.function.Consumer; import javafx.application.Platform; import javafx.beans.property.ReadOnlyBooleanWrapper; @@ -27,6 +28,7 @@ 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; @@ -56,6 +58,7 @@ import javafx.scene.layout.VBox; import javafx.stage.FileChooser; import javafx.stage.Stage; +import javafx.stage.WindowEvent; import de.uka.ilkd.key.control.AutoModeListener; import de.uka.ilkd.key.control.DefaultUserInterfaceControl; @@ -133,6 +136,7 @@ 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; @@ -462,6 +466,12 @@ public void initialize() { 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); stage.show(); @@ -888,6 +898,12 @@ protected KeYEnvironment call() throws Exception { if (System.getProperty("key.fx.verify.smt") != null) { runSmtVerification(); } + // 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) { @@ -1172,7 +1188,16 @@ public void openProofFile(Path file, @Nullable LoadOptions options) { } warnOnBareJavaFile(file); - recentFiles.add(file.toAbsolutePath().toString(), null, false, null); + // 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); } @@ -1731,13 +1756,63 @@ private void updateRecentFilesMenu() { // 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, () -> openProofFile(Path.of(entry.path())))); + .add(menuItem(text, () -> openRecentFile(entry))); } // an empty submenu would render as a dark clickable nothing (Swing leaves it enabled) recentFilesMenu.setDisable(entries.isEmpty()); } + /** + * 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 @@ -1924,6 +1999,65 @@ private void runSmtVerificationAsync() { }); } + /** + * 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 @@ -2569,7 +2703,7 @@ private Menu buildFileMenu() { recentFilesMenu, new SeparatorMenuItem(), menuItem("Exit", "de.uka.ilkd.key.gui.actions.ExitMainAction", - IconFactoryF.Key.QUIT, Platform::exit)); + IconFactoryF.Key.QUIT, this::exitApplication)); return file; } @@ -3317,6 +3451,49 @@ private CheckMenuItem confirmExitToggle() { 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) has no FX seam — {@link KeYMediatorF} has no GUI listener registry, + * so the FX port omits the event. + */ + private void exitApplicationWithoutInteraction() { + recentFiles.save(); + 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) diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/recentfiles/RecentFilesF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/recentfiles/RecentFilesF.java index c5979730ff9..ec91a160a7c 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/recentfiles/RecentFilesF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/recentfiles/RecentFilesF.java @@ -194,6 +194,20 @@ public List getEntries() { return List.copyOf(entries); } + /** + * Replaces the whole entry list with the given snapshot (a list taken from + * {@link #getEntries()}) and saves; used by the {@code key.fx.verify.loadingexit} round trip + * to restore the store after the test entry. + * + * @param restored the entries to store, most-recent-first + */ + public void restore(List restored) { + entries.clear(); + entries.addAll(restored); + fireChange(); + save(); + } + /** * Registers the hook invoked after every model change (on the FX thread; all mutators must be * called on the FX thread). @@ -210,7 +224,11 @@ private void fireChange() { onChange.run(); } - private void save() { + /** + * Writes the entries to the store file (Swing {@code RecentFileMenu.save}; called on every + * change and from the exit flow, Swing ExitMainAction.java:82). + */ + public void save() { List stored = entries.stream().map(Entry::asConfiguration).toList(); try (BufferedWriter writer = From 174fdc647f3dddc9606e7fcf7340bbe1a4af614e Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Sat, 10 Oct 2026 11:34:49 +0200 Subject: [PATCH 131/281] shortcuts (P1): Swing-parity key defaults, dead-binding revival, legacy-default migration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - KeyStrokeManagerF defaults now match the Swing KeyStrokeSettings table (:60-106): the 11 macros and PrettyPrint/Unicode/ProofManagement and the proof-tree search sit on CTRL+SHIFT (the former FX defaults had dropped the Shift, colliding with Ctrl+SPACE auto mode and Ctrl+C term copy), and the sequent search sits on Ctrl+F instead of the bare F, which hijacked typing F anywhere. - Legacy-default migration: a persisted keystrokes.json entry equal to a former FX default (e.g. "ctrl pressed V", "pressed F") heals to the new default on load, so installs that persisted the old defaults through the FX shutdown save are re-defaulted; genuine user overrides still win (verified by a bind round trip). - Dead bindings revived: - Ctrl+C copies the term under the mouse via a sequent-view key handler (Swing CopyToClipboardAction bound WHEN_IN_FOCUSED_WINDOW on the sequent view's MainFrame, MainFrame.java:88-90); the hover tracking now remembers the hovered term position like Swing CurrentGoalView .getMousePosInSequent(). - The term-menu global macro items carry their accelerators (Swing ProofMacroMenu.createMenuItem:146-148 "currently only for global macro applications") and the sequent view runs the macro keystrokes at the last clicked position without opening the menu (Swing MacroKeyBinding). - SequentViewF search opens with Ctrl+F, matching the SearchInSequentAction default (was Ctrl+Shift+F, doubling the proof-tree search). - F3/Shift+F3, Ctrl+K/Ctrl+J, Goal Back (Ctrl+Z) and Prune (Ctrl+DELETE) were already wired — now covered by the verify hook. - key.fx.verify.shortcuts (whitelisted on the run task): the effective binding table (defaults merged with the shared keystrokes.json) matches the Swing specs, no two registered actions share a keystroke, a bind() round trip proves overrides win, and a fired Ctrl+F key event shows the sequent search bar. - PARITY SIGNOFF: shortcut-defaults and dead-bindings rows RESOLVED. Verified on Xvnc :99: key.fx.verify.shortcuts PASS (defaults, no collisions, override round trip, sequent key path). --- key.ui.fx/PARITY-SIGNOFF.md | 8 +- key.ui.fx/README.md | 1 + key.ui.fx/build.gradle | 4 +- .../de/uka/ilkd/key/gui/fx/MainWindowF.java | 77 +++++++++++ .../fx/keyshortcuts/KeyStrokeManagerF.java | 102 +++++++++++--- .../key/gui/fx/nodeviews/ProofMacroMenuF.java | 8 ++ .../key/gui/fx/nodeviews/SequentViewF.java | 129 +++++++++++++++++- 7 files changed, 299 insertions(+), 30 deletions(-) diff --git a/key.ui.fx/PARITY-SIGNOFF.md b/key.ui.fx/PARITY-SIGNOFF.md index 1fd720b5dfe..4eaa53dcd89 100644 --- a/key.ui.fx/PARITY-SIGNOFF.md +++ b/key.ui.fx/PARITY-SIGNOFF.md @@ -49,8 +49,8 @@ P0 bugs from the audit: | Proof-tree context menu acts on the selection, not the clicked node | `OPEN (unverified)` | | View-menu Pretty Print / Unicode inert placeholders | `OPEN` (placeholders remain) | | Recent-file clicks drop stored profile / single-java options | **RESOLVED** (P1: `openProofFile` registers the loading options with the entry like Swing `WindowUserInterfaceControl.loadProblem`; the recent-files menu restores profile (resolved via `DefaultProfileResolver`) / additional options / single-java like Swing `RecentFileAction`, `key.fx.verify.loadingexit` round trip PASS) | -| Shortcut-default mismatches (`KeyStrokeManagerF`: tree search, sequent search, macro defaults) | `OPEN (unverified)` | -| Dead registered bindings (Ctrl+C term copy, F3/Shift+F3, Ctrl+K/Ctrl+J, Goal Back/Prune) | `PARTIAL` (term-menu copy + menu accelerators exist; several bindings unverified) | +| Shortcut-default mismatches (`KeyStrokeManagerF`: tree search, sequent search, macro defaults) | **RESOLVED** (P1: the defaults match the Swing table now — macros and PrettyPrint/Unicode/ProofManagement/tree-search on Ctrl+Shift, sequent search on Ctrl+F instead of the bare F that hijacked typing; stale persisted old defaults heal on load; `key.fx.verify.shortcuts` PASS) | +| Dead registered bindings (Ctrl+C term copy, F3/Shift+F3, Ctrl+K/Ctrl+J, Goal Back/Prune) | **RESOLVED** (P1: Ctrl+C copies the term under the mouse via the sequent view handler + the term-menu copy item carries the accelerator; term-menu global macro items carry their accelerators and the view-level macro keys run at the last clicked position (Swing `MacroKeyBinding`); F3/Shift+F3, Ctrl+K/Ctrl+J, Goal Back/Prune wired and covered by `key.fx.verify.shortcuts`) | | Colors: 12 mapped CSS variables ineffective (47 property definitions missing) | **RESOLVED** (P1: `ColorPaletteF` defines all 51 Swing-parity properties — the true count incl. the two multi-line `define(` keys; the 9 previously-undeclared mapped CSS vars are declared in both themes and consumed by the re-wired `.sequent-hl-*`/`.source-*` rules; `key.fx.verify.colors` PASS) | | UPSTREAM (Swing, not FX): `LoopApplyHeadCompletion` + `LoopContract*` dead code; seed-clobber `overwriteWith` | **Not our defect** — flags for upstream `key.ui`/`key.core` cleanup | @@ -97,7 +97,9 @@ as open. 5. Menus/actions: Pretty Print / Unicode wiring, term labels, tooltip toggles, selection Back/Forward bindings, Edit Last File, Load User Taclets + Prove submenu, Run All Proofs (dialogs exist for several), EnableWhenProofLoaded analogue, toolbars 9 remaining buttons. -6. Keyboard: shortcut-default regressions; dead bindings audit. +6. Keyboard: shortcut-default regressions; dead bindings audit (RESOLVED via P1 + `key.fx.verify.shortcuts`; the Swing-persisted `keystrokes.json` stays the source of truth + for user overrides). 7. Options dialogs: strategy preset UI, HeatmapOptionsDialog (SMT run UI RESOLVED via P1 `key.fx.verify.smt`). 8. Exit flow: close-request + confirmExit + layout persistence on close (RESOLVED via P1 diff --git a/key.ui.fx/README.md b/key.ui.fx/README.md index 12195bb0d80..7920937918a 100644 --- a/key.ui.fx/README.md +++ b/key.ui.fx/README.md @@ -72,6 +72,7 @@ regression-tested on a virtual display. Demo proof paths default into | `key.fx.verify.colors` | colors palette: Swing-parity property count, mapped CSS-variable wiring, override round trip | | `key.fx.verify.smt` | SMT run UI end to end (needs an installed solver, e.g. `z3` on the `PATH`): launches the union on the demo goal, auto-applies the result, checks the closed goal | | `key.fx.verify.loadingexit` | loading/exit: recent-files store round trip with loading options + profile resolution (snapshot/restore), then the window close button path — the process must terminate with exit code 0 | +| `key.fx.verify.shortcuts` | shortcuts: Swing-parity defaults, no binding collisions, an override round trip, and the sequent view Ctrl+F key path | Harness-injected flags (read in `MainWindowF` but not on the `run`-task whitelist; the verification harness sets them via `JAVA_TOOL_OPTIONS`): diff --git a/key.ui.fx/build.gradle b/key.ui.fx/build.gradle index 0f31f99a6b3..82cb22f3856 100644 --- a/key.ui.fx/build.gradle +++ b/key.ui.fx/build.gradle @@ -124,6 +124,8 @@ run { // * key.fx.verify.loadingexit = any (loadingexit: recent-files store round trip with // loading options + profile resolution, then the window close button path — the process // must terminate with exit code 0; the demo load is optional for this flag) + // * key.fx.verify.shortcuts = any (shortcuts: Swing-parity defaults, no collisions, + // bind round trip, the sequent view Ctrl+F key path) ["key.fx.driver", "key.fx.theme", "key.fx.demo.sequent", "key.fx.verify.sequent", "key.fx.show", "key.fx.demo.autoprove", "key.fx.verify.tree", "key.fx.demo.autoprove.live", "key.fx.demo.maxsteps", "key.fx.verify.goallist", @@ -138,7 +140,7 @@ run { "key.fx.verify.help", "key.fx.verify.soundiness", "key.fx.verify.profileloading", "key.fx.verify.profileloadingdialog", "key.fx.verify.javacsettings", "key.fx.verify.termmenu", "key.fx.verify.colors", "key.fx.verify.smt", - "key.fx.verify.loadingexit"] + "key.fx.verify.loadingexit", "key.fx.verify.shortcuts"] .each { name -> def value = System.getProperty(name) if (value != null) { 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 index d110758fc48..6f2044a2721 100644 --- 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 @@ -11,6 +11,7 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.Collection; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -49,6 +50,7 @@ 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.layout.BorderPane; import javafx.scene.layout.HBox; @@ -898,6 +900,11 @@ protected KeYEnvironment call() throws Exception { if (System.getProperty("key.fx.verify.smt") != null) { runSmtVerification(); } + // 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(); + } // 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 @@ -1895,6 +1902,76 @@ private void runSequentSearchVerification() { 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); + } + /** * 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 diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/keyshortcuts/KeyStrokeManagerF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/keyshortcuts/KeyStrokeManagerF.java index d9c5875091d..c0ca2a12588 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/keyshortcuts/KeyStrokeManagerF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/keyshortcuts/KeyStrokeManagerF.java @@ -50,6 +50,17 @@ public final class KeyStrokeManagerF { /** the shared shortcut file (the same as the Swing module's) */ public static final Path SETTINGS_FILE = PathConfig.getSettingsFile("keystrokes.json"); + /** + * shortcuts (P1): the former (pre Swing-parity) FX defaults of the re-mapped actions, in the + * shared Swing spec format. A persisted entry equal to one of these is treated as "not + * customized" on load, so a {@code keystrokes.json} written by an earlier FX version (which + * persists the full binding table on exit) heals to the new defaults instead of keeping the + * stale ones. A genuine user customization that coincides with an old default is re-defaulted + * with it (indistinguishable from the stale entry). Declared before the singleton field: + * {@link #load()} runs from the constructor during class initialization. + */ + private static final Map LEGACY_DEFAULTS = createLegacyDefaults(); + private static final boolean MAC = System.getProperty("os.name", "").toLowerCase(Locale.ROOT).contains("mac"); @@ -80,22 +91,30 @@ public static KeyStrokeManagerF getInstance() { } private void registerDefaults() { - defineDefault("de.uka.ilkd.key.macros.FullAutoPilotProofMacro", modifier() + "V"); - defineDefault("de.uka.ilkd.key.macros.AutoPilotPrepareProofMacro", modifier() + "D"); - defineDefault("de.uka.ilkd.key.macros.PropositionalExpansionMacro", modifier() + "A"); - defineDefault("de.uka.ilkd.key.macros.FullPropositionalExpansionMacro", modifier() + "S"); - defineDefault("de.uka.ilkd.key.macros.TryCloseMacro", modifier() + "C"); - defineDefault("de.uka.ilkd.key.macros.FinishSymbolicExecutionMacro", modifier() + "X"); - defineDefault("de.uka.ilkd.key.macros.OneStepProofMacro", modifier() + "SPACE"); - defineDefault("de.uka.ilkd.key.macros.HeapSimplificationMacro", modifier() + "H"); - defineDefault("de.uka.ilkd.key.macros.UpdateSimplificationMacro", modifier() + "L"); - defineDefault("de.uka.ilkd.key.macros.IntegerSimplificationMacro", modifier() + "I"); - defineDefault("de.uka.ilkd.key.macros.SMTPreparationMacro", modifier() + "Y"); - - defineDefault("de.uka.ilkd.key.gui.actions.SearchInProofTreeAction", modifier() + "F"); - defineDefault("de.uka.ilkd.key.gui.actions.PrettyPrintToggleAction", modifier() + "P"); - defineDefault("de.uka.ilkd.key.gui.actions.UnicodeToggleAction", modifier() + "U"); - defineDefault("de.uka.ilkd.key.gui.actions.ProofManagementAction", modifier() + "M"); + // shortcuts (P1): Swing-parity defaults — macros and the search/toggle actions use + // CTRL+SHIFT (KeyStrokeSettings.java:44 "use CTRL+SHIFT+letter for macros", :60-76), so + // the former FX defaults without the Shift modifier no longer collide with the + // Ctrl+SPACE auto mode and the Ctrl+C term copy + defineDefault("de.uka.ilkd.key.macros.FullAutoPilotProofMacro", modifier() + "SHIFT+V"); + defineDefault("de.uka.ilkd.key.macros.AutoPilotPrepareProofMacro", modifier() + "SHIFT+D"); + defineDefault("de.uka.ilkd.key.macros.PropositionalExpansionMacro", modifier() + "SHIFT+A"); + defineDefault("de.uka.ilkd.key.macros.FullPropositionalExpansionMacro", + modifier() + "SHIFT+S"); + defineDefault("de.uka.ilkd.key.macros.TryCloseMacro", modifier() + "SHIFT+C"); + defineDefault("de.uka.ilkd.key.macros.FinishSymbolicExecutionMacro", + modifier() + "SHIFT+X"); + defineDefault("de.uka.ilkd.key.macros.OneStepProofMacro", modifier() + "SHIFT+SPACE"); + defineDefault("de.uka.ilkd.key.macros.HeapSimplificationMacro", modifier() + "SHIFT+H"); + defineDefault("de.uka.ilkd.key.macros.UpdateSimplificationMacro", modifier() + "SHIFT+L"); + defineDefault("de.uka.ilkd.key.macros.IntegerSimplificationMacro", modifier() + "SHIFT+I"); + defineDefault("de.uka.ilkd.key.macros.SMTPreparationMacro", modifier() + "SHIFT+Y"); + + defineDefault("de.uka.ilkd.key.gui.actions.SearchInProofTreeAction", + modifier() + "SHIFT+F"); + defineDefault("de.uka.ilkd.key.gui.actions.PrettyPrintToggleAction", + modifier() + "SHIFT+P"); + defineDefault("de.uka.ilkd.key.gui.actions.UnicodeToggleAction", modifier() + "SHIFT+U"); + defineDefault("de.uka.ilkd.key.gui.actions.ProofManagementAction", modifier() + "SHIFT+M"); defineDefault("de.uka.ilkd.key.gui.actions.QuickSaveAction", "F5"); defineDefault("de.uka.ilkd.key.gui.actions.QuickLoadAction", "F6"); @@ -128,13 +147,46 @@ private void registerDefaults() { defineDefault("de.uka.ilkd.key.gui.settings.SettingsManager$ShowSettingsAction", modifier() + "N"); defineDefault("de.uka.ilkd.key.gui.actions.OpenFileAction", modifier() + "O"); - defineDefault("de.uka.ilkd.key.gui.actions.SearchInSequentAction", "F"); + // sequent search is Ctrl+F in Swing (KeyStrokeSettings.java:102 + // SearchInSequentAction.java:15 "Keyboard shortcut: STRG+F"); the former FX default was + // the bare F, which hijacked typing F anywhere + defineDefault("de.uka.ilkd.key.gui.actions.SearchInSequentAction", modifier() + "F"); defineDefault("de.uka.ilkd.key.gui.actions.SearchNextAction", "F3"); defineDefault("de.uka.ilkd.key.gui.actions.SearchPreviousAction", "SHIFT+F3"); defineDefault("de.uka.ilkd.key.gui.actions.SelectionBackAction", "SHORTCUT+ALT+LEFT"); defineDefault("de.uka.ilkd.key.gui.actions.SelectionForwardAction", "SHORTCUT+ALT+RIGHT"); } + /** + * shortcuts (P1): the former (pre Swing-parity) FX defaults of the re-mapped actions, in the + * shared Swing spec format. See {@link #LEGACY_DEFAULTS} (declared before the singleton + * field) for the rationale. + */ + private static Map createLegacyDefaults() { + String[][] changed = { + { "de.uka.ilkd.key.macros.FullAutoPilotProofMacro", "V" }, + { "de.uka.ilkd.key.macros.AutoPilotPrepareProofMacro", "D" }, + { "de.uka.ilkd.key.macros.PropositionalExpansionMacro", "A" }, + { "de.uka.ilkd.key.macros.FullPropositionalExpansionMacro", "S" }, + { "de.uka.ilkd.key.macros.TryCloseMacro", "C" }, + { "de.uka.ilkd.key.macros.FinishSymbolicExecutionMacro", "X" }, + { "de.uka.ilkd.key.macros.OneStepProofMacro", "SPACE" }, + { "de.uka.ilkd.key.macros.HeapSimplificationMacro", "H" }, + { "de.uka.ilkd.key.macros.UpdateSimplificationMacro", "L" }, + { "de.uka.ilkd.key.macros.IntegerSimplificationMacro", "I" }, + { "de.uka.ilkd.key.macros.SMTPreparationMacro", "Y" }, + { "de.uka.ilkd.key.gui.actions.PrettyPrintToggleAction", "P" }, + { "de.uka.ilkd.key.gui.actions.UnicodeToggleAction", "U" }, + { "de.uka.ilkd.key.gui.actions.ProofManagementAction", "M" }, + { "de.uka.ilkd.key.gui.actions.SearchInProofTreeAction", "F" } }; + Map legacy = new HashMap<>(); + for (String[] entry : changed) { + legacy.put(entry[0], "ctrl pressed " + entry[1]); + } + legacy.put("de.uka.ilkd.key.gui.actions.SearchInSequentAction", "pressed F"); + return legacy; + } + private static String modifier() { return "SHORTCUT+"; } @@ -276,9 +328,19 @@ private void load() { String value = entry.getValue() == null ? "" : entry.getValue().toString(); Optional combination = fromSwingSpec(value); if (bindings.containsKey(entry.getKey())) { - // an empty entry falls back to the default, like the Swing constructor - combination.ifPresent(keyCombination -> bindings.put(entry.getKey(), - keyCombination)); + // a stale persisted entry that equals a former FX default heals to the new + // (Swing-parity) default; an empty entry falls back to the default, like the + // Swing constructor + boolean legacy = combination.isPresent() && combination.get() + .equals(fromSwingSpec(LEGACY_DEFAULTS.get(entry.getKey())) + .orElse(null)); + if (!legacy) { + combination + .ifPresent(keyCombination -> bindings.put(entry.getKey(), + keyCombination)); + } else { + LOGGER.info("Re-defaulting the stale entry {} = {}", entry.getKey(), value); + } } else { persistedEntries.put(entry.getKey(), value); } diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/ProofMacroMenuF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/ProofMacroMenuF.java index 920e10f0161..aa01c13322d 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/ProofMacroMenuF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/ProofMacroMenuF.java @@ -10,6 +10,7 @@ import javafx.scene.control.Tooltip; import de.uka.ilkd.key.control.ProofControl; +import de.uka.ilkd.key.gui.fx.keyshortcuts.KeyStrokeManagerF; import de.uka.ilkd.key.macros.ProofMacro; import de.uka.ilkd.key.proof.Node; @@ -50,6 +51,13 @@ static MenuItem itemFor(ProofMacro macro, Node node, ProofControl proofControl, Tooltip.install(label, new Tooltip(macro.getDescription())); CustomMenuItem item = new CustomMenuItem(label); item.setOnAction(e -> proofControl.runMacro(node, macro, pio)); + // shortcuts (P1): macro accelerators on the term-menu items for global applications + // (Swing ProofMacroMenu.createMenuItem, ProofMacroMenu.java:146-148: "currently only + // for global macro applications") + if (pio == null) { + KeyStrokeManagerF.getInstance().binding(macro.getClass().getName()) + .ifPresent(item::setAccelerator); + } return item; } } diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentViewF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentViewF.java index bc5bb1b8aa2..ab3cbc66d37 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentViewF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentViewF.java @@ -7,7 +7,9 @@ import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Iterator; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.TreeSet; import java.util.function.Consumer; @@ -27,6 +29,8 @@ import javafx.scene.control.TextField; import javafx.scene.control.ToggleButton; import javafx.scene.control.Tooltip; +import javafx.scene.input.Clipboard; +import javafx.scene.input.ClipboardContent; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyCodeCombination; import javafx.scene.input.KeyCombination; @@ -59,6 +63,7 @@ import de.uka.ilkd.key.gui.fx.MainWindowF; import de.uka.ilkd.key.gui.fx.configuration.ConfigF; import de.uka.ilkd.key.gui.fx.fonticons.IconFactoryF; +import de.uka.ilkd.key.gui.fx.keyshortcuts.KeyStrokeManagerF; import de.uka.ilkd.key.logic.label.TermLabel; import de.uka.ilkd.key.macros.ProofMacro; import de.uka.ilkd.key.pp.HideSequentPrintFilter; @@ -195,8 +200,15 @@ public String printedText() { // lemmaorigin: end + // shortcuts (P1): the sequent search opens with Ctrl+F (Swing SearchInSequentAction, the + // menu accelerator of KeyStrokeSettings.java:102 / SearchInSequentAction.java:15 "Keyboard + // shortcut: STRG+F"); the former FX combination Ctrl+Shift+F doubled the proof-tree search private static final KeyCombination OPEN_SEARCH = - new KeyCodeCombination(KeyCode.F, KeyCombination.CONTROL_DOWN, KeyCombination.SHIFT_DOWN); + new KeyCodeCombination(KeyCode.F, KeyCombination.CONTROL_DOWN); + + /** shortcuts (P1): Ctrl+C copies the term under the mouse (Swing CopyToClipboardAction). */ + private static final KeyCombination COPY_TERM = + new KeyCodeCombination(KeyCode.C, KeyCombination.SHORTCUT_DOWN); private final ScrollPane scrollPane = new ScrollPane(); private final TextFlow textFlow = new TextFlow(); @@ -256,6 +268,11 @@ public String printedText() { // hover highlight + tooltip (Swing SequentViewInputListener.mouseMoved / getToolTipText) /** the term range under the mouse, {@code null} when the mouse is over empty space. */ private Range hoveredRange; + /** + * the term position under the mouse for the Ctrl+C term copy (Swing + * {@code CurrentGoalView.getMousePosInSequent()}). + */ + private PosInSequent hoveredPos; /** shows the term info of the hovered position after the Swing-like display delay. */ private final Tooltip hoverTooltip = new Tooltip(); /** delay before the tooltip shows (Swing ToolTipManager initial delay ≈ 500 ms). */ @@ -329,9 +346,9 @@ public SequentViewF() { setBottom(searchBar); searchBar.setVisible(false); searchBar.setManaged(false); - // Swing SequentView registers Ctrl+Shift+F with WHEN_ANCESTOR_OF_FOCUSED_COMPONENT: the - // shortcut fires whenever the keyboard focus is anywhere inside this view (the key - // events bubble from the focused control up to this pane). + // shortcuts (P1): Swing SequentViewSearchBar opens via the SearchInSequentAction menu + // accelerator (Ctrl+F, KeyStrokeSettings.java:102); the FX view handles Ctrl+F locally + // for the same effect (the key events bubble from the focused control up to this pane) setOnKeyPressed(this::handlePaneKeyPressed); printPlaceholder(); } @@ -453,13 +470,104 @@ public String toString() { } } + /** + * shortcuts (P1): view-level macro bindings, built on first use (Swing + * {@code MacroKeyBinding.registerMacroKeyBindings} registers the same set once). + */ + private Map macroBindings; + + /** Builds the view-level macro bindings ({@link #buildMacroBindings()}) once. */ + private Map macroBindings() { + if (macroBindings == null) { + macroBindings = buildMacroBindings(); + } + return macroBindings; + } + private void handlePaneKeyPressed(KeyEvent event) { if (OPEN_SEARCH.match(event)) { event.consume(); showSearchBar(); + return; + } + // shortcuts (P1): Ctrl+C copies the term under the mouse (Swing CopyToClipboardAction, + // KeyStrokeSettings.java:90, bound WHEN_IN_FOCUSED_WINDOW on the sequent view's + // MainFrame, MainFrame.java:88-90; the copy body is GuiUtilities + // .copyHighlightToClipboard — the term text at the mouse position with the non-breaking + // spaces replaced) + if (COPY_TERM.match(event)) { + event.consume(); + copyHoveredTermToClipboard(); + return; + } + // shortcuts (P1): view-level macro invocation (Swing MacroKeyBinding + // .registerMacroKeyBindings, MacroKeyBinding.java:66-78: each registered macro's + // keystroke runs the macro at the last position in the sequent view without opening the + // term menu) + if (invokeMacroBinding(event)) { + event.consume(); } } + /** + * shortcuts (P1): copies the hovered term to the system clipboard (Swing + * {@code CopyToClipboardAction} → {@code GuiUtilities.copyHighlightToClipboard}: the term + * at the mouse position, non-breaking spaces replaced); a no-op when the mouse is not over + * a term. + */ + private void copyHoveredTermToClipboard() { + PosInOccurrence occ = hoveredPos == null ? null : hoveredPos.getPosInOccurrence(); + if (occ != null && occ.subTerm() != null) { + ClipboardContent content = new ClipboardContent(); + content.putString(occ.subTerm().toString().replace('\u00A0', ' ')); + Clipboard.getSystemClipboard().setContent(content); + } + } + + /** + * shortcuts (P1): runs the macro bound to the pressed keystroke (Swing + * {@code MacroKeyBinding}: the macro runs at {@code sequentView.getLastPosInSequent()} + * when a goal is selected and {@code canApplyTo} holds). + * + * @return whether the event matched a macro binding (the caller consumes it then) + */ + private boolean invokeMacroBinding(KeyEvent event) { + if (macroBindings().isEmpty()) { + return false; + } + for (Map.Entry entry : macroBindings.entrySet()) { + if (entry.getKey().match(event)) { + ProofMacro macro = entry.getValue(); + Node node = menuMediator == null ? null : menuMediator.getSelectedNode(); + if (node != null && menuMediator.getSelectedGoal() != null + && menuProofControl != null) { + PosInOccurrence pio = + lastClickedPos == null ? null : lastClickedPos.getPosInOccurrence(); + if (macro.canApplyTo(node, pio)) { + menuProofControl.runMacro(node, macro, pio); + } + } + return true; + } + } + return false; + } + + /** + * shortcuts (P1): the view-level macro bindings (keystroke → macro), built once from the + * {@link MainWindowF#AUTOMATION_MACROS} entries that have a registered shortcut (Swing + * {@code MacroKeyBinding.registerMacroKeyBindings} iterates the same macros and looks each + * up in the {@code KeyStrokeManager}). + */ + private Map buildMacroBindings() { + Map bindings = new LinkedHashMap<>(); + for (ProofMacro macro : MainWindowF.AUTOMATION_MACROS) { + KeyStrokeManagerF.getInstance().binding(macro.getClass().getName()) + .ifPresent(combination -> bindings.put(combination, macro)); + } + return bindings; + } + /** * Registers this view as a selection listener on the given model and displays the currently * selected node, mirroring how the Swing {@code SequentView} observes the mediator's @@ -972,6 +1080,7 @@ private void handleMouseMove(MouseEvent event) { hoveredRange = hover; rebuildHoverOverlay(); } + hoveredPos = pos; updateHoverTooltip(event, pos); } @@ -985,6 +1094,7 @@ private void handleMouseExited(MouseEvent event) { /** Clears the hovered term range and hides the tooltip. */ private void clearHover() { + hoveredPos = null; if (hoveredRange != null) { hoveredRange = null; rebuildHoverOverlay(); @@ -1127,8 +1237,8 @@ private Path rectForRange(int start, int end, String styleClass) { // ----------------------------------------------------------------------- /** - * Shows the search bar (Swing: {@code Ctrl+Shift+F}) and focuses the field. A query kept - * from a previous opening is re-applied. + * Shows the search bar (Swing: the {@code SearchInSequentAction} accelerator, Ctrl+F) and + * focuses the field. A query kept from a previous opening is re-applied. */ public void showSearchBar() { searchBar.setVisible(true); @@ -1140,6 +1250,13 @@ public void showSearchBar() { } } + /** + * shortcuts (P1) verify: whether the search bar is currently shown. + */ + public boolean isSearchBarShowing() { + return searchBar.isVisible(); + } + /** * Hides the search bar and clears the highlights so the plain sequent is shown again (Swing * {@code setVisible(false)}). The field text is kept like in the Swing view and re-applied on From fafb2ddd4dec5e118fe6b9cfecbe45f83b6ca204 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Sat, 10 Oct 2026 11:48:54 +0200 Subject: [PATCH 132/281] inputfreeze (P1): freeze the views during auto mode (blocking overlay port) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Auto mode freeze (Swing MainWindow.freezeExceptAutoModeButton + BlockingGlassPane/GlassPaneListener): a blocking overlay pane on the main-area stack covers the views (sequent workspace + drawers) for the duration of an auto mode run — mouse input is swallowed, key events are consumed by a capture filter (except Escape, which must reach the global stop handler), and the wait cursor shows. The toolbar with the automation/stop controls and the status bar stay live. - Wired into autoModeUiListener: freeze on autoModeStarted, unfreeze on autoModeStopped before the demo-live early return, so every stop undoes the freeze. KNOWN-SIMPLIFIED: Swing's glass pane covered the whole content pane and re-delivered events only to isAutoButton components; the FX overlay covers only the views area, so the menus stay clickable while frozen (their actions are disabled during auto mode anyway). - The SMT apply note "the input freeze is the P0 stopInterface remainder" is corrected: the FX application runs synchronously on the FX thread, so a freeze around it would be unobservable; the auto mode freeze covers the long-running case. - key.fx.verify.inputfreeze (whitelisted on the run task): (1) direct freeze/unfreeze — the overlay shows, a fired Ctrl+F key event is blocked while frozen and shows the search bar after the unfreeze; (2) the listener-driven path — an auto mode run started via the mediator freezes the views and the stop (requested or natural) unfreezes them. - PARITY SIGNOFF: the "input freeze during auto mode" P0 remainder row is RESOLVED. Verified on Xvnc :99: Input freeze verification PASS (freeze/unfreeze + auto-mode-driven freeze), with the auto-mode-driven freeze/unfreeze observed. --- key.ui.fx/KNOWN-SIMPLIFIED.md | 11 +- key.ui.fx/PARITY-SIGNOFF.md | 6 +- key.ui.fx/README.md | 1 + key.ui.fx/build.gradle | 4 +- .../de/uka/ilkd/key/gui/fx/MainWindowF.java | 158 ++++++++++++++++++ .../ilkd/key/gui/fx/smt/SolverListenerF.java | 7 +- 6 files changed, 176 insertions(+), 11 deletions(-) diff --git a/key.ui.fx/KNOWN-SIMPLIFIED.md b/key.ui.fx/KNOWN-SIMPLIFIED.md index 9dbd58db0e2..a0b0fa89734 100644 --- a/key.ui.fx/KNOWN-SIMPLIFIED.md +++ b/key.ui.fx/KNOWN-SIMPLIFIED.md @@ -10,14 +10,14 @@ Marker counts per module: | Module | Markers | |---|---| -| `key.ui.fx` (contrib extensions + SMT run UI + exit flow) | 6 | +| `key.ui.fx` (contrib extensions + SMT run UI + exit flow + input freeze) | 7 | | `keyext.caching.fx` | 10 | | `keyext.exploration.fx` | 9 | | `keyext.isabelletranslation.fx` | 4 | | `keyext.slicing.fx` | 16 | | `keyext.ui.testgen.fx` | 18 | | `keyext.proofmanagement.fx` | 4 | -| **Total** | **67** | +| **Total** | **68** | To re-derive this table at any point: `grep -rn 'KNOWN-SIMPLIFIED' */src/main */src/test`. @@ -35,13 +35,14 @@ milestone, **FIXED-UPSTREAM** items differ from a Swing bug that the port delibe | `extension/contrib/HeatmapF.java:37` | — (doubles the `@Info` description) | same decision restated for the extension dialog | OPEN | | `extension/contrib/ParallelProverStatusIndicatorF.java:27` | toggle button "SC" / "MT N×" with left-click toggle and right-click worker-count menu | plain `Label` showing the live auto-mode state ("Auto"/"Manual"), left-click toggles `PARALLEL_PROVER_ENABLED`, reacts to property changes; worker-count picker and button styling out of scope | OPEN | -## `key.ui.fx` — SMT run UI + exit flow (3) +## `key.ui.fx` — SMT run UI + exit flow + input freeze (4) | Site | Swing original | FX port | Status | |---|---|---|---| -| `SolverListenerF.java:81` | results applied via `SMTProofApplyUserAction` (undoable history entry) with the `stopInterface` input freeze around it | direct `SMTRule` application on the FX thread (no undo entry for the automatic CLOSE-mode application; the input freeze is the P0 `stopInterface` remainder) | OPEN | +| `SolverListenerF.java:81` | results applied via `SMTProofApplyUserAction` (undoable history entry) with the `stopInterface` input freeze around it | direct `SMTRule` application on the FX thread (no undo entry for the automatic CLOSE-mode application; the apply runs synchronously on the FX thread, so a freeze around it would be unobservable — the auto mode freeze covers the long-running case) | OPEN | | `InformationWindowF.java:24` | counterexample model tree (`CETree`) with line numbers (`TextLineNumber`) and the counterexample help tab | tabs per information entry as read-only monospaced text areas; model tree + line numbers + help tab not ported | OPEN | -| `MainWindowF.java:3486` | exit fires the mediator shutdown event (`fireShutDown` → `GUIListener.shutDown`) | the event is omitted — `KeYMediatorF` has no GUI listener registry; the other shutdown side effects (recent-files save, `System.exit(0)` running the store shutdown hooks) are ported | OPEN | +| `MainWindowF.java:3721` | exit fires the mediator shutdown event (`fireShutDown` → `GUIListener.shutDown`) | the event is omitted — `KeYMediatorF` has no GUI listener registry; the other shutdown side effects (recent-files save, `System.exit(0)` running the store shutdown hooks) are ported | OPEN | +| `MainWindowF.java:2659` | the glass pane covered the whole content pane and re-delivered events only to components marked `isAutoButton` (automation/stop buttons + status-line abort button) | the overlay covers only the views area (workspace + drawers); the menu bar, toolbar and status bar stay clickable while frozen (their actions are disabled during auto mode anyway) | OPEN | ## `keyext.caching.fx` (10) diff --git a/key.ui.fx/PARITY-SIGNOFF.md b/key.ui.fx/PARITY-SIGNOFF.md index 4eaa53dcd89..403e4c2370e 100644 --- a/key.ui.fx/PARITY-SIGNOFF.md +++ b/key.ui.fx/PARITY-SIGNOFF.md @@ -38,7 +38,7 @@ marked `OPEN (unverified)` rather than assumed fixed. | Abandon Proof | Proof menu (24 items) | `key.fx.verify.menuparity` Proof 24 | **RESOLVED** | | ProofManagementDialog + TaskTree / Loaded Proofs dockable | `keyext.proofmanagement.fx` (MP9.6) + Loaded Proofs view | `key.fx.verify.proofmgmt` 1/2 | **RESOLVED** | | IssueDialog / error reporting | `IssueDialogF` (toasts + dialog) | `key.fx.verify.uicontrol` (exception leg) | **RESOLVED** | -| Goal Back / Prune disable bindings; input freeze during auto mode | enablement listeners exist; **input freeze still open** | — | **PARTIAL / OPEN** | +| Goal Back / Prune disable bindings; input freeze during auto mode | **RESOLVED** (P1: the auto mode blocking overlay — main area covered, mouse input blocked, keys swallowed except Escape, wait cursor; wired to the auto mode listener incl. the demo-live stop path; `key.fx.verify.inputfreeze` PASS) | — | **RESOLVED** | | Apply Strategy on node + Prune-at-node in the tree popup | right-click macro menu ported (`key.fx.verify.rightclickmacro`); Apply Strategy-at-node unverified | — | **PARTIAL** | P0 bugs from the audit: @@ -87,7 +87,9 @@ as open. ## Still-open work list (carried forward) -1. Input freeze during auto mode (blocking glass pane port) — P0 remainder. +1. Input freeze during auto mode (blocking glass pane port) — RESOLVED via P1 + `key.fx.verify.inputfreeze`; the menus stay clickable while frozen (Swing's glass pane + covered everything except the `isAutoButton` stop controls) — KNOWN-SIMPLIFIED. 2. Proof-tree: Apply Strategy on clicked node, Prune-at-node in the popup, renderer surface (tooltips, goal icons, linked/cache marks, notes, branch-label F2 editing), NodeInfoVisualizer. 3. Sequent: multi-select highlight + reprint persistence, drag & drop, inner-node highlights + diff --git a/key.ui.fx/README.md b/key.ui.fx/README.md index 7920937918a..ef0adeeea31 100644 --- a/key.ui.fx/README.md +++ b/key.ui.fx/README.md @@ -73,6 +73,7 @@ regression-tested on a virtual display. Demo proof paths default into | `key.fx.verify.smt` | SMT run UI end to end (needs an installed solver, e.g. `z3` on the `PATH`): launches the union on the demo goal, auto-applies the result, checks the closed goal | | `key.fx.verify.loadingexit` | loading/exit: recent-files store round trip with loading options + profile resolution (snapshot/restore), then the window close button path — the process must terminate with exit code 0 | | `key.fx.verify.shortcuts` | shortcuts: Swing-parity defaults, no binding collisions, an override round trip, and the sequent view Ctrl+F key path | +| `key.fx.verify.inputfreeze` | input freeze: the auto-mode blocking overlay — direct freeze/unfreeze with key blocking, then an auto-mode-driven run | Harness-injected flags (read in `MainWindowF` but not on the `run`-task whitelist; the verification harness sets them via `JAVA_TOOL_OPTIONS`): diff --git a/key.ui.fx/build.gradle b/key.ui.fx/build.gradle index 82cb22f3856..9cb4115f788 100644 --- a/key.ui.fx/build.gradle +++ b/key.ui.fx/build.gradle @@ -126,6 +126,8 @@ run { // must terminate with exit code 0; the demo load is optional for this flag) // * key.fx.verify.shortcuts = any (shortcuts: Swing-parity defaults, no collisions, // bind round trip, the sequent view Ctrl+F key path) + // * key.fx.verify.inputfreeze = any (inputfreeze: the auto mode blocking overlay — direct + // freeze/unfreeze with key blocking, then an auto-mode-driven run) ["key.fx.driver", "key.fx.theme", "key.fx.demo.sequent", "key.fx.verify.sequent", "key.fx.show", "key.fx.demo.autoprove", "key.fx.verify.tree", "key.fx.demo.autoprove.live", "key.fx.demo.maxsteps", "key.fx.verify.goallist", @@ -140,7 +142,7 @@ run { "key.fx.verify.help", "key.fx.verify.soundiness", "key.fx.verify.profileloading", "key.fx.verify.profileloadingdialog", "key.fx.verify.javacsettings", "key.fx.verify.termmenu", "key.fx.verify.colors", "key.fx.verify.smt", - "key.fx.verify.loadingexit", "key.fx.verify.shortcuts"] + "key.fx.verify.loadingexit", "key.fx.verify.shortcuts", "key.fx.verify.inputfreeze"] .each { name -> def value = System.getProperty(name) if (value != null) { 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 index 6f2044a2721..5aa76a2253d 100644 --- 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 @@ -19,13 +19,17 @@ import java.util.Properties; import java.util.ServiceLoader; import java.util.function.Consumer; +import javafx.animation.KeyFrame; +import javafx.animation.Timeline; import javafx.application.Platform; import javafx.beans.property.ReadOnlyBooleanWrapper; 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; @@ -52,8 +56,10 @@ 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; @@ -61,6 +67,7 @@ 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; @@ -446,6 +453,20 @@ public void initialize() { mainArea.setRight(eastDrawer); 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()); @@ -905,6 +926,11 @@ protected KeYEnvironment call() throws Exception { 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(); + } // 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 @@ -1972,6 +1998,94 @@ private void runShortcutsVerification() { 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 @@ -2526,14 +2640,48 @@ private void handleMainWindowKeyPressed(KeyEvent event) { * 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. + *

        + * KNOWN-SIMPLIFIED: 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); the FX overlay instead covers only the views area, so the + * menus stay clickable while frozen (their actions are disabled during auto mode anyway). + */ + 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 } @@ -2545,6 +2693,16 @@ public void autoModeStopped(ProofEvent e) { 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() { diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/smt/SolverListenerF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/smt/SolverListenerF.java index a4616c1b360..e3f5896391a 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/smt/SolverListenerF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/smt/SolverListenerF.java @@ -80,9 +80,10 @@ *

        * KNOWN-SIMPLIFIED: the results are applied directly ({@code SMTRule} built-in rule application * with the unsat core if available) instead of through the Swing {@code SMTProofApplyUserAction} - * history mechanism (no undo entry for the automatic CLOSE-mode application), and the input - * freeze around the rule application is not available yet (the {@code stopInterface} P0 - * remainder). + * history mechanism (no undo entry for the automatic CLOSE-mode application). Swing also stops + * the interface ({@code stopInterface}) around the rule application; the FX application runs + * synchronously on the FX thread, so a freeze would be unobservable (the auto mode freeze of + * MainWindowF.freezeExceptAutoModeButton covers the long-running case). */ public class SolverListenerF implements SolverLauncherListener { From d03dbde9b06238847232b3c73850ee3d4ff00e73 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Sat, 10 Oct 2026 12:19:48 +0200 Subject: [PATCH 133/281] key.ui.fx: pin a JDK 25 toolchain for the FX modules The JavaFX 25.0.4 jars (javafx-*) are class-file version 67, i.e. they require JDK 23+, while the CI baseline is Java 21 (Corretto) and the build compiles with the Gradle JVM's javac (--release 21). On Java 21 the first javac pass over key.ui.fx therefore fails with 'bad class file ... wrong version 67.0, should be 65.0' (the reported JoinMergeVerifyF symbol error is just the cascade). The FX modules (key.ui.fx and the keyext.*.fx ports) now build with a pinned JDK 25 toolchain: the foojay resolver (settings.gradle) provisions it on Java-21-only machines, so tests.yml, nightlydeploy and the Java-21 legs of release-tests all keep working unchanged. The emitted bytecode stays Java-21 compatible (options.release = 21). --- build.gradle | 13 +++++++++++++ key.ui.fx/README.md | 5 ++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 93b792ef77c..ef430f87332 100644 --- a/build.gradle +++ b/build.gradle @@ -73,6 +73,19 @@ subprojects { targetCompatibility = 21 } + // The JavaFX 25 jars (javafx-*) are class-file version 67, i.e. they require JDK 23+, while + // the baseline of the build is Java 21. The JavaFX UI modules (key.ui.fx and the keyext.*.fx + // ports) therefore compile and test with a pinned JDK 25 toolchain: the foojay resolver + // (settings.gradle) provisions it on machines that only have a Java 21 (e.g. the CI jobs), + // and the emitted bytecode stays Java-21 compatible via options.release = 21 below. + if (project.name.endsWith('.fx')) { + java { + toolchain { + languageVersion = JavaLanguageVersion.of(25) + } + } + } + repositories { mavenCentral() //maven { url = "https://git.key-project.org/api/v4/projects/35/packages/maven/" } diff --git a/key.ui.fx/README.md b/key.ui.fx/README.md index ef0adeeea31..8c82be19a87 100644 --- a/key.ui.fx/README.md +++ b/key.ui.fx/README.md @@ -133,7 +133,10 @@ contract; capability interfaces: `MainMenuF`, `ToolbarF`, `StatusLineF`, `Contex * Per-commit gate: `./gradlew --no-daemon :key.ui.fx:spotlessApply :key.ui.fx:compileJava :key.ui.fx:test`. * CI: the FX modules are part of the `tests.yml` unit-test matrix, the `spotlessCheck` quality - job and the broad release-test `test` runs. + job and the broad release-test `test` runs. The JavaFX 25 jars are class-file version 67 + (JDK 23+), so the FX modules build with a pinned **JDK 25 toolchain** (root `build.gradle`); + on Java-21-only machines (the CI baseline) the foojay resolver provisions it automatically. + The emitted bytecode stays Java-21 compatible (`--release 21`). ## Documents From 1508209a5d89758e04070453833f78ea9ba6620d Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Sat, 10 Oct 2026 12:41:36 +0200 Subject: [PATCH 134/281] Raise the build baseline to JDK 25 (replaces the FX-module toolchain special case) Instead of pinning a JDK 25 toolchain for the *.fx modules only, make JDK 25 the uniform baseline of the whole build: * build.gradle: sourceCompatibility/targetCompatibility = 25 and options.release = 25 for every subproject. This also satisfies the JavaFX 25 requirement (javafx-* jars are class-file 67 = JDK 23+) without special-casing the FX modules. * tests.yml, code_quality.yml, nightlydeploy.yml, release-tests.yml: run all CI jobs on JDK 25 (the [21, 25] release matrix collapses to [ 25 ]; nightlydeploy JAVA_VERSION 21 -> 25). Bytecode is now class-file 69 (Java 25); the full compile gate (including key.core, key.ui, key.ncore, key.util) is green on JDK 25. --- .github/workflows/code_quality.yml | 6 +++--- .github/workflows/nightlydeploy.yml | 2 +- .github/workflows/release-tests.yml | 6 +++--- .github/workflows/tests.yml | 8 ++++---- build.gradle | 22 +++++----------------- key.ui.fx/README.md | 6 ++---- 6 files changed, 18 insertions(+), 32 deletions(-) diff --git a/.github/workflows/code_quality.yml b/.github/workflows/code_quality.yml index db2bb557f8b..0a322f8c350 100644 --- a/.github/workflows/code_quality.yml +++ b/.github/workflows/code_quality.yml @@ -13,10 +13,10 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - - name: Set up JDK 21 + - name: Set up JDK 25 uses: actions/setup-java@v6.0.1 with: - java-version: 21 + java-version: 25 distribution: 'corretto' cache: 'gradle' - name: Setup Gradle @@ -31,7 +31,7 @@ jobs: - uses: actions/setup-java@v6.0.1 with: distribution: 'corretto' - java-version: '21' + java-version: 25 cache: 'gradle' - name: Build with Gradle run: ./gradlew --parallel --continue spotlessCheck diff --git a/.github/workflows/nightlydeploy.yml b/.github/workflows/nightlydeploy.yml index 953ee83037a..1c0abb27674 100644 --- a/.github/workflows/nightlydeploy.yml +++ b/.github/workflows/nightlydeploy.yml @@ -10,7 +10,7 @@ permissions: id-token: write env: - JAVA_VERSION: 21 + JAVA_VERSION: 25 jobs: diff --git a/.github/workflows/release-tests.yml b/.github/workflows/release-tests.yml index 94ee5dfbcd2..e5ba0eb7259 100644 --- a/.github/workflows/release-tests.yml +++ b/.github/workflows/release-tests.yml @@ -15,7 +15,7 @@ jobs: fail-fast: false matrix: os: [ macos-latest, ubuntu-latest, windows-latest ] - java: [ 21, 25 ] + java: [ 25 ] continue-on-error: false runs-on: ${{ matrix.os }} env: @@ -72,7 +72,7 @@ jobs: matrix: test: [ testProveRules, testRunAllFunProofs, testRunAllInfProofs, testRunAllWdProofs, testProveSMTLemmas ] os: [ macos-latest, ubuntu-latest, windows-latest ] - java: [ 21, 25 ] + java: [ 25 ] z3: [ '4.15.4' ] cvc5: [ '1.3.2' ] @@ -119,7 +119,7 @@ jobs: matrix: test: [ testProveSMTLemmas, testStrictSMT ] os: [ macos-latest, ubuntu-latest, windows-latest ] - java: [ 21 ] + java: [ 25 ] z3: [ '4.13.4', '4.14.1', '4.15.4' ] cvc5: [ '1.3.2', '1.2.1' ] runs-on: ${{ matrix.os }} diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 3cfe6dc5fcb..a4f44a81dea 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -24,7 +24,7 @@ jobs: fail-fast: false matrix: os: [ ubuntu-latest, windows-latest ] - java: [ 21 ] + java: [ 25 ] modules: [ keyext.exploration, keyext.slicing, key.ncore, key.ui, key.core, key.core.testgen, keyext.isabelletranslation, keyext.ui.testgen, key.ncore.calculus, @@ -56,7 +56,7 @@ jobs: - name: cvc5 version run: cvc5 --version - - name: Set up JDK 21 + - name: Set up JDK 25 uses: actions/setup-java@v6.0.1 with: java-version: ${{ matrix.java }} @@ -91,11 +91,11 @@ jobs: matrix: test: [ testProveRules, testRunAllFunProofs, testRunAllInfProofs, testRunAllWdProofs, testMtStress, testMt2w, testMt4w ] os: [ ubuntu-latest ] - java: [ 21 ] + java: [ 25 ] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v7 - - name: Set up JDK 21 + - name: Set up JDK 25 uses: actions/setup-java@v6.0.1 with: java-version: ${{ matrix.java }} diff --git a/build.gradle b/build.gradle index ef430f87332..195353bd2c0 100644 --- a/build.gradle +++ b/build.gradle @@ -69,21 +69,8 @@ subprojects { version = rootProject.version java { - sourceCompatibility = 21 - targetCompatibility = 21 - } - - // The JavaFX 25 jars (javafx-*) are class-file version 67, i.e. they require JDK 23+, while - // the baseline of the build is Java 21. The JavaFX UI modules (key.ui.fx and the keyext.*.fx - // ports) therefore compile and test with a pinned JDK 25 toolchain: the foojay resolver - // (settings.gradle) provisions it on machines that only have a Java 21 (e.g. the CI jobs), - // and the emitted bytecode stays Java-21 compatible via options.release = 21 below. - if (project.name.endsWith('.fx')) { - java { - toolchain { - languageVersion = JavaLanguageVersion.of(25) - } - } + sourceCompatibility = 25 + targetCompatibility = 25 } repositories { @@ -122,8 +109,9 @@ subprojects { tasks.withType(JavaCompile).configureEach { // Setting UTF-8 as the java source encoding. options.encoding = "UTF-8" - // Setting the release to Java 21 - options.release = 21 + // Setting the release to Java 25 (also required by the JavaFX 25 jars, which are + // class-file version 67 = JDK 23+) + options.release = 25 } tasks.withType(Javadoc).configureEach { diff --git a/key.ui.fx/README.md b/key.ui.fx/README.md index 8c82be19a87..f2acdc8842d 100644 --- a/key.ui.fx/README.md +++ b/key.ui.fx/README.md @@ -133,10 +133,8 @@ contract; capability interfaces: `MainMenuF`, `ToolbarF`, `StatusLineF`, `Contex * Per-commit gate: `./gradlew --no-daemon :key.ui.fx:spotlessApply :key.ui.fx:compileJava :key.ui.fx:test`. * CI: the FX modules are part of the `tests.yml` unit-test matrix, the `spotlessCheck` quality - job and the broad release-test `test` runs. The JavaFX 25 jars are class-file version 67 - (JDK 23+), so the FX modules build with a pinned **JDK 25 toolchain** (root `build.gradle`); - on Java-21-only machines (the CI baseline) the foojay resolver provisions it automatically. - The emitted bytecode stays Java-21 compatible (`--release 21`). + job and the broad release-test `test` runs. The **whole build targets JDK 25** (JavaFX 25 jars + are class-file version 67 = JDK 23+); the CI matrices use Corretto/Temurin 25 accordingly. ## Documents From 5148bfc3a65ed2817dca90d7ca59369578b74149 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Sat, 10 Oct 2026 13:44:40 +0200 Subject: [PATCH 135/281] P2a sequent interaction: left-click term menu, shift+click focussed auto mode, search prefill, strategy Go/merge lock MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Difference audit 2026-10-10, items D28/D29/D32/D35 (Swing parity): * SequentViewF: plain left click (no Ctrl) on a term opens the taclet term menu at the mouse position (Swing CurrentGoalViewListener.mouseClicked :68-86, CurrentGoalViewMenu) with the POPUP_DELAY=400 ms reopen guard of Swing SequentViewListener (:29/:51, stamped on menu close like the Swing PopupMenuListener :79-97); while a popup is open the hover highlight is frozen (Swing refreshHighlightning). Shift+click starts the focussed auto mode at the clicked position via ProofControl.startFocussedAutoMode (Swing :56-58, AbstractProofControl:627-642). Ctrl+click keeps the FX selection path. * SequentViewF: the search bar prefills with the hovered term text, escaped when the RegExp toggle is active (Swing SearchInSequentAction:32-37 + SequentViewSearchBar.searchFor:220-230); a no-op without hover. * StrategySelectionViewF: the Go/Stop button (Swing btnGo + AutoModeAction:109-177) driven by the mediator's running property, and the parallel-prover merge lock reflection (Swing reflectParallelProverMergeLock:869-895 — merge-point group forced to 'skip' + greyed while the multi-core prover is active, view-only). * New whitelisted key.fx.verify.sequentmenu hook: term menu build (31 entries on the Agatha demo), the delay guard, both search prefills and the real focussed auto mode run — PASS on Xvnc :99. --- key.ui.fx/build.gradle | 5 +- .../de/uka/ilkd/key/gui/fx/MainWindowF.java | 15 + .../key/gui/fx/nodeviews/SequentViewF.java | 288 ++++++++++++++++++ .../fx/strategy/StrategySelectionViewF.java | 132 +++++++- 4 files changed, 437 insertions(+), 3 deletions(-) diff --git a/key.ui.fx/build.gradle b/key.ui.fx/build.gradle index 9cb4115f788..bf9da883e30 100644 --- a/key.ui.fx/build.gradle +++ b/key.ui.fx/build.gradle @@ -128,6 +128,8 @@ run { // bind round trip, the sequent view Ctrl+F key path) // * key.fx.verify.inputfreeze = any (inputfreeze: the auto mode blocking overlay — direct // freeze/unfreeze with key blocking, then an auto-mode-driven run) + // * key.fx.verify.sequentmenu = any (P2a: run the sequent interaction self test — left-click + // term menu + POPUP_DELAY guard, search prefill, shift+click focussed auto mode) ["key.fx.driver", "key.fx.theme", "key.fx.demo.sequent", "key.fx.verify.sequent", "key.fx.show", "key.fx.demo.autoprove", "key.fx.verify.tree", "key.fx.demo.autoprove.live", "key.fx.demo.maxsteps", "key.fx.verify.goallist", @@ -142,7 +144,8 @@ run { "key.fx.verify.help", "key.fx.verify.soundiness", "key.fx.verify.profileloading", "key.fx.verify.profileloadingdialog", "key.fx.verify.javacsettings", "key.fx.verify.termmenu", "key.fx.verify.colors", "key.fx.verify.smt", - "key.fx.verify.loadingexit", "key.fx.verify.shortcuts", "key.fx.verify.inputfreeze"] + "key.fx.verify.loadingexit", "key.fx.verify.shortcuts", "key.fx.verify.inputfreeze", + "key.fx.verify.sequentmenu"] .each { name -> def value = System.getProperty(name) if (value != null) { 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 index 5aa76a2253d..66ef3635d5e 100644 --- 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 @@ -676,6 +676,9 @@ private void wireSequentView() { 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 // the hook doubles as the :99 verification signal (same line as the standalone driver) @@ -931,6 +934,18 @@ protected KeYEnvironment call() throws Exception { 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); + } // 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 diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentViewF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentViewF.java index ab3cbc66d37..cf6ecafb931 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentViewF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentViewF.java @@ -15,7 +15,9 @@ import java.util.function.Consumer; import java.util.regex.Matcher; import java.util.regex.Pattern; +import javafx.animation.KeyFrame; import javafx.animation.PauseTransition; +import javafx.animation.Timeline; import javafx.geometry.Insets; import javafx.geometry.Point2D; import javafx.scene.control.Button; @@ -273,6 +275,26 @@ public String printedText() { * {@code CurrentGoalView.getMousePosInSequent()}). */ private PosInSequent hoveredPos; + // termmenu: left-click term menu (Swing SequentViewListener) + /** + * termmenu: the delay after closing a popup menu before another menu may be opened (Swing + * {@code SequentViewListener.POPUP_DELAY}, SequentViewListener.java:29): the click that + * dismissed a popup (or a rapid re-click) must not instantly reopen a new menu. + */ + public static final int POPUP_DELAY = 400; + /** + * termmenu: the time of the last popup-menu close (Swing + * {@code SequentViewListener.lastPopupCloseTime}, SequentViewListener.java:31), stamped by + * the menu close handler of {@link #showTermMenu}. + */ + private long lastPopupCloseTime; + /** + * termmenu: whether a popup menu is currently open — the hover highlight is frozen while it + * is (Swing {@code SequentViewListener} toggles {@code sequentView.refreshHighlightning} in + * the PopupMenuListener, SequentViewListener.java:79-97, so the highlighted term does not + * change under the open menu). + */ + private boolean termMenuOpen; /** shows the term info of the hovered position after the Swing-like display delay. */ private final Tooltip hoverTooltip = new Tooltip(); /** delay before the tooltip shows (Swing ToolTipManager initial delay ≈ 500 ms). */ @@ -771,6 +793,33 @@ private void handleMouseClick(MouseEvent event) { return; } PosInSequent pos = table.getPosInSequent(charIndex, filter); + // termmenu: the Swing click branches (Swing CurrentGoalViewListener.mouseClicked, + // CurrentGoalViewListener.java:56-86): Shift+click starts the focussed auto mode at the + // clicked position and does not show a menu (CurrentGoalViewListener.java:56-58); + // plain left click (no Ctrl) opens the taclet term menu at the clicked position + // (CurrentGoalViewListener.java:68-86), guarded by the popup delay + // (SequentViewListener.POPUP_DELAY checked at CurrentGoalViewListener.java:51); Ctrl+ + // click keeps the FX selection path (the Swing left-click does not select, but the + // selection drives the FX status line, see setOnPosSelected) + if (event.isShiftDown()) { + startFocussedAutoModeAt(pos); + return; + } + if (event.getButton() == MouseButton.PRIMARY && !event.isControlDown()) { + lastClickedPos = pos; // lemmaorigin: remember the clicked term + if (pos != null) { + // the selection path stays live (status line) like the Swing hover highlight + highlightedRange = pos.getBounds() != null && pos.getBounds().length() > 0 + ? pos.getBounds() + : null; + rebuildRuns(); + onPosSelected.accept(pos); + if (canOpenTermMenu()) { + showTermMenu(event, pos); + } + } + return; + } lastClickedPos = pos; // lemmaorigin: remember the clicked term (Swing context-menu target) Range bounds = pos != null ? pos.getBounds() : null; highlightedRange = bounds != null && bounds.length() > 0 ? bounds : null; @@ -778,6 +827,105 @@ private void handleMouseClick(MouseEvent event) { onPosSelected.accept(pos); } + // termmenu: begin — left-click term menu + focussed auto mode (Swing + // CurrentGoalViewListener.mouseClicked + SequentViewListener) + /** + * termmenu: whether a term menu may be opened right now — the popup delay of Swing + * {@code SequentViewListener.mouseClicked} (CurrentGoalViewListener.java:51): a click within + * {@link #POPUP_DELAY} milliseconds after a popup closed only re-highlights, it never opens + * a new menu. + * + * @return {@code true} if at least {@link #POPUP_DELAY} ms elapsed since the last menu close + */ + boolean canOpenTermMenu() { + return !termMenuOpen + && Math.abs(System.currentTimeMillis() - lastPopupCloseTime) >= POPUP_DELAY; + } + + /** + * termmenu: stamps the close handler of the given popup menu (the hover-highlight freeze + * and the {@link #POPUP_DELAY} close time of Swing {@code SequentViewListener.showPopup}'s + * PopupMenuListener, SequentViewListener.java:79-97) — shared by the left-click term menu + * and the right-click context menu. + * + * @param menu the popup menu about to be shown + */ + private void stampMenuOnHidden(ContextMenu menu) { + termMenuOpen = true; + menu.setOnHidden(e -> { + termMenuOpen = false; + // Swing popupMenuWillBecomeInvisible/popupMenuCanceled stamp the close time + lastPopupCloseTime = System.currentTimeMillis(); + }); + } + + /** + * termmenu: Shift+click focussed auto mode (Swing CurrentGoalViewListener.mouseClicked, + * CurrentGoalViewListener.java:56-58): starts the automatic strategy restricted to the + * clicked position of the selected goal via + * {@code ProofControl.startFocussedAutoMode(PosInOccurrence, Goal)} (Swing + * AbstractProofControl.startFocussedAutoMode, AbstractProofControl.java:627-642 — the goal's + * rule app manager is exchanged for a {@code FocussedRuleApplicationManager} and the + * regular auto mode is started for the single selected goal). A no-op without menu context, + * goal or a resolvable position. + * + * @param pos the clicked sequent position, may be {@code null} + * @return {@code true} if the focussed auto mode was started + */ + boolean startFocussedAutoModeAt(PosInSequent pos) { + if (menuMediator == null || menuProofControl == null || pos == null) { + return false; + } + Goal goal = menuMediator.getSelectedGoal(); + if (goal == null) { + return false; + } + menuProofControl.startFocussedAutoMode(pos.getPosInOccurrence(), goal); + return true; + } + + /** + * termmenu: builds and shows the taclet term menu at the given position and mouse location + * (Swing {@code CurrentGoalViewMenu} built in CurrentGoalViewListener.mouseClicked:68-86 and + * shown at the mouse with a {@code -5, -5} offset by SequentViewListener.showPopup,78-103). + * The menu is the plain term menu (no macro replacement — the Swing left-click branch is + * independent of the "Right Click for Proof Macros" setting). While the menu is open the + * hover highlight is frozen and the close time is stamped for the + * {@link #POPUP_DELAY} guard (Swing PopupMenuListener, SequentViewListener.java:79-97). + * A no-op without menu context or goal. + * + * @param event the mouse event the menu is shown at + * @param pos the (non-null) sequent position the menu is built for + */ + private void showTermMenu(MouseEvent event, PosInSequent pos) { + if (menuMediator == null || menuProofControl == null) { + return; + } + Goal goal = menuMediator.getSelectedGoal(); + if (goal == null) { + return; + } + List entries = + SequentMenuModelF.build(pos, menuMediator, menuProofControl, null, null); + ContextMenu menu = SequentTermContextMenuF.build(entries, + new SequentTermContextMenuF.MenuContext(menuMediator, menuProofControl, goal, pos, + null, this::printSequent)); + stampMenuOnHidden(menu); + // Swing showPopup shows the menu at the mouse position with a (-5, -5) offset + menu.show(textFlow, event.getScreenX() - 5, event.getScreenY() - 5); + } + + /** + * termmenu: whether a term menu is currently open (the hover highlight is frozen while it + * is, see {@link #termMenuOpen}). + * + * @return {@code true} if a popup menu is open + */ + boolean isTermMenuOpen() { + return termMenuOpen; + } + // termmenu: end + // termmenu: begin — right-click sequent context menu (SequentMenuModelF + // SequentTermContextMenuF) /** @@ -809,6 +957,9 @@ private void showContextMenu(MouseEvent event) { // is active ({@link #buildRightClickMenu} falls back to the term menu when no macro is // applicable) ContextMenu menu = buildRightClickMenu(pos, fallback); + // termmenu: the right-click popup shares the hover freeze + close-time stamping of the + // left-click term menu (Swing SequentViewListener.showPopup handles both popups) + stampMenuOnHidden(menu); menu.show(textFlow, event.getScreenX(), event.getScreenY()); } @@ -988,6 +1139,125 @@ private PosInSequent firstIndexedPos() { } // menu: MP7 — end + // termmenu: begin — key.fx.verify.sequentmenu (P2a: left-click term menu, POPUP_DELAY guard, + // search prefill, shift+click focussed auto mode) + /** + * termmenu: headless self test of the P2a sequent interaction ({@code + * key.fx.verify.sequentmenu}), run after the demo load from MainWindowF. Checks, in order: + *

          + *
        1. the term menu built for the first indexed position is non-empty (the taclet entries of + * Swing {@code CurrentGoalViewMenu});
        2. + *
        3. the {@link #POPUP_DELAY} guard: immediately after a menu close a new menu is + * suppressed, after the delay it may open again (Swing + * {@code CurrentGoalViewListener.mouseClicked:51});
        4. + *
        5. the search-bar prefill from the hovered position (Swing + * {@code SearchInSequentAction}: plain and RegExp-escaped);
        6. + *
        7. the Shift+click wiring: a click without position is safely ignored, and the real + * focussed auto mode starts at the position (observed via the mediator's running property; + * it is stopped after the report — the run itself is asynchronous).
        8. + *
        + * + * @return {@code "... PASS - ..."} or {@code "... FAIL - ..."} + */ + public String verifySequentMenu() { + Goal goal = menuMediator == null ? null : menuMediator.getSelectedGoal(); + PosInSequent pos = firstIndexedPos(); + if (goal == null || pos == null) { + return "SKIP - no goal/position (no printed sequent)"; + } + List failures = new ArrayList<>(); + // 1. the term menu at the position + List entries = + SequentMenuModelF.build(pos, menuMediator, menuProofControl, null, null); + ContextMenu menu = SequentTermContextMenuF.build(entries, + new SequentTermContextMenuF.MenuContext(menuMediator, menuProofControl, goal, pos, + null, this::printSequent)); + List labels = menuLabels(menu.getItems()); + if (labels.isEmpty()) { + failures.add("the term menu is empty"); + } + // 2. the popup delay guard: a close stamps the time, a reopen needs the elapsed delay + boolean openAllowedBefore = canOpenTermMenu(); + lastPopupCloseTime = System.currentTimeMillis(); + boolean suppressed = !canOpenTermMenu(); + lastPopupCloseTime = System.currentTimeMillis() - POPUP_DELAY - 1; + boolean openAllowedAfter = canOpenTermMenu(); + if (!openAllowedBefore || !suppressed || !openAllowedAfter) { + failures.add("POPUP_DELAY guard broken (before=" + openAllowedBefore + ", suppressed=" + + suppressed + ", after=" + openAllowedAfter + ")"); + } + lastPopupCloseTime = 0; // restore the open state for the remaining checks + // 3. the search prefill from the hovered position (plain + RegExp-escaped); the Swing + // prefill uses the hovered TERM, so the test picks the first position whose text is a + // single-line term (the first indexed position covers the whole sequent, which a + // TextField cannot hold) + PosInSequent prefillPos = null; + for (int i = 0; i < printed.length() && prefillPos == null; i++) { + PosInSequent candidate = getSequentPosAt(i); + if (candidate == null) { + continue; + } + String text = getHighlightedText(candidate); + if (!text.isBlank() && !text.contains("\n")) { + prefillPos = candidate; + } + } + if (prefillPos == null) { + return "FAIL - no single-line term position for the search prefill test"; + } + PosInSequent savedHovered = hoveredPos; + boolean savedRegex = regexToggle.isSelected(); + hoveredPos = prefillPos; + String expected = getHighlightedText(prefillPos); + showSearchBar(); + if (!expected.equals(searchField.getText())) { + failures.add("search prefill mismatch: expected '" + expected + "', got '" + + searchField.getText() + "'"); + } + regexToggle.setSelected(true); + // the reprint of the first search invalidated the hover state (printSequent → + // clearHover, the Swing setText behaviour) — re-arm the hover for the escape check + hoveredPos = prefillPos; + showSearchBar(); + String escaped = expected.replaceAll("[-\\[\\]{}()*+?.,\\\\^$|#s]", "\\\\$0"); + if (!escaped.equals(searchField.getText())) { + failures.add("regexp search prefill mismatch: expected '" + escaped + "', got '" + + searchField.getText() + "'"); + } + regexToggle.setSelected(savedRegex); + hoveredPos = savedHovered; + hideSearchBar(); + if (!failures.isEmpty()) { + return "FAIL - " + String.join("; ", failures); + } + // 4. the real focussed auto mode at the position (Swing CurrentGoalViewListener:56-58); + // it runs asynchronously — the stop is scheduled below and reported through the log + boolean started = startFocussedAutoModeAt(pos); + if (!started) { + return "FAIL - startFocussedAutoModeAt returned false"; + } + Timeline[] holder = new Timeline[1]; + int[] ticks = { 0 }; + holder[0] = new Timeline(new KeyFrame(Duration.millis(200), event -> { + boolean running = menuMediator.autoModeRunningProperty().get(); + if (running && ++ticks[0] < 60) { + return; // keep polling (max ~12 s), then request the stop + } + holder[0].stop(); + boolean stillRunning = menuMediator.autoModeRunningProperty().get(); + if (stillRunning) { + menuProofControl.stopAutoMode(); + } + LOGGER.info("Sequent menu verification focussed auto mode: {}", + stillRunning ? "stopped" : "finished"); + })); + holder[0].setCycleCount(javafx.animation.Animation.INDEFINITE); + holder[0].play(); + return "PASS - term menu " + labels.size() + " entries, POPUP_DELAY guard, search " + + "prefill, focussed auto mode started"; + } + // termmenu: end + /** * The character index of the printed text under the given mouse event (the same mapping * {@link #handleMouseClick} uses), or {@code -1} if the event does not address a character. @@ -1064,6 +1334,12 @@ public PosInSequent getSequentPosAt(int charIndex) { * highlight rectangle and the tooltip shows the term info of the hovered position. */ private void handleMouseMove(MouseEvent event) { + // termmenu: the hover highlight is frozen while a popup menu is open (Swing + // SequentViewListener toggles sequentView.refreshHighlightning in the PopupMenuListener, + // SequentViewListener.java:79-97, so the highlighted term does not change under the menu) + if (termMenuOpen) { + return; + } InitialPositionTable table = getInitialPositionTable(); if (table == null || printed == null) { return; @@ -1241,6 +1517,18 @@ private Path rectForRange(int start, int end, String styleClass) { * focuses the field. A query kept from a previous opening is re-applied. */ public void showSearchBar() { + // searchprefill: the Swing Ctrl+F action prefills the field with the hovered term text + // (Swing SearchInSequentAction.actionPerformed, SearchInSequentAction.java:32-37: + // searchFor(view.getHighlightedText()) with the mouse position's term, escaped when the + // RegExp toggle is active, SequentViewSearchBar.searchFor:220-230); a no-op when the + // mouse is not over a term (the previously entered query is kept) + if (hoveredPos != null) { + String prefill = getHighlightedText(hoveredPos); + if (regexToggle.isSelected()) { + prefill = prefill.replaceAll("[-\\[\\]{}()*+?.,\\\\^$|#s]", "\\\\$0"); + } + searchField.setText(prefill); + } searchBar.setVisible(true); searchBar.setManaged(true); searchField.selectAll(); diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/strategy/StrategySelectionViewF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/strategy/StrategySelectionViewF.java index 7c11ddf89ee..9a2d2be1ce3 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/strategy/StrategySelectionViewF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/strategy/StrategySelectionViewF.java @@ -11,6 +11,7 @@ import java.util.Objects; import javafx.geometry.Insets; import javafx.geometry.Pos; +import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.control.RadioButton; import javafx.scene.control.ScrollPane; @@ -24,6 +25,7 @@ import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; +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; @@ -31,6 +33,8 @@ import de.uka.ilkd.key.proof.Proof; import de.uka.ilkd.key.proof.init.JavaProfile; import de.uka.ilkd.key.proof.init.Profile; +import de.uka.ilkd.key.settings.GeneralSettings; +import de.uka.ilkd.key.settings.ProofIndependentSettings; import de.uka.ilkd.key.settings.ProofSettings; import de.uka.ilkd.key.settings.StrategySettings; import de.uka.ilkd.key.strategy.Strategy; @@ -81,8 +85,8 @@ * {@code .strategy-status} (driver status label, not part of this view). *

        * Deliberately deferred: the strategy preset combo box (built-in and user-defined presets with - * save/stash/rename), the parallel-prover merge lock, the auto-prove "go" button, keyboard - * shortcuts and the timeout setting (not shown by the Swing view either). + * save/stash/rename) and the timeout setting (not shown by the Swing view either). The + * parallel-prover merge lock and the auto-prove "go" button are implemented (strategy markers). */ public class StrategySelectionViewF extends ScrollPane { @@ -114,6 +118,15 @@ public class StrategySelectionViewF extends ScrollPane { private final VBox content = new VBox(); + /** + * strategy: the "Go"/"Stop" button (Swing {@code StrategySelectionView.btnGo}, a plain + * button carrying the shared {@code AutoModeAction}, StrategySelectionView.java:157-208): + * starts the automatic proof search of the selected proof, or stops a running one. The label + * follows the auto mode via {@link KeYMediatorF#autoModeRunningProperty()} (Swing + * {@code AutoModeAction.autoModeStarted/autoModeStopped}, AutoModeAction.java:109-138). + */ + private final Button goButton = new Button("Go"); + /** * Edits {@link StrategySettings#getMaxSteps()} ("Max. Rule Applications"), the * {@code MaxRuleAppSlider} of the Swing view. @@ -136,6 +149,12 @@ public class StrategySelectionViewF extends ScrollPane { private KeYSelectionModel selectionModel; private Proof proof; + /** + * strategy: the shared mediator (via {@link #attachMediator}), driving the {@link #goButton}; + * {@code null} until attached (the button stays disabled). + */ + private KeYMediatorF mediator; + /** * Set while the widgets are updated programmatically from the settings ({@link #refresh}). * Guards the widget listeners so that they do not write the freshly read values back and @@ -161,6 +180,16 @@ public StrategySelectionViewF() { content.setSpacing(6); setContent(content); + // strategy: the Go/Stop button row (Swing btnGo, StrategySelectionView.java:207-208/179) + goButton.getStyleClass().add("strategy-go"); + goButton.setTooltip(new Tooltip("Start or stop the automatic proof search")); + goButton.setDisable(true); + goButton.setOnAction(e -> handleGoButton()); + HBox goRow = new HBox(8, goButton); + goRow.getStyleClass().add("strategy-go-row"); + goRow.setAlignment(Pos.CENTER_LEFT); + content.getChildren().add(goRow); + // "Max. Rule Applications": label + spinner, the MaxRuleAppSlider of the Swing view Label maxStepsLabel = new Label(DEFINITION.getMaxRuleApplicationsLabel()); maxStepsLabel.getStyleClass().add("strategy-maxsteps-label"); @@ -205,6 +234,102 @@ public void attach(KeYSelectionModel model) { refresh(model.getSelectedProof()); } + // strategy: begin — the Go/Stop button + the parallel-prover merge lock (Swing + // StrategySelectionView.btnGo + reflectParallelProverMergeLock) + /** + * strategy: supplies the mediator driving the {@link #goButton} and registers the + * parallel-prover merge-lock listener (Swing {@code StrategySelectionView} constructor + * property-change listener, StrategySelectionView.java:161-165). Idempotent: re-attaching + * the same mediator is a no-op. + * + * @param newMediator the shared mediator, may be {@code null} to detach + */ + public void attachMediator(KeYMediatorF newMediator) { + if (this.mediator == newMediator) { + return; + } + this.mediator = newMediator; + if (newMediator == null) { + goButton.setDisable(true); + goButton.setText("Go"); + return; + } + // the label follows the auto mode (Swing AutoModeAction.autoModeStarted/autoModeStopped, + // AutoModeAction.java:109-138: "Stop" while running, "Start"/"Continue" otherwise) + newMediator.autoModeRunningProperty().addListener( + (obs, wasRunning, running) -> updateGoButton(running)); + updateGoButton(newMediator.autoModeRunningProperty().get()); + // the merge lock follows the parallel-prover toggle (the Swing view re-registers the + // same property-change listener in its constructor) + ProofIndependentSettings.DEFAULT_INSTANCE.getGeneralSettings() + .addPropertyChangeListener(GeneralSettings.PARALLEL_PROVER_ENABLED, + evt -> reflectParallelProverMergeLock()); + reflectParallelProverMergeLock(); + } + + /** + * strategy: the Go/Stop button action (Swing AutoModeAction.actionPerformed, + * AutoModeAction.java:154-177): starts the automatic proof search of the selected proof, or + * stops a running one. While auto mode runs the input freeze blocks the rest of the UI (the + * Freeze of the Swing MainWindow) — the freeze keeps {@code Escape} (the Swing + * {@code AutoModeAction.STOP_KEY}) live, which stops the run. + */ + private void handleGoButton() { + if (mediator == null) { + return; + } + if (mediator.autoModeRunningProperty().get()) { + mediator.stopAutoMode(); + } else { + mediator.startAutoMode(); + } + } + + /** + * strategy: updates the Go/Stop button label (Swing AutoModeAction: "Stop" while the auto + * mode runs, "Start"/"Continue" otherwise — the FX button always shows "Go" when idle). + * + * @param running whether the auto mode is running + */ + private void updateGoButton(boolean running) { + goButton.setText(running ? "Stop" : "Go"); + goButton.setTooltip(new Tooltip(running ? "Stop the automatic proof search" + : "Start the automatic proof search")); + } + + /** + * strategy: reflects in the strategy view that the merge rule is unavailable while the + * multi-core prover is active: the merge-point option group is forced to skip and + * greyed with an explanatory tooltip (Swing + * {@code StrategySelectionView.reflectParallelProverMergeLock}, + * StrategySelectionView.java:869-895). This is view-only — the stored strategy property is + * left untouched, so the user's real choice reappears when they switch back to the + * single-core prover (the merge rule is additionally disabled at the engine level during + * parallel runs, {@code MergeRule.isApplicable}). + */ + private void reflectParallelProverMergeLock() { + boolean mt = + ProofIndependentSettings.DEFAULT_INSTANCE.getGeneralSettings() + .isParallelProverEnabled(); + List mergeButtons = propertyButtons.get(StrategyProperties.MPS_OPTIONS_KEY); + if (mergeButtons == null) { + return; + } + for (RadioButton button : mergeButtons) { + if (mt) { + button.setSelected(StrategyProperties.MPS_SKIP.equals(button.getUserData())); + button.setDisable(true); + button.setTooltip(new Tooltip("The merge rule is disabled while the multi-core " + + "prover is active (forced to 'skip'). Switch to the single-core prover " + + "to use it.")); + } else { + button.setDisable(false); + button.setTooltip(null); + } + } + } + // strategy: end + /** * Shows the strategy settings of the given proof, the counter-part of * {@code StrategySelectionView.refresh(Proof)}: the radio buttons reflect the proof's active @@ -239,6 +364,9 @@ public void refresh(Proof newProof) { } finally { updating = false; } + // strategy: the merge lock reflects on every refresh (Swing + // StrategySelectionView.refresh calls reflectParallelProverMergeLock, :863) + reflectParallelProverMergeLock(); } /** From c042b85ed037fdfa93657f31f3eaea8f62e80867 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Sat, 10 Oct 2026 13:45:03 +0200 Subject: [PATCH 136/281] PARITY-SIGNOFF: fold the 2026-10-10 difference audit into the tracking docs The ~37 new Swing-JavaFX differences from the second sweep, with stable IDs A1-A10/B11-B18/C19-C27/D28-D37 mapped onto the P2a/P2b/P3a/P3b/P3c/P4 batches; D28/D29/D32/D35 marked RESOLVED via P2a (key.fx.verify.sequentmenu PASS). --- key.ui.fx/PARITY-SIGNOFF.md | 69 +++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/key.ui.fx/PARITY-SIGNOFF.md b/key.ui.fx/PARITY-SIGNOFF.md index 403e4c2370e..53cbf084edd 100644 --- a/key.ui.fx/PARITY-SIGNOFF.md +++ b/key.ui.fx/PARITY-SIGNOFF.md @@ -115,3 +115,72 @@ as open. The KNOWN-SIMPLIFIED ledger mirrors the module-level subset of this list with per-site file:line markers and statuses (`OPEN` / `FIXED-UPSTREAM` / `WONT-REPLICATE`). This sign-off is re-run after each milestone batch that closes any of the items above. + +# Difference audit — Swing↔JavaFX (2026-10-10) + +The 2026-10-08 sign-off covered the documented deviations (the 68 KNOWN-SIMPLIFIED markers plus +the still-open list above). This section records the **new, previously undocumented** +differences found by a second Swing↔JavaFX sweep, with stable IDs used by the P2/P3/P4 batches +(`weigl/ocfx-p2-*`, `weigl/ocfx-p3-*`, `weigl/ocfx-p4-*`). Statuses: OPEN / RESOLVED (batch) +/ WONT-REPLICATE. + +## A. Dialogs missing or reduced + +| ID | Finding (Swing reference) | Status | +|----|---------------------------|--------| +| A1 | Contract/invariant configurator dialogs: the Swing registration `WindowUserInterfaceControl.java:74-82` (FunctionalOperationContract/DependencyContract/LoopInvariantRule/BlockContractInternal/BlockContractExternal completions + `ContractConfigurator.java`) — the FX registry (`WindowUserInterfaceControlF.java:126-135`) holds only `MergeRuleCompletionF` | RESOLVED (P2b) | +| A2 | `LemmaSelectionDialog` (lemmatagenerator, 161 lines) not ported | RESOLVED (P2b) | +| A3 | `LoadUserTacletsDialog` reduced (Swing 499 lines) | RESOLVED (P2b) | +| A4 | Proof Statistics: no export (Swing exports HTML/text) | OPEN (P3c) | +| A5 | About/License reduced to toasts instead of dialogs | OPEN (P3c) | +| A6 | GitHub-issue prefill dropped (bug-reporting URL parameters) | OPEN (P3c) | +| A7 | Feedback: no zip archive of logs | OPEN (P3c) | +| A8 | ToolTip options: "Save as Default" behaviour | OPEN (P3c) | +| A9 | `EnsureSourceConsistency` info dialog | RESOLVED (P2b) | +| A10 | Dialog triage remainder of the sweep | OPEN (triaged in P4) | + +## B. Menus / actions / toolbar + +| ID | Finding (Swing reference) | Status | +|----|---------------------------|--------| +| B11 | Dynamic "Goal Back" label (`GoalBackAction.java:113-125`) vs. static `MainWindowF.java:3325-3327` | OPEN (P3b) | +| B12 | Macro superset + category submenus + ProofScript actions vs. the 4-macro `AUTOMATION_MACROS` | OPEN (P3b) | +| B13 | Extension PATH nesting (extension menus cannot nest) | OPEN (P4) | +| B14 | Per-extension toolbars merged into one | OPEN (P4, decide) | +| B15 | Toolbar GoalBack/Prune have no enablement | OPEN (P3b) | +| B16 | Popup dialogs replaced by toasts (per site) | OPEN (P4, decide) | +| B17 | TermLabelMenu persistence/styling | OPEN (P3b) | +| B18 | Recent-Files empty menu not disabled + font-size item order | OPEN (P3b) | + +## C. Proof tree + +| ID | Finding (Swing reference) | Status | +|----|---------------------------|--------| +| C19 | Per-proof view state cache (expansion/selection per proof) | OPEN (P3a) | +| C20 | Linearized (flat) proof view mode | OPEN (P3a) | +| C21 | OSS child nodes in the tree | OPEN (P3a) | +| C22 | Whole-tree actions (expand all/collapse all/...) | OPEN (P3a) | +| C23 | Popup items incl. SubtreeStatistics | OPEN (P3a) | +| C24 | `PROOF_TREE` extension seam (tree popup contributions) | OPEN (P4) | +| C25 | Node-filter counting rule | OPEN (P3a) | +| C26 | Hidden-tab passivation (Swing docking detail) | OPEN (P4, likely WONT-REPLICATE) | +| C27 | Auto-mode partial subtree updates | OPEN (P3a) | + +## D. Sequent / strategy / goal list / status bar + +| ID | Finding (Swing reference) | Status | +|----|---------------------------|--------| +| D28 | Left-click taclet menu + `POPUP_DELAY` guard (`CurrentGoalViewListener.java:48-95`, `SequentViewListener.java:29`) | RESOLVED (P2a) | +| D29 | Shift+click focussed auto mode (`CurrentGoalViewListener.java:56-58`) | RESOLVED (P2a) | +| D30 | Extension tooltip strings unwired | OPEN (P4) | +| D31 | Sequent line width computed from the viewport | OPEN (P3c) | +| D32 | Sequent search prefill from the hovered term (`SearchInSequentAction.java:32-37`) | RESOLVED (P2a) | +| D33 | Fonts vs. `KEY_FONT_*` settings | OPEN (P3c) | +| D34 | `SequentViewDock` missing | OPEN (P3c) | +| D35 | Strategy tab Go button + parallel-prover merge-point lock (`StrategySelectionView.java:157-208/869-895`) | RESOLVED (P2a) | +| D36 | `KeyboardShortcuts` extension seams absent | OPEN (P4) | +| D37 | Status-line task progress bar (log-only stubs, `WindowUserInterfaceControlF.java:317-327`) | OPEN (P3c) | + +P2a (`weigl/ocfx-p2-sequent`, merged 2026-10-10) resolved D28/D29/D32/D35 — +`key.fx.verify.sequentmenu` PASS (term menu 31 entries, POPUP_DELAY guard, search prefill, +focussed auto mode) on Xvnc :99. From abaeae9db331cccc6db06bba33c9fae023d69b5a Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Sat, 10 Oct 2026 14:42:51 +0200 Subject: [PATCH 137/281] key.ui.fx: P2b contract completions + dialogs (A1/A2/A3/A9) Port the Swing contract/invariant completions to JavaFX (new package de.uka.ilkd.key.gui.fx.contractcompletions): - ContractSelectionPanelF/ContractConfiguratorF: contract-cell rendering, CONTRACT_TYPE_ORDER sort, gray-out auxiliary logic, modal OK/Cancel, double-click OK and ESC cancel; AuxiliaryContractConfiguratorF with the abstract AuxiliaryContractSelectionPanelF; BlockContractSelectionPanelF (BlockContractImpl.combine); computeContract static helper. - InvariantConfiguratorF singleton with the InvariantDialogF port (per-candidate tabs, per-heap INV/MOD sub-tabs, variant + InfFlow fields, parse/buildInvariant -> configurate, \strictly_nothing locset hack, Store shallow-clones the Map[], Apply/Store disabled on error, cancel -> RuleAbortException); abbrev map wired from the mediator at init. - Five completions registered in MainWindowF beside MergeRuleCompletionF: FunctionalOperationContract, DependencyContract (FX ChoiceDialog heap chooser), LoopInvariantRule, BlockContractInternal/External. - Lemma workflow (A2/A3): ItemChooserF + LemmaSelectionDialogF (TacletFilter, FX-thread marshaling via latch); LoadUserTacletsDialogF grows the axiom-file list, the unsoundness InfoDialog with the don't-show-again setting and the Help window; filesForAxioms wired through the loader call. - A9: KeYMediatorF.ensureProofLoaded port + EnsureSourceConsistency info dialog. - Add key.core.infflow dependency (InformationFlowContractImpl type key), CSS contract-cell*/invariant-*/item-chooser*/axioms-* classes in both themes. - key.fx.verify.dialogs hook: completion registry completeness + configurator skeletons + lemma filter cancel/ok rounds + item chooser semantics; PASS on Xvnc :99. Audit A1/A2/A3/A9 RESOLVED (P2b); interactive end-to-end contract application documented as not headless-drivable without a JML example. --- key.ui.fx/PARITY-SIGNOFF.md | 16 + key.ui.fx/README.md | 2 + key.ui.fx/build.gradle | 8 +- .../de/uka/ilkd/key/core/fx/KeYMediatorF.java | 11 + .../de/uka/ilkd/key/gui/fx/MainWindowF.java | 81 ++- .../gui/fx/WindowUserInterfaceControlF.java | 11 + .../AuxiliaryContractConfiguratorF.java | 126 ++++ .../AuxiliaryContractSelectionPanelF.java | 98 +++ .../BlockContractExternalCompletionF.java | 73 ++ .../BlockContractInternalCompletionF.java | 73 ++ .../BlockContractSelectionPanelF.java | 47 ++ .../ContractConfiguratorF.java | 138 ++++ .../ContractSelectionPanelF.java | 278 ++++++++ .../DependencyContractCompletionF.java | 164 +++++ ...unctionalOperationContractCompletionF.java | 67 ++ .../InvariantConfiguratorF.java | 663 ++++++++++++++++++ .../LoopInvariantRuleCompletionF.java | 89 +++ .../key/gui/fx/dialogs/DialogsVerifyF.java | 365 ++++++++++ .../ilkd/key/gui/fx/dialogs/ItemChooserF.java | 296 ++++++++ .../gui/fx/dialogs/LemmaSelectionDialogF.java | 177 +++++ .../fx/dialogs/LoadUserTacletsDialogF.java | 197 +++++- .../de/uka/ilkd/key/gui/fx/theme/key-dark.css | 105 +++ .../uka/ilkd/key/gui/fx/theme/key-light.css | 71 ++ 23 files changed, 3110 insertions(+), 46 deletions(-) create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/contractcompletions/AuxiliaryContractConfiguratorF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/contractcompletions/AuxiliaryContractSelectionPanelF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/contractcompletions/BlockContractExternalCompletionF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/contractcompletions/BlockContractInternalCompletionF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/contractcompletions/BlockContractSelectionPanelF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/contractcompletions/ContractConfiguratorF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/contractcompletions/ContractSelectionPanelF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/contractcompletions/DependencyContractCompletionF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/contractcompletions/FunctionalOperationContractCompletionF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/contractcompletions/InvariantConfiguratorF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/contractcompletions/LoopInvariantRuleCompletionF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/dialogs/DialogsVerifyF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/dialogs/ItemChooserF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/dialogs/LemmaSelectionDialogF.java diff --git a/key.ui.fx/PARITY-SIGNOFF.md b/key.ui.fx/PARITY-SIGNOFF.md index 53cbf084edd..6e465d72988 100644 --- a/key.ui.fx/PARITY-SIGNOFF.md +++ b/key.ui.fx/PARITY-SIGNOFF.md @@ -184,3 +184,19 @@ differences found by a second Swing↔JavaFX sweep, with stable IDs used by the P2a (`weigl/ocfx-p2-sequent`, merged 2026-10-10) resolved D28/D29/D32/D35 — `key.fx.verify.sequentmenu` PASS (term menu 31 entries, POPUP_DELAY guard, search prefill, focussed auto mode) on Xvnc :99. + +P2b (`weigl/ocfx-p2-dialogs`, merged 2026-10-10) resolved A1/A2/A3/A9 — the five contract +completions (`FunctionalOperationContractCompletionF`, `DependencyContractCompletionF`, +`LoopInvariantRuleCompletionF`, `BlockContractInternalCompletionF`, +`BlockContractExternalCompletionF`) are registered in the FX control next to +`MergeRuleCompletionF` (Swing parity WindowUserInterfaceControl.java:74-82); the dialogs +(`ContractConfiguratorF`, `AuxiliaryContractConfiguratorF`, `BlockContractSelectionPanelF`, +`ContractSelectionPanelF`, `InvariantConfiguratorF`, `LemmaSelectionDialogF` + `ItemChooserF`, +`LoadUserTacletsDialogF` axiom panel/warning/help, `EnsureSourceConsistency` info dialog) are +ported; `key.fx.verify.dialogs` PASS (registry + dialog skeletons) on Xvnc :99. + +Interactive end-to-end contract application (completing a use-operation-contract rule app on a +spec-bearing method) is not headless-drivable without a JML-annotated example in +`key.ui/examples`; it stays covered by the skeletons above plus the to-be-added example in a +later batch. HTML-styled contract cells of the Swing configurator are plain text in the FX port +(KNOWN-SIMPLIFIED, see the class javadocs). diff --git a/key.ui.fx/README.md b/key.ui.fx/README.md index f2acdc8842d..99d204aa9a1 100644 --- a/key.ui.fx/README.md +++ b/key.ui.fx/README.md @@ -58,6 +58,8 @@ regression-tested on a virtual display. Demo proof paths default into | `key.fx.verify.updatehighlight` | update-highlight overlay (needs `useQuery.key`) | | `key.fx.verify.notifications` | notification framework (task-finished, proof-closed, exception) | | `key.fx.verify.termmenu` | headless sequent context-menu model (25 checks) | +| `key.fx.verify.sequentmenu` | left-click sequent term menu + POPUP_DELAY guard, search prefill, shift+click focussed auto mode (P2a) | +| `key.fx.verify.dialogs` | contract-completion registry + dialog skeletons (P2b): contract/auxiliary configurators, lemma selection dialog, item chooser | | `key.fx.verify.tacletmatch=1\|hold` | interactive taclet application (`hold` keeps the dialog open) | | `key.fx.verify.uicontrol` | `WindowUserInterfaceControlF` seam: status line, IssueDialogF, LogViewF, AutoDismissDialogF | | `key.fx.verify.docking` | named layout slots, maximize/restore, close-while-maximized | diff --git a/key.ui.fx/build.gradle b/key.ui.fx/build.gradle index bf9da883e30..b7e5dc9464b 100644 --- a/key.ui.fx/build.gradle +++ b/key.ui.fx/build.gradle @@ -34,6 +34,10 @@ dependencies { // well-definedness profile (Swing key.ui depends on key.core.wd the same way) implementation project(":key.core.wd") + // contractcompletions: the contract type order needs the information-flow contract + // implementation class (Swing key.ui depends on key.core.infflow the same way) + implementation project(":key.core.infflow") + // Java/JML source editor with syntax highlighting (Milestone M2). implementation(libs.richtextfx) @@ -130,6 +134,8 @@ run { // freeze/unfreeze with key blocking, then an auto-mode-driven run) // * key.fx.verify.sequentmenu = any (P2a: run the sequent interaction self test — left-click // term menu + POPUP_DELAY guard, search prefill, shift+click focussed auto mode) + // * key.fx.verify.dialogs = any (P2b: run the contract-completion registry + dialog + // skeletons — contract/auxiliary configurators, lemma selection dialog, item chooser) ["key.fx.driver", "key.fx.theme", "key.fx.demo.sequent", "key.fx.verify.sequent", "key.fx.show", "key.fx.demo.autoprove", "key.fx.verify.tree", "key.fx.demo.autoprove.live", "key.fx.demo.maxsteps", "key.fx.verify.goallist", @@ -145,7 +151,7 @@ run { "key.fx.verify.profileloadingdialog", "key.fx.verify.javacsettings", "key.fx.verify.termmenu", "key.fx.verify.colors", "key.fx.verify.smt", "key.fx.verify.loadingexit", "key.fx.verify.shortcuts", "key.fx.verify.inputfreeze", - "key.fx.verify.sequentmenu"] + "key.fx.verify.sequentmenu", "key.fx.verify.dialogs"] .each { name -> def value = System.getProperty(name) if (value != null) { 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 index 9fd93daa859..4da3cf345ae 100644 --- 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 @@ -178,6 +178,17 @@ 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} */ 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 index 66ef3635d5e..86a3d94aba8 100644 --- 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 @@ -81,7 +81,15 @@ 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.DialogsVerifyF; 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.LoadUserTacletsDialogF; import de.uka.ilkd.key.gui.fx.dialogs.RunAllProofsF; import de.uka.ilkd.key.gui.fx.docking.DockLayoutStore; @@ -171,7 +179,6 @@ import de.uka.ilkd.key.util.KeYResourceManager; import de.uka.ilkd.key.util.MiscTools; -import org.key_project.util.collection.DefaultImmutableSet; import org.key_project.util.collection.ImmutableList; import org.key_project.util.collection.ImmutableSet; import org.key_project.util.javafx.FxUtil; @@ -298,6 +305,22 @@ in order to generate and prove the proof obligations for the correctness of (non // 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()); } /** @@ -946,6 +969,14 @@ protected KeYEnvironment call() throws Exception { : 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 = DialogsVerifyF.run(stage, selectionModel.getSelectedProof(), + userInterface); + 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 @@ -1331,9 +1362,8 @@ private void loadUserDefinedTaclets() { } Path fileForTaclets = result.get().fileForTaclets(); boolean loadAsLemmata = result.get().generateProofObligations(); - // menu: the Swing axiom-file list of the dialog is not ported (KNOWN-DEFERRED, see - // LoadUserTacletsDialogF) — the loader never gets axiom files. - List filesForAxioms = List.of(); + // 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 = @@ -1391,9 +1421,8 @@ private void proveUserDefinedTaclets() { } Path fileForTaclets = result.get().fileForTaclets(); boolean loadAsLemmata = result.get().generateProofObligations(); - // menu: the Swing axiom-file list of the dialog is not ported (KNOWN-DEFERRED, see - // LoadUserTacletsDialogF) - List filesForAxioms = List.of(); + // 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(); @@ -1520,17 +1549,12 @@ private void handleTacletLoadException(Throwable exception) { private void runTacletSoundnessLoader(TacletLoader tacletLoader, InitConfig originalConfig, boolean loadAsLemmata, boolean isOnlyUsedForProvingTaclets, LemmaLoaderListener listener) { - // menu: instead of the Swing LemmaSelectionDialog, keep all "supported" taclets (the - // dialog's default "show only supported" filter) — the selection dialog is not ported - TacletSoundnessPOLoader.TacletFilter filter = tacletInfos -> { - ImmutableSet supported = DefaultImmutableSet.nil(); - for (TacletSoundnessPOLoader.TacletInfo info : tacletInfos) { - if (!info.isNotSupported()) { - supported = supported.add(info.getTaclet()); - } - } - return supported; - }; + // 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(); @@ -3886,15 +3910,28 @@ private CheckMenuItem rightClickMacroToggle() { * // 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). Only the - * // Swing info dialog of the toggle action (EnsureSourceConsistencyToggleAction.java:42-47) - * // is dropped. + * // 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 -> gs.setEnsureSourceConsistency(item.isSelected())); + 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; } 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 index eee7b1fdbfd..7e0678588b7 100644 --- 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 @@ -159,6 +159,17 @@ 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 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 contractPanel, + final Services services, final T[] contracts, final String title) { + this.contractPanel = contractPanel; + contractPanel.setContracts(contracts, title); + + Button okButton = new Button("OK"); + okButton.setOnAction(e -> { + successful = true; + stage.close(); + }); + Button cancelButton = new Button("Cancel"); + cancelButton.setOnAction(e -> { + successful = false; + stage.close(); + }); + HBox buttonPanel = new HBox(5, okButton, cancelButton); + buttonPanel.setAlignment(Pos.CENTER_RIGHT); + buttonPanel.setPadding(new Insets(5)); + + BorderPane root = new BorderPane(); + root.setCenter(contractPanel); + root.setBottom(buttonPanel); + Scene scene = new Scene(root, 820, 560); + ThemeManager.getInstance().style(scene); + // Swing: double-click on the list = OK + contractPanel.setOnMouseClicked(e -> { + if (e.getClickCount() == 2) { + successful = true; + stage.close(); + } + }); + // Swing: GuiUtilities.attachClickOnEscListener(cancelButton) + scene.getAccelerators().put(new KeyCodeCombination(KeyCode.ESCAPE), () -> { + successful = false; + stage.close(); + }); + stage.setTitle(name); + stage.setScene(scene); + } + + /** Shows the dialog modally and blocks until it is closed. */ + public void show() { + if (!FxUtil.isFxThread()) { + FxUtil.runLater(this::show); + return; + } + stage.initModality(Modality.APPLICATION_MODAL); + stage.showAndWait(); + } + + /** Non-blocking variant for the {@code key.fx.verify.*} hooks. */ + public void showNonBlocking() { + stage.initModality(Modality.NONE); + stage.show(); + } + + /** Closes the dialog as if OK had been pressed (verification harness). */ + public void requestOk() { + successful = true; + stage.close(); + } + + /** Closes the dialog as if Cancel had been pressed (verification harness). */ + public void requestCancel() { + successful = false; + stage.close(); + } + + /** @return the stage (verification harness) */ + public Stage getStage() { + return stage; + } + + /** Swing {@code wasSuccessful}. */ + public boolean wasSuccessful() { + return successful; + } + + /** Swing {@code getContract}. */ + public T getContract() { + return contractPanel.getContract(); + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/contractcompletions/AuxiliaryContractSelectionPanelF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/contractcompletions/AuxiliaryContractSelectionPanelF.java new file mode 100644 index 00000000000..e90e5e7cf4f --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/contractcompletions/AuxiliaryContractSelectionPanelF.java @@ -0,0 +1,98 @@ +/* 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 java.util.List; +import javafx.geometry.Insets; +import javafx.scene.control.Label; +import javafx.scene.control.ListView; +import javafx.scene.control.SelectionMode; +import javafx.scene.text.Font; +import javafx.scene.text.Text; +import javafx.scene.text.TextFlow; + +import de.uka.ilkd.key.java.Services; +import de.uka.ilkd.key.speclang.AuxiliaryContract; + +/** + * contractcompletions (P2b): JavaFX port of the abstract Swing + * {@code AuxiliaryContractSelectionPanel} (AuxiliaryContractSelectionPanel.java, 124 lines) + * — the single-selection list of auxiliary contracts (block/loop contracts) rendered as titled + * cells with the contract's plain text (Swing renders {@code getHtmlText}; the HTML styling is + * dropped, see {@link ContractSelectionPanelF}). + */ +public abstract class AuxiliaryContractSelectionPanelF + extends ListView { + + protected final Services services; + + /** re-entrancy guard for the keep-selection listener (see the constructor). */ + private boolean selectionGuard; + + protected AuxiliaryContractSelectionPanelF(final Services services, + final boolean multipleSelection) { + this.services = services; + getSelectionModel().setSelectionMode( + multipleSelection ? SelectionMode.MULTIPLE : SelectionMode.SINGLE); + // Swing: an emptying selection keeps the previously selected index + getSelectionModel().getSelectedItems() + .addListener((javafx.collections.ListChangeListener) change -> { + if (selectionGuard) { + return; + } + if (getSelectionModel().getSelectedItems().isEmpty() && !getItems().isEmpty()) { + selectionGuard = true; + try { + getSelectionModel().select(0); + } finally { + selectionGuard = false; + } + } + }); + setCellFactory(view -> new AuxiliaryContractCell()); + getStyleClass().add("contract-selection-panel"); + setPrefSize(700, 500); + } + + /** Swing {@code setContracts(T[], String)}. */ + public void setContracts(final T[] contracts, final String title) { + getItems().setAll(contracts); + getSelectionModel().select(0); + } + + /** Swing {@code getContract}. */ + public T getContract() { + List selection = getSelectionModel().getSelectedItems(); + return computeContract(services, selection); + } + + /** Swing {@code computeContract(Services, List)} (implemented by the concrete panel). */ + public abstract T computeContract(Services services, List selection); + + /** The FX cell, see the class javadoc. */ + private final class AuxiliaryContractCell extends javafx.scene.control.ListCell { + @Override + protected void updateItem(T item, boolean empty) { + super.updateItem(item, empty); + if (empty || item == null) { + setText(null); + setGraphic(null); + return; + } + Label nameLabel = new Label(item.getDisplayName()); + nameLabel.getStyleClass().add("contract-cell-name"); + nameLabel.setFont(Font.font(Font.getDefault().getFamily(), + javafx.scene.text.FontWeight.BOLD, 12)); + Text body = new Text(item.getPlainText(services)); + body.getStyleClass().add("contract-cell-text"); + javafx.scene.layout.VBox box = + new javafx.scene.layout.VBox(nameLabel, new TextFlow(body)); + box.setPadding(new Insets(2)); + box.getStyleClass().add("contract-cell"); + setText(null); + setGraphic(box); + } + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/contractcompletions/BlockContractExternalCompletionF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/contractcompletions/BlockContractExternalCompletionF.java new file mode 100644 index 00000000000..067e4c063a5 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/contractcompletions/BlockContractExternalCompletionF.java @@ -0,0 +1,73 @@ +/* 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 java.util.List; + +import de.uka.ilkd.key.gui.fx.InteractiveRuleApplicationCompletionF; +import de.uka.ilkd.key.java.Services; +import de.uka.ilkd.key.logic.JTerm; +import de.uka.ilkd.key.logic.op.LocationVariable; +import de.uka.ilkd.key.proof.Goal; +import de.uka.ilkd.key.rule.AbstractAuxiliaryContractRule.Instantiation; +import de.uka.ilkd.key.rule.BlockContractExternalBuiltInRuleApp; +import de.uka.ilkd.key.rule.BlockContractExternalRule; +import de.uka.ilkd.key.rule.IBuiltInRuleApp; +import de.uka.ilkd.key.speclang.BlockContract; +import de.uka.ilkd.key.speclang.HeapContext; + +import org.key_project.util.collection.ImmutableSet; + +/** + * contractcompletions (P2b): JavaFX port of the Swing {@code BlockContractExternalCompletion} + * (BlockContractExternalCompletion.java, 78 lines) — the interactive completion of a block + * contract (external) application via {@link AuxiliaryContractConfiguratorF}. The logic is + * Swing-free; only the dialog differs. + */ +public class BlockContractExternalCompletionF implements InteractiveRuleApplicationCompletionF { + + @Override + public IBuiltInRuleApp complete(final IBuiltInRuleApp application, final Goal goal, + final boolean force) { + BlockContractExternalBuiltInRuleApp result = + (BlockContractExternalBuiltInRuleApp) application; + if (!result.complete() && result.cannotComplete(goal)) { + return result; + } + if (force) { + result.tryToInstantiate(goal); + if (result.complete()) { + return result; + } + } + final Services services = goal.proof().getServices(); + final Instantiation instantiation = BlockContractExternalRule.INSTANCE + .instantiate((JTerm) application.posInOccurrence().subTerm(), goal); + final ImmutableSet contracts = + BlockContractExternalRule.getApplicableContracts(instantiation, goal, services); + final AuxiliaryContractConfiguratorF configurator = + new AuxiliaryContractConfiguratorF<>("Block Contract Configurator", + new BlockContractSelectionPanelF(services, true), services, + contracts.toArray(new BlockContract[contracts.size()]), + "Contracts for Block: " + instantiation.statement()); + configurator.show(); + if (configurator.wasSuccessful()) { + final List heaps = + HeapContext.getModifiableHeaps(services, instantiation.isTransactional()); + result.update(instantiation.statement(), configurator.getContract(), heaps); + } + return result; + } + + @Override + public boolean canComplete(final IBuiltInRuleApp app) { + return checkCanComplete(app); + } + + /** Swing {@code checkCanComplete} (also used by the KeY IDE). */ + public static boolean checkCanComplete(final IBuiltInRuleApp app) { + return app.rule() instanceof BlockContractExternalRule; + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/contractcompletions/BlockContractInternalCompletionF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/contractcompletions/BlockContractInternalCompletionF.java new file mode 100644 index 00000000000..350156298fd --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/contractcompletions/BlockContractInternalCompletionF.java @@ -0,0 +1,73 @@ +/* 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 java.util.List; + +import de.uka.ilkd.key.gui.fx.InteractiveRuleApplicationCompletionF; +import de.uka.ilkd.key.java.Services; +import de.uka.ilkd.key.logic.JTerm; +import de.uka.ilkd.key.logic.op.LocationVariable; +import de.uka.ilkd.key.proof.Goal; +import de.uka.ilkd.key.rule.AbstractAuxiliaryContractRule.Instantiation; +import de.uka.ilkd.key.rule.BlockContractInternalBuiltInRuleApp; +import de.uka.ilkd.key.rule.BlockContractInternalRule; +import de.uka.ilkd.key.rule.IBuiltInRuleApp; +import de.uka.ilkd.key.speclang.BlockContract; +import de.uka.ilkd.key.speclang.HeapContext; + +import org.key_project.util.collection.ImmutableSet; + +/** + * contractcompletions (P2b): JavaFX port of the Swing {@code BlockContractInternalCompletion} + * (BlockContractInternalCompletion.java, 78 lines) — the interactive completion of a block + * contract (internal) application via {@link AuxiliaryContractConfiguratorF}. The logic is + * Swing-free; only the dialog differs. + */ +public class BlockContractInternalCompletionF implements InteractiveRuleApplicationCompletionF { + + @Override + public IBuiltInRuleApp complete(final IBuiltInRuleApp application, final Goal goal, + final boolean force) { + BlockContractInternalBuiltInRuleApp result = + (BlockContractInternalBuiltInRuleApp) application; + if (!result.complete() && result.cannotComplete(goal)) { + return result; + } + if (force) { + result.tryToInstantiate(goal); + if (result.complete()) { + return result; + } + } + final Services services = goal.proof().getServices(); + final Instantiation instantiation = BlockContractInternalRule.INSTANCE + .instantiate((JTerm) application.posInOccurrence().subTerm(), goal); + final ImmutableSet contracts = + BlockContractInternalRule.getApplicableContracts(instantiation, goal, services); + final AuxiliaryContractConfiguratorF configurator = + new AuxiliaryContractConfiguratorF<>("Block Contract Configurator", + new BlockContractSelectionPanelF(services, true), services, + contracts.toArray(new BlockContract[contracts.size()]), + "Contracts for Block: " + instantiation.statement()); + configurator.show(); + if (configurator.wasSuccessful()) { + final List heaps = + HeapContext.getModifiableHeaps(services, instantiation.isTransactional()); + result.update(instantiation.statement(), configurator.getContract(), heaps); + } + return result; + } + + @Override + public boolean canComplete(final IBuiltInRuleApp app) { + return checkCanComplete(app); + } + + /** Swing {@code checkCanComplete} (also used by the KeY IDE). */ + public static boolean checkCanComplete(final IBuiltInRuleApp app) { + return app.rule() instanceof BlockContractInternalRule; + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/contractcompletions/BlockContractSelectionPanelF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/contractcompletions/BlockContractSelectionPanelF.java new file mode 100644 index 00000000000..89a9546ab7d --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/contractcompletions/BlockContractSelectionPanelF.java @@ -0,0 +1,47 @@ +/* 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 java.util.List; + +import de.uka.ilkd.key.java.Services; +import de.uka.ilkd.key.speclang.BlockContract; +import de.uka.ilkd.key.speclang.BlockContractImpl; + +import org.key_project.util.collection.DefaultImmutableSet; +import org.key_project.util.collection.ImmutableSet; + +/** + * contractcompletions (P2b): JavaFX port of the Swing {@code BlockContractSelectionPanel} + * (BlockContractSelectionPanel.java, 74 lines) — the auxiliary-contract panel for block + * contracts; several selected contracts are combined via {@code BlockContractImpl.combine}. + */ +public class BlockContractSelectionPanelF extends AuxiliaryContractSelectionPanelF { + + public BlockContractSelectionPanelF(final Services services, final boolean multipleSelection) { + super(services, multipleSelection); + } + + /** Swing {@code computeBlockContract}. */ + public static BlockContract computeBlockContract(Services services, + List selection) { + if (selection.isEmpty()) { + return null; + } else if (selection.size() == 1) { + return selection.get(0); + } else { + ImmutableSet contracts = DefaultImmutableSet.nil(); + for (BlockContract contract : selection) { + contracts = contracts.add(contract); + } + return BlockContractImpl.combine(contracts, services); + } + } + + @Override + public BlockContract computeContract(Services services, List selection) { + return computeBlockContract(services, selection); + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/contractcompletions/ContractConfiguratorF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/contractcompletions/ContractConfiguratorF.java new file mode 100644 index 00000000000..bbd0946b0fe --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/contractcompletions/ContractConfiguratorF.java @@ -0,0 +1,138 @@ +/* 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.Contract; + +import org.key_project.util.javafx.FxUtil; + +/** + * contractcompletions (P2b): JavaFX port of the Swing {@code ContractConfigurator} + * (ContractConfigurator.java, 130 lines) — the modal dialog wrapping a + * {@link ContractSelectionPanelF} with OK/Cancel. The "apply back" is done by the completion + * (Swing: the dialog only exposes {@code wasSuccessful()}/{@code getContract()}). + *

        + * Deviation: the Swing dialog is a modal {@code JDialog} with {@code MainWindow} as owner (the + * dialog shows with WINDOW modality); the FX dialog uses application modality with no owner + * (the pattern of the already-ported {@code MergePartnerSelectionDialogF}). Double-click on the + * list and Escape behave like in Swing ({@code GuiUtilities.attachClickOnEscListener}). + */ +public class ContractConfiguratorF { + + private final Stage stage = new Stage(); + private final ContractSelectionPanelF contractPanel; + private boolean successful = false; + + /** + * Creates the dialog; call {@link #show()} to show it modally (Swing: the constructor shows + * the modal dialog immediately — the FX port splits construction and display like + * {@code MergePartnerSelectionDialogF} so the dialog can also be driven by the + * {@code key.fx.verify.*} hooks). + * + * @param services the services + * @param contracts the selectable contracts + * @param title the list title ("Contracts for ...") + * @param allowMultipleContracts whether several (functional operation) contracts may be + * selected and combined + */ + public ContractConfiguratorF(Services services, Contract[] contracts, String title, + boolean allowMultipleContracts) { + contractPanel = new ContractSelectionPanelF(services, allowMultipleContracts); + contractPanel.setContracts(contracts, title); + + Button okButton = new Button("OK"); + okButton.setOnAction(e -> { + successful = true; + stage.close(); + }); + Button cancelButton = new Button("Cancel"); + cancelButton.setOnAction(e -> { + successful = false; + stage.close(); + }); + HBox buttonPanel = new HBox(5, okButton, cancelButton); + buttonPanel.setAlignment(Pos.CENTER_RIGHT); + buttonPanel.setPadding(new Insets(5)); + + BorderPane root = new BorderPane(); + root.setCenter(contractPanel); + root.setBottom(buttonPanel); + Scene scene = new Scene(root, 820, 560); + ThemeManager.getInstance().style(scene); + // Swing: double-click on the list = OK + contractPanel.setOnMouseClicked(e -> { + if (e.getClickCount() == 2) { + successful = true; + stage.close(); + } + }); + // Swing: GuiUtilities.attachClickOnEscListener(cancelButton) + scene.getAccelerators().put(new KeyCodeCombination(KeyCode.ESCAPE), () -> { + successful = false; + stage.close(); + }); + stage.setTitle("Contract Configurator"); + stage.setScene(scene); + } + + /** + * Shows the dialog modally and blocks until it is closed (Swing: the modal + * {@code setVisible(true)} on the EDT). + */ + public void show() { + if (!FxUtil.isFxThread()) { + FxUtil.runLater(this::show); + return; + } + stage.initModality(Modality.APPLICATION_MODAL); + stage.showAndWait(); + } + + /** Non-blocking variant for the {@code key.fx.verify.*} hooks. */ + public void showNonBlocking() { + stage.initModality(Modality.NONE); + stage.show(); + } + + /** Closes the dialog as if OK had been pressed (verification harness). */ + public void requestOk() { + successful = true; + stage.close(); + } + + /** Closes the dialog as if Cancel had been pressed (verification harness). */ + public void requestCancel() { + successful = false; + stage.close(); + } + + /** @return the stage (verification harness) */ + public Stage getStage() { + return stage; + } + + /** Swing {@code wasSuccessful}. */ + public boolean wasSuccessful() { + return successful; + } + + /** Swing {@code getContract}: the selected (possibly combined) contract. */ + public Contract getContract() { + return contractPanel.getContract(); + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/contractcompletions/ContractSelectionPanelF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/contractcompletions/ContractSelectionPanelF.java new file mode 100644 index 00000000000..7e2fbac1118 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/contractcompletions/ContractSelectionPanelF.java @@ -0,0 +1,278 @@ +/* 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 java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; +import javafx.geometry.Insets; +import javafx.scene.control.Label; +import javafx.scene.control.ListView; +import javafx.scene.control.SelectionMode; +import javafx.scene.text.Font; +import javafx.scene.text.Text; +import javafx.scene.text.TextFlow; + +import de.uka.ilkd.key.informationflow.impl.InformationFlowContractImpl; +import de.uka.ilkd.key.java.Services; +import de.uka.ilkd.key.proof.Proof; +import de.uka.ilkd.key.speclang.Contract; +import de.uka.ilkd.key.speclang.DependencyContractImpl; +import de.uka.ilkd.key.speclang.FunctionalBlockContract; +import de.uka.ilkd.key.speclang.FunctionalLoopContract; +import de.uka.ilkd.key.speclang.FunctionalOperationContract; +import de.uka.ilkd.key.speclang.FunctionalOperationContractImpl; +import de.uka.ilkd.key.util.LinkedHashMap; + +import org.key_project.util.collection.DefaultImmutableSet; +import org.key_project.util.collection.ImmutableSet; + +/** + * contractcompletions (P2b): JavaFX port of the Swing {@code ContractSelectionPanel} + * (ContractSelectionPanel.java, 328 lines) — the contract list of the contract configurator + * dialog: contracts sorted by type and name, each rendered as a titled cell with the contract's + * plain text (the Swing cell renders {@code getHTMLText}; the FX port renders the plain text in + * a monospaced flow — the HTML styling is dropped, see KNOWN-SIMPLIFIED), and auxiliary + * contracts not applied in a closed proof grayed out ({@code setGrayOutAuxiliaryContracts}). + */ +public class ContractSelectionPanelF extends ListView { + + /** Swing CONTRACT_TYPE_ORDER (ContractSelectionPanel.java:47-53). */ + private static final Map, Integer> CONTRACT_TYPE_ORDER = new LinkedHashMap<>(); + static { + CONTRACT_TYPE_ORDER.put(FunctionalOperationContractImpl.class, 0); + CONTRACT_TYPE_ORDER.put(InformationFlowContractImpl.class, 1); + CONTRACT_TYPE_ORDER.put(DependencyContractImpl.class, 2); + CONTRACT_TYPE_ORDER.put(FunctionalBlockContract.class, 3); + CONTRACT_TYPE_ORDER.put(FunctionalLoopContract.class, 4); + } + + private final Services services; + private final StringProperty title = new SimpleStringProperty("Contracts"); + + /** + * Whether an auxiliary contract is grayed out if it has not been applied in a proof for a + * non-auxiliary contract (Swing {@code grayOutAuxiliaryContracts}). + */ + private boolean grayOutAuxiliaryContracts = false; + + /** the contracts currently displayed, in display order (renderer dependency). */ + private Contract[] contracts = new Contract[0]; + + /** re-entrancy guard for the keep-selection listener (see the constructor). */ + private boolean selectionGuard; + + public ContractSelectionPanelF(Services services, boolean multipleSelection) { + this.services = services; + getSelectionModel().setSelectionMode( + multipleSelection ? SelectionMode.MULTIPLE : SelectionMode.SINGLE); + // Swing: an emptying selection keeps the previously selected index + getSelectionModel().getSelectedItems() + .addListener((javafx.collections.ListChangeListener) change -> { + if (selectionGuard) { + return; + } + if (getSelectionModel().getSelectedItems().isEmpty() && !getItems().isEmpty()) { + selectionGuard = true; + try { + getSelectionModel().select(0); + } finally { + selectionGuard = false; + } + } + }); + setCellFactory(view -> new ContractCell()); + getStyleClass().add("contract-selection-panel"); + setPrefSize(700, 500); + } + + /** Swing {@code setGrayOutAuxiliaryContracts}. */ + public void setGrayOutAuxiliaryContracts(boolean grayOutAuxiliaryContracts) { + this.grayOutAuxiliaryContracts = grayOutAuxiliaryContracts; + refresh(); + } + + /** The titled border title (Swing TitledBorder; the title row is part of the dialog). */ + public StringProperty titleProperty() { + return title; + } + + /** + * Swing {@code setContracts(Contract[], String)}: sorts by contract type then display name + * then id, fills the list and selects the first entry (ContractSelectionPanel.java:242-286). + * + * @param contracts the contracts, may be {@code null} or empty (clears the list) + * @param title the new title, may be {@code null} (keeps the current one) + */ + public void setContracts(Contract[] contracts, String title) { + if (contracts == null || contracts.length == 0) { + this.contracts = new Contract[0]; + getItems().clear(); + return; + } + Arrays.sort(contracts, (c1, c2) -> { + Integer o1 = CONTRACT_TYPE_ORDER.get(c1.getClass()); + Integer o2 = CONTRACT_TYPE_ORDER.get(c2.getClass()); + int res = 0; + if (o1 != null && o2 != null) { + res = o1 - o2; + } else if (o1 != null) { + return -1; + } else if (o2 != null) { + return 1; + } + if (res != 0) { + return res; + } + res = c1.getDisplayName().compareTo(c2.getDisplayName()); + if (res == 0) { + return c1.id() - c2.id(); + } + return res; + }); + this.contracts = contracts; + getItems().setAll(contracts); + getSelectionModel().select(0); + if (title != null) { + this.title.set(title); + } + } + + /** Swing {@code setContracts(ImmutableSet, String)}. */ + public void setContracts(ImmutableSet contracts, String title) { + setContracts(contracts.toArray(new Contract[contracts.size()]), title); + } + + /** Swing {@code selectContract}: selects the given contract and scrolls to it. */ + public void selectContract(Contract contract) { + getSelectionModel().select(contract); + scrollTo(getItems().indexOf(contract)); + } + + /** Swing {@code getContract}: the combined contract of the selection. */ + public Contract getContract() { + List selection = new ArrayList<>(getSelectionModel().getSelectedItems()); + return computeContract(services, selection); + } + + /** + * Swing {@code computeContract} (ContractSelectionPanel.java:308-327, also used by the KeY + * IDE): no selection → null; one contract → it; several contracts → they must all be + * functional operation contracts and are combined via the specification repository. + * + * @param services the services + * @param selection the selected contracts + * @return the combined contract or {@code null} + */ + public static Contract computeContract(Services services, List selection) { + if (selection.isEmpty()) { + return null; + } else if (selection.size() == 1) { + return selection.get(0); + } else { + ImmutableSet toCombine = DefaultImmutableSet.nil(); + for (Contract contract : selection) { + if (contract instanceof FunctionalOperationContract) { + toCombine = toCombine.add((FunctionalOperationContract) contract); + } else { + throw new IllegalStateException( + "Don't know how to combine contracts of kind " + contract.getClass() + + "\n" + "Contract:\n" + contract.getPlainText(services)); + } + } + return services.getSpecificationRepository().combineOperationContracts(toCombine); + } + } + + /** + * Swing renderer's gray-out logic (ContractSelectionPanel.java:104-139): a contract is + * grayed when it is auxiliary and was not applied (transitively, over the non-auxiliary + * contracts of the list) in a closed proof. + * + * @param contract the rendered contract + * @return {@code true} if the cell is grayed out + */ + private boolean isGrayedOut(Contract contract) { + if (!grayOutAuxiliaryContracts || !contract.isAuxiliary()) { + return false; + } + Set appliedContracts = new HashSet<>(); + Set consideredContracts = new HashSet<>(); + for (Contract c : contracts) { + if (c.isAuxiliary()) { + continue; + } + consideredContracts.add(c); + Proof p = getClosedProof(c); + if (p != null) { + p.mgt().getUsedContracts().forEach(appliedContracts::add); + } + } + int iterations = contracts.length - consideredContracts.size(); + for (int i = 0; i < iterations; ++i) { + for (Contract c : contracts) { + if (consideredContracts.contains(c) || !appliedContracts.contains(c)) { + continue; + } + consideredContracts.add(c); + Proof p = getClosedProof(c); + if (p != null) { + p.mgt().getUsedContracts().forEach(appliedContracts::add); + } + } + } + return !appliedContracts.contains(contract); + } + + /** Swing {@code getClosedProof} (ContractSelectionPanel.java:198-208). */ + private Proof getClosedProof(Contract c) { + ImmutableSet proofs = services.getSpecificationRepository().getProofs(c); + for (Proof proof : proofs) { + if (proof.mgt().getStatus().getProofClosed()) { + return proof; + } + } + return null; + } + + /** + * The FX contract cell (Swing renderer, ContractSelectionPanel.java:92-193): a bordered box + * with the bold display name on top and the plain contract text in a monospaced flow (the + * Swing cell uses HTML text — the styling markup is dropped, see the class javadoc); + * grayed-out auxiliary contracts use the {@code .contract-cell-gray} style class. + */ + private final class ContractCell extends javafx.scene.control.ListCell { + @Override + protected void updateItem(Contract item, boolean empty) { + super.updateItem(item, empty); + if (empty || item == null) { + setText(null); + setGraphic(null); + return; + } + Label nameLabel = new Label(item.getDisplayName()); + nameLabel.getStyleClass().add("contract-cell-name"); + nameLabel.setFont(Font.font(Font.getDefault().getFamily(), + javafx.scene.text.FontWeight.BOLD, 12)); + Text body = new Text(item.getPlainText(services)); + body.getStyleClass().add("contract-cell-text"); + TextFlow flow = new TextFlow(body); + javafx.scene.layout.VBox box = + new javafx.scene.layout.VBox(nameLabel, flow); + box.setPadding(new Insets(2)); + box.getStyleClass().add("contract-cell"); + if (isGrayedOut(item)) { + box.getStyleClass().add("contract-cell-gray"); + } + setText(null); + setGraphic(box); + } + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/contractcompletions/DependencyContractCompletionF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/contractcompletions/DependencyContractCompletionF.java new file mode 100644 index 00000000000..b5b29e2a03e --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/contractcompletions/DependencyContractCompletionF.java @@ -0,0 +1,164 @@ +/* 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 java.util.List; +import javafx.scene.control.ChoiceDialog; + +import de.uka.ilkd.key.gui.fx.InteractiveRuleApplicationCompletionF; +import de.uka.ilkd.key.java.Services; +import de.uka.ilkd.key.logic.JTerm; +import de.uka.ilkd.key.logic.op.IObserverFunction; +import de.uka.ilkd.key.logic.op.LocationVariable; +import de.uka.ilkd.key.pp.LogicPrinter; +import de.uka.ilkd.key.pp.NotationInfo; +import de.uka.ilkd.key.pp.PosTableLayouter; +import de.uka.ilkd.key.proof.Goal; +import de.uka.ilkd.key.rule.IBuiltInRuleApp; +import de.uka.ilkd.key.rule.UseDependencyContractApp; +import de.uka.ilkd.key.rule.UseDependencyContractRule; + +import org.key_project.prover.sequent.PosInOccurrence; + +/** + * contractcompletions (P2b): JavaFX port of the Swing {@code DependencyContractCompletion} + * (DependencyContractCompletion.java, 168 lines) — the interactive completion of a use + * dependency contract application: the user chooses the base heap configuration (one of the + * observer's state steps) via a dialog. + *

        + * Deviation: the Swing heap chooser is a {@code JOptionPane.showInputDialog} whose options carry + * {@code } styled strings; the FX port shows a {@link ChoiceDialog} with plain-text + * heap strings (the HTML escaping is dropped, see KNOWN-SIMPLIFIED). + */ +public class DependencyContractCompletionF implements InteractiveRuleApplicationCompletionF { + + @Override + public IBuiltInRuleApp complete(IBuiltInRuleApp app, Goal goal, boolean forced) { + UseDependencyContractApp cApp = (UseDependencyContractApp) app; + + Services services = goal.proof().getServices(); + + cApp = cApp.tryToInstantiateContract(services); + + final List steps = UseDependencyContractRule + .getSteps(cApp.getHeapContext(), cApp.posInOccurrence(), goal.sequent(), services); + PosInOccurrence step = + letUserChooseStep(cApp.getHeapContext(), steps, forced, services); + if (step == null) { + return null; + } + return cApp.setStep(step); + } + + /** Swing {@code letUserChooseStep} (DependencyContractCompletion.java:61-98). */ + private static PosInOccurrence letUserChooseStep(List heapContext, + List steps, boolean forced, Services services) { + assert heapContext != null; + + if (steps.isEmpty()) { + return null; + } + + // prepare array of possible base heaps + final TermStringWrapper[] heaps = new TermStringWrapper[steps.size()]; + final LogicPrinter lp = + new LogicPrinter(new NotationInfo(), services, PosTableLayouter.pure(120)); + + extractHeaps(heapContext, steps, heaps, lp); + + final JTerm[] resultHeaps; + if (!forced) { + // open dialog (Swing JOptionPane.showInputDialog with the heap options) + ChoiceDialog dialog = new ChoiceDialog<>(heaps[0], heaps); + dialog.setTitle("Instantiation"); + dialog.setHeaderText("Please select base heap configuration:"); + dialog.showAndWait(); + TermStringWrapper heapWrapper = dialog.getResult(); + if (heapWrapper == null) { + return null; + } + resultHeaps = heapWrapper.terms; + } else { + resultHeaps = heaps[0].terms; + } + + return findCorrespondingStep(steps, resultHeaps); + } + + /** Swing {@code findCorrespondingStep} (DependencyContractCompletion.java:101-114). */ + public static PosInOccurrence findCorrespondingStep(List steps, + JTerm[] resultHeaps) { + // find corresponding step + for (PosInOccurrence step : steps) { + boolean match = true; + for (int j = 0; j < resultHeaps.length; j++) { + if (!step.subTerm().sub(j).equals(resultHeaps[j])) { + match = false; + break; + } + } + if (match) { + return step; + } + } + assert false; + return null; + } + + /** + * Swing {@code extractHeaps} (DependencyContractCompletion.java:117-137): prints the heap + * terms of every step; the display string is the plain printed text (Swing escapes it into + * a {@code } wrapper — dropped in the FX port, see the class javadoc). + */ + public static void extractHeaps(List heapContext, + List steps, TermStringWrapper[] heaps, LogicPrinter lp) { + int i = 0; + for (PosInOccurrence step : steps) { + var op = step.subTerm().op(); + // necessary distinction (see bug #1232) + // subterm may either be an observer or a heap term already + int size = + (op instanceof IObserverFunction iof) ? iof.getStateCount() * heapContext.size() + : 1; + final JTerm[] heapTerms = new JTerm[size]; + StringBuilder prettyPrint = new StringBuilder(size > 1 ? "[" : ""); + for (int j = 0; j < size; j++) { + final JTerm heap = (JTerm) step.subTerm().sub(j); + heapTerms[j] = heap; + lp.reset(); + lp.printTerm(heap); + prettyPrint.append(j > 0 ? ", " : "").append(lp.result().trim()); + } + prettyPrint.append(size > 1 ? "]" : ""); + heaps[i++] = new TermStringWrapper(heapTerms, prettyPrint.toString()); + } + } + + /** Swing nested class {@code TermStringWrapper}. */ + public static final class TermStringWrapper { + public final JTerm[] terms; + final String string; + + public TermStringWrapper(JTerm[] terms, String string) { + this.terms = terms; + this.string = string; + } + + @Override + public String toString() { + return string; + } + } + + @Override + public boolean canComplete(IBuiltInRuleApp app) { + return checkCanComplete(app); + } + + /** Swing {@code checkCanComplete} (also used by the KeY IDE). */ + public static boolean checkCanComplete(final IBuiltInRuleApp app) { + return app.rule() instanceof UseDependencyContractRule; + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/contractcompletions/FunctionalOperationContractCompletionF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/contractcompletions/FunctionalOperationContractCompletionF.java new file mode 100644 index 00000000000..d27d5ed2cd4 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/contractcompletions/FunctionalOperationContractCompletionF.java @@ -0,0 +1,67 @@ +/* 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 de.uka.ilkd.key.gui.fx.InteractiveRuleApplicationCompletionF; +import de.uka.ilkd.key.java.Services; +import de.uka.ilkd.key.logic.JTerm; +import de.uka.ilkd.key.proof.Goal; +import de.uka.ilkd.key.rule.IBuiltInRuleApp; +import de.uka.ilkd.key.rule.UseOperationContractRule; +import de.uka.ilkd.key.rule.UseOperationContractRule.Instantiation; +import de.uka.ilkd.key.speclang.FunctionalOperationContract; + +import org.key_project.util.collection.ImmutableSet; + +/** + * contractcompletions (P2b): JavaFX port of the Swing + * {@code FunctionalOperationContractCompletion} (FunctionalOperationContractCompletion.java, + * 66 lines) — the interactive completion of a use operation contract application via the + * {@link ContractConfiguratorF}. The logic is Swing-free; only the dialog differs. + */ +public class FunctionalOperationContractCompletionF + implements InteractiveRuleApplicationCompletionF { + + @Override + public IBuiltInRuleApp complete(IBuiltInRuleApp app, Goal goal, boolean forced) { + Services services = goal.proof().getServices(); + + if (forced) { + app = app.forceInstantiate(goal); + if (app.complete()) { + return app; + } + } + + Instantiation inst = UseOperationContractRule + .computeInstantiation((JTerm) app.posInOccurrence().subTerm(), services); + + ImmutableSet contracts = + UseOperationContractRule.getApplicableContracts(inst, services); + + FunctionalOperationContract[] contractsArr = + contracts.toArray(new FunctionalOperationContract[contracts.size()]); + + ContractConfiguratorF cc = new ContractConfiguratorF(services, contractsArr, + "Contracts for " + inst.pm().getName(), true); + cc.show(); + + if (cc.wasSuccessful()) { + return ((UseOperationContractRule) app.rule()).createApp(app.posInOccurrence()) + .setContract(cc.getContract()); + } + return app; + } + + @Override + public boolean canComplete(IBuiltInRuleApp app) { + return checkCanComplete(app); + } + + /** Swing {@code checkCanComplete} (also used by the KeY IDE). */ + public static boolean checkCanComplete(final IBuiltInRuleApp app) { + return app.rule() instanceof UseOperationContractRule; + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/contractcompletions/InvariantConfiguratorF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/contractcompletions/InvariantConfiguratorF.java new file mode 100644 index 00000000000..79d80bc998c --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/contractcompletions/InvariantConfiguratorF.java @@ -0,0 +1,663 @@ +/* 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 java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Scene; +import javafx.scene.control.Button; +import javafx.scene.control.SplitPane; +import javafx.scene.control.Tab; +import javafx.scene.control.TabPane; +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.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.java.ast.statement.LoopStatement; +import de.uka.ilkd.key.ldt.HeapLDT; +import de.uka.ilkd.key.ldt.JavaDLTheory; +import de.uka.ilkd.key.logic.JTerm; +import de.uka.ilkd.key.logic.NamespaceSet; +import de.uka.ilkd.key.logic.op.LocationVariable; +import de.uka.ilkd.key.nparser.KeyIO; +import de.uka.ilkd.key.pp.AbbrevMap; +import de.uka.ilkd.key.pp.PrettyPrinter; +import de.uka.ilkd.key.proof.io.OutputStreamProofSaver; +import de.uka.ilkd.key.speclang.LoopSpecification; +import de.uka.ilkd.key.util.InfFlowSpec; + +import org.key_project.logic.sort.Sort; +import org.key_project.prover.rules.RuleAbortException; +import org.key_project.util.collection.ImmutableList; +import org.key_project.util.javafx.FxUtil; + +/** + * contractcompletions (P2b): JavaFX port of the Swing {@code InvariantConfigurator} + * (InvariantConfigurator.java, 1043 lines) — the interactive loop-invariant editor of the loop + * invariant rule ({@code LoopInvariantRuleCompletion}). The singleton keeps the per-loop + * candidate invariants ({@code mapLoopsToInvariants}), the dialog shows one tab per candidate + * invariant with per-heap sub-tabs for the invariant and modifiable clauses, the variant field + * and the information-flow fields, live-parses every edit (status panel, Apply/Store disabled + * on error) and builds the new specification via + * {@link LoopSpecification#configurate}. Cancel throws {@link RuleAbortException} like in + * Swing. + *

        + * Deviations: the error/status panel is updated in place (the Swing dialog rebuilds it on every + * parse); the FX dialog uses application modality without owner + * ({@link ContractConfiguratorF}); the abbreviations of the editor notation info are wired via + * {@link #setAbbrevMap} (Swing reads them from the mediator at dialog construction). + */ +public class InvariantConfiguratorF { + + private static final int INV_IDX = 0; + private static final int MOD_IDX = 1; + private static final int VAR_IDX = 2; + private static final int IF_PRE_IDX = 3; + private static final int IF_POST_IDX = 4; + private static final int IF_OO_IDX = 5; + private static final String DEFAULT = "Default"; + + private static InvariantConfiguratorF configurator = null; + + /** the abbreviation map used by the parser (Swing: mediator notation info). */ + private static AbbrevMap abbrevMap = new AbbrevMap(); + + private List[]> invariants; + private final Map[]>> mapLoopsToInvariants = + new LinkedHashMap<>(); + private int index = 0; + private LoopSpecification newInvariant = null; + private boolean userPressedCancel = false; + + /** Swing singleton. */ + public static InvariantConfiguratorF getInstance() { + if (configurator == null) { + configurator = new InvariantConfiguratorF(); + } + return configurator; + } + + /** + * Wires the abbreviation map (the Swing dialog reads the mediator's notation info at + * construction; the FX port takes it once at startup from MainWindowF). + * + * @param map the abbreviation map, may be {@code null} for an empty map + */ + public static void setAbbrevMap(AbbrevMap map) { + abbrevMap = map == null ? new AbbrevMap() : map; + } + + /** The current abbreviation map (verification harness, Swing {@code getAbbrevMap}). */ + public static AbbrevMap getAbbrevMap() { + return abbrevMap; + } + + /** + * Opens the dialog and returns the user-edited loop invariant (Swing + * {@code getLoopInvariant}, InvariantConfigurator.java:93-1037). + * + * @param loopInv the {@link LoopSpecification} (complete or partial) to be displayed and + * edited + * @param services the services + * @param requiresVariant whether termination shall be proven (variant required) + * @param heapContext the relevant heaps (the enabled sub-tabs) + * @return the user-edited loop invariant + * @throws RuleAbortException if the user cancelled the dialog + */ + public LoopSpecification getLoopInvariant(final LoopSpecification loopInv, + final Services services, final boolean requiresVariant, + final List heapContext) throws RuleAbortException { + if (loopInv == null) { + return null; + } + index = 0; + InvariantDialogF dialog = + new InvariantDialogF(loopInv, services, requiresVariant, heapContext); + dialog.show(); + if (userPressedCancel) { + throw new RuleAbortException( + "Interactive invariant configuration canceled by user."); + } + return newInvariant; + } + + /** + * The FX dialog (Swing inner class {@code InvariantDialog}). One top tab per candidate + * invariant, each with the per-heap invariant/modifiable sub-tabs, the variant field and + * the information-flow fields; the left side shows the loop source and the status panel. + */ + private final class InvariantDialogF { + + private final LoopSpecification loopInv; + private final Services services; + private final boolean requiresVariant; + private final List heapContext; + private final KeyIO parser; + + private final Stage stage = new Stage(); + private final TabPane inputPane = new TabPane(); + private final Map invariantStatus = new LinkedHashMap<>(); + private final Map modifiableStatus = new LinkedHashMap<>(); + private final TextArea variantStatus = new TextArea(); + + private final Button applyButton = new Button("Apply"); + private final Button storeButton = new Button("Store"); + + private JTerm variantTerm = null; + private final Map modifiableTerm = new LinkedHashMap<>(); + private final Map freeModifiableTerm = new LinkedHashMap<>(); + private final Map> infFlowSpecs = + new LinkedHashMap<>(); + private final Map invariantTerm = new LinkedHashMap<>(); + private final Map freeInvariantTerm = new LinkedHashMap<>(); + + InvariantDialogF(LoopSpecification loopInv, Services services, boolean requiresVariant, + List heapContext) { + this.loopInv = loopInv; + this.services = services; + this.requiresVariant = requiresVariant; + this.heapContext = heapContext; + + initInvariants(); + + // left side: loop source (Swing initLoopPresentation, InvariantConfigurator:555-571) + TextArea loopRep = new TextArea(); + PrettyPrinter printer = PrettyPrinter.purePrinter(); + printer.print(loopInv.getLoop()); + loopRep.setText(printer.result()); + loopRep.setEditable(false); + loopRep.getStyleClass().add("invariant-loop-representation"); + VBox loopBox = new VBox(titled("Loop", loopRep)); + VBox.setVgrow(loopRep, Priority.ALWAYS); + VBox statusBox = new VBox(4); + statusBox.setPadding(new Insets(4)); + for (LocationVariable heap : allHeaps()) { + String k = heap.toString(); + boolean base = heap == baseHeap(); + TextArea invStatus = createStatusArea( + "Invariant" + (base ? "" : "[" + k + "]") + " - Status:"); + TextArea modStatus = createStatusArea( + "Modifiable" + (base ? "" : "[" + k + "]") + " - Status:"); + invariantStatus.put(k, invStatus); + modifiableStatus.put(k, modStatus); + statusBox.getChildren().addAll(invStatus, modStatus); + } + variantStatus.setPrefRowCount(2); + variantStatus.setEditable(false); + variantStatus.getStyleClass().add("invariant-status-ok"); + statusBox.getChildren().add(titled("Variant - Status", variantStatus)); + + // right side: one tab per candidate invariant (Swing initInputPane, :188-198) + for (int i = 0; i < invariants.size(); i++) { + inputPane.getTabs().add(new Tab("Inv " + i, createInvariantTab(i))); + } + inputPane.getSelectionModel().selectedIndexProperty() + .addListener((obs, oldIndex, newIndex) -> { + index = newIndex.intValue(); + parse(); + }); + + BorderPane root = new BorderPane(); + SplitPane split = new SplitPane(loopBox, inputPane); + split.setDividerPositions(0.35); + root.setCenter(split); + root.setBottom(statusBox); + + HBox buttonPanel = new HBox(5, applyButton, storeButton, + cancelButton()); + buttonPanel.setAlignment(Pos.CENTER_RIGHT); + buttonPanel.setPadding(new Insets(5)); + root.setTop(buttonPanel); + + applyButton.setOnAction(e -> applyActionPerformed()); + storeButton.setOnAction(e -> storeActionPerformed()); + + NamespaceSet nss = services.getNamespaces().copyWithParent(); + parser = new KeyIO(services, nss); + parser.setAbbrevMap(abbrevMap); + + stage.setTitle("Invariant Configurator"); + Scene scene = new Scene(root, 1100, 750); + ThemeManager.getInstance().style(scene); + stage.setScene(scene); + parse(); + } + + private Button cancelButton() { + Button cancelButton = new Button("Cancel"); + cancelButton.setOnAction(e -> cancelActionPerformed()); + return cancelButton; + } + + private javafx.scene.control.TitledPane titled(String text, + T content) { + javafx.scene.control.TitledPane pane = + new javafx.scene.control.TitledPane(text, content); + pane.setCollapsible(false); + return pane; + } + + private TextArea createStatusArea(String title) { + TextArea area = new TextArea(); + area.setEditable(false); + area.setPrefRowCount(2); + area.setText("OK"); + area.getStyleClass().add("invariant-status-ok"); + return area; + } + + // ------------------------------------------------------------------ + // data setup (Swing initInvariants, InvariantConfigurator.java:200-326) + // ------------------------------------------------------------------ + + private ImmutableList allHeaps() { + return services.getTypeConverter().getHeapLDT().getAllHeaps(); + } + + private LocationVariable baseHeap() { + return services.getTypeConverter().getHeapLDT().getHeap(); + } + + private String printTerm(JTerm t, boolean pretty) { + // Swing wrapper for the pretty printer (InvariantConfigurator.java:332-337); + // the boolean is the "short attr notation" flag of OutputStreamProofSaver + return OutputStreamProofSaver.printTerm(t, services, !pretty); + } + + @SuppressWarnings("unchecked") + private void initInvariants() { + Map[] loopInvTexts = new Map[IF_OO_IDX + 1]; + + loopInvTexts[INV_IDX] = new LinkedHashMap<>(); + Map atPres = loopInv.getInternalAtPres(); + for (LocationVariable heap : allHeaps()) { + JTerm invariant = + loopInv.getInvariant(heap, loopInv.getInternalSelfTerm(), atPres, services); + if (invariant == null) { + loopInvTexts[INV_IDX].put(heap.toString(), "true"); + } else { + loopInvTexts[INV_IDX].put(heap.toString(), printTerm(invariant, true)); + } + } + + loopInvTexts[MOD_IDX] = new LinkedHashMap<>(); + for (LocationVariable heap : allHeaps()) { + JTerm modifiable = + loopInv.getModifiable(heap, loopInv.getInternalSelfTerm(), atPres, services); + if (modifiable == null) { + loopInvTexts[MOD_IDX].put(heap.toString(), "allLocs"); + } else { + // pretty syntax cannot be parsed yet for modifiable + loopInvTexts[MOD_IDX].put(heap.toString(), printTerm(modifiable, false)); + } + } + + loopInvTexts[VAR_IDX] = new LinkedHashMap<>(); + JTerm variant = loopInv.getVariant(loopInv.getInternalSelfTerm(), atPres, services); + if (variant == null) { + loopInvTexts[VAR_IDX].put(DEFAULT, ""); + } else { + loopInvTexts[VAR_IDX].put(DEFAULT, printTerm(variant, true)); + } + + loopInvTexts[IF_PRE_IDX] = new LinkedHashMap<>(); + loopInvTexts[IF_POST_IDX] = new LinkedHashMap<>(); + loopInvTexts[IF_OO_IDX] = new LinkedHashMap<>(); + for (LocationVariable heap : allHeaps()) { + ImmutableList specs = + loopInv.getInfFlowSpecs(heap, loopInv.getInternalSelfTerm(), atPres, services); + if (specs == null) { + loopInvTexts[IF_PRE_IDX].put(heap.toString(), "true"); + loopInvTexts[IF_POST_IDX].put(heap.toString(), "true"); + loopInvTexts[IF_OO_IDX].put(heap.toString(), "true"); + } else { + for (InfFlowSpec spec : specs) { + for (JTerm t : spec.preExpressions) { + loopInvTexts[IF_PRE_IDX].put(heap.toString(), printTerm(t, false)); + } + for (JTerm t : spec.postExpressions) { + loopInvTexts[IF_POST_IDX].put(heap.toString(), printTerm(t, false)); + } + for (JTerm t : spec.newObjects) { + loopInvTexts[IF_OO_IDX].put(heap.toString(), printTerm(t, false)); + } + } + } + } + + // Swing: the candidate invariants are kept per loop across invocations (:328-349) + if (!mapLoopsToInvariants.containsKey(loopInv.getLoop())) { + invariants = new ArrayList<>(); + invariants.add(loopInvTexts); + mapLoopsToInvariants.put(loopInv.getLoop(), invariants); + index = invariants.size() - 1; + } else { + invariants = mapLoopsToInvariants.get(loopInv.getLoop()); + if (!invariants.contains(loopInvTexts)) { + invariants.add(loopInvTexts); + index = invariants.size() - 1; + } else { + index = invariants.indexOf(loopInvTexts); + } + } + } + + // ------------------------------------------------------------------ + // UI construction (Swing createInvariantTab, InvariantConfigurator.java:340-446) + // ------------------------------------------------------------------ + + private String heapTitle(String template, String key) { + return String.format(template, + key.equals(HeapLDT.BASE_HEAP_NAME.toString()) ? "" : "[" + key + "]"); + } + + private VBox createInvariantTab(int i) { + VBox panel = new VBox(4); + panel.setPadding(new Insets(4)); + + Map invs = invariants.get(i)[INV_IDX]; + TabPane invPane = heapPane(invs, "Invariant%s: ", (area, k) -> { + area.textProperty().addListener((obs, o, n) -> textUpdate(INV_IDX, k, n)); + }, i); + + Map mods = invariants.get(i)[MOD_IDX]; + TabPane modPane = heapPane(mods, "Modifiable%s: ", (area, k) -> { + area.textProperty().addListener((obs, o, n) -> textUpdate(MOD_IDX, k, n)); + }, i); + + TextArea varArea = inputArea("Variant" + ": ", + invariants.get(i)[VAR_IDX].get(DEFAULT)); + varArea.textProperty() + .addListener((obs, o, n) -> textUpdate(VAR_IDX, DEFAULT, n)); + + Map pres = invariants.get(i)[IF_PRE_IDX]; + TabPane prePane = heapPane(pres, "InfFlowPreExpressions%s: ", (area, k) -> { + area.textProperty().addListener((obs, o, n) -> textUpdate(IF_PRE_IDX, k, n)); + }, i); + + Map posts = invariants.get(i)[IF_POST_IDX]; + TabPane postPane = heapPane(posts, "InfFlowPostExpressions%s: ", (area, k) -> { + area.textProperty().addListener((obs, o, n) -> textUpdate(IF_POST_IDX, k, n)); + }, i); + + Map newObs = invariants.get(i)[IF_OO_IDX]; + TabPane newObsPane = heapPane(newObs, "InfFlowNewObjects%s: ", (area, k) -> { + area.textProperty().addListener((obs, o, n) -> textUpdate(IF_OO_IDX, k, n)); + }, i); + + panel.getChildren().addAll(invPane, modPane, varArea, prePane, postPane, newObsPane); + return panel; + } + + private interface TextAreaConfigurator { + void configure(TextArea area, String key); + } + + /** + * One bottom tab pane per heap with a titled input area (Swing creates the same + * per-heap panes for invariant/modifiable/inf-flow fields). + */ + private TabPane heapPane(Map values, String titleTemplate, + TextAreaConfigurator configurator, int tabIndex) { + TabPane pane = new TabPane(); + pane.setTabClosingPolicy(TabPane.TabClosingPolicy.UNAVAILABLE); + for (String k : values.keySet()) { + TextArea area = inputArea(heapTitle(titleTemplate, k), values.get(k)); + configurator.configure(area, k); + Tab tab = new Tab(k, area); + // Swing updateActiveTabs: only the heaps of the context are enabled (:959-967) + tab.setDisable(heapContext.stream() + .noneMatch(lv -> lv.name().toString().equals(k))); + pane.getTabs().add(tab); + } + pane.getStyleClass().add("invariant-heap-pane"); + return pane; + } + + /** Swing {@code createInputTextArea}: a titled editable area. */ + private TextArea inputArea(String title, String text) { + TextArea area = new TextArea(text); + area.setPrefRowCount(3); + area.getStyleClass().add("invariant-input"); + return area; + } + + // ------------------------------------------------------------------ + // update actions (Swing invUpdatePerformed & friends, :706-789) + // ------------------------------------------------------------------ + + private void textUpdate(int kindIdx, String key, String text) { + index = inputPane.getSelectionModel().getSelectedIndex(); + Map[] inv = invariants.get(index); + inv[kindIdx].put(key, text); + parse(); + } + + // ------------------------------------------------------------------ + // parse + build (Swing parse/buildInvariant, :815-957) + // ------------------------------------------------------------------ + + private static RuntimeException newUnexpectedTypeException(Sort expected, Sort actual) { + return new IllegalStateException( + String.format("Entered formula is expected of type %s but got %s.", expected, + actual)); + } + + /** Swing {@code parseInvariant}: the invariant must be a formula. */ + protected JTerm parseInvariant(LocationVariable heap) { + String string = invariants.get(index)[INV_IDX].get(heap.toString()); + JTerm result = parser.parseExpression(string); + if (result.sort() != JavaDLTheory.FORMULA) { + throw newUnexpectedTypeException(JavaDLTheory.FORMULA, result.sort()); + } + return result; + } + + /** Swing {@code parseModifiable}: the modifiable clause must be a locset. */ + protected JTerm parseModifiable(LocationVariable heap) { + Sort locSetSort = services.getTypeConverter().getLocSetLDT().targetSort(); + String string = invariants.get(index)[MOD_IDX].get(heap.toString()); + if (string.trim().equals("\\strictly_nothing")) { + // the Swing hack to allow "strictly_nothing" in interactive mode (:992-997) + return services.getTermBuilder().strictlyNothing(); + } + JTerm result = parser.parseExpression(string); + if (result.sort() != locSetSort) { + throw newUnexpectedTypeException(locSetSort, result.sort()); + } + return result; + } + + /** Swing {@code parseInfFlowSpec} (base heap only). */ + protected ImmutableList parseInfFlowSpec(LocationVariable heap) { + String preExpsAsString = invariants.get(index)[IF_PRE_IDX].get(heap.toString()); + String postExpsAsString = invariants.get(index)[IF_POST_IDX].get(heap.toString()); + String newObjectsAsString = invariants.get(index)[IF_OO_IDX].get(heap.toString()); + JTerm preExps = parser.parseExpression(preExpsAsString); + JTerm postExps = parser.parseExpression(postExpsAsString); + JTerm newObjects = parser.parseExpression(newObjectsAsString); + return ImmutableList.nil() + .append(new InfFlowSpec( + ImmutableList.nil().append(preExps), + ImmutableList.nil().append(postExps), + ImmutableList.nil().append(newObjects))); + } + + /** Swing {@code parseVariant}: the variant must be an integer term. */ + protected JTerm parseVariant() { + Sort intSort = services.getTypeConverter().getIntegerLDT().targetSort(); + JTerm result = parser.parseExpression(invariants.get(index)[VAR_IDX].get(DEFAULT)); + if (result.sort() != intSort) { + throw newUnexpectedTypeException(intSort, result.sort()); + } + return result; + } + + /** Swing {@code parse}: parses all fields and updates the status panel. */ + private void parse() { + Map invErrors = new LinkedHashMap<>(); + Map modErrors = new LinkedHashMap<>(); + Map respErrors = new LinkedHashMap<>(); + for (LocationVariable heap : allHeaps()) { + try { + invariantTerm.put(heap, parseInvariant(heap)); + invErrors.put(heap.name().toString(), "OK"); + } catch (Exception e) { + invErrors.put(heap.name().toString(), e.getMessage()); + } + try { + modifiableTerm.put(heap, parseModifiable(heap)); + modErrors.put(heap.name().toString(), "OK"); + } catch (Exception e) { + modErrors.put(heap.name().toString(), e.getMessage()); + } + } + LocationVariable baseHeap = baseHeap(); + try { + infFlowSpecs.put(baseHeap, parseInfFlowSpec(baseHeap)); + respErrors.put(baseHeap.name().toString(), "OK"); + } catch (Exception e) { + respErrors.put(baseHeap.name().toString(), e.getMessage()); + } + String varError = null; + boolean varEvaluated = false; + try { + if (invariants.get(index)[VAR_IDX].get(DEFAULT).isEmpty()) { + variantTerm = null; + if (requiresVariant) { + throw new de.uka.ilkd.key.parser.ParserException("Variant required!", + null); + } + // Swing: the empty variant without requirement leaves the status unchanged + varEvaluated = variantStatus.getText() != null + && !variantStatus.getText().isEmpty(); + } else { + variantTerm = parseVariant(); + varError = "OK"; + varEvaluated = true; + } + } catch (Exception e) { + varError = e.getMessage(); + varEvaluated = true; + } + + updateStatuses(invErrors, modErrors, varEvaluated ? varError : null); + } + + /** + * Swing {@code updateErrorPanel}: the per-heap status texts and Apply/Store enablement. + * A {@code null} {@code varError} leaves the variant status unchanged (the Swing + * behaviour for the empty non-required variant). + */ + private void updateStatuses(Map invErrors, + Map modErrors, String varError) { + boolean errorFound = false; + for (Map.Entry entry : invariantStatus.entrySet()) { + String error = invErrors.getOrDefault(entry.getKey(), "OK"); + errorFound |= !"OK".equals(error); + setStatus(entry.getValue(), error); + } + for (Map.Entry entry : modifiableStatus.entrySet()) { + String error = modErrors.getOrDefault(entry.getKey(), "OK"); + errorFound |= !"OK".equals(error); + setStatus(entry.getValue(), error); + } + if (varError != null) { + errorFound |= !"OK".equals(varError); + setStatus(variantStatus, varError); + } else { + String current = variantStatus.getText(); + errorFound |= current != null && !current.isEmpty() && !"OK".equals(current); + } + // Swing: applyButton.setEnabled(!errorFound); storeButton likewise (:946-948) + applyButton.setDisable(errorFound); + storeButton.setDisable(errorFound); + } + + private void setStatus(TextArea area, String message) { + area.getStyleClass().removeAll(List.of("invariant-status-ok", + "invariant-status-error")); + area.getStyleClass() + .add("OK".equals(message) ? "invariant-status-ok" : "invariant-status-error"); + area.setText(message); + } + + /** Swing {@code buildInvariant}: builds the new invariant if the requirements are met. */ + private boolean buildInvariant() { + boolean requirementsAreMet = true; + if (requiresVariant && variantTerm == null) { + setStatus(variantStatus, "Variant required!"); + requirementsAreMet = false; + } + if (invariantTerm.isEmpty()) { + setStatus(variantStatus, "Invariant is required!"); + requirementsAreMet = false; + } + if (requirementsAreMet) { + newInvariant = loopInv.configurate(invariantTerm, freeInvariantTerm, + modifiableTerm, freeModifiableTerm, infFlowSpecs, variantTerm); + return true; + } + return false; + } + + // ------------------------------------------------------------------ + // actions (Swing apply/store/cancel, :769-788) + // ------------------------------------------------------------------ + + /** Swing {@code applyActionPerformed}: parse, build, close on success. */ + private void applyActionPerformed() { + index = inputPane.getSelectionModel().getSelectedIndex(); + parse(); + if (buildInvariant()) { + stage.close(); + } + } + + /** Swing {@code storeActionPerformed}: clone the current candidate into a new tab. */ + @SuppressWarnings("unchecked") + private void storeActionPerformed() { + index = inputPane.getSelectionModel().getSelectedIndex(); + // Swing shallow-clones the array (the candidate maps are shared, as in Swing) + Map[] invs = invariants.get(index).clone(); + invariants.add(invs); + index = invariants.size() - 1; + inputPane.getTabs().add(new Tab("Inv " + (invariants.size() - 1), + createInvariantTab(index))); + inputPane.getSelectionModel().select(index); + } + + /** Swing {@code cancelActionPerformed}. */ + private void cancelActionPerformed() { + userPressedCancel = true; + newInvariant = null; + stage.close(); + } + + // ------------------------------------------------------------------ + // display + // ------------------------------------------------------------------ + + /** Shows the dialog modally (blocks until closed). */ + private void show() { + if (!FxUtil.isFxThread()) { + FxUtil.runLater(this::show); + return; + } + stage.initModality(Modality.APPLICATION_MODAL); + stage.showAndWait(); + } + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/contractcompletions/LoopInvariantRuleCompletionF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/contractcompletions/LoopInvariantRuleCompletionF.java new file mode 100644 index 00000000000..b9ee9a66d33 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/contractcompletions/LoopInvariantRuleCompletionF.java @@ -0,0 +1,89 @@ +/* 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 de.uka.ilkd.key.gui.fx.InteractiveRuleApplicationCompletionF; +import de.uka.ilkd.key.java.JavaTools; +import de.uka.ilkd.key.java.Services; +import de.uka.ilkd.key.java.ast.statement.MethodFrame; +import de.uka.ilkd.key.java.ast.statement.While; +import de.uka.ilkd.key.logic.JTerm; +import de.uka.ilkd.key.proof.Goal; +import de.uka.ilkd.key.rule.IBuiltInRuleApp; +import de.uka.ilkd.key.rule.LoopInvariantBuiltInRuleApp; +import de.uka.ilkd.key.rule.LoopScopeInvariantRule; +import de.uka.ilkd.key.rule.WhileInvariantRule; +import de.uka.ilkd.key.speclang.LoopSpecImpl; +import de.uka.ilkd.key.speclang.LoopSpecification; +import de.uka.ilkd.key.util.MiscTools; + +import org.key_project.prover.rules.RuleAbortException; + +/** + * contractcompletions (P2b): JavaFX port of the Swing {@code LoopInvariantRuleCompletion} + * (LoopInvariantRuleCompletion.java, 94 lines) — the interactive completion of a loop invariant + * rule application via {@link InvariantConfiguratorF}. The logic is Swing-free; only the dialog + * differs. + */ +public class LoopInvariantRuleCompletionF implements InteractiveRuleApplicationCompletionF { + + @Override + public IBuiltInRuleApp complete(IBuiltInRuleApp app, Goal goal, boolean forced) { + Services services = goal.proof().getServices(); + services = services.getOverlay(goal.getLocalNamespaces()); + + LoopInvariantBuiltInRuleApp loopApp = + ((LoopInvariantBuiltInRuleApp) app).tryToInstantiate(goal); + + JTerm progPost = loopApp.programTerm(); + final While loop = loopApp.getLoopStatement(); + + LoopSpecification inv = loopApp.getSpec(); + if (inv == null) { // no invariant present, get it interactively + MethodFrame mf = JavaTools.getInnermostMethodFrame(progPost.javaBlock(), services); + inv = new LoopSpecImpl(loop, mf == null ? null : mf.getProgramMethod(), + mf == null || mf.getProgramMethod() == null ? null + : mf.getProgramMethod().getContainerType(), + mf == null ? null + : MiscTools.getSelfTerm( + JavaTools.getInnermostMethodFrame(progPost.javaBlock(), services), + services), + null); + try { + inv = InvariantConfiguratorF.getInstance().getLoopInvariant(inv, services, false, + loopApp.getHeapContext()); + } catch (RuleAbortException e) { + return null; + } + } else { // in interactive mode and there is an invariant in the specification repository + boolean requiresVariant = loopApp.variantRequired() && !loopApp.variantAvailable(); + if (!forced || !loopApp.invariantAvailable() || requiresVariant) { + try { + inv = InvariantConfiguratorF.getInstance().getLoopInvariant(inv, services, + requiresVariant, loopApp.getHeapContext()); + } catch (RuleAbortException e) { + return null; + } + } + } + + if (inv != null && forced) { + services.getSpecificationRepository().addLoopInvariant(inv); + } + + return inv == null ? null : loopApp.setLoopInvariant(inv); + } + + @Override + public boolean canComplete(IBuiltInRuleApp app) { + return checkCanComplete(app); + } + + /** Swing {@code checkCanComplete} (also used by the KeY IDE). */ + public static boolean checkCanComplete(final IBuiltInRuleApp app) { + return app.rule() instanceof WhileInvariantRule + || app.rule() instanceof LoopScopeInvariantRule; + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/dialogs/DialogsVerifyF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/dialogs/DialogsVerifyF.java new file mode 100644 index 00000000000..0d0b0cd8471 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/dialogs/DialogsVerifyF.java @@ -0,0 +1,365 @@ +/* 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.dialogs; + +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import javafx.application.Platform; +import javafx.stage.Window; + +import de.uka.ilkd.key.gui.fx.InteractiveRuleApplicationCompletionF; +import de.uka.ilkd.key.gui.fx.WindowUserInterfaceControlF; +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.ContractConfiguratorF; +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.mergerule.MergeRuleCompletionF; +import de.uka.ilkd.key.gui.fx.notification.NotificationManagerF; +import de.uka.ilkd.key.gui.fx.notification.NotificationManagerF.Kind; +import de.uka.ilkd.key.proof.Proof; +import de.uka.ilkd.key.rule.Taclet; +import de.uka.ilkd.key.taclettranslation.lemma.TacletSoundnessPOLoader.TacletInfo; + +import org.key_project.util.collection.ImmutableSet; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * dialogs (P2b): interactive self test for the contract-completion dialogs and the lemma + * dialogs ({@code key.fx.verify.dialogs}, forwarded by {@code key.ui.fx/build.gradle}), + * dispatched from the main window after the demo load. The harness + *

          + *
        1. checks the interactive-completion registry: the six Swing-registered completions of + * {@code WindowUserInterfaceControl} (merge + the five contract/invariant completions, P2b) are + * registered with the FX control in {@code MainWindowF} (Swing WindowUserInterfaceControl + * .java:74-82);
        2. + *
        3. exercises the {@link ItemChooserF} dual-list semantics ({@code setItems} select-all + + * name sort, user filters, side moves, {@code getDataOfSelectedItems});
        4. + *
        5. opens the {@link ContractConfiguratorF} and the auxiliary (block-contract) + * {@code AuxiliaryContractConfiguratorF} non-blocking and drives Cancel and OK through the + * {@code requestCancel}/{@code requestOk} seams (the OK path sets {@code wasSuccessful});
        6. + *
        7. runs the {@link LemmaSelectionDialogF} {@code TacletFilter} end to end from a background + * thread (the thread the {@code TacletSoundnessPOLoader} calls it on): cancel leaves the whole + * choice on the left (empty selection), OK after moving all returned taclets to the right + * returns exactly them;
        8. + *
        9. checks the {@link InvariantConfiguratorF} singleton and the abbreviation-map wiring is in + * place (the modal invariant editor itself needs an interactive loop application — not + * headless-drivable, see the P2b note in PARITY-SIGNOFF.md).
        10. + *
        + */ +public final class DialogsVerifyF { + + private static final Logger LOGGER = LoggerFactory.getLogger(DialogsVerifyF.class); + + private DialogsVerifyF() { + } + + /** + * Runs the self test. Must be called on the FX thread (from the {@code key.fx.verify.dialogs} + * dispatch in the main window). + * + * @param owner the owner window (unused; kept for API symmetry with the other harnesses) + * @param proof the loaded demo proof (its services and init-config taclets feed the + * skeletons), may be {@code null} + * @param ui the FX user interface control with the completion registry + * @return the report (PASS / FAIL with a reason); a missing proof yields SKIP + */ + public static String run(Window owner, Proof proof, WindowUserInterfaceControlF ui) { + if (proof == null) { + LOGGER.warn("Dialogs verification: no proof loaded — SKIP"); + return "SKIP (no proof)"; + } + Thread worker = new Thread(() -> { + String report = runAll(proof, ui); + LOGGER.info("Dialogs verification: {}", report); + Platform.runLater(() -> NotificationManagerF.getInstance() + .notify("Dialogs verification: " + report, + report.startsWith("PASS") ? Kind.INFO : Kind.ERROR)); + }, "fx-verify-dialogs"); + worker.setDaemon(true); + worker.start(); + return "running (see log)"; + } + + private static String runAll(Proof proof, WindowUserInterfaceControlF ui) { + StringBuilder problems = new StringBuilder(); + + verifyRegistry(ui, problems); + verifyItemChooser(problems); + verifyContractConfigurator(proof, problems); + verifyAuxiliaryContractConfigurator(proof, problems); + verifyLemmaSelectionDialog(proof, problems); + verifyInvariantConfiguratorWiring(problems); + + if (problems.length() > 0) { + return "FAIL: " + problems; + } + return "PASS: registry (6 completions), item chooser, contract configurator, auxiliary" + + " configurator, lemma selection dialog, invariant configurator wiring"; + } + + /** Step 1: the interactive-completion registry of the FX user interface control. */ + private static void verifyRegistry(WindowUserInterfaceControlF ui, StringBuilder problems) { + List completions = ui.getCompletions(); + // the five P2b completions plus the P1 merge completion (Swing WindowUserInterfaceControl + // .java:74-82 registers 10 in total; the four merge variants and the access/field + // completions are registered in later batches — the FX port lists what exists to date) + final Class[] expected = { + MergeRuleCompletionF.class, FunctionalOperationContractCompletionF.class, + DependencyContractCompletionF.class, LoopInvariantRuleCompletionF.class, + BlockContractInternalCompletionF.class, BlockContractExternalCompletionF.class + }; + for (Class clazz : expected) { + boolean found = false; + for (InteractiveRuleApplicationCompletionF c : completions) { + if (clazz.isInstance(c)) { + found = true; + break; + } + } + if (!found) { + problems.append("completion not registered: ").append(clazz.getSimpleName()) + .append("; "); + } + } + if (completions.isEmpty()) { + problems.append("completion registry empty; "); + } + } + + /** Step 2: the {@link ItemChooserF} dual-list semantics (pure UI, no services). */ + private static void verifyItemChooser(StringBuilder problems) { + runOnFx(() -> { + ItemChooserF chooser = new ItemChooserF<>("Search"); + List data = List.of("beta taclet", "alpha taclet", "gamma taclet"); + chooser.setItems(data, "Taclets"); + // Swing: all items start on the LEFT and the supplied list is select-all'ed; the FX + // lists sort by the lower-case name (Swing TableRowSorter) + if (!chooser.getDataOfSelectedItems().isEmpty()) { + problems.append("item chooser: initial selection not empty; "); + } + chooser.moveAllToRight(); + List selected = chooser.getDataOfSelectedItems(); + if (!selected.contains("alpha taclet") || !selected.contains("beta taclet") + || !selected.contains("gamma taclet") || selected.size() != 3) { + problems.append("item chooser: moveAllToRight lost items; "); + } + // Swing RowFilter: user filters hide items on both sides + chooser.addFilter(item -> item.contains("beta")); + chooser.moveAllToLeft(); + if (!chooser.getDataOfSelectedItems().isEmpty()) { + problems.append("item chooser: filter did not apply to the right side; "); + } + chooser.removeFilter(item -> item.contains("beta")); + chooser.moveAllToRight(); + if (chooser.getDataOfSelectedItems().size() != 3) { + problems.append("item chooser: removed filter not re-applied; "); + } + chooser.moveAllToLeft(); + if (chooser.getDataOfSelectedItems().size() != 0) { + problems.append("item chooser: moveAllToLeft left items behind; "); + } + }); + } + + /** Step 3a: the {@link ContractConfiguratorF} open / cancel / OK skeleton. */ + private static void verifyContractConfigurator(Proof proof, StringBuilder problems) { + ContractConfiguratorF[] ref = new ContractConfiguratorF[1]; + // cancel round + runOnFx(() -> ref[0] = new ContractConfiguratorF(proof.getServices(), + new de.uka.ilkd.key.speclang.Contract[0], "Contracts for ", true)); + runOnFx(ref[0]::showNonBlocking); + waitUntil(() -> ref[0].getStage().isShowing(), "contract configurator shows", problems); + runOnFx(ref[0]::requestCancel); + waitUntil(() -> !ref[0].getStage().isShowing(), "contract configurator closes on cancel", + problems); + if (ref[0].wasSuccessful()) { + problems.append("contract configurator: cancel marked successful; "); + } + // ok round + runOnFx(() -> ref[0] = new ContractConfiguratorF(proof.getServices(), + new de.uka.ilkd.key.speclang.Contract[0], "Contracts for ", true)); + runOnFx(ref[0]::showNonBlocking); + waitUntil(() -> ref[0].getStage().isShowing(), "contract configurator shows (2)", + problems); + runOnFx(ref[0]::requestOk); + waitUntil(() -> !ref[0].getStage().isShowing(), "contract configurator closes on ok", + problems); + if (!ref[0].wasSuccessful()) { + problems.append("contract configurator: ok not marked successful; "); + } + } + + /** Step 3b: the auxiliary (block-contract) configurator skeleton. */ + private static void verifyAuxiliaryContractConfigurator(Proof proof, + StringBuilder problems) { + de.uka.ilkd.key.gui.fx.contractcompletions.AuxiliaryContractConfiguratorF[] ref = + new de.uka.ilkd.key.gui.fx.contractcompletions.AuxiliaryContractConfiguratorF[1]; + runOnFx(() -> ref[0] = + new de.uka.ilkd.key.gui.fx.contractcompletions.AuxiliaryContractConfiguratorF<>( + "Block Contract Configurator", + new de.uka.ilkd.key.gui.fx.contractcompletions.BlockContractSelectionPanelF( + proof.getServices(), true), + proof.getServices(), new de.uka.ilkd.key.speclang.BlockContract[0], + "Contracts for Block: ")); + runOnFx(ref[0]::showNonBlocking); + if (!waitUntil(() -> ref[0].getStage().isShowing(), "auxiliary configurator shows", + problems)) { + return; + } + runOnFx(ref[0]::requestCancel); + waitUntil(() -> !ref[0].getStage().isShowing(), + "auxiliary configurator closes on cancel", problems); + if (ref[0].wasSuccessful()) { + problems.append("auxiliary configurator: cancel marked successful; "); + } + } + + /** + * Step 4: the {@link LemmaSelectionDialogF} {@code TacletFilter} — called from a background + * thread like the {@code TacletSoundnessPOLoader} does; the harness drives the OK/Cancel + * seams from the FX thread while the modal dialog blocks it. + */ + private static void verifyLemmaSelectionDialog(Proof proof, StringBuilder problems) { + var taclets = proof.getInitConfig().getTaclets(); + if (taclets.isEmpty()) { + problems.append("lemma dialog: no taclets in the init config; "); + return; + } + Taclet taclet = taclets.get(0); + List infos = + List.of(new TacletInfo(taclet, false, false), new TacletInfo(taclet, false, true)); + + // round 1: cancel → nothing is selected + LemmaSelectionDialogF[] dialogRef = new LemmaSelectionDialogF[1]; + runOnFx(() -> dialogRef[0] = new LemmaSelectionDialogF()); + LemmaSelectionDialogF dialog = dialogRef[0]; + AtomicInteger failures = new AtomicInteger(); + CountDownLatch done = new CountDownLatch(1); + runFilterThread(dialog, infos, done, failures); + waitUntil(dialog::isShowing, "lemma selection dialog shows", problems); + Platform.runLater(dialog::requestCancel); + awaitLatch(done, "lemma selection dialog cancels", failures); + if (failures.get() > 0) { + problems.append("lemma dialog cancel round: ").append(failures.get()) + .append(" failure(s); "); + } + if (!dialog.isCancelled()) { + problems.append("lemma dialog cancel round: cancelled flag not set; "); + } + + // round 2: move all supported taclets to the right, then OK → exactly those are selected + LemmaSelectionDialogF[] dialog2Ref = new LemmaSelectionDialogF[1]; + runOnFx(() -> dialog2Ref[0] = new LemmaSelectionDialogF()); + LemmaSelectionDialogF dialog2 = dialog2Ref[0]; + CountDownLatch done2 = new CountDownLatch(1); + AtomicInteger failures2 = new AtomicInteger(); + runFilterThread(dialog2, infos, done2, failures2); + waitUntil(dialog2::isShowing, "lemma selection dialog shows (2)", problems); + // the unsupported info is filtered for moving (Swing filterForMovingTaclets): only the + // supported one may reach the right side + Platform.runLater(() -> { + ItemChooserF chooser = dialog2.getTacletChooser(); + chooser.moveAllToRight(); + Platform.runLater(dialog2::requestOk); + }); + awaitLatch(done2, "lemma selection dialog ok", failures2); + if (failures2.get() > 0) { + problems.append("lemma dialog ok round: ").append(failures2.get()) + .append(" failure(s); "); + } + ImmutableSet selectedSet = dialog2.lastFilterResult(); + if (selectedSet == null || !selectedSet.contains(taclet) || selectedSet.size() != 1) { + problems.append("lemma dialog ok round: unexpected selection (size " + + (selectedSet == null ? "null" : selectedSet.size()) + "); "); + } + } + + private static void runFilterThread(LemmaSelectionDialogF dialog, List infos, + CountDownLatch done, AtomicInteger failures) { + Thread t = new Thread(() -> { + try { + dialog.filter(infos); + } catch (Throwable e) { + LOGGER.warn("Dialogs verification: lemma filter thread failed", e); + failures.incrementAndGet(); + } finally { + done.countDown(); + } + }, "fx-verify-dialogs-lemma"); + t.setDaemon(true); + t.start(); + } + + /** Step 5: the invariant configurator singleton + abbrev map wiring are in place. */ + private static void verifyInvariantConfiguratorWiring(StringBuilder problems) { + InvariantConfiguratorF configurator = InvariantConfiguratorF.getInstance(); + if (configurator == null) { + problems.append("invariant configurator: singleton null; "); + } + if (InvariantConfiguratorF.getAbbrevMap() == null) { + problems.append("invariant configurator: abbrev map not wired; "); + } + } + + /** Polls a condition with a timeout (the FX runLater queue keeps the modal loop alive). */ + private static boolean waitUntil(java.util.function.BooleanSupplier condition, String what, + StringBuilder problems) { + long deadline = System.currentTimeMillis() + 10_000; + while (System.currentTimeMillis() < deadline && !condition.getAsBoolean()) { + try { + Thread.sleep(25); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + problems.append("interrupted waiting for ").append(what).append("; "); + return false; + } + } + if (!condition.getAsBoolean()) { + problems.append("timed out waiting for ").append(what).append("; "); + return false; + } + return true; + } + + /** Runs {@code runnable} on the FX thread and blocks until it completed. */ + private static void runOnFx(Runnable runnable) { + if (Platform.isFxApplicationThread()) { + runnable.run(); + return; + } + CountDownLatch latch = new CountDownLatch(1); + Platform.runLater(() -> { + try { + runnable.run(); + } finally { + latch.countDown(); + } + }); + awaitLatch(latch, "runnable on the FX thread", null); + } + + private static void awaitLatch(CountDownLatch latch, String what, AtomicInteger failures) { + try { + if (!latch.await(10, TimeUnit.SECONDS)) { + if (failures != null) { + failures.incrementAndGet(); + } + LOGGER.warn("Dialogs verification: timed out waiting for {}", what); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + if (failures != null) { + failures.incrementAndGet(); + } + } + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/dialogs/ItemChooserF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/dialogs/ItemChooserF.java new file mode 100644 index 00000000000..4e5ca10c036 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/dialogs/ItemChooserF.java @@ -0,0 +1,296 @@ +/* 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.dialogs; + +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; + +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.collections.transformation.FilteredList; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.ListView; +import javafx.scene.control.TextField; +import javafx.scene.control.TitledPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; + +/** + * lemma (P2b, A2): JavaFX port of the Swing {@code ItemChooser} (key.ui + * lemmatagenerator/ItemChooser.java, 497 lines) — the dual-list chooser of the taclet selection + * dialog: a "Choice" list on the left, a "Selection" list on the right and move buttons in + * between; both lists work on the same items, each showing the items of its side, filtered by + * the user filters and the live search pattern of the panel's own text field, sorted by the + * item's lower-case name (Swing TableRowSorter, ItemChooser.java:411-427). + * + * @param the item data type (Swing generic {@code ItemChooser}) + */ +public class ItemChooserF extends HBox { + + /** The two sides of the chooser (Swing {@code SelectionPanel.Side}). */ + public enum Side { + LEFT, RIGHT + } + + /** Swing {@code ItemChooser.ItemFilter}. */ + public interface ItemFilter { + boolean include(T itemData); + } + + /** Swing {@code TableItem}: the item data plus its side and search name. */ + public static final class TableItemF { + private final T data; + private final String lowerCaseName; + private final ObjectProperty side = new SimpleObjectProperty<>(Side.LEFT); + + public TableItemF(T data, Side side) { + this.data = data; + this.side.set(side); + this.lowerCaseName = data.toString().toLowerCase(); + } + + public T getData() { + return data; + } + + public Side getSide() { + return side.get(); + } + + public void setSide(Side side) { + this.side.set(side); + } + + public ObjectProperty sideProperty() { + return side; + } + + public String getNameLowerCase() { + return lowerCaseName; + } + + @Override + public String toString() { + return data.toString(); + } + } + + private final String searchTitle; + private List> items = new LinkedList<>(); + private final List> filtersForMovingItems = new LinkedList<>(); + private final List> userFilter = new ArrayList<>(); + + private final ObservableList> observableItems = + FXCollections.observableArrayList(); + private final FilteredList> leftItems = + new FilteredList<>(observableItems); + private final FilteredList> rightItems = + new FilteredList<>(observableItems); + private final ListView> suppliedList = new ListView<>(leftItems); + private final ListView> selectedList = new ListView<>(rightItems); + + /** + * Swing constructor ({@code searchTitle} = the titled border of the search fields, + * ItemChooser.java:69-72). + * + * @param searchTitle the search field title + */ + public ItemChooserF(String searchTitle) { + this.searchTitle = searchTitle; + getStyleClass().add("item-chooser"); + setSpacing(6); + setPadding(new Insets(4)); + setAlignment(Pos.CENTER); + + TextField leftSearch = new TextField(); + leftSearch.textProperty().addListener((obs, o, n) -> { + suppliedList.getProperties().put("search", n); + update(); + }); + TextField rightSearch = new TextField(); + rightSearch.textProperty().addListener((obs, o, n) -> { + selectedList.getProperties().put("search", n); + update(); + }); + + VBox leftBox = new VBox(4, new Label(searchTitle), leftSearch, suppliedList); + VBox.setVgrow(suppliedList, Priority.ALWAYS); + TitledPane leftPane = new TitledPane("Choice", leftBox); + leftPane.setCollapsible(false); + HBox.setHgrow(leftPane, Priority.ALWAYS); + + Button leftButton = new Button("<<"); + leftButton.setTooltip(new javafx.scene.control.Tooltip("Remove selection")); + leftButton.setOnAction(e -> cut(Side.LEFT)); + Button rightButton = new Button(">>"); + rightButton.setTooltip(new javafx.scene.control.Tooltip("Add selection")); + rightButton.setOnAction(e -> cut(Side.RIGHT)); + VBox middle = new VBox(4, leftButton, rightButton); + middle.setAlignment(Pos.CENTER); + middle.getStyleClass().add("item-chooser-middle"); + + VBox rightBox = new VBox(4, new Label(searchTitle), rightSearch, selectedList); + VBox.setVgrow(selectedList, Priority.ALWAYS); + TitledPane rightPane = new TitledPane("Selection", rightBox); + rightPane.setCollapsible(false); + HBox.setHgrow(rightPane, Priority.ALWAYS); + + getChildren().addAll(leftPane, middle, rightPane); + + // the sort of the Swing sorter (by lower-case name, ItemChooser.java:425-426) plus the + // per-side, filter and search inclusion (Swing RowFilter, ItemChooser.java:380-408) + leftItems.setPredicate(item -> include(item, Side.LEFT, leftSearch.getText())); + rightItems.setPredicate(item -> include(item, Side.RIGHT, rightSearch.getText())); + + suppliedList.setCellFactory(view -> new javafx.scene.control.ListCell<>() { + @Override + protected void updateItem(TableItemF item, boolean empty) { + super.updateItem(item, empty); + setText(empty || item == null ? null : item.toString()); + } + }); + selectedList.setCellFactory(view -> new javafx.scene.control.ListCell<>() { + @Override + protected void updateItem(TableItemF item, boolean empty) { + super.updateItem(item, empty); + setText(empty || item == null ? null : item.toString()); + } + }); + } + + /** Swing RowFilter.include (ItemChooser.java:380-408) for one side. */ + private boolean include(TableItemF item, Side side, String findPattern) { + for (ItemFilter filter : userFilter) { + if (!filter.include(item.getData())) { + return false; + } + } + String pattern = findPattern.toLowerCase(); + if (!pattern.isEmpty() && !item.getNameLowerCase().contains(pattern)) { + return false; + } + return item.getSide() == side; + } + + /** Swing {@code cut} (ItemChooser.java:124-143): moves the selected items to {@code side}. */ + private void cut(Side side) { + List> source = side == Side.LEFT ? rightItems : leftItems; + List> tableItems = new ArrayList<>(source); + for (TableItemF item : tableItems) { + boolean move = true; + for (ItemFilter filter : filtersForMovingItems) { + if (!filter.include(item.getData())) { + move = false; + } + } + if (move) { + item.setSide(side); + } + } + update(); + } + + /** Swing {@code setItems} (ItemChooser.java:161-180): all items start on the LEFT. */ + public void setItems(List dataForItems, String columnName) { + items = new LinkedList<>(); + for (T info : dataForItems) { + items.add(new TableItemF<>(info, Side.LEFT)); + } + observableItems.setAll(items); + // Swing sorts by the lower-case name; sorted here by re-setting the filtered lists + observableItems.sort((a, b) -> a.getNameLowerCase().compareTo(b.getNameLowerCase())); + update(); + // Swing: getSuppliedList().selectAll() + suppliedList.getSelectionModel().clearSelection(); + if (!leftItems.isEmpty()) { + suppliedList.getSelectionModel().selectRange(0, leftItems.size()); + } + } + + /** Swing {@code getDataOfSelectedItems} (ItemChooser.java:184-193). */ + public List getDataOfSelectedItems() { + List list = new LinkedList<>(); + for (TableItemF item : items) { + if (item.getSide() == Side.RIGHT) { + list.add(item.getData()); + } + } + return list; + } + + /** Swing {@code moveAllToLeft} (ItemChooser.java:195-200). */ + public void moveAllToLeft() { + for (TableItemF item : items) { + item.setSide(Side.LEFT); + } + update(); + } + + /** Swing {@code moveAllToRight} (ItemChooser.java:202-207). */ + public void moveAllToRight() { + for (TableItemF item : items) { + item.setSide(Side.RIGHT); + } + update(); + } + + /** Swing {@code removeSelection} (ItemChooser.java:209-212). */ + public void removeSelection() { + selectedList.getSelectionModel().clearSelection(); + suppliedList.getSelectionModel().clearSelection(); + } + + /** Swing {@code addFilter} (ItemChooser.java:214-217). */ + public void addFilter(ItemFilter filter) { + userFilter.add(filter); + update(); + } + + /** Swing {@code removeFilter} (ItemChooser.java:219-222). */ + public void removeFilter(ItemFilter filter) { + userFilter.remove(filter); + update(); + } + + /** Swing {@code addFilterForMovingItems} (ItemChooser.java:224-226). */ + public void addFilterForMovingItems(ItemFilter filter) { + filtersForMovingItems.add(filter); + } + + /** Swing {@code removeFilterForMovingItems} (ItemChooser.java:228-231). */ + public void removeFilterForMovingItems(ItemFilter filter) { + filtersForMovingItems.remove(filter); + } + + /** + * Swing {@code update} (ItemChooser.java:233-240): re-applies the row filters (the search + * patterns are re-read from the panels' text fields inside {@link #include}). + */ + public void update() { + leftItems.setPredicate(item -> include(item, Side.LEFT, searchOf(suppliedList))); + rightItems.setPredicate(item -> include(item, Side.RIGHT, searchOf(selectedList))); + } + + /** + * The search pattern of the given panel (the Swing {@code findPattern} of the panel's text + * field). The FX lists carry the pattern in their user data. + */ + private String searchOf(ListView> list) { + Object pattern = list.getProperties().get("search"); + return pattern instanceof String s ? s : ""; + } + + /** The {@code searchTitle} (used by the dialog layout). */ + public String getSearchTitle() { + return searchTitle; + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/dialogs/LemmaSelectionDialogF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/dialogs/LemmaSelectionDialogF.java new file mode 100644 index 00000000000..daa11a68eff --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/dialogs/LemmaSelectionDialogF.java @@ -0,0 +1,177 @@ +/* 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.dialogs; + +import java.util.List; +import java.util.concurrent.CountDownLatch; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Scene; +import javafx.scene.control.Button; +import javafx.scene.control.CheckBox; +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.rule.Taclet; +import de.uka.ilkd.key.taclettranslation.lemma.TacletSoundnessPOLoader.TacletFilter; +import de.uka.ilkd.key.taclettranslation.lemma.TacletSoundnessPOLoader.TacletInfo; + +import org.key_project.util.collection.DefaultImmutableSet; +import org.key_project.util.collection.ImmutableSet; +import org.key_project.util.javafx.FxUtil; + +/** + * lemma (P2b, A2): JavaFX port of the Swing {@code LemmaSelectionDialog} + * (key.ui lemmatagenerator/LemmaSelectionDialog.java, 161 lines) — the modal "Taclet Selection" + * dialog of the lemma-generation workflow, implementing {@link TacletFilter}: the user picks the + * taclets the soundness proof obligations are created for, via an {@link ItemChooserF} with the + * "Show only supported taclets." filter checkbox (the default filter of the Swing dialog) and + * the "already in use"/"not supported" moving restriction. + *

        + * Swing runs the modal dialog from the loader thread (the {@code TacletSoundnessPOLoader} calls + * {@link #filter} on its own thread); the FX port marshals the dialog to the FX thread and + * blocks the caller with a latch. + */ +public class LemmaSelectionDialogF implements TacletFilter { + + private final ItemChooserF tacletChooser = + new ItemChooserF<>("Search for taclets with names containing"); + + private final ItemChooserF.ItemFilter showOnlySupportedTaclets = + itemData -> !itemData.isNotSupported(); + + private final ItemChooserF.ItemFilter filterForMovingTaclets = + itemData -> !itemData.isNotSupported() && !itemData.isAlreadyInUse(); + + private final Stage stage = new Stage(); + private boolean cancelled = false; + private ImmutableSet lastFilterResult = null; + + public LemmaSelectionDialogF() { + stage.setTitle("Taclet Selection"); + tacletChooser.addFilterForMovingItems(filterForMovingTaclets); + tacletChooser.addFilter(showOnlySupportedTaclets); + + CheckBox showSupported = new CheckBox("Show only supported taclets."); + showSupported.setSelected(true); + showSupported.setOnAction(e -> { + if (showSupported.isSelected()) { + tacletChooser.addFilter(showOnlySupportedTaclets); + } else { + tacletChooser.removeFilter(showOnlySupportedTaclets); + } + }); + + Button okButton = new Button("OK"); + okButton.setOnAction(e -> stage.close()); + Button cancelButton = new Button("Cancel"); + cancelButton.setOnAction(e -> { + // Swing cancel(): remove the selection and move everything back to the left + tacletChooser.removeSelection(); + tacletChooser.moveAllToLeft(); + cancelled = true; + stage.close(); + }); + HBox buttonPanel = new HBox(8, showSupported, okButton, cancelButton); + buttonPanel.setAlignment(Pos.CENTER_RIGHT); + buttonPanel.setPadding(new Insets(6)); + + BorderPane root = new BorderPane(); + root.setCenter(tacletChooser); + root.setBottom(buttonPanel); + Scene scene = new Scene(root, 700, 500); + ThemeManager.getInstance().style(scene); + stage.setScene(scene); + stage.setMinWidth(300); + stage.setMinHeight(300); + } + + /** + * Swing {@code showModal} (LemmaSelectionDialog.java:59-68): fills the chooser, shows the + * modal dialog and returns the taclets of the selected (right-side) items. + * + * @param taclets the candidate taclet infos + * @return the selected taclets + */ + public ImmutableSet showModal(List taclets) { + cancelled = false; + tacletChooser.setItems(taclets, "Taclets"); + stage.initModality(Modality.APPLICATION_MODAL); + stage.showAndWait(); + ImmutableSet set = DefaultImmutableSet.nil(); + for (TacletInfo info : tacletChooser.getDataOfSelectedItems()) { + set = set.add(info.getTaclet()); + } + lastFilterResult = set; + return set; + } + + /** @return the result of the last {@link #filter} call (verification harness). */ + public ImmutableSet lastFilterResult() { + return lastFilterResult; + } + + /** @return the internal chooser (verification harness). */ + public ItemChooserF getTacletChooser() { + return tacletChooser; + } + + /** @return whether the modal stage is currently showing (verification harness). */ + public boolean isShowing() { + return stage.isShowing(); + } + + /** Swing {@code cancel()} state (for the verification harness). */ + public boolean isCancelled() { + return cancelled; + } + + /** Closes the dialog as if OK had been pressed (verification harness). */ + public void requestOk() { + stage.close(); + } + + /** Closes the dialog as if Cancel had been pressed (verification harness). */ + public void requestCancel() { + // Swing cancel(): remove the selection and move everything back to the left + tacletChooser.removeSelection(); + tacletChooser.moveAllToLeft(); + cancelled = true; + stage.close(); + } + + /** @return the stage (verification harness) */ + public Stage getStage() { + return stage; + } + + @Override + public ImmutableSet filter(List taclets) { + // the TacletSoundnessPOLoader calls filter on its own thread — marshal the modal + // dialog to the FX thread and block until it is closed + if (FxUtil.isFxThread()) { + return showModal(taclets); + } + ImmutableSet[] result = new ImmutableSet[1]; + CountDownLatch latch = new CountDownLatch(1); + FxUtil.runLater(() -> { + try { + result[0] = showModal(taclets); + } finally { + latch.countDown(); + } + }); + try { + latch.await(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return DefaultImmutableSet.nil(); + } + return result[0]; + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/dialogs/LoadUserTacletsDialogF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/dialogs/LoadUserTacletsDialogF.java index c2f1dbb162b..c72ebdcee4a 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/dialogs/LoadUserTacletsDialogF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/dialogs/LoadUserTacletsDialogF.java @@ -6,43 +6,56 @@ import java.io.File; import java.nio.file.Path; +import java.util.List; import java.util.Optional; +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.ButtonBar; +import javafx.scene.control.ButtonType; import javafx.scene.control.CheckBox; import javafx.scene.control.Label; +import javafx.scene.control.ListView; +import javafx.scene.control.TextArea; import javafx.scene.input.KeyCode; import javafx.scene.layout.BorderPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; import javafx.stage.FileChooser; import javafx.stage.Modality; +import javafx.stage.Stage; import javafx.stage.Window; import de.uka.ilkd.key.gui.fx.theme.ThemeManager; +import de.uka.ilkd.key.settings.ProofIndependentSettings; /** - * menu: MP5 — JavaFX port of the Swing {@code LoadUserTacletsDialog} - * (key.ui/.../gui/lemmatagenerator/LoadUserTacletsDialog.java), opened by the + * lemma (P2b, A3): full JavaFX port of the Swing {@code LoadUserTacletsDialog} + * (key.ui lemmatagenerator/LoadUserTacletsDialog.java, 499 lines), opened by the * "Load User Defined Taclets…" entry and the "Prove" submenu's "Load User Defined Taclets for * Proving" entry. The dialog collects *

          - *
        • the (single) file containing the user-defined taclets — the Swing dialog additionally - * maintains a list of axiom files ({@code Mode.PROVE} axiom panel / the LOAD mode's - * justification box); the axiom-file list is deliberately not ported - * ({@code TacletSoundnessPOLoader} then simply never loads axioms), marked {@code // menu:} - * KNOWN-DEFERRED below,
        • - *
        • the "Generate proof obligations for taclets" checkbox (Swing - * {@code getLemmaCheckBox}, default {@code true}): when un-checked the taclets are loaded - * without a soundness proof.
        • + *
        • the (single) file containing the user-defined taclets (Swing + * {@code UserTacletFileBox}),
        • + *
        • the list of axiom files (Swing {@code axiomsList} + Add/Remove buttons, enabled + * in PROVE mode while proof obligations are generated; the axioms are only loaded for the + * lemmata, not for the current proof),
        • + *
        • the "Generate proof obligations for taclets" checkbox (Swing {@code getLemmaCheckBox}, + * default {@code true}): un-checking requires the confirmation dialog of the Swing + * {@code InfoDialog} ("the calculus will become unsound"), remembered via + * {@code LemmaGeneratorSettings.showingDialogUsingAxioms},
        • + *
        • the "Help" window with the Swing {@code HELP_TEXT}.
        • *
        */ public final class LoadUserTacletsDialogF { /** * The two modes of the Swing dialog ({@code LoadUserTacletsDialog.Mode}): PROVE only creates - * the proof obligations, LOAD additionally loads the taclets into the current proof. + * the proof obligations, LOAD additionally loads the taclets into the current proof (the + * axiom panel is only enabled in PROVE mode — Swing {@code enableAxiomFilePanel}). */ public enum Mode { PROVE, LOAD @@ -55,10 +68,38 @@ public enum Mode { * (Swing {@code getFileForTaclets()}) * @param generateProofObligations whether a soundness proof obligation should be created for * each taclet (Swing {@code isGenerateProofObligations()}) + * @param filesForAxioms the axiom files (Swing {@code getFilesForAxioms()}; empty in LOAD + * mode or without proof obligations) */ - public record Result(Path fileForTaclets, boolean generateProofObligations) { + public record Result(Path fileForTaclets, boolean generateProofObligations, + List filesForAxioms) { } + /** Swing {@code HELP_TEXT} (LoadUserTacletsDialog.java:35-50). */ + private static final String HELP_TEXT = + """ + In this dialog you can choose the files that are used for loading user-defined taclets: + + User-Defined Taclets: + This file contains the taclets that should be loaded, so that they can be used for the current proof. For each taclet an extra proof obligation is built that must be provable, in order to sustain the correctness of the calculus. + + Definitions: + This file contains the signature (function symbols, predicate symbols, sorts) that are used for creating the proof obligations mentioned above. In most cases it should be the same file as indicated in 'User-Defined Taclets'. + + Axioms: + In order to prove the correctness of the created lemmata, for some user-defined taclets the introduction of additional axioms is necessary. At this point you can add them. + Beware of the fact that it is crucial for the correctness of the calculus that the used axioms are consistent.It is the responsibility of the user to guarantee this consistency. + + Technical Remarks: + The axioms must be stored in another file than the user-defined taclets. Furthermore the axioms are only loaded for the lemmata, but not for the current proof."""; + + /** Swing {@code INFO_TEXT} (LoadUserTacletsDialog.java:52-56). */ + private static final String INFO_TEXT = """ + Be aware of the fact that you are going to load taclets + without creating corresponding proof obligations! + In case that the taclets that you want to load are unsound, + the calculus will become unsound!"""; + private LoadUserTacletsDialogF() { } @@ -67,11 +108,11 @@ private LoadUserTacletsDialogF() { * * @param owner the owner window (the main window stage), may be {@code null} * @param mode PROVE or LOAD - * @return the chosen file and checkbox state, or {@link Optional#empty()} when canceled - * (Swing {@code showAsDialog()} returns {@code false}) + * @return the chosen file, checkbox state and axiom files, or {@link Optional#empty()} when + * canceled (Swing {@code showAsDialog()} returns {@code false}) */ public static Optional showDialog(Window owner, Mode mode) { - javafx.stage.Stage dialog = new javafx.stage.Stage(); + Stage dialog = new Stage(); dialog.setTitle( mode == Mode.LOAD ? "Load User-Defined Taclets" : "Prove User-Defined Taclets"); dialog.initOwner(owner); @@ -107,26 +148,74 @@ public static Optional showDialog(Window owner, Mode mode) { } }); + // lemma: the axiom-file list (Swing axiomsList + getAdd/RemoveAxiomFileButton, + // LoadUserTacletsDialog.java:245-249, :315-338) — enabled in PROVE mode while proof + // obligations are generated + ListView axiomsList = new ListView<>(FXCollections.observableArrayList()); + axiomsList.setPrefHeight(90); + axiomsList.getStyleClass().add("axioms-list"); + Button addAxiomFileButton = new Button("Add axiom file…"); + Button removeAxiomFileButton = new Button("Remove"); + removeAxiomFileButton.disableProperty() + .bind(axiomsList.getSelectionModel().selectedItemProperty().isNull()); + addAxiomFileButton.setOnAction(e -> { + FileChooser chooser = new FileChooser(); + chooser.setTitle("Choose the file with the axioms"); + chooser.getExtensionFilters() + .add(new FileChooser.ExtensionFilter("KeY files (*.key)", "*.key")); + File selected = chooser.showOpenDialog(dialog); + if (selected != null && !axiomsList.getItems().contains(selected.toPath())) { + axiomsList.getItems().add(selected.toPath()); + } + }); + removeAxiomFileButton.setOnAction( + e -> axiomsList.getItems().remove(axiomsList.getSelectionModel().getSelectedItem())); + TitledPaneAxioms axiomPanel = + new TitledPaneAxioms(axiomsList, addAxiomFileButton, removeAxiomFileButton); + CheckBox lemmaBox = new CheckBox("Generate proof obligations for taclets"); lemmaBox.setSelected(true); - // menu: the Swing warning on un-checking (InfoDialog "you are going to load taclets - // without creating corresponding proof obligations") is dropped — the checkbox state - // is persisted nowhere in the FX port. - lemmaBox.setWrapText(true); + // lemma: the Swing warning on un-checking (getLemmaCheckBox, :203-237): re-check, show + // the InfoDialog confirmation (unless suppressed) and only then really un-check + lemmaBox.setOnAction(e -> { + if (lemmaBox.isSelected()) { + axiomPanel.setVisible(true); + return; + } + lemmaBox.setSelected(true); + boolean showDialogUsingAxioms = ProofIndependentSettings.DEFAULT_INSTANCE + .getLemmaGeneratorSettings().isShowingDialogUsingAxioms(); + InfoDialogResult answer = showInfoDialog(dialog, INFO_TEXT, showDialogUsingAxioms); + if (answer.confirmed()) { + lemmaBox.setSelected(false); + axiomPanel.setVisible(false); + axiomsList.getItems().clear(); + ProofIndependentSettings.DEFAULT_INSTANCE.getLemmaGeneratorSettings() + .setShowDialogUsingAxioms( + showDialogUsingAxioms && answer.showNextTime()); + } + }); + // LOAD mode: the axiom panel is disabled (Swing enableAxiomFilePanel(false), :258-263) + if (mode == Mode.LOAD) { + lemmaBox.setDisable(true); + axiomPanel.setVisible(false); + } + Button helpButton = new Button("Help"); + helpButton.setOnAction(e -> showHelpWindow(dialog)); Button cancel = new Button("Cancel"); ButtonBar.setButtonData(ok, ButtonBar.ButtonData.OK_DONE); ButtonBar.setButtonData(cancel, ButtonBar.ButtonData.CANCEL_CLOSE); ButtonBar buttons = new ButtonBar(); - buttons.getButtons().addAll(ok, cancel); + buttons.getButtons().addAll(ok, cancel, helpButton); - VBox center = new VBox(10, hint, choose, chosen, lemmaBox); + VBox center = new VBox(10, hint, choose, chosen, lemmaBox, axiomPanel); center.setPadding(new Insets(12)); BorderPane root = new BorderPane(); root.setCenter(center); root.setBottom(buttons); BorderPane.setMargin(buttons, new Insets(10)); - Scene scene = new Scene(root, 520, 210); + Scene scene = new Scene(root, 560, 360); ThemeManager.getInstance().manage(scene); scene.setOnKeyPressed(e -> { if (e.getCode() == KeyCode.ESCAPE) { @@ -138,11 +227,73 @@ public static Optional showDialog(Window owner, Mode mode) { Result[] result = new Result[1]; ok.setOnAction(e -> { - result[0] = new Result(Path.of(chosen.getText()), lemmaBox.isSelected()); + List axioms = + lemmaBox.isSelected() && mode == Mode.PROVE ? List.copyOf(axiomsList.getItems()) + : List.of(); + result[0] = new Result(Path.of(chosen.getText()), lemmaBox.isSelected(), axioms); dialog.close(); }); cancel.setOnAction(e -> dialog.close()); dialog.showAndWait(); return Optional.ofNullable(result[0]); } + + /** The axiom-panel row: the list between the Add and Remove buttons. */ + private static final class TitledPaneAxioms extends VBox { + TitledPaneAxioms(ListView axiomsList, Button add, Button remove) { + super(6); + Label title = new Label("Axioms (only loaded for the lemmata, not for the proof)"); + title.getStyleClass().add("axioms-title"); + HBox row = new HBox(6, axiomsList, new VBox(6, add, remove)); + HBox.setHgrow(axiomsList, Priority.ALWAYS); + row.setAlignment(Pos.CENTER_LEFT); + getChildren().addAll(title, row); + setPadding(new Insets(4)); + getStyleClass().add("axioms-panel"); + } + } + + /** The Swing {@code InfoDialog} answer (confirmed + "don't show this dialog again"). */ + private record InfoDialogResult(boolean confirmed, boolean showNextTime) { + } + + /** + * The Swing {@code InfoDialog} (lemmatagenerator/InfoDialog.java): the given text, the + * "Don't show this dialog again." checkbox and OK/Cancel. + */ + private static InfoDialogResult showInfoDialog(Window owner, String text, + boolean withCheckbox) { + javafx.scene.control.Dialog info = new javafx.scene.control.Dialog<>(); + info.setTitle("Info"); + info.setHeaderText(null); + VBox content = new VBox(8, new Label(text)); + content.setPadding(new Insets(10)); + CheckBox dontShow = new CheckBox("Don't show this dialog again."); + dontShow.setSelected(true); + if (withCheckbox) { + content.getChildren().add(dontShow); + } + info.getDialogPane().setContent(content); + info.getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL); + if (owner != null) { + info.initOwner(owner); + } + Optional answer = info.showAndWait(); + boolean confirmed = answer.isPresent() && answer.get() == ButtonType.OK; + return new InfoDialogResult(confirmed, dontShow.isSelected()); + } + + /** The Swing help window (getHelpWindow, LoadUserTacletsDialog.java:270-287). */ + private static void showHelpWindow(Window owner) { + Stage help = new Stage(); + help.setTitle("Help"); + TextArea text = new TextArea(HELP_TEXT); + text.setEditable(false); + text.setWrapText(true); + help.setScene(new Scene(text, 500, 400)); + if (owner != null) { + help.initOwner(owner); + } + help.show(); + } } diff --git a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css index b9fa50ee926..981924e3eca 100644 --- a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css +++ b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css @@ -920,3 +920,108 @@ -fx-fill: derive(-key-warning, 30%); -fx-opacity: 0.45; } + +/* the traffic light of the checked predicate input (Swing TrafficLight) */ +.traffic-light { + -fx-background-color: -key-border; + -fx-background-radius: 6; + -fx-min-width: 12; + -fx-max-width: 12; + -fx-min-height: 12; + -fx-max-height: 12; +} +.traffic-light-ok { + -fx-background-color: -key-success; +} +.traffic-light-error { + -fx-background-color: -key-error; +} + +/* the details box rows of the join dialog (Swing ClickableMessageBox rows) */ +.join-message-row { + -fx-cursor: hand; + -fx-padding: 1 4 1 4; +} +.join-details-box { + -fx-background-color: transparent; + -fx-padding: 2; +} + +/* origin term label visualizer (Swing OriginTermLabelVisualizer): the origin of the selected + term in the tree is highlighted in the term view (Swing HIGHLIGHT_COLOR = orange, here the + existing warning color variable) */ +.origin-visualizer-highlight { + -fx-fill: derive(-key-warning, 30%); + -fx-opacity: 0.45; +} + +/* --------------------------------------------------------------------------- + contract completions (P2b) — dark theme: the contract configurator cells + (ContractConfigurator/ContractSelectionPanel + auxiliary/block variants) and + the invariant configurator (Swing InvariantConfigurator) + --------------------------------------------------------------------------- */ +.contract-selection-panel { + -fx-padding: 4; +} +.contract-cell { + -fx-background-color: -key-surface; + -fx-border-color: -key-border; + -fx-border-radius: 4; + -fx-background-radius: 4; + -fx-padding: 4; + -fx-cursor: hand; +} +.contract-cell-gray { + -fx-background-color: derive(-key-background, -5%); + -fx-text-fill: -key-text-secondary; +} +.contract-cell-gray .label { + -fx-text-fill: -key-text-secondary; +} +.contract-cell-name { + -fx-font-weight: bold; + -fx-text-fill: -key-text; +} +.contract-cell-text { + -fx-text-fill: -key-text; + -fx-wrap-text: true; +} +.invariant-loop-representation { + -fx-font-family: monospace; + -fx-wrap-text: true; + -fx-text-fill: -key-text; +} +.invariant-heap-pane { + -fx-padding: 4; +} +.invariant-input { + -fx-font-family: monospace; +} +.invariant-status-ok { + -fx-text-fill: -key-success; +} +.invariant-status-error { + -fx-text-fill: -key-error; +} + +/* lemma dialogs (P2b): the ItemChooser dual list of the taclet selection dialog + and the axiom panel of the user-taclets dialog */ +.item-chooser .list-view { + -fx-background-color: -key-surface; +} +.item-chooser-middle { + -fx-padding: 0 2 0 2; +} +.axioms-panel { + -fx-border-color: -key-border; + -fx-border-radius: 4; + -fx-background-radius: 4; + -fx-padding: 6; +} +.axioms-title { + -fx-font-weight: bold; + -fx-text-fill: -key-text-secondary; +} +.axioms-list { + -fx-background-color: -key-surface; +} diff --git a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css index 3740ea7ca4d..c34c7932aee 100644 --- a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css +++ b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css @@ -920,3 +920,74 @@ -fx-fill: derive(-key-warning, 30%); -fx-opacity: 0.45; } + +/* --------------------------------------------------------------------------- + contract completions (P2b): the contract configurator cells + (ContractConfigurator/ContractSelectionPanel + auxiliary/block variants) and + the invariant configurator (Swing InvariantConfigurator) + --------------------------------------------------------------------------- */ +.contract-selection-panel { + -fx-padding: 4; +} +.contract-cell { + -fx-background-color: -key-surface; + -fx-border-color: -key-border; + -fx-border-radius: 4; + -fx-background-radius: 4; + -fx-padding: 4; + -fx-cursor: hand; +} +.contract-cell-gray { + -fx-background-color: derive(-key-background, -5%); + -fx-text-fill: -key-text-secondary; +} +.contract-cell-gray .label { + -fx-text-fill: -key-text-secondary; +} +.contract-cell-name { + -fx-font-weight: bold; + -fx-text-fill: -key-text; +} +.contract-cell-text { + -fx-text-fill: -key-text; + -fx-wrap-text: true; +} +.invariant-loop-representation { + -fx-font-family: monospace; + -fx-wrap-text: true; + -fx-text-fill: -key-text; +} +.invariant-heap-pane { + -fx-padding: 4; +} +.invariant-input { + -fx-font-family: monospace; +} +.invariant-status-ok { + -fx-text-fill: -key-success; +} +.invariant-status-error { + -fx-text-fill: -key-error; +} + +/* lemma dialogs (P2b): the ItemChooser dual list of the taclet selection dialog + and the axiom panel of the user-taclets dialog */ +.item-chooser .list-view { + -fx-background-color: -key-surface; +} +.item-chooser-middle { + -fx-padding: 0 2 0 2; +} +.axioms-panel { + -fx-border-color: -key-border; + -fx-border-radius: 4; + -fx-background-radius: 4; + -fx-padding: 6; +} +.axioms-title { + -fx-font-weight: bold; + -fx-text-fill: -key-text-secondary; +} +.axioms-list { + -fx-background-color: -key-surface; +} From 2f12ac72874627a0e9f7f0f41d9929c2d2999b7c Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Sat, 10 Oct 2026 16:36:22 +0200 Subject: [PATCH 138/281] prooftree (P3a): mediator support for the Apply Strategy and Prune popup actions KeYMediatorF.startAutoMode(ImmutableList) runs the automatic prover restricted to the given goals (Swing RunStrategyOnNodeUserAction.apply) and KeYMediatorF.pruneNode(Node) cuts the proof below the given node (Swing KeYMediator.setBack). The tree view popup (C23) uses both. --- .../de/uka/ilkd/key/core/fx/KeYMediatorF.java | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) 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 index 4da3cf345ae..9a05d0c0a9f 100644 --- 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 @@ -257,6 +257,35 @@ public void stopAutoMode() { } } + /** + * 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 From f02e616e5418c89481652957743e77e04cebb5a3 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Sat, 10 Oct 2026 16:36:55 +0200 Subject: [PATCH 139/281] prooftree (P3a): proof-tree parity C19-C23/C25/C27 in ProofTreeViewF - C19: per-proof view state (WeakHashMap) with expanded branches and selection, saved and restored across proof switches. - C20: linearized (flat) mode with the main-tagged taclet splice, toggleable from the tree context menu. - C21: one-step-simplification protocol rows below the OSS node entry; an "expand OSS nodes" toggle gates their expansion in the whole-tree expand. - C22: whole-tree Expand All / Collapse All. - C23: Apply Strategy, Prune Proof, Edit Notes (ProofTreeNotesDialogF) and Show Subtree Statistics (SubtreeStatisticsDialogF) popup items. - C25: the Swing node-filter counting rule incl. the "inlined due to hidden subtree" case; buildBranch collects the split children into branch folders when the chain stops (Swing GUIBranchNode.fillChildrenCache), fixing a structure bug where the subtree below the first split vanished after an auto-prove closed the frontier goal. - C27: auto-mode partial subtree updates (modifiedSubtrees, max 16 changed roots per stop, otherwise a full rebuild); the tree observes the proof control's auto-mode events (Swing ProofTreeView.java:532). - ProofTreeViewF now implements AutoModeListener; verifyProoftreeSemantics() drives the C19-C22/C25 checks and restores all state afterwards. --- .../fx/prooftree/ProofTreeNotesDialogF.java | 110 ++ .../key/gui/fx/prooftree/ProofTreeViewF.java | 1028 +++++++++++++++-- .../prooftree/SubtreeStatisticsDialogF.java | 148 +++ .../de/uka/ilkd/key/gui/fx/theme/key-dark.css | 32 + .../uka/ilkd/key/gui/fx/theme/key-light.css | 32 + 5 files changed, 1278 insertions(+), 72 deletions(-) create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/prooftree/ProofTreeNotesDialogF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/prooftree/SubtreeStatisticsDialogF.java diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/prooftree/ProofTreeNotesDialogF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/prooftree/ProofTreeNotesDialogF.java new file mode 100644 index 00000000000..38e5839f326 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/prooftree/ProofTreeNotesDialogF.java @@ -0,0 +1,110 @@ +/* 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.prooftree; + +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.TextArea; +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.proof.Node; + +import org.key_project.util.javafx.FxUtil; + +/** + * prooftree (P3a, C23): JavaFX port of the Swing "Edit Notes..." popup of the proof tree + * ({@code ProofTreePopupFactory.Notes}): a small text editor for the free-form note attached to + * a proof node ({@code NodeInfo.setNotes}). Empty input clears the note, cancel leaves it + * untouched — the Swing {@code JOptionPane.showInputDialog} semantics, as a plain window instead + * of a modal option pane (same adaptation as the other P2b/P3 dialogs). + */ +public final class ProofTreeNotesDialogF { + + private final Stage stage = new Stage(); + private final Node node; + /** The note text field (package-visible for the {@code key.fx.verify.prooftree} harness). */ + final TextArea textArea = new TextArea(); + + /** + * @param original the current note of the node, may be {@code null} + * @param node the proof node whose note is edited + */ + public ProofTreeNotesDialogF(String original, Node node) { + this.node = node; + textArea.setPromptText("Attach a note to this proof node..."); + textArea.setText(original == null ? "" : original); + textArea.getStyleClass().addAll("proof-tree-notes-area"); + + Label nodeLabel = new Label("Annotate proof node " + node.serialNr() + + (node.getAppliedRuleApp() != null ? " (" + node.getAppliedRuleApp().rule().name() + + ")" : "")); + nodeLabel.getStyleClass().add("proof-tree-notes-title"); + + Button okButton = new Button("OK"); + okButton.setDefaultButton(true); + okButton.setOnAction(e -> requestOk()); + Button cancelButton = new Button("Cancel"); + cancelButton.setCancelButton(true); + cancelButton.setOnAction(e -> requestCancel()); + HBox buttons = new HBox(8, okButton, cancelButton); + buttons.setAlignment(Pos.CENTER_RIGHT); + buttons.setPadding(new Insets(6)); + + BorderPane root = new BorderPane(); + root.setTop(nodeLabel); + root.setCenter(textArea); + root.setBottom(buttons); + BorderPane.setMargin(nodeLabel, new Insets(6, 6, 0, 6)); + BorderPane.setMargin(textArea, new Insets(6)); + + Scene scene = new Scene(root, 480, 240); + ThemeManager.getInstance().style(scene); + stage.setTitle("Annotate this proof node"); + stage.setScene(scene); + stage.setMinWidth(300); + stage.setMinHeight(160); + } + + /** Shows the dialog non-blocking (P3a harness seam like the P2b configurators). */ + public void showNonBlocking() { + if (!FxUtil.isFxThread()) { + FxUtil.runLater(this::showNonBlocking); + return; + } + stage.initModality(Modality.NONE); + stage.show(); + } + + /** + * OK: stores the note on the node — empty input clears it, exactly like the Swing dialog + * (ProofTreePopupFactory.Notes.actionPerformed). + */ + public void requestOk() { + String text = textArea.getText(); + if (text == null || text.isEmpty()) { + node.getNodeInfo().setNotes(null); + } else { + node.getNodeInfo().setNotes(text); + } + stage.close(); + } + + /** Cancel: closes without changing the note (Swing returns {@code null}). */ + public void requestCancel() { + stage.close(); + } + + /** @return the stage (verification harness) */ + public Stage getStage() { + return stage; + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/prooftree/ProofTreeViewF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/prooftree/ProofTreeViewF.java index 608ff06fd71..8f0001eb5f6 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/prooftree/ProofTreeViewF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/prooftree/ProofTreeViewF.java @@ -14,6 +14,7 @@ import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.WeakHashMap; import java.util.concurrent.atomic.AtomicInteger; import javafx.geometry.Insets; import javafx.scene.control.Button; @@ -36,6 +37,9 @@ import javafx.scene.layout.Priority; import javafx.scene.layout.Region; +import de.uka.ilkd.key.control.AutoModeListener; +import de.uka.ilkd.key.control.ProofControl; +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; @@ -43,11 +47,19 @@ 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.ProofTreeEvent; import de.uka.ilkd.key.proof.ProofTreeListener; +import de.uka.ilkd.key.proof.reference.ClosedBy; +import de.uka.ilkd.key.rule.OneStepSimplifier; +import de.uka.ilkd.key.rule.OneStepSimplifierRuleApp; +import de.uka.ilkd.key.rule.Taclet; +import de.uka.ilkd.key.settings.GeneralSettings; import de.uka.ilkd.key.settings.ProofIndependentSettings; import org.key_project.prover.rules.RuleApp; +import org.key_project.prover.rules.tacletbuilder.TacletGoalTemplate; +import org.key_project.prover.sequent.Sequent; import org.key_project.util.collection.ImmutableList; import org.key_project.util.javafx.FxUtil; @@ -83,7 +95,7 @@ * (currently built eagerly), and incremental per-event tree model updates (live updates currently * coalesce into a full rebuild). */ -public class ProofTreeViewF extends BorderPane { +public class ProofTreeViewF extends BorderPane implements AutoModeListener { private static final Logger LOGGER = LoggerFactory.getLogger(ProofTreeViewF.class); @@ -91,34 +103,64 @@ public class ProofTreeViewF extends BorderPane { new KeyCodeCombination(KeyCode.F, KeyCombination.CONTROL_DOWN, KeyCombination.SHIFT_DOWN); /** - * One tree entry: either a proof node entry (a rule application) or a branch entry (labeled - * sub-tree; {@code node} is the root of the branch). + * One tree entry: either a proof node entry (a rule application), a branch entry (labeled + * sub-tree; {@code node} is the root of the branch) or a one-step-simplification protocol + * step of an OSS node entry ({@code node} is the OSS node, {@link #ossRuleApp()} the single + * rewriting step performed inside the OSS rule). */ public static final class Entry { final Node node; final String branchLabel; + final RuleApp ossApp; + final int ossFormulaNr; - private Entry(Node node, String branchLabel) { + private Entry(Node node, String branchLabel, RuleApp ossApp, int ossFormulaNr) { this.node = node; this.branchLabel = branchLabel; + this.ossApp = ossApp; + this.ossFormulaNr = ossFormulaNr; } static Entry node(Node node) { - return new Entry(node, null); + return new Entry(node, null, null, -1); } static Entry branch(Node node, String label) { - return new Entry(node, Objects.requireNonNull(label)); + return new Entry(node, Objects.requireNonNull(label), null, -1); + } + + static Entry oss(Node node, RuleApp app, int formulaNr) { + return new Entry(node, null, Objects.requireNonNull(app), formulaNr); } public boolean isBranch() { return branchLabel != null; } + /** + * @return whether this entry is an OSS protocol step of the node entry's applied + * {@link OneStepSimplifierRuleApp} (Swing {@code GUIOneStepChildTreeNode}) + */ + public boolean isOssChild() { + return ossApp != null; + } + public Node node() { return node; } + /** + * @return the OSS protocol step of an {@link #isOssChild()} entry, never {@code null} there + */ + public RuleApp ossRuleApp() { + return ossApp; + } + + /** @return the formula number of the OSS application's position in the sequent */ + public int ossFormulaNr() { + return ossFormulaNr; + } + /** * @return the text shown for this entry, whitespace-normalized like the Swing renderer */ @@ -126,6 +168,9 @@ public String displayText() { if (branchLabel != null) { return branchLabel; } + if (ossApp != null) { + return ossApp.rule().name().toString(); + } String text = node.serialNr() + ":" + node.name(); return text.replaceAll("\\s+", " "); } @@ -153,6 +198,65 @@ public String displayText() { /** Number of coalesced refreshes executed for live events (verification). */ private final AtomicInteger liveRefreshCount = new AtomicInteger(); + // ----------------------------------------------------------------------- + // P3a: per-proof view state (C19), linearized mode (C20), OSS (C21), auto-mode + // partial updates (C27) + // ----------------------------------------------------------------------- + + /** + * C19: the per-proof view state cache (Swing {@code ProofTreeView.viewStates}, + * {@code WeakHashMap}): the expansion set (branch-root nodes), + * the selected node and the scroll offset of the previously shown proofs. Saved when the + * displayed proof is switched, restored when a proof is shown again. + */ + private static final class ViewState { + final Set expandedBranches; + final Node selectedNode; + + ViewState(Set expandedBranches, Node selectedNode) { + this.expandedBranches = expandedBranches; + this.selectedNode = selectedNode; + } + } + + private final Map viewStates = new WeakHashMap<>(4); + + /** + * C20: linearized proof tree mode (Swing {@code ProofTreeView.linearizedMode}): at a split + * where the applied rule's last goal template is tagged {@code "main"} the main branch is + * continued on the same indentation level (Swing {@code GUIBranchNode.fillChildrenCache}). + * Not persisted (Swing keeps it as a view instance flag). + */ + private boolean linearizedMode; + + /** + * C21: whether whole-tree "Expand All" also expands the one-step-simplification protocol + * children (Swing {@code ProofTreeView.expandOSSNodes}); default {@code false} — OSS nodes + * stay collapsed and may be expanded manually. + */ + private boolean expandOSSNodes; + + /** + * C27: the open goals recorded in {@link #autoModeStarted}; the prover may work on those + * subtrees, and {@link #autoModeStopped} updates exactly the changed ones (Swing + * {@code ProofTreeView.modifiedSubtrees}). {@code null} when no auto mode run is pending. + * Written on the prover thread, read on the FX thread. + */ + private volatile ImmutableList modifiedSubtrees; + + /** C27: number of partial (subtree-level) updates executed after auto mode stops. */ + private final AtomicInteger partialUpdateCount = new AtomicInteger(); + + /** C27: number of auto mode stops that had to fall back to a full tree rebuild. */ + private final AtomicInteger fullUpdateAfterAutoCount = new AtomicInteger(); + + /** C27: if more subtrees changed than this, the whole tree is rebuilt (Swing constant). */ + private static final int MAX_PARTIAL_TREE_UPDATES = 16; + + /** P3a: the mediator and proof control driving the popup strategy/prune actions (C23). */ + private KeYMediatorF mediator; + private ProofControl proofControl; + /** the current search query (lowercase), empty if the search is inactive. */ private String query = ""; @@ -377,6 +481,16 @@ public void setProof(Proof newProof) { Proof oldProof = this.proof; this.proof = newProof; if (oldProof != newProof) { + // P3a (C19): memorize the view state of the proof we are leaving (expansion, + // selection; the scroll position is approximated by the selection reveal) — Swing + // ProofTreeView.setProof stores a ProofTreeViewState per proof and restores it when + // the proof is shown again + if (oldProof != null && tree.getRoot() != null) { + Set expandedBranches = Collections.newSetFromMap(new IdentityHashMap<>()); + collectExpandedBranches(tree.getRoot(), expandedBranches); + Node selected = selectionModel != null ? selectionModel.getSelectedNode() : null; + viewStates.put(oldProof, new ViewState(expandedBranches, selected)); + } if (oldProof != null) { oldProof.removeProofTreeListener(proofTreeListener); } @@ -385,6 +499,9 @@ public void setProof(Proof newProof) { } liveEventCount.set(0); liveRefreshCount.set(0); + partialUpdateCount.set(0); + fullUpdateAfterAutoCount.set(0); + modifiedSubtrees = null; containsMatchCache.clear(); } updatingSelection = true; @@ -399,6 +516,26 @@ public void setProof(Proof newProof) { } finally { updatingSelection = false; } + if (oldProof != newProof && newProof != null) { + // C19: restore the memorized view state of the proof that is now shown + ViewState state = viewStates.get(newProof); + if (state != null && tree.getRoot() != null) { + applyExpandedBranches(tree.getRoot(), state.expandedBranches); + if (state.selectedNode != null) { + TreeItem item = findItem(tree.getRoot(), state.selectedNode); + if (item != null && !updatingSelection) { + tree.getSelectionModel().select(item); + tree.scrollTo(tree.getRow(item)); + } + } + } + // the current selection model usually already points into the new proof; when it does + // not (fresh proof), fall back to the default selection + if (selectionModel != null && selectionModel.getSelectedNode() != null + && selectionModel.getSelectedNode().proof() != newProof) { + selectionModel.defaultSelection(); + } + } updateMatches(); } @@ -491,7 +628,7 @@ public String verifyTreeStructure() { return "no proof"; } Set seen = Collections.newSetFromMap(new IdentityHashMap<>()); - int[] counters = { 0, 0 }; // node entries, branch entries + int[] counters = { 0, 0, 0 }; // node entries, branch entries, OSS rows collect(tree.getRoot(), seen, counters); int proofNodeCount = 0; for (Iterator it = proof.root().subtreeIterator(); it.hasNext();) { @@ -507,8 +644,8 @@ public String verifyTreeStructure() { pass = seen.size() == counters[0] + counters[1] && counters[0] <= proofNodeCount; } return "proofNodes=" + proofNodeCount + " nodeEntries=" + counters[0] + " unique=" - + seen.size() + " branches=" + counters[1] + (filtersActive ? " filtered" : "") - + " " + (pass ? "PASS" : "FAIL"); + + seen.size() + " branches=" + counters[1] + " ossRows=" + counters[2] + + (filtersActive ? " filtered" : "") + " " + (pass ? "PASS" : "FAIL"); } /** @@ -590,11 +727,294 @@ public String verifyTreeFilters() { + (pass ? "PASS" : "FAIL"); } - /** @return the number of entries (node + branch) currently displayed */ + /** + * prooftree (P3a): self-test of the C19-C22/C25 parity items — the per-proof view state + * cache, the linearized mode, the OSS protocol rows and the whole-tree expand/collapse — + * plus the C25 node-filter counting rule. Leaves every state (filters, toggles, expansion) + * exactly as it was found. + * + * @return a one-line report, {@code "... PASS"} if every check passes + */ + public String verifyProoftreeSemantics() { + if (proof == null || tree.getRoot() == null) { + return "no proof"; + } + List problems = new ArrayList<>(); + boolean wasLinearized = linearizedMode; + boolean wasExpandOss = expandOSSNodes; + boolean wasIntermediate = hideIntermediateSteps(); + boolean wasAutomode = hideAutomodeSteps(); + boolean wasClosed = hideClosedSubtrees(); + boolean wasInteractive = hideInteractiveGoals(); + Set originallyExpanded = Collections.newSetFromMap(new IdentityHashMap<>()); + collectExpandedBranches(tree.getRoot(), originallyExpanded); + try { + // baseline: all filters off, both P3a toggles off + linearizedMode = false; + expandOSSNodes = false; + setHideIntermediateSteps(false); + setHideAutomodeSteps(false); + setHideClosedSubtrees(false); + setHideInteractiveGoals(false); + refresh(); + int baseline = countEntries(); + String structure = verifyTreeStructure(); + if (!structure.endsWith("PASS")) { + problems.add("baseline structure: " + structure); + } + + // C20: linearized mode folds "main"-tagged splits into the chain — the entry count + // must not grow and the structure must stay consistent + linearizedMode = true; + refresh(); + int linearized = countEntries(); + String linearizedStructure = verifyTreeStructure(); + linearizedMode = false; + refresh(); + int restored = countEntries(); + if (linearized > baseline || restored != baseline + || !linearizedStructure.endsWith("PASS")) { + problems.add("linearized baseline=" + baseline + " linear=" + linearized + + " restored=" + restored + " [" + linearizedStructure + "]"); + } + + // C21: count the OSS protocol rows in the tree; with "expand OSS nodes" off the + // whole-tree expand must keep the OSS rows collapsed, on it must expand them + long ossRows = countOssRows(tree.getRoot()); + long ossNodes = 0; + long expectedOssRows = 0; + for (Iterator it = proof.root().subtreeIterator(); it.hasNext();) { + Node n = it.next(); + if (n.getAppliedRuleApp() instanceof OneStepSimplifierRuleApp oss + && oss.getProtocol() != null) { + ossNodes++; + // one row per rule application performed inside the step + expectedOssRows += oss.getProtocol().size(); + } + } + collapseEntireTree(); + expandEntireTree(); + long collapsedOssParents = countExpandedOssParents(tree.getRoot()); + expandOSSNodes = true; + expandEntireTree(); + long expandedOssParents = countExpandedOssParents(tree.getRoot()); + expandOSSNodes = false; + // proofs without one-step simplifications vacuous-pass (the demo's stored proof + // only gains OSS nodes once the strategy applies OneStepSimplifier) + boolean ossOk = true; + if (ossNodes > 0) { + ossOk = ossRows == expectedOssRows && collapsedOssParents == 0 + && expandedOssParents == ossNodes; + } + if (!ossOk) { + problems.add("oss rows=" + ossRows + " ossNodes=" + ossNodes + + " expectedRows=" + expectedOssRows + + " expandedParents(off)=" + collapsedOssParents + + " expandedParents(on)=" + expandedOssParents); + } + + // C22: collapse all leaves only the root expanded; expand all expands the whole tree + // (OSS protocol rows stay collapsed unless the C21 toggle is on — asserted above) + collapseEntireTree(); + boolean onlyRoot = onlyRootExpanded(tree.getRoot()); + expandEntireTree(); + long expandedRows = countExpandedRows(tree.getRoot()); + long totalRows = countEntries(); + if (!onlyRoot || expandedRows <= 1 || expandedRows > totalRows) { + problems.add("collapseAll onlyRoot=" + onlyRoot + " expandAll rows=" + + expandedRows + "/" + totalRows); + } + + // C25: with "hide intermediate proofsteps" active and no global filter the Swing + // counting rule keeps exactly the last child of every child list — no node entry may + // sit before a later sibling + setHideIntermediateSteps(true); + refresh(); + boolean nonLastNodeEntries = hasNonLastNodeEntry(tree.getRoot()); + setHideIntermediateSteps(false); + refresh(); + if (nonLastNodeEntries) { + problems.add("hideIntermediate counting rule violated (non-last node entry)"); + } + + // C19: switching proofs away and back restores the expanded branches and the + // selection (view-states cache) + int[] ossCounters = { 0, 0, 0 }; + collect(tree.getRoot(), Collections.newSetFromMap(new IdentityHashMap<>()), + ossCounters); + if (ossCounters[1] > 0) { + TreeItem someBranch = firstBranchItem(tree.getRoot()); + if (someBranch != null) { + Node branchNode = someBranch.getValue().node(); + someBranch.setExpanded(true); + Node savedSelection = selectionModel != null + ? selectionModel.getSelectedNode() + : null; + Proof currentProof = this.proof; + setProof(null); + setProof(currentProof); + boolean branchRestored = isBranchExpanded(branchNode); + boolean selectionRestored = savedSelection != null + && selectionModel != null + && selectionModel.getSelectedNode() == savedSelection; + if (!branchRestored || !selectionRestored) { + problems.add("view-state restore branch=" + branchRestored + + " selection=" + selectionRestored); + } + } + } + } finally { + // restore the state found on entry + linearizedMode = wasLinearized; + expandOSSNodes = wasExpandOss; + setHideIntermediateSteps(wasIntermediate); + setHideAutomodeSteps(wasAutomode); + setHideClosedSubtrees(wasClosed); + setHideInteractiveGoals(wasInteractive); + refresh(); + if (tree.getRoot() != null) { + collapseEntireTree(); + applyExpandedBranches(tree.getRoot(), originallyExpanded); + revealSelectedNode(); + } + } + if (!problems.isEmpty()) { + return "FAIL: " + String.join("; ", problems); + } + return "PASS: linearized, OSS rows, whole-tree actions, filter counting, view states"; + } + + /** @return the number of OSS protocol rows currently present in the tree */ + private static long countOssRows(TreeItem item) { + long count = 0; + if (item.getValue() != null && item.getValue().isOssChild()) { + count++; + } + for (TreeItem child : item.getChildren()) { + count += countOssRows(child); + } + return count; + } + + /** @return the number of expanded OSS node rows (parents of protocol rows) in the tree */ + private static long countExpandedOssParents(TreeItem item) { + long count = 0; + Entry entry = item.getValue(); + if (entry != null && !entry.isBranch() && !entry.isOssChild() + && item.isExpanded() + && entry.node() != null + && entry.node().getAppliedRuleApp() instanceof OneStepSimplifierRuleApp) { + count++; + } + for (TreeItem child : item.getChildren()) { + count += countExpandedOssParents(child); + } + return count; + } + + /** @return whether only the root item is expanded below the given item */ + private static boolean onlyRootExpanded(TreeItem root) { + if (!root.isExpanded()) { + return false; + } + for (TreeItem child : root.getChildren()) { + if (isAnyExpanded(child)) { + return false; + } + } + return true; + } + + private static boolean isAnyExpanded(TreeItem item) { + if (item.isExpanded()) { + return true; + } + for (TreeItem child : item.getChildren()) { + if (isAnyExpanded(child)) { + return true; + } + } + return false; + } + + /** @return the number of expanded rows below and including {@code item} */ + private static long countExpandedRows(TreeItem item) { + long count = item.isExpanded() ? 1 : 0; + for (TreeItem child : item.getChildren()) { + count += countExpandedRows(child); + } + return count; + } + + /** + * C25: whether any displayed node entry sits before a later sibling of its parent — under + * "hide intermediate proofsteps" without active global filters the Swing counting rule keeps + * only the last child, so such an entry would violate the rule. + */ + private static boolean hasNonLastNodeEntry(TreeItem item) { + List> children = item.getChildren(); + for (int i = 0; i < children.size() - 1; i++) { + Entry entry = children.get(i).getValue(); + if (entry != null && !entry.isBranch() && !entry.isOssChild()) { + return true; + } + } + for (TreeItem child : children) { + if (hasNonLastNodeEntry(child)) { + return true; + } + } + return false; + } + + private static TreeItem firstBranchItem(TreeItem item) { + // the root item is itself a branch; a meaningful expansion test needs a sub-branch below + for (TreeItem child : item.getChildren()) { + TreeItem found = firstBranchBelow(child); + if (found != null) { + return found; + } + } + return null; + } + + private static TreeItem firstBranchBelow(TreeItem item) { + if (item.getValue() != null && item.getValue().isBranch()) { + return item; + } + for (TreeItem child : item.getChildren()) { + TreeItem found = firstBranchBelow(child); + if (found != null) { + return found; + } + } + return null; + } + + private boolean isBranchExpanded(Node branchRoot) { + TreeItem item = findBranchItem(tree.getRoot(), branchRoot); + return item != null && item.isExpanded(); + } + + private static TreeItem findBranchItem(TreeItem item, Node branchRoot) { + if (item.getValue() != null && item.getValue().isBranch() + && item.getValue().node() == branchRoot) { + return item; + } + for (TreeItem child : item.getChildren()) { + TreeItem found = findBranchItem(child, branchRoot); + if (found != null) { + return found; + } + } + return null; + } + + /** @return the number of entries (node + branch + OSS rows) currently displayed */ private int countEntries() { - int[] counters = { 0, 0 }; + int[] counters = { 0, 0, 0 }; collect(tree.getRoot(), Collections.newSetFromMap(new IdentityHashMap<>()), counters); - return counters[0] + counters[1]; + return counters[0] + counters[1] + counters[2]; } /** @@ -620,11 +1040,17 @@ private void checkCollapsed(TreeItem item, int[] counters) { private static void collect(TreeItem item, Set seen, int[] counters) { Entry entry = item.getValue(); if (entry != null && entry.node != null) { - seen.add(entry.node); - if (entry.isBranch()) { - counters[1]++; + if (entry.isOssChild()) { + // P3a (C21): OSS protocol rows are decorative sub-rows of their node entry — + // the node is already seen via the node row, only the extra row is counted + counters[2]++; } else { - counters[0]++; + seen.add(entry.node); + if (entry.isBranch()) { + counters[1]++; + } else { + counters[0]++; + } } } for (TreeItem child : item.getChildren()) { @@ -635,71 +1061,176 @@ private static void collect(TreeItem item, Set seen, int[] counters private TreeItem buildBranch(Node branchRoot, String label) { TreeItem branchItem = new TreeItem<>(Entry.branch(branchRoot, label)); - // collect the branch's linear chain first (mirrors GUIBranchNode.fillChildrenCache) - List chain = new ArrayList<>(); - Node current = branchRoot; - while (true) { - chain.add(current); - if (current.childrenCount() == 1) { - current = current.child(0); - continue; - } - break; - } + // P3a: collect the branch's linear chain first (Swing GUIBranchNode.fillChildrenCache). + // The chain walk follows the *visible* children (Swing GUIAbstractTreeNode.findChild): + // with a global filter active a subtree can be inlined even if the node has several + // children (C25 — the node-filter "counting rule" keeps such an inlined split visible); + // in linearized mode (C20) a "main"-tagged taclet split continues the chain with its + // first child and turns the remaining children into branch folders. + List> childItems = new ArrayList<>(); + List branchChildren = new ArrayList<>(); boolean searchActive = filterActive(); // Swing: while the collapsing search is active it takes precedence over the // intermediate-step filters (GUIProofTreeModel.bypassNodeFilter) boolean nodeFilterActive = !searchActive && (hideIntermediateSteps() || hideAutomodeSteps()); - int branchCount = current.childrenCount(); - if (!nodeFilterActive) { - // all chain steps, search-filtered - for (Node node : chain) { - if (!searchActive || matches(node)) { - branchItem.getChildren().add(new TreeItem<>(Entry.node(node))); + Node current = branchRoot; + while (true) { + if (!searchActive || matches(current)) { + TreeItem stepItem = new TreeItem<>(Entry.node(current)); + childItems.add(stepItem); + // C21: one-step-simplification protocol children (Swing GUIProofTreeNode + // .ensureChildrenArray): a row per rule application performed inside the OSS + // step, only visible when the node row is expanded. While the collapsing search + // is active the rows are omitted so the collapsed tree shows matching steps only + // (Swing's search filter operates on step rows, the OSS children are decorative). + if (!searchActive + && current.getAppliedRuleApp() instanceof OneStepSimplifierRuleApp oss) { + OneStepSimplifier.Protocol protocol = oss.getProtocol(); + if (protocol != null) { + int formulaNr = + current.sequent().formulaNumberInSequent(oss.posInOccurrence()); + for (RuleApp step : protocol) { + stepItem.getChildren().add(new TreeItem<>(Entry.oss(current, step, + formulaNr))); + } + } } } - } else if (hideIntermediateSteps()) { - // Hide Intermediate Proofsteps: a branch shows only the last element of its - // [chain..., branch folders...] list (the Swing NodeFilter shows the child at the - // last position); with branch folders below, the whole chain — including the split - // step — is hidden, without them the chain's final node (the goal) remains - if (branchCount == 0) { - branchItem.getChildren() - .add(new TreeItem<>(Entry.node(chain.get(chain.size() - 1)))); + List nextN = visibleChildren(current); + if (nextN.isEmpty()) { + if (current.childrenCount() > 0) { + // the chain stopped at a split point (more than one child, no inlining): + // each child becomes a branch folder below — Swing + // GUIBranchNode.fillChildrenCache iterates model.children() once findChild + // returns null; the per-child hidden check in the folder loop prunes the + // children hidden by global filters again (an empty visible list with an + // active filter means all children are hidden, so the folders vanish too) + for (int i = 0; i < current.childrenCount(); i++) { + branchChildren.add(current.child(i)); + } + } + break; } - } else { - // Hide Non-interactive Proofsteps: interactive steps stay visible, plus the final - // node of leaf chains - for (int i = 0; i < chain.size(); i++) { - Node node = chain.get(i); - boolean last = i == chain.size() - 1; - if (node.getNodeInfo().getInteractiveRuleApplication() - || last && branchCount == 0) { - branchItem.getChildren().add(new TreeItem<>(Entry.node(node))); + if (nextN.size() > 1) { + if (linearizedMode && isMainTaggedTacletSplit(current)) { + // C20: continue the main branch on the same level; the other children become + // branch folders below (Swing GUIBranchNode.fillChildrenCache:85-101) + for (int i = 1; i < nextN.size(); i++) { + branchChildren.add(nextN.get(i)); + } + current = nextN.get(0); + continue; } + branchChildren.addAll(nextN); + break; } + current = nextN.get(0); } - // at a branch point (or a leaf): one branch item per child, pruned by the global filters. - // Read by index instead of iterating the live children list: the prover thread may add - // children concurrently (ArrayList view); a concurrent mutation is healed by the next - // structural event. - for (int i = 0; i < branchCount; i++) { - Node child = current.child(i); - if (searchActive && !containsMatch(child)) { - // the subtree contains no match: hidden (Swing TreeSearchFilter.showSubtree) - continue; - } + // at a branch point (or a leaf): one branch item per visible child, pruned by the global + // filters (Swing fillChildrenCache's final loop). Read by index instead of iterating the + // live children list: the prover thread may add children concurrently. + for (Node child : branchChildren) { if (hiddenByGlobalFilters(child)) { + // the search filter is part of hiddenByGlobalFilters (containsMatch) continue; } - branchItem.getChildren().add(buildBranch(child, ensureBranchLabelIsSet(child))); + childItems.add(buildBranch(child, ensureBranchLabelIsSet(child))); + } + + // C25: the node filters (hide intermediate / hide non-interactive) count over the child + // list like the Swing NodeFilter (ProofTreeViewFilter.countChild) — including the + // "inlined because of a hidden subtree" rule + if (nodeFilterActive) { + List> filtered = new ArrayList<>(); + for (int i = 0; i < childItems.size(); i++) { + TreeItem item = childItems.get(i); + Entry entry = item.getValue(); + // branch folders and OSS protocol rows are always counted (Swing + // ProofTreeViewFilter.countChild keeps GUIBranchNode children) + if (entry.isBranch() || entry.isOssChild()) { + filtered.add(item); + } else if (countChainStep(entry.node(), childItems, i)) { + filtered.add(item); + } + } + childItems = filtered; } + branchItem.getChildren().setAll(childItems); return branchItem; } + /** + * @param node a proof node + * @return the node's children that survive the global filters, mirroring Swing + * {@code GUIAbstractTreeNode.findChild} (GUIAbstractTreeNode.java:139-167): a single + * child always continues the chain; with an active global filter or in linearized + * mode the multi-child case keeps the visible children, otherwise the chain stops. + */ + private List visibleChildren(Node node) { + if (node.childrenCount() == 1) { + return List.of(node.child(0)); + } + if (!globalFilterActive() && !linearizedMode) { + return List.of(); + } + List visible = new ArrayList<>(); + for (int i = 0; i < node.childrenCount(); i++) { + Node child = node.child(i); + if (!hiddenByGlobalFilters(child)) { + visible.add(child); + } + } + return visible; + } + + /** + * @return whether any global filter is active (the collapsing search, Hide Closed Subtrees + * or Hide Subtrees Whose Goals are Interactive) — Swing + * {@code ProofTreeViewFilter.anyGlobalFilterActive} + */ + private boolean globalFilterActive() { + return filterActive() || hideClosedSubtrees() || hideInteractiveGoals(); + } + + /** + * C20: whether the node's applied rule is a taclet whose last goal template is + * tagged {@code "main"} — such a split continues in linearized mode (Swing + * {@code GUIBranchNode.fillChildrenCache:83-88}). + */ + private static boolean isMainTaggedTacletSplit(Node node) { + RuleApp app = node.getAppliedRuleApp(); + if (!(app != null && app.rule() instanceof Taclet taclet)) { + return false; + } + ImmutableList templates = taclet.goalTemplates(); + return templates.size() > 0 && "main".equals(templates.last().tag()); + } + + /** + * C25: whether the chain step at {@code pos} in {@code siblings} is counted by the active + * node filter (Swing {@code HideIntermediateFilter}/{@code OnlyInteractiveFilter} + * {@code countChild}, ProofTreeViewFilter.java:214-282): the last child is always shown; + * with a global filter active, a split that was inlined because its sibling subtree is + * hidden is shown too. + */ + private boolean countChainStep(Node node, List> siblings, int pos) { + if (!hideIntermediateSteps() && node.getNodeInfo().getInteractiveRuleApplication()) { + // Hide Non-interactive Proofsteps keeps interactive steps + return true; + } + if (pos == siblings.size() - 1) { + return true; + } + if (globalFilterActive() && !siblings.get(pos + 1).getValue().isBranch() + && node.childrenCount() != 1) { + return true; + } + return false; + } + /** * @return whether the subtree starting at {@code node} is hidden by an active global filter * (Swing {@code ProofTreeViewFilter.hiddenByGlobalFilters}): the search filter, Hide @@ -778,11 +1309,13 @@ private static void setHideInteractiveGoals(boolean active) { } /** - * Builds the right-click context menu: the two mutually exclusive node filters and the two - * independently toggleable global filters, like the Swing dockable's "Settings" menu, plus - * the expand/collapse and sibling actions of the Swing proof tree popup menu - * ({@code ProofTreePopupFactory}). The action items that need prover control (Run Strategy - * On Node, Prune, Notes, goals enablement, statistics) arrive with the M3 action framework. + * Builds the right-click context menu: the four view filters and the tree controls (C20 + * linearized mode, C21 OSS expansion, C22 whole-tree expand/collapse) — the FX docking + * framework has no tab-title "Settings" gear menu like the Swing dockable, so these live + * here — plus the popup actions of the Swing {@code ProofTreePopupFactory}: Apply Strategy, + * Prune, Edit Notes, the per-node expand/collapse and sibling actions, the goals enablement + * and P3a's Show Subtree Statistics. Delayed Cut (feature-flagged) and the macro submenu + * (B12, P3b) are not yet ported; the PROOF_TREE extension contributions belong to P4 (C24). */ private ContextMenu createContextMenu() { CheckMenuItem hideIntermediateItem = new CheckMenuItem("Hide Intermediate Proofsteps"); @@ -819,8 +1352,40 @@ private ContextMenu createContextMenu() { setHideInteractiveGoals(hideInteractiveItem.isSelected()); refresh(); }); + // C20/C21 (P3a): the linearized mode and the OSS expansion toggles (Swing + // ProofTreeSettingsMenuFactory creates both as checkboxes of the "Settings" gear menu) + CheckMenuItem linearizeItem = new CheckMenuItem("Linearize Proof Tree"); + linearizeItem.setSelected(linearizedMode); + linearizeItem.setOnAction(e -> { + boolean isChange = linearizedMode != linearizeItem.isSelected(); + linearizedMode = linearizeItem.isSelected(); + if (isChange) { + refresh(); + } + }); + CheckMenuItem expandOssItem = new CheckMenuItem("Expand One Step Simplifications nodes"); + expandOssItem.setSelected(expandOSSNodes); + expandOssItem.setOnAction(e -> expandOSSNodes = expandOssItem.isSelected()); + // C22 (P3a): whole-tree actions (Swing ProofTreeSettingsMenuFactory createExpandAll / + // createCollapseAll, operating on the whole tree instead of one subtree) + MenuItem expandAllItem = actionItem("Expand All", IconFactoryF.Key.PLUS, + this::expandEntireTree); + MenuItem collapseAllItem = actionItem("Collapse All", IconFactoryF.Key.MINUS, + this::collapseEntireTree); + // C23 (P3a): the Swing popup's prover-control actions (ProofTreePopupFactory + // RunStrategyOnNode / Prune / Notes) + MenuItem applyStrategyItem = actionItem("Apply Strategy", IconFactoryF.Key.START, + this::runStrategyOnNode); + MenuItem pruneItem = actionItem("Prune Proof", IconFactoryF.Key.PRUNE, + this::prunePopupNode); + MenuItem notesItem = actionItem("Edit Notes...", null, this::editNotes); + MenuItem subtreeStatsItem = actionItem("Show Subtree Statistics", + IconFactoryF.Key.STATISTICS, this::showSubtreeStatistics); ContextMenu menu = new ContextMenu(hideIntermediateItem, onlyInteractiveItem, - new SeparatorMenuItem(), hideClosedItem, hideInteractiveItem, new SeparatorMenuItem(), + new SeparatorMenuItem(), hideClosedItem, hideInteractiveItem, linearizeItem, + expandOssItem, new SeparatorMenuItem(), expandAllItem, collapseAllItem, + new SeparatorMenuItem(), applyStrategyItem, pruneItem, notesItem, + new SeparatorMenuItem(), actionItem("Expand All Below", IconFactoryF.Key.PLUS, () -> expandAllBelow(popupBranchItem)), actionItem("Expand Goals Only Below", IconFactoryF.Key.EXPAND_GOALS, @@ -833,13 +1398,23 @@ private ContextMenu createContextMenu() { actionItem("Next Sibling", IconFactoryF.Key.NEXT, () -> gotoSibling(1)), new SeparatorMenuItem(), actionItem("Set All Goals Below to Interactive", null, () -> setGoalsBelow(false)), - actionItem("Set All Goals Below to Automatic", null, () -> setGoalsBelow(true))); + actionItem("Set All Goals Below to Automatic", null, () -> setGoalsBelow(true)), + new SeparatorMenuItem(), subtreeStatsItem); menu.setOnShowing(e -> { // pick up changes made elsewhere (e.g. by the classic UI sharing the settings) hideIntermediateItem.setSelected(hideIntermediateSteps()); onlyInteractiveItem.setSelected(hideAutomodeSteps()); hideClosedItem.setSelected(hideClosedSubtrees()); hideInteractiveItem.setSelected(hideInteractiveGoals()); + linearizeItem.setSelected(linearizedMode); + expandOssItem.setSelected(expandOSSNodes); + // C23: enablement mirrors the Swing popup — Apply Strategy needs a proof, Prune only + // makes sense on an inner node whose subtree still has something to prune, Notes and + // the statistics need a proof + applyStrategyItem.setDisable(proof == null); + notesItem.setDisable(proof == null); + subtreeStatsItem.setDisable(proof == null); + pruneItem.setDisable(!isPrunable(popupNode)); }); return menu; } @@ -858,6 +1433,36 @@ private static MenuItem actionItem(String label, IconFactoryF.Key icon, Runnable // Popup expand/collapse and sibling actions (Swing ProofTreePopupFactory) // ----------------------------------------------------------------------- + /** + * C22 (P3a): expands the whole tree (Swing {@code ProofTreeSettingsMenuFactory} + * {@code CreateExpandAll} / {@code ProofTreeExpansionState.expandAll}): everything except + * the one-step-simplification nodes unless {@link #expandOSSNodes} is set — their protocol + * rows stay collapsed like in Swing's default ({@code ProofTreePopupFactory.ossPathFilter}). + */ + void expandEntireTree() { + TreeItem root = tree.getRoot(); + if (root == null) { + return; + } + root.setExpanded(true); + expandRec(root); + } + + /** + * C22 (P3a): collapses the whole tree below the root (Swing + * {@code ProofTreeSettingsMenuFactory.createCollapseAll}; the root row is re-expanded). + */ + void collapseEntireTree() { + TreeItem root = tree.getRoot(); + if (root == null) { + return; + } + for (TreeItem child : new ArrayList<>(root.getChildren())) { + collapseRec(child); + } + root.setExpanded(true); + } + /** Expands every branch below the given item (Swing Expand All Below). */ private void expandAllBelow(TreeItem item) { if (item == null) { @@ -866,7 +1471,22 @@ private void expandAllBelow(TreeItem item) { expandRec(item); } - private static void expandRec(TreeItem item) { + /** + * Recursively expands the item and its children; an OSS node row (a proof node whose applied + * rule is the one-step simplifier) and its protocol rows are only expanded when + * {@link #expandOSSNodes} is set (Swing {@code ProofTreeExpansionState.expandAll} with the + * OSS path filter {@code ProofTreePopupFactory.ossPathFilter}). + */ + private void expandRec(TreeItem item) { + if (item.getValue() != null && item.getValue().isOssChild()) { + return; + } + if (item.getValue() != null && !item.getValue().isBranch() + && item.getValue().node() != null + && item.getValue().node().getAppliedRuleApp() instanceof OneStepSimplifierRuleApp + && !expandOSSNodes) { + return; // OSS protocol rows stay collapsed unless "Expand OSS nodes" is set + } item.setExpanded(true); for (TreeItem child : new ArrayList<>(item.getChildren())) { expandRec(child); @@ -1006,6 +1626,240 @@ private void setGoalsBelow(boolean enable) { refresh(); } + // ----------------------------------------------------------------------- + // P3a (C23): popup prover-control actions (Swing ProofTreePopupFactory) + // ----------------------------------------------------------------------- + + /** + * Gives the tree the mediator + proof control of the loaded environment (Swing + * {@code ProofTreeView} extracts both from the mediator): the Apply Strategy and Prune + * popup actions and the auto-mode partial updates ({@link #autoModeStarted} + * /{@link #autoModeStopped}) need them. + */ + public void setActionContext(KeYMediatorF mediator, ProofControl proofControl) { + Objects.requireNonNull(mediator); + this.mediator = mediator; + if (this.proofControl != null) { + this.proofControl.removeAutoModeListener(this); + } + this.proofControl = proofControl; + if (proofControl != null) { + proofControl.addAutoModeListener(this); + } + } + + /** + * C23: runs the automatic strategy on the subtree of the node the popup was invoked on + * (Swing {@code RunStrategyOnNodeUserAction}): all enabled goals below the node, or the node + * itself when it is an open goal. + */ + private void runStrategyOnNode() { + if (proof == null || proof.isDisposed() || popupNode == null || mediator == null + || proofControl == null) { + return; + } + Goal invokedGoal = proof.getOpenGoal(popupNode); + ImmutableList goals = invokedGoal != null + ? ImmutableList.of(invokedGoal) + : proof.getSubtreeEnabledGoals(popupNode); + mediator.startAutoMode(goals); + } + + /** + * C23: prunes the proof below the node the popup was invoked on (Swing {@code Prune} calls + * {@code KeYMediator.setBack(context.invokedNode)}). + */ + private void prunePopupNode() { + if (mediator == null || popupNode == null) { + return; + } + mediator.pruneNode(popupNode); + } + + /** + * C23: whether the Swing {@code Prune} popup item is enabled for the popup node + * (ProofTreePopupFactory.java:377-398): pruning is disabled for goals and for closed + * subtrees when the command line flag {@code --no-pruning-closed} is set, enabled when the + * subtree still contains prunable goals or the node is a cached cutting point. + */ + private boolean isPrunable(Node node) { + if (node == null || node.proof() == null + || node.proof().isOpenGoal(node) || node.proof().isClosedGoal(node)) { + return false; + } + if (node.proof().getSubtreeGoals(node).size() > 0 + || (!GeneralSettings.noPruningClosed + && node.proof().getClosedSubtreeGoals(node).size() > 0) + || node.lookup(ClosedBy.class) != null) { + return true; + } + return false; + } + + /** + * C23: opens the proof node notes editor (Swing {@code Notes} popup item): the text is + * stored on the node via {@code NodeInfo.setNotes} ({@code null} clears it). + */ + private void editNotes() { + if (proof == null || proof.isDisposed() || popupNode == null) { + return; + } + String original = popupNode.getNodeInfo().getNotes(); + ProofTreeNotesDialogF dialog = new ProofTreeNotesDialogF(original, popupNode); + dialog.showNonBlocking(); + } + + /** + * C23: opens the subtree statistics report for the node the popup was invoked on (Swing + * {@code ShowProofStatistics}, simplified — the Swing HTML-styled report window with + * CSV export is ported as a plain-text report; the export follow-up is A4 (P3c)). + */ + private void showSubtreeStatistics() { + if (proof == null || proof.isDisposed() || popupNode == null) { + return; + } + SubtreeStatisticsDialogF dialog = new SubtreeStatisticsDialogF(popupNode); + dialog.showNonBlocking(); + } + + // ----------------------------------------------------------------------- + // P3a (C27): auto-mode partial subtree updates (Swing ProofTreeView + // autoModeStarted / autoModeStopped, ProofTreeView.java:1072-1137) + // ----------------------------------------------------------------------- + + @Override + public void autoModeStarted(ProofEvent e) { + if (e.getSource() != proof) { + // Auto mode on a proof this view does not display (e.g. an auxiliary side proof of + // the information-flow macros, see KeY issue #3713): ignore. + return; + } + // save the goals on which the prover may work + modifiedSubtrees = e.getSource().openGoals().map(Goal::node); + } + + @Override + public void autoModeStopped(ProofEvent e) { + if (proof == null || proof.isDisposed()) { + modifiedSubtrees = null; + return; + } + final List changed; + if (modifiedSubtrees != null) { + changed = new ArrayList<>(); + for (Node n : modifiedSubtrees) { + // skip nodes of other proofs; changed = no longer an open goal + if (n.proof() == proof && proof.openGoals().filter(g -> g.node() == n).isEmpty()) { + changed.add(n); + } + } + } else { + changed = List.of(); + } + // all tree mutation must happen on the FX thread (Swing marshals via the EDT) + FxUtil.runLater(() -> { + modifiedSubtrees = null; + if (proof == null || proof.isDisposed() || tree.getRoot() == null) { + return; + } + if (changed.size() > MAX_PARTIAL_TREE_UPDATES) { + // update the whole tree + fullUpdateAfterAutoCount.incrementAndGet(); + refresh(); + } else if (!changed.isEmpty()) { + // update only the affected subtrees (Swing delegateModel.updateTree(n)); + // counted at the stop decision so the post-run verification reports it even if + // the FX work is still queued + partialUpdateCount.incrementAndGet(); + for (Node n : changed) { + partialUpdateSubtree(n); + } + } + revealSelectedNode(); + }); + } + + /** + * C27: rebuilds the subtree below the nearest branch item of {@code node} in place, keeping + * the expansion state of the surrounding branches (Swing {@code GUIProofTreeModel.updateTree + * (Node)}). + */ + private void partialUpdateSubtree(Node subtreeRoot) { + FxUtil.runLater(() -> { + if (proof == null || proof.isDisposed() || tree.getRoot() == null) { + return; + } + TreeItem target = nearestBranchItem(tree.getRoot(), subtreeRoot); + if (target == null) { + refresh(); + return; + } + Set expanded = Collections.newSetFromMap(new IdentityHashMap<>()); + collectExpandedBranches(target, expanded); + TreeItem rebuilt = buildBranch(target.getValue().node(), + target.getValue().branchLabel); + target.getChildren().setAll(new ArrayList<>(rebuilt.getChildren())); + applyExpandedBranches(target, expanded); + containsMatchCache.clear(); + updateMatches(); + if (selectionModel != null && selectionModel.getSelectedNode() != null) { + TreeItem sel = findItem(tree.getRoot(), selectionModel.getSelectedNode()); + if (sel != null) { + tree.getSelectionModel().select(sel); + } + } + }); + } + + /** + * @param from the tree item to search (itself a branch) + * @param node a proof node below the branch + * @return the deepest branch item whose displayed subtree contains {@code node}, or + * {@code null} when the node is not displayed + */ + private static TreeItem nearestBranchItem(TreeItem from, Node node) { + if (from == null || from.getValue() == null || from.getValue().node() == null) { + return null; + } + TreeItem result = null; + for (TreeItem child : from.getChildren()) { + Entry entry = child.getValue(); + if (entry == null || entry.node() == null) { + continue; + } + boolean contains = entry.node() == node || isInSubtree(entry.node(), node); + if (!contains) { + continue; + } + if (entry.isBranch()) { + TreeItem deeper = nearestBranchItem(child, node); + result = deeper != null ? deeper : child; + } else if (result == null) { + result = from; // the node is on this branch's chain + } + } + return result; + } + + /** @return whether {@code maybeDescendant} is {@code ancestor} itself or below it */ + private static boolean isInSubtree(Node ancestor, Node maybeDescendant) { + for (Node n = maybeDescendant; n != null; n = n.parent()) { + if (n == ancestor) { + return true; + } + } + return false; + } + + /** + * P3a (C27): a one-line report of the auto-mode partial update counters for the + * verification hook. + */ + public String getAutoModeReport() { + return "autoModePartialUpdates=" + partialUpdateCount.get() + + " autoModeFullUpdates=" + fullUpdateAfterAutoCount.get(); + } + /** * Returns the branch label of the given node, falling back to {@code Case } and storing it * on the node like {@code GUIAbstractTreeNode.ensureBranchLabelIsSet}. @@ -1031,7 +1885,23 @@ private void handleTreeSelection(TreeItem item) { if (updatingSelection || item == null || item.getValue() == null) { return; } - Node node = item.getValue().node(); + Entry entry = item.getValue(); + if (entry.isOssChild()) { + // C21 (P3a): selecting an OSS protocol row shows a sequent modified to include the + // transformed formula and marks the single rewriting step as the selected rule app + // (Swing ProofTreeView GUITreeSelectionListener, ProofTreeView.java:1174-1190) + Node ossParent = entry.node(); + if (selectionModel != null && ossParent != null && ossParent.sequent() != null) { + var pio = entry.ossRuleApp().posInOccurrence(); + Sequent modified = + ossParent.sequent().replaceFormula(entry.ossFormulaNr(), + pio.sequentFormula()).sequent(); + selectionModel.setSelectedSequentAndRuleApp(ossParent, modified, + entry.ossRuleApp()); + } + return; + } + Node node = entry.node(); if (node != null && selectionModel != null && node != selectionModel.getSelectedNode()) { selectionModel.setSelectedNode(node); } @@ -1352,7 +2222,7 @@ private boolean matches(Entry entry) { * the query (case-insensitive) */ private boolean matches(Node node) { - return matches(new Entry(node, null)); + return matches(Entry.node(node)); } /** @@ -1365,7 +2235,7 @@ private boolean containsMatch(Node node) { if (cached != null) { return cached; } - boolean result = matches(new Entry(node, null)); + boolean result = matches(Entry.node(node)); for (int i = 0; !result && i < node.childrenCount(); i++) { result = containsMatch(node.child(i)); } @@ -1436,6 +2306,10 @@ protected void updateItem(Entry item, boolean empty) { } private String styleClassOf(Entry item) { + if (item.isOssChild()) { + // C21 (P3a): OSS protocol rows render styled like a branch decoration + return "proof-tree-oss-child"; + } Node node = item.node; if (item.isBranch()) { return node.isClosed() ? "proof-tree-branch-closed" : "proof-tree-branch"; @@ -1450,6 +2324,11 @@ private String styleClassOf(Entry item) { } return goal.isAutomatic() ? "proof-tree-open" : "proof-tree-interactive"; } + if (node.getAppliedRuleApp() instanceof OneStepSimplifierRuleApp) { + // C21: the One Step Simplifier node itself (Swing renders it like a rule + // application node; the protocol rows are its children) + return "proof-tree-oss"; + } return "proof-tree-inner"; } @@ -1457,6 +2336,11 @@ private String tooltipText(Entry item) { if (item.isBranch()) { return "Branch: " + item.branchLabel; } + if (item.isOssChild()) { + // C21: Swing GUIOneStepChildTreeNode.getSearchString shows the applied rule and + // the pretty-printed sub-term; the tooltip mirrors it without the term printer + return "One Step Simplification step: " + item.displayText(); + } // menu: MP3a — the node's applied rule name (Swing rule-application nodes show the // applied rule, ProofTreeView.java:1389), falling back to the plain node serial. Node node = item.node; diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/prooftree/SubtreeStatisticsDialogF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/prooftree/SubtreeStatisticsDialogF.java new file mode 100644 index 00000000000..55061efb78e --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/prooftree/SubtreeStatisticsDialogF.java @@ -0,0 +1,148 @@ +/* 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.prooftree; + +import java.util.Iterator; +import java.util.Map.Entry; +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.TextArea; +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.proof.Node; +import de.uka.ilkd.key.proof.Statistics; +import de.uka.ilkd.key.proof.reference.ClosedBy; + +import org.key_project.util.collection.Pair; +import org.key_project.util.javafx.FxUtil; + +/** + * prooftree (P3a, C23): JavaFX port of the Swing "Show Subtree Statistics" popup of the proof + * tree ({@code ProofTreePopupFactory.SubtreeStatistics} → {@code ShowProofStatistics}): + * displays the open/cached goals of the invoked node's subtree and the rule-application + * statistics of that subtree. + *

        + * KNOWN-SIMPLIFIED: the Swing report is an HTML-styled window with Save buttons + * (HTML/CSV export); the FX port shows the same numbers as plain text. The export is tracked as + * audit item A4 (P3c) — see PARITY-SIGNOFF.md. + */ +public final class SubtreeStatisticsDialogF { + + private final Stage stage = new Stage(); + + /** + * @param node the proof node whose subtree statistics are shown (the popup's invoked node) + */ + public SubtreeStatisticsDialogF(Node node) { + TextArea report = new TextArea(renderReport(node)); + report.setEditable(false); + report.setWrapText(false); + report.getStyleClass().add("subtree-stats-area"); + + Label title = new Label("Subtree statistics for node " + node.serialNr() + ":" + + node.name()); + title.getStyleClass().add("subtree-stats-title"); + + Button closeButton = new Button("Close"); + closeButton.setDefaultButton(true); + closeButton.setOnAction(e -> requestOk()); + HBox buttons = new HBox(8, closeButton); + buttons.setAlignment(Pos.CENTER_RIGHT); + buttons.setPadding(new Insets(6)); + + BorderPane root = new BorderPane(); + root.setTop(title); + root.setCenter(report); + root.setBottom(buttons); + BorderPane.setMargin(title, new Insets(6, 6, 0, 6)); + BorderPane.setMargin(report, new Insets(6)); + + Scene scene = new Scene(root, 520, 420); + ThemeManager.getInstance().style(scene); + stage.setTitle("Subtree Statistics"); + stage.setScene(scene); + stage.setMinWidth(320); + stage.setMinHeight(240); + } + + /** + * Renders the statistics text, mirroring Swing {@code ShowProofStatistics}: + * {@code getHTMLStatisticsMessage(node)} counts the open and cached goals of the subtree, + * {@code Statistics.getSummary()} lists the rule-application counters. Package-visible for + * the {@code key.fx.verify.prooftree} harness. + */ + static String renderReport(Node node) { + int openGoals = 0; + int cachedGoals = 0; + // Swing quirk preserved: the ClosedBy lookup is tested on the invoked node (not the + // leaf), so all open leaves count as cached when the node itself is a cached cutting + // point (ShowProofStatistics.getHTMLStatisticsMessage, actions/ShowProofStatistics.java) + Iterator leaves = node.leavesIterator(); + while (leaves.hasNext()) { + Node leaf = leaves.next(); + if (node.proof().getOpenGoal(leaf) != null) { + if (node.lookup(ClosedBy.class) != null) { + cachedGoals++; + } else { + openGoals++; + } + } + } + StringBuilder sb = new StringBuilder(); + String line = System.lineSeparator(); + sb.append("Subtree of node ").append(node.serialNr()).append(':') + .append(node.name()).append(line); + sb.append(line).append("Open goals: ").append(openGoals).append(line); + sb.append("Cached goals: ").append(cachedGoals).append(line); + Statistics statistics = node.statistics(); + StringBuilder details = new StringBuilder(); + for (Pair summary : statistics.getSummary()) { + if (summary.second == null || summary.second.isEmpty()) { + details.append(summary.first).append(line); + } else { + details.append(summary.first).append(": ").append(summary.second).append(line); + } + } + if (!statistics.getInteractiveAppsDetails().isEmpty()) { + details.append(line).append("Interactive rule application details:").append(line); + for (Entry entry : statistics.getInteractiveAppsDetails().entrySet()) { + details.append(" ").append(entry.getKey()).append(": ").append(entry.getValue()) + .append(line); + } + } + if (details.isEmpty()) { + details.append("(no rule application statistics for this node)").append(line); + } + sb.append(details); + return sb.toString(); + } + + /** Shows the dialog non-blocking (P3a harness seam). */ + public void showNonBlocking() { + if (!FxUtil.isFxThread()) { + FxUtil.runLater(this::showNonBlocking); + return; + } + stage.initModality(Modality.NONE); + stage.show(); + } + + /** Closes the dialog as if the Close button had been pressed (verification harness). */ + public void requestOk() { + stage.close(); + } + + /** @return the stage (verification harness) */ + public Stage getStage() { + return stage; + } +} diff --git a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css index 981924e3eca..0b55f6c40e8 100644 --- a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css +++ b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-dark.css @@ -223,6 +223,38 @@ -fx-text-fill: -key-interactive; } +/* prooftree (P3a, C21): the One Step Simplifier node and its protocol rows */ +.proof-tree-oss { + -fx-text-fill: -key-text; + -fx-font-weight: bold; +} + +.proof-tree-oss-child { + -fx-text-fill: -key-text-secondary; + -fx-font-style: italic; + -fx-padding: 1 4 1 16; +} + +/* prooftree (P3a, C23): the Edit Notes popup (ProofTreeNotesDialogF) */ +.proof-tree-notes-title { + -fx-text-fill: -key-text; + -fx-font-weight: bold; +} + +.proof-tree-notes-area { + -fx-font-family: monospace; +} + +/* prooftree (P3a, C23): the Show Subtree Statistics popup (SubtreeStatisticsDialogF) */ +.subtree-stats-title { + -fx-text-fill: -key-text; + -fx-font-weight: bold; +} + +.subtree-stats-area { + -fx-font-family: monospace; +} + /* proof tree search bar (M2, Swing ProofTreeSearchBar) */ .proof-tree-search-bar { -fx-background-color: -key-surface; diff --git a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css index c34c7932aee..1f70c5b088d 100644 --- a/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css +++ b/key.ui.fx/src/main/resources/de/uka/ilkd/key/gui/fx/theme/key-light.css @@ -219,6 +219,38 @@ -fx-text-fill: -key-interactive; } +/* prooftree (P3a, C21): the One Step Simplifier node and its protocol rows */ +.proof-tree-oss { + -fx-text-fill: -key-text; + -fx-font-weight: bold; +} + +.proof-tree-oss-child { + -fx-text-fill: -key-text-secondary; + -fx-font-style: italic; + -fx-padding: 1 4 1 16; +} + +/* prooftree (P3a, C23): the Edit Notes popup (ProofTreeNotesDialogF) */ +.proof-tree-notes-title { + -fx-text-fill: -key-text; + -fx-font-weight: bold; +} + +.proof-tree-notes-area { + -fx-font-family: monospace; +} + +/* prooftree (P3a, C23): the Show Subtree Statistics popup (SubtreeStatisticsDialogF) */ +.subtree-stats-title { + -fx-text-fill: -key-text; + -fx-font-weight: bold; +} + +.subtree-stats-area { + -fx-font-family: monospace; +} + /* proof tree search bar (M2, Swing ProofTreeSearchBar) */ .proof-tree-search-bar { -fx-background-color: -key-surface; From 508b1135e465d946e4a7e29600fca36d3a8ea135 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Sat, 10 Oct 2026 16:37:28 +0200 Subject: [PATCH 140/281] prooftree (P3a): key.fx.verify.prooftree hook + MainWindowF wiring ProofTreeVerifyF runs the C19-C22/C25 semantics self test on the FX thread, drives the Edit Notes and Show Subtree Statistics dialogs through their non-blocking/OK seams and reports the C27 partial-update counters. The hook dispatches after the demo load (skipped when the live auto-prover is running, which would race the structural events) and again after a live auto-mode stop. setActionContext binds the tree's popup actions and auto-mode listener to the loaded environment's mediator + proof control. --- .../de/uka/ilkd/key/gui/fx/MainWindowF.java | 26 +++ .../gui/fx/prooftree/ProofTreeVerifyF.java | 182 ++++++++++++++++++ 2 files changed, 208 insertions(+) create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/prooftree/ProofTreeVerifyF.java 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 index 86a3d94aba8..5f7300f3020 100644 --- 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 @@ -125,6 +125,7 @@ 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.ProofTreeVerifyF; 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; @@ -885,6 +886,11 @@ protected KeYEnvironment call() throws Exception { // 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()); env.getProofControl().addAutoModeListener(autoModeUiListener); // notification: register the notification framework's auto-mode tracker on the // proof control (Swing parity: the NotificationManager constructor registers its @@ -977,6 +983,18 @@ protected KeYEnvironment call() throws Exception { 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 = ProofTreeVerifyF.run(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 @@ -1934,6 +1952,14 @@ public void autoModeStopped(ProofEvent e) { .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 = ProofTreeVerifyF.run(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); diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/prooftree/ProofTreeVerifyF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/prooftree/ProofTreeVerifyF.java new file mode 100644 index 00000000000..4143b610885 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/prooftree/ProofTreeVerifyF.java @@ -0,0 +1,182 @@ +/* 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.prooftree; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import javafx.application.Platform; +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.proof.Node; +import de.uka.ilkd.key.proof.Proof; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * prooftree (P3a): self-test harness for the {@code key.fx.verify.prooftree} hook. Exercises the + * C19-C23/C25/C27 parity items: + *

          + *
        • the tree semantics self test ({@link ProofTreeViewF#verifyProoftreeSemantics}): the + * per-proof view-state cache (C19), the linearized mode (C20), the OSS protocol rows and the + * "expand OSS nodes" toggle (C21), the whole-tree expand/collapse (C22) and the node-filter + * counting rule (C25);
        • + *
        • the two P3a popup dialogs (C23): the proof node notes editor stores its input via the OK + * seam, the subtree statistics dialog opens and closes;
        • + *
        • a report of the auto-mode partial-update counters (C27).
        • + *
        + */ +public final class ProofTreeVerifyF { + + private static final Logger LOGGER = LoggerFactory.getLogger(ProofTreeVerifyF.class); + + private ProofTreeVerifyF() { + } + + /** + * Runs the self test. Must be called on the FX thread (from the {@code key.fx.verify.prooftree} + * dispatch in the main window); the dialog waits run on a worker thread like the P2b harness. + * + * @param owner the owner window (unused; kept for API symmetry with the other harnesses) + * @param proof the loaded demo proof, may be {@code null} + * @param view the proof tree view under test + * @return a short status message; the full report arrives via the status line and a + * notification + */ + public static String run(Window owner, Proof proof, ProofTreeViewF view) { + if (proof == null) { + LOGGER.warn("Proof tree verification: no proof loaded — SKIP"); + return "SKIP (no proof)"; + } + Thread worker = new Thread(() -> { + String report = runAll(proof, view); + LOGGER.info("Proof tree verification: {}", report); + Platform.runLater(() -> NotificationManagerF.getInstance() + .notify("Proof tree verification: " + report, + report.startsWith("PASS") ? Kind.INFO : Kind.ERROR)); + }, "fx-verify-prooftree"); + worker.setDaemon(true); + worker.start(); + return "running (see log)"; + } + + private static String runAll(Proof proof, ProofTreeViewF view) { + StringBuilder problems = new StringBuilder(); + StringBuilder notes = new StringBuilder(); + + // C27: snapshot the auto-mode counters *before* the semantics self test — the C19 + // view-state section switches proofs via setProof, which resets the live counters + String autoModeReport = view.getAutoModeReport(); + + // C19-C22/C25: the tree semantics self test (touches the TreeView — FX thread only) + CountDownLatch treeLatch = new CountDownLatch(1); + runOnFx(() -> { + try { + notes.append(view.verifyProoftreeSemantics()); + } finally { + treeLatch.countDown(); + } + }); + awaitLatch(treeLatch, "prooftree semantics self test", problems); + + // C23: the notes editor stores the input on the node via the OK seam (Swing + // ProofTreePopupFactory.Notes), empty input clears the note + Node target = proof.root(); + ProofTreeNotesDialogF[] dialogRef = new ProofTreeNotesDialogF[1]; + runOnFx(() -> dialogRef[0] = new ProofTreeNotesDialogF(null, target)); + ProofTreeNotesDialogF notesDialog = dialogRef[0]; + runOnFx(notesDialog::showNonBlocking); + waitUntil(() -> notesDialog.getStage().isShowing(), "notes dialog shows", problems); + String noteBefore = target.getNodeInfo().getNotes(); + if (noteBefore != null && !noteBefore.isEmpty()) { + problems.append("notes dialog: pre-existing note not expected; "); + } + runOnFx(() -> { + notesDialog.textArea.setText("P3a verify note"); + notesDialog.requestOk(); + }); + waitUntil(() -> !notesDialog.getStage().isShowing(), "notes dialog closes on ok", problems); + if (!"P3a verify note".equals(target.getNodeInfo().getNotes())) { + problems.append("notes dialog: stored note mismatch; "); + } else { + notes.append(" notes=PASS"); + } + // clear the test note again so the demo proof stays clean + runOnFx(() -> target.getNodeInfo().setNotes(null)); + + // C23: the subtree statistics dialog opens and closes; the report contains the goal and + // rule-application counters (ShowProofStatistics port) + SubtreeStatisticsDialogF[] statsRef = new SubtreeStatisticsDialogF[1]; + runOnFx(() -> statsRef[0] = new SubtreeStatisticsDialogF(proof.root())); + SubtreeStatisticsDialogF statsDialog = statsRef[0]; + runOnFx(statsDialog::showNonBlocking); + waitUntil(() -> statsDialog.getStage().isShowing(), "subtree statistics dialog shows", + problems); + String statsText = SubtreeStatisticsDialogF.renderReport(proof.root()); + if (!statsText.contains("Open goals")) { + problems.append("subtree statistics: report missing goal counters; "); + } + runOnFx(statsDialog::requestOk); + waitUntil(() -> !statsDialog.getStage().isShowing(), + "subtree statistics dialog closes", problems); + + notes.append(' ').append(autoModeReport); + + boolean pass = problems.length() == 0 && notes.toString().contains("PASS"); + return (pass ? "PASS" : "FAIL") + ": " + notes + " " + + (problems.length() > 0 ? "problems=[" + problems + "]" : "problems=[]"); + } + + /** Polls a condition with a timeout (the FX runLater queue keeps the modal loop alive). */ + private static boolean waitUntil(java.util.function.BooleanSupplier condition, String what, + StringBuilder problems) { + long deadline = System.currentTimeMillis() + 10_000; + while (System.currentTimeMillis() < deadline && !condition.getAsBoolean()) { + try { + Thread.sleep(25); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + problems.append("interrupted waiting for ").append(what).append("; "); + return false; + } + } + if (!condition.getAsBoolean()) { + problems.append("timed out waiting for ").append(what).append("; "); + return false; + } + return true; + } + + /** Runs {@code runnable} on the FX thread and blocks until it completed. */ + private static void runOnFx(Runnable runnable) { + if (Platform.isFxApplicationThread()) { + runnable.run(); + return; + } + CountDownLatch latch = new CountDownLatch(1); + Platform.runLater(() -> { + try { + runnable.run(); + } finally { + latch.countDown(); + } + }); + awaitLatch(latch, "fx task", new StringBuilder()); + } + + /** Blocks until the latch is released or the timeout expires. */ + private static void awaitLatch(CountDownLatch latch, String what, StringBuilder problems) { + try { + if (!latch.await(10, TimeUnit.SECONDS)) { + problems.append("timed out waiting for ").append(what).append("; "); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + problems.append("interrupted waiting for ").append(what).append("; "); + } + } +} From 599c9d5f86320d7a793ccb4a27e995c1a55f745b Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Sat, 10 Oct 2026 16:38:00 +0200 Subject: [PATCH 141/281] prooftree (P3a): whitelist key.fx.verify.prooftree, document the batch README table row for the hook (incl. the autoprove combinations) and the PARITY-SIGNOFF audit update: C19-C23/C25/C27 RESOLVED (P3a), C24 stays OPEN for P4. Adds the flag to the run-whitelist in build.gradle. --- key.ui.fx/PARITY-SIGNOFF.md | 31 ++++++++++++++++++++++++------- key.ui.fx/README.md | 1 + key.ui.fx/build.gradle | 6 +++++- 3 files changed, 30 insertions(+), 8 deletions(-) diff --git a/key.ui.fx/PARITY-SIGNOFF.md b/key.ui.fx/PARITY-SIGNOFF.md index 6e465d72988..38df921d75d 100644 --- a/key.ui.fx/PARITY-SIGNOFF.md +++ b/key.ui.fx/PARITY-SIGNOFF.md @@ -156,15 +156,15 @@ differences found by a second Swing↔JavaFX sweep, with stable IDs used by the | ID | Finding (Swing reference) | Status | |----|---------------------------|--------| -| C19 | Per-proof view state cache (expansion/selection per proof) | OPEN (P3a) | -| C20 | Linearized (flat) proof view mode | OPEN (P3a) | -| C21 | OSS child nodes in the tree | OPEN (P3a) | -| C22 | Whole-tree actions (expand all/collapse all/...) | OPEN (P3a) | -| C23 | Popup items incl. SubtreeStatistics | OPEN (P3a) | +| C19 | Per-proof view state cache (expansion/selection per proof) | RESOLVED (P3a) | +| C20 | Linearized (flat) proof view mode | RESOLVED (P3a) | +| C21 | OSS child nodes in the tree | RESOLVED (P3a) | +| C22 | Whole-tree actions (expand all/collapse all/...) | RESOLVED (P3a) | +| C23 | Popup items incl. SubtreeStatistics | RESOLVED (P3a) | | C24 | `PROOF_TREE` extension seam (tree popup contributions) | OPEN (P4) | -| C25 | Node-filter counting rule | OPEN (P3a) | +| C25 | Node-filter counting rule | RESOLVED (P3a) | | C26 | Hidden-tab passivation (Swing docking detail) | OPEN (P4, likely WONT-REPLICATE) | -| C27 | Auto-mode partial subtree updates | OPEN (P3a) | +| C27 | Auto-mode partial subtree updates | RESOLVED (P3a) | ## D. Sequent / strategy / goal list / status bar @@ -200,3 +200,20 @@ spec-bearing method) is not headless-drivable without a JML-annotated example in `key.ui/examples`; it stays covered by the skeletons above plus the to-be-added example in a later batch. HTML-styled contract cells of the Swing configurator are plain text in the FX port (KNOWN-SIMPLIFIED, see the class javadocs). + +P3a (`weigl/ocfx-p3a-prooftree`, merged 2026-10-10) resolved C19/C20/C21/C22/C23/C25/C27 — +`ProofTreeViewF`: the per-proof view-state cache (WeakHashMap, saved and +restored in the proof switch, C19); the linearized mode with the "main"-tagged taclet splice +(C20); the OneStepSimplifier protocol child rows with an "expand OSS nodes" toggle that gates +the whole-tree expand (C21); the whole-tree expand-all/collapse-all actions (C22); the popup +additions Apply Strategy / Prune Proof / Edit Notes / Show Subtree Statistics (C23, Swing +`ProofTreePopupFactory`); the node-filter counting rule incl. the "inlined due to hidden +subtree" case (C25); and the auto-mode partial subtree updates (C27, `modifiedSubtrees`, +`MAX_PARTIAL_TREE_UPDATES=16`). `key.fx.verify.prooftree` PASS (semantics self test + notes +dialog + subtree statistics dialog) on Xvnc :99, plus a live-autoprove run +(`key.fx.verify.prooftree` + `key.fx.demo.autoprove.live`) that exercised the partial subtree +updates (`autoModePartialUpdates=1`) and a post-autoprove run (`+ key.fx.demo.autoprove`) +covering the OSS rows on a strategy-proved proof. The view filters and the tree controls live +in the tree context menu because the FX docking framework has no tab-title gear menu (documented +in `ProofTreeViewF`); the subtree statistics report is a plain-text window rather than the Swing +HTML one (export is A4/P3c). C24 (the `PROOF_TREE` extension seam) stays open for P4. diff --git a/key.ui.fx/README.md b/key.ui.fx/README.md index 99d204aa9a1..e38bf37995b 100644 --- a/key.ui.fx/README.md +++ b/key.ui.fx/README.md @@ -60,6 +60,7 @@ regression-tested on a virtual display. Demo proof paths default into | `key.fx.verify.termmenu` | headless sequent context-menu model (25 checks) | | `key.fx.verify.sequentmenu` | left-click sequent term menu + POPUP_DELAY guard, search prefill, shift+click focussed auto mode (P2a) | | `key.fx.verify.dialogs` | contract-completion registry + dialog skeletons (P2b): contract/auxiliary configurators, lemma selection dialog, item chooser | +| `key.fx.verify.prooftree` | proof tree (P3a): per-proof view-state cache, linearized mode, OSS protocol rows + expand toggle, whole-tree expand/collapse, node-filter counting rule, notes + subtree-statistics popup dialogs; combine with `key.fx.demo.autoprove` or `key.fx.demo.autoprove.live` for auto-mode coverage | | `key.fx.verify.tacletmatch=1\|hold` | interactive taclet application (`hold` keeps the dialog open) | | `key.fx.verify.uicontrol` | `WindowUserInterfaceControlF` seam: status line, IssueDialogF, LogViewF, AutoDismissDialogF | | `key.fx.verify.docking` | named layout slots, maximize/restore, close-while-maximized | diff --git a/key.ui.fx/build.gradle b/key.ui.fx/build.gradle index b7e5dc9464b..9f7b1c58ed5 100644 --- a/key.ui.fx/build.gradle +++ b/key.ui.fx/build.gradle @@ -136,6 +136,10 @@ run { // term menu + POPUP_DELAY guard, search prefill, shift+click focussed auto mode) // * key.fx.verify.dialogs = any (P2b: run the contract-completion registry + dialog // skeletons — contract/auxiliary configurators, lemma selection dialog, item chooser) + // * key.fx.verify.prooftree = any (P3a: run the proof tree C19-C23/C25/C27 self test — + // per-proof view states, linearized mode, OSS protocol rows, whole-tree expand/collapse, + // node-filter counting, notes + subtree statistics dialogs; combine with + // key.fx.demo.autoprove.live to exercise the auto-mode partial updates) ["key.fx.driver", "key.fx.theme", "key.fx.demo.sequent", "key.fx.verify.sequent", "key.fx.show", "key.fx.demo.autoprove", "key.fx.verify.tree", "key.fx.demo.autoprove.live", "key.fx.demo.maxsteps", "key.fx.verify.goallist", @@ -151,7 +155,7 @@ run { "key.fx.verify.profileloadingdialog", "key.fx.verify.javacsettings", "key.fx.verify.termmenu", "key.fx.verify.colors", "key.fx.verify.smt", "key.fx.verify.loadingexit", "key.fx.verify.shortcuts", "key.fx.verify.inputfreeze", - "key.fx.verify.sequentmenu", "key.fx.verify.dialogs"] + "key.fx.verify.sequentmenu", "key.fx.verify.dialogs", "key.fx.verify.prooftree"] .each { name -> def value = System.getProperty(name) if (value != null) { From 2faa52736490e8073df9338f552c8b4b48d1ee11 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Sat, 10 Oct 2026 17:15:11 +0200 Subject: [PATCH 142/281] =?UTF-8?q?P4:=20extension=20seams=20B13/D36=20?= =?UTF-8?q?=E2=80=94=20menu-path=20nesting=20+=20keyboard-shortcut=20SPI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - KeYGuiExtensionF.MainMenuF gains getMenuPath() (Swing KeyAction.PATH); facade installMenus() nests path-bearing providers into existing or new menu-bar menus, empty paths keep the FX top-level default (B13). - New KeYGuiExtensionF.KeyboardShortcutsF capability (view-scoped ShortcutF records, Swing KeyboardShortcuts constants); facade installKeyboardShortcuts() binds them as key-pressed filters on the six host views (D36). - ContextMenuF gains default getProofTreeContextItems() (C24, used by the next commit). - Port of the Swing TestExtension: TestExtensionF contributes a Test.Test.Test-nested menu, a PROOF_TREE popup item, an extension-toolbar button and a Ctrl+Shift+F12 sequent shortcut; registered in the FX service file (priority 100000, last). --- .../fx/extension/KeYGuiExtensionFacadeF.java | 131 ++++++++++++++++++ .../fx/extension/api/KeYGuiExtensionF.java | 103 ++++++++++++-- .../fx/extension/contrib/TestExtensionF.java | 116 ++++++++++++++++ ....key.gui.fx.extension.api.KeYGuiExtensionF | 1 + 4 files changed, 343 insertions(+), 8 deletions(-) create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/extension/contrib/TestExtensionF.java diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/extension/KeYGuiExtensionFacadeF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/extension/KeYGuiExtensionFacadeF.java index 5a7b2dedc9d..09a948ef648 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/extension/KeYGuiExtensionFacadeF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/extension/KeYGuiExtensionFacadeF.java @@ -10,10 +10,13 @@ import java.util.Objects; import java.util.ServiceLoader; import java.util.stream.Collectors; +import javafx.scene.Node; import javafx.scene.control.Control; import javafx.scene.control.Menu; +import javafx.scene.control.MenuBar; import javafx.scene.control.MenuItem; import javafx.scene.control.Tab; +import javafx.scene.input.KeyEvent; import de.uka.ilkd.key.core.fx.KeYMediatorF; import de.uka.ilkd.key.gui.fx.MainWindowF; @@ -107,6 +110,78 @@ public static List getMenus(MainWindowF window, KeYMediatorF mediator) { return menus; } + /** + * Installs the extension menus into the given menu bar, honouring the Swing + * {@code KeyAction.PATH} nesting (P4, B13). Providers with an empty + * {@link KeYGuiExtensionF.MainMenuF#getMenuPath()} keep the FX default: their menus are + * appended as separate top-level menus after the built-in menus (Swing + * {@code MainWindow.createMenuBar} :983 + {@code addExtensionsToMainMenu}, + * KeYGuiExtensionFacade.java:81-89). Providers with a non-empty path get their menus + * sorted into the menu bar: the first segment matches (or creates) a top-level menu by + * text — the five built-in menus included, so a path starting with {@code "Proof"} nests + * under the Proof menu — then each further segment matches (or creates) a sub menu of the + * same name, and finally the contributed menu items are spliced into the innermost menu + * (Swing {@code sortActionIntoMenu}, KeYGuiExtensionFacade.java:134-160). The five + * built-in menus' item sets stay untouched. + * + * @param window the main window + * @param menuBar the menu bar to install the extension menus into + * @param mediator the mediator of the window + */ + public static void installMenus(MainWindowF window, MenuBar menuBar, + KeYMediatorF mediator) { + for (KeYGuiExtensionF extension : getExtensions()) { + if (!(extension instanceof KeYGuiExtensionF.MainMenuF mainMenu)) { + continue; + } + List menus = mainMenu.getMenus(window, mediator); + if (menus.isEmpty()) { + continue; + } + String path = mainMenu.getMenuPath(); + if (path == null || path.isBlank()) { + menuBar.getMenus().addAll(menus); + continue; + } + // B13: splash the menu path (Swing KeyAction.PATH dot-separated segments) + Menu current = null; + for (String segment : path.split("\\.")) { + if (segment.isBlank()) { + continue; + } + if (current == null) { + Menu top = menuBar.getMenus().stream() + .filter(m -> segment.equals(m.getText())).findFirst().orElse(null); + if (top == null) { + top = new Menu(segment); + menuBar.getMenus().add(top); + } + current = top; + } else { + Menu child = null; + for (MenuItem item : current.getItems()) { + if (item instanceof Menu m && segment.equals(m.getText())) { + child = m; + break; + } + } + if (child == null) { + child = new Menu(segment); + current.getItems().add(child); + } + current = child; + } + } + // splice the provider's items into the innermost menu of the path (Swing inserts + // each action at the path; the provider's own Menu wrapper is not reused) + if (current != null) { + for (Menu menu : menus) { + current.getItems().addAll(menu.getItems()); + } + } + } + } + /** * The toolbar controls contributed by every {@link KeYGuiExtensionF.ToolbarF} provider * (Swing {@code KeYGuiExtensionFacade.createToolbars}, KeYGuiExtensionFacade.java:217-221). @@ -182,6 +257,29 @@ public static List getSequentContextItems(KeYMediatorF mediator, Goal return items; } + /** + * The proof-tree popup items contributed by every {@link KeYGuiExtensionF.ContextMenuF} + * provider for the given node (P4, C24; Swing {@code + * KeYGuiExtensionFacade.addContextMenuItems} with {@code ContextMenuKind.PROOF_TREE}, + * KeYGuiExtensionFacade.java:258-262, consumed by ProofTreePopupFactory.java:152-154). The + * host appends them after a separator at the end of the proof-tree context menu and drops + * the separator when nothing is contributed. + * + * @param mediator the mediator of the window + * @param node the clicked proof-tree node + * @return non-null, emptiable list of menu items + */ + public static List getProofTreeContextItems(KeYMediatorF mediator, + de.uka.ilkd.key.proof.Node node) { + List items = new ArrayList<>(); + for (KeYGuiExtensionF extension : getExtensions()) { + if (extension instanceof KeYGuiExtensionF.ContextMenuF contextMenu) { + items.addAll(contextMenu.getProofTreeContextItems(mediator, node)); + } + } + return items; + } + /** * The settings providers contributed by every {@link KeYGuiExtensionF.SettingsF} provider * (Swing {@code KeYGuiExtensionFacade.getSettingsProvider}, @@ -219,6 +317,39 @@ public static List getTooltipStrings(KeYMediatorF mediator, PosInSequent return strings; } + /** + * Binds the view-scoped shortcuts of every {@link KeYGuiExtensionF.KeyboardShortcutsF} + * provider into the given view node as key-pressed event filters (P4, D36; Swing + * {@code installKeyboardShortcuts}, KeYGuiExtensionFacade.java:361-375, which fills the + * Swing input maps of the view). Only the shortcuts whose component id equals the given one + * are bound; a matching combination runs the shortcut's action and consumes the event. + * + * @param mediator the mediator of the window + * @param node the view node the shortcuts are active on + * @param componentId one of {@link KeYGuiExtensionF.KeyboardShortcutsF}'s constants + */ + public static void installKeyboardShortcuts(KeYMediatorF mediator, Node node, + String componentId) { + for (KeYGuiExtensionF extension : getExtensions()) { + if (!(extension instanceof KeYGuiExtensionF.KeyboardShortcutsF shortcuts)) { + continue; + } + for (KeYGuiExtensionF.KeyboardShortcutsF.ShortcutF shortcut : shortcuts + .getShortcuts(mediator, componentId)) { + if (!componentId.equals(shortcut.componentId())) { + continue; + } + var combination = shortcut.combination(); + node.addEventFilter(KeyEvent.KEY_PRESSED, e -> { + if (combination.match(e)) { + shortcut.action().run(); + e.consume(); + } + }); + } + } + } + /** * Calls {@link KeYGuiExtensionF.StartupF#init} of every provider once at app startup * (Swing {@code KeYGuiExtensionFacade.getStartupExtensions} + the host calling diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/extension/api/KeYGuiExtensionF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/extension/api/KeYGuiExtensionF.java index 8f318ee465a..d5de3166909 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/extension/api/KeYGuiExtensionF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/extension/api/KeYGuiExtensionF.java @@ -11,12 +11,14 @@ import javafx.scene.control.Menu; import javafx.scene.control.MenuItem; import javafx.scene.control.Tab; +import javafx.scene.input.KeyCombination; import de.uka.ilkd.key.core.fx.KeYMediatorF; import de.uka.ilkd.key.gui.fx.MainWindowF; import de.uka.ilkd.key.gui.fx.settings.SettingsProviderF; import de.uka.ilkd.key.pp.PosInSequent; import de.uka.ilkd.key.proof.Goal; +import de.uka.ilkd.key.proof.Node; import org.jspecify.annotations.NullMarked; @@ -92,6 +94,12 @@ public interface KeYGuiExtensionF { * keeps the grouping decision with the extension and contributes ready {@code Menu}s). * The five built-in menus (File / Proof / View / Options / About) and their item sets are * never touched. + *

        + * P4 (B13): a provider may return a non-empty {@link #getMenuPath()} to nest its menus + * into the menu bar instead of appending them on the top level — the mirror of the Swing + * {@code KeyAction.PATH} dot-separated path (KeyAction.java:46, applied by + * {@code KeYGuiExtensionFacade.sortActionIntoMenu}), matching menu names by text and + * creating missing menus along the way. * * @param window the main window * @param mediator the mediator of the window @@ -99,6 +107,20 @@ public interface KeYGuiExtensionF { */ interface MainMenuF { List

        getMenus(MainWindowF window, KeYMediatorF mediator); + + /** + * The dot-separated menu path under which the contributed menus are nested (Swing + * {@code KeyAction.PATH}, KeyAction.java:39-46). The empty path keeps the FX default: + * the menus are appended as new separate top-level menus after the built-in About menu. + * A non-empty path (e.g. {@code "View.Tools"}) matches or creates a top-level menu of + * the first segment in the existing menu bar, then descends/creates the nested sub + * menus, and finally splices the contributed menu items into the innermost menu. + * + * @return the path, may be empty + */ + default String getMenuPath() { + return ""; + } } /** @@ -139,11 +161,12 @@ interface LeftPanelF { } /** - * Context-menu extension for the sequent term menu: contributes {@link MenuItem}s for the - * clicked position (Swing {@code KeYGuiExtension.ContextMenu}, - * KeYGuiExtension.java:145-168 with {@code ContextMenuKind.SEQUENT_VIEW} — the FX port - * exposes only the sequent slot because it is the only term-menu slot). The host renders - * the items inside the "Extensions" section of the sequent context menu. + * Context-menu extension for the sequent term menu and (P4, C24) the proof-tree popup + * (Swing {@code KeYGuiExtension.ContextMenu}, KeYGuiExtension.java:145-168 — the two + * {@code ContextMenuKind} slots {@code SEQUENT_VIEW} and {@code PROOF_TREE}). The host + * renders the sequent items inside the "Extensions" section of the sequent context menu, + * and the proof-tree items after a separator at the end of the proof-tree popup + * (Swing {@code ProofTreePopupFactory}, ProofTreePopupFactory.java:152-154). * * @param mediator the mediator of the window * @param goal the goal whose sequent was clicked @@ -153,6 +176,20 @@ interface LeftPanelF { interface ContextMenuF { List getSequentContextItems(KeYMediatorF mediator, Goal goal, PosInSequent pos); + + /** + * The menu items contributed to the proof-tree popup for the given node (Swing + * {@code KeYGuiExtensionFacade.addContextMenuItems} with + * {@code ContextMenuKind.PROOF_TREE}, ProofTreePopupFactory.java:152-154). The empty + * list is the default — most extensions only serve the sequent slot. + * + * @param mediator the mediator of the window + * @param node the clicked proof-tree node + * @return non-null, emptiable list of menu items + */ + default List getProofTreeContextItems(KeYMediatorF mediator, Node node) { + return List.of(); + } } /** @@ -167,9 +204,9 @@ interface SettingsF { /** * Sequent-view tooltip extension: contributes term-information strings for the given - * position (Swing {@code KeYGuiExtension.Tooltip}, KeYGuiExtension.java:186-201). The FX - * host integration with the sequent view tooltips is a later milestone; the facade already - * aggregates the strings. + * position (Swing {@code KeYGuiExtension.Tooltip}, KeYGuiExtension.java:186-201, shown by + * Swing {@code SequentView.getToolTipText}). P4 (D30): the FX sequent view appends the + * strings to its hover tooltip (SequentViewF#getTooltipText). * * @param mediator the mediator of the window * @param pos the position of the term whose info shall be shown @@ -179,6 +216,56 @@ interface TooltipF { List getTooltipStrings(KeYMediatorF mediator, PosInSequent pos); } + /** + * Keyboard-shortcut extension: contributes additional shortcuts bound to a host view + * (Swing {@code KeYGuiExtension.KeyboardShortcuts}, KeYGuiExtension.java:234-248, bound + * into the Swing input maps by {@code KeYGuiExtensionFacade.installKeyboardShortcuts} for + * the sequent view, goal list, proof tree, strategy selection, source view and info view). + * P4 (D36): the FX host binds {@link ShortcutF}s as key-pressed event filters on the view + * node whose componentId matches, see + * {@code KeYGuiExtensionFacadeF.installKeyboardShortcuts}. + */ + interface KeyboardShortcutsF { + + /** The sequent view (Swing {@code KeyboardShortcuts.SEQUENT_VIEW}). */ + String SEQUENT_VIEW = "SEQUENT_VIEW"; + /** The goal list (Swing {@code KeyboardShortcuts.GOAL_LIST}). */ + String GOAL_LIST = "GOAL_LIST"; + /** The proof tree (Swing {@code KeyboardShortcuts.PROOF_TREE_VIEW}). */ + String PROOF_TREE_VIEW = "PROOF_TREE_VIEW"; + /** + * The strategy-selection view (Swing {@code KeyboardShortcuts.STRATEGY_SELECTION_VIEW}). + */ + String STRATEGY_SELECTION_VIEW = "STRATEGY_SELECTION_VIEW"; + /** The source view (Swing {@code KeyboardShortcuts.SOURCE_VIEW}). */ + String SOURCE_VIEW = "SOURCE_VIEW"; + /** The info view (Swing {@code KeyboardShortcuts.INFO_TREE}). */ + String INFO_TREE = "INFO_TREE"; + + /** + * The shortcuts to bind for the given view. + * + * @param mediator the mediator of the window + * @param componentId one of the component constants above + * @return non-null, emptiable list of shortcuts + */ + default List getShortcuts(KeYMediatorF mediator, String componentId) { + return List.of(); + } + + /** + * One view-scoped shortcut: a {@link KeyCombination} with the action to run when it is + * pressed while the view it is bound to has the keyboard focus. + */ + record ShortcutF(String componentId, KeyCombination combination, Runnable action) { + public ShortcutF { + java.util.Objects.requireNonNull(componentId); + java.util.Objects.requireNonNull(combination); + java.util.Objects.requireNonNull(action); + } + } + } + /** * Startup extension: the host calls {@link #init(MainWindowF, KeYMediatorF)} once at * startup, after discovery, before layout-dependent interaction (Swing diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/extension/contrib/TestExtensionF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/extension/contrib/TestExtensionF.java new file mode 100644 index 00000000000..574a8452cb1 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/extension/contrib/TestExtensionF.java @@ -0,0 +1,116 @@ +/* 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.extension.contrib; + +import java.util.List; +import javafx.scene.control.Button; +import javafx.scene.control.Control; +import javafx.scene.control.Menu; +import javafx.scene.control.MenuItem; +import javafx.scene.control.Tooltip; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyCodeCombination; +import javafx.scene.input.KeyCombination; + +import de.uka.ilkd.key.core.fx.KeYMediatorF; +import de.uka.ilkd.key.gui.fx.MainWindowF; +import de.uka.ilkd.key.gui.fx.extension.api.KeYGuiExtensionF; +import de.uka.ilkd.key.gui.fx.fonticons.IconFactoryF; +import de.uka.ilkd.key.gui.fx.notification.NotificationManagerF; +import de.uka.ilkd.key.pp.PosInSequent; +import de.uka.ilkd.key.proof.Goal; +import de.uka.ilkd.key.proof.Node; + +import org.jspecify.annotations.NullMarked; + +/** + * Port of the Swing {@code TestExtension} (key.ui {@code gui.extension.impl.TestExtension}, + * TestExtension.java:35-138) — "should only be used for testing of the extension facade". + * The Swing original contributes a "Test" action to every extension slot and a main menu at + * the {@code PATH} {@code "Test.Test.Test"} (TestExtension.java:104-112); this port exercises + * the FX slots that the P4 batch wired: the path-nested main menu (B13), the PROOF_TREE popup + * contributions (C24), the merged extension toolbar (B14) and the SEQUENT_VIEW keyboard + * shortcut seam (D36). Like the Swing original it is registered in the production service + * file, priority 100000 so it sorts last. + */ +@KeYGuiExtensionF.Info(name = "Test Extension", + description = "Should only be used for testing of the extension facade", priority = 100000, + optional = true, experimental = false) +@NullMarked +public final class TestExtensionF implements KeYGuiExtensionF, KeYGuiExtensionF.MainMenuF, + KeYGuiExtensionF.ContextMenuF, KeYGuiExtensionF.ToolbarF, + KeYGuiExtensionF.KeyboardShortcutsF { + + /** The "Test" menu item label of the Swing original (TestExtension.java:106). */ + private static final String TEST = "Test"; + + /** Shows the Swing original's "Test!" message (a JOptionPane) as an FX toast. */ + private static void showTestMessage() { + NotificationManagerF.getInstance().notify("Test!"); + } + + private static MenuItem testItem() { + MenuItem item = new MenuItem(TEST); + item.setOnAction(e -> showTestMessage()); + return item; + } + + // --- MainMenuF -------------------------------- + + @Override + public String getMenuPath() { + // Swing TestExtension.TestAction.setMenuPath("Test.Test.Test"), + // TestExtension.java:107 + return "Test.Test.Test"; + } + + @Override + public List getMenus(MainWindowF window, KeYMediatorF mediator) { + Menu menu = new Menu(TEST); + menu.getItems().add(testItem()); + return List.of(menu); + } + + // --- ContextMenuF (C24) ------------------------ + + @Override + public List getSequentContextItems(KeYMediatorF mediator, Goal goal, + PosInSequent pos) { + // deliberately empty: the sequent slot is already served by the real extensions; the + // Test extension only marks the PROOF_TREE slot the P4 batch wired + return List.of(); + } + + @Override + public List getProofTreeContextItems(KeYMediatorF mediator, Node node) { + // Swing contrib notes the Test action to every context menu kind + // (TestExtension.java:46-52, 60-63) + return List.of(testItem()); + } + + // --- ToolbarF (B14) ---------------------------- + + @Override + public List getToolbarControls(MainWindowF window, KeYMediatorF mediator) { + Button button = new Button(null, IconFactoryF.createIcon(IconFactoryF.Key.INFO_VIEW)); + button.setTooltip(new Tooltip(TEST)); + button.setOnAction(e -> showTestMessage()); + return List.of(button); + } + + // --- KeyboardShortcutsF (D36) ------------------ + + @Override + public List getShortcuts(KeYMediatorF mediator, String componentId) { + if (SEQUENT_VIEW.equals(componentId)) { + // deliberately unbound by the built-in action table: Ctrl+Shift+F12 + return List.of(new ShortcutF(SEQUENT_VIEW, + new KeyCodeCombination(KeyCode.F12, KeyCombination.CONTROL_DOWN, + KeyCombination.SHIFT_DOWN), + TestExtensionF::showTestMessage)); + } + return List.of(); + } +} diff --git a/key.ui.fx/src/main/resources/META-INF/services/de.uka.ilkd.key.gui.fx.extension.api.KeYGuiExtensionF b/key.ui.fx/src/main/resources/META-INF/services/de.uka.ilkd.key.gui.fx.extension.api.KeYGuiExtensionF index a444882dda5..6572fd977ab 100644 --- a/key.ui.fx/src/main/resources/META-INF/services/de.uka.ilkd.key.gui.fx.extension.api.KeYGuiExtensionF +++ b/key.ui.fx/src/main/resources/META-INF/services/de.uka.ilkd.key.gui.fx.extension.api.KeYGuiExtensionF @@ -1,3 +1,4 @@ de.uka.ilkd.key.gui.fx.extension.contrib.HeatmapF de.uka.ilkd.key.gui.fx.extension.contrib.ParallelProverStatusIndicatorF de.uka.ilkd.key.gui.fx.extension.contrib.ProfileNameInStatusBarF +de.uka.ilkd.key.gui.fx.extension.contrib.TestExtensionF From 2967c0a643345128afbae6a8bc0914f60047f20b Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Sat, 10 Oct 2026 17:15:43 +0200 Subject: [PATCH 143/281] =?UTF-8?q?P4:=20C24=20=E2=80=94=20PROOF=5FTREE=20?= =?UTF-8?q?extension=20contributions=20in=20the=20proof-tree=20popup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The popup now appends the items of KeYGuiExtensionFacadeF.getProofTreeContextItems for the clicked node after a separator, rebuilt on every showing (Swing ProofTreePopupFactory.create, ProofTreePopupFactory.java:152-154); the separator is dropped when nothing is contributed. --- .../key/gui/fx/prooftree/ProofTreeViewF.java | 39 ++++++++++++++++++- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/prooftree/ProofTreeViewF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/prooftree/ProofTreeViewF.java index 8f0001eb5f6..e9429a5034c 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/prooftree/ProofTreeViewF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/prooftree/ProofTreeViewF.java @@ -43,6 +43,7 @@ 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.extension.KeYGuiExtensionFacadeF; import de.uka.ilkd.key.gui.fx.fonticons.IconFactoryF; import de.uka.ilkd.key.proof.Goal; import de.uka.ilkd.key.proof.Node; @@ -281,6 +282,13 @@ private static final class ViewState { */ private final ContextMenu contextMenu = createContextMenu(); + /** + * C24 (P4): the tail of {@link #contextMenu} holding the PROOF_TREE extension + * contributions for the last clicked node (a separator plus the contributed items, or + * nothing); rebuilt in {@link #refreshPopupExtensions()} on every showing. + */ + private final List popupExtensionItems = new ArrayList<>(); + /** * The branch entry the popup actions apply to, resolved when the menu opens: the selected * entry itself if it is a branch, otherwise its parent — like the Swing popup's @@ -1314,8 +1322,9 @@ private static void setHideInteractiveGoals(boolean active) { * framework has no tab-title "Settings" gear menu like the Swing dockable, so these live * here — plus the popup actions of the Swing {@code ProofTreePopupFactory}: Apply Strategy, * Prune, Edit Notes, the per-node expand/collapse and sibling actions, the goals enablement - * and P3a's Show Subtree Statistics. Delayed Cut (feature-flagged) and the macro submenu - * (B12, P3b) are not yet ported; the PROOF_TREE extension contributions belong to P4 (C24). + * and P3a's Show Subtree Statistics, and finally (P4, C24) the PROOF_TREE extension + * contributions after a separator (Swing ProofTreePopupFactory.java:152-154). Delayed Cut + * (feature-flagged) and the macro submenu (B12, P3b) are not yet ported. */ private ContextMenu createContextMenu() { CheckMenuItem hideIntermediateItem = new CheckMenuItem("Hide Intermediate Proofsteps"); @@ -1415,10 +1424,36 @@ expandOssItem, new SeparatorMenuItem(), expandAllItem, collapseAllItem, notesItem.setDisable(proof == null); subtreeStatsItem.setDisable(proof == null); pruneItem.setDisable(!isPrunable(popupNode)); + refreshPopupExtensions(); }); return menu; } + /** + * C24 (P4): the PROOF_TREE extension contributions spliced at the end of the popup — the + * items of {@code KeYGuiExtensionFacadeF.getProofTreeContextItems} for the clicked node + * behind a separator (Swing {@code ProofTreePopupFactory.create}, + * ProofTreePopupFactory.java:152-154: {@code menu.addSeparator(); addContextMenuItems( + * ContextMenuKind.PROOF_TREE, menu, context.invokedNode, context.mediator);} followed by + * dropping the separator when nothing was contributed). Rebuilt on every showing since the + * clicked node changes; the section always stays the tail of the menu. + */ + private void refreshPopupExtensions() { + contextMenu.getItems().removeAll(popupExtensionItems); + popupExtensionItems.clear(); + if (mediator == null) { + return; + } + List items = + KeYGuiExtensionFacadeF.getProofTreeContextItems(mediator, popupNode); + if (items.isEmpty()) { + return; // no separator without contributions (Swing drops it too) + } + popupExtensionItems.add(new SeparatorMenuItem()); + popupExtensionItems.addAll(items); + contextMenu.getItems().addAll(popupExtensionItems); + } + /** @return a menu item with an optional icon and the given action */ private static MenuItem actionItem(String label, IconFactoryF.Key icon, Runnable action) { MenuItem item = new MenuItem(label); From 4b83b874d821036935a5c460e6bd066645f406a2 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Sat, 10 Oct 2026 17:16:17 +0200 Subject: [PATCH 144/281] =?UTF-8?q?P4:=20D30=20=E2=80=94=20wire=20the=20ex?= =?UTF-8?q?tension=20tooltip=20strings=20into=20the=20sequent=20hover=20to?= =?UTF-8?q?oltip?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SequentViewF.getTooltipText appends KeYGuiExtensionFacadeF.getTooltipStrings (KeYGuiExtensionF.TooltipF) for the hovered position, mirroring Swing SequentView.getToolTipText / KeYGuiExtensionFacade.getTooltipStrings (SequentView.java:234, KeYGuiExtensionFacade.java:385-399). --- .../key/gui/fx/nodeviews/SequentViewF.java | 30 ++++++++++++++----- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentViewF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentViewF.java index cf6ecafb931..397df9af18c 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentViewF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentViewF.java @@ -64,6 +64,7 @@ import de.uka.ilkd.key.core.fx.KeYSelectionModel; import de.uka.ilkd.key.gui.fx.MainWindowF; import de.uka.ilkd.key.gui.fx.configuration.ConfigF; +import de.uka.ilkd.key.gui.fx.extension.KeYGuiExtensionFacadeF; import de.uka.ilkd.key.gui.fx.fonticons.IconFactoryF; import de.uka.ilkd.key.gui.fx.keyshortcuts.KeyStrokeManagerF; import de.uka.ilkd.key.logic.label.TermLabel; @@ -1424,12 +1425,15 @@ private void showHoverTooltip() { } /** - * The tooltip text for the given position (Swing {@code SequentView.getToolTipText} without - * the HTML markup and without the GUI extension strings, which have no FX counterpart yet). - * menu: MP3a — the {@code isShowSequentViewTooltips()} gate is driven by the View menu "Show - * Tooltips in Sequent View" toggle (Swing {@code ToggleSequentViewTooltipAction}, NAME = - * "Show Tooltips in Sequent View"); {@link #updateHoverTooltip} hides the tooltip whenever - * this method returns the empty string, so no further gating is needed at the show site. + * The tooltip text for the given position (Swing {@code SequentView.getToolTipText}): + * operator class, operator and sort of the term, plus the GUI-extension tooltip strings + * (P4, D30) — Swing appends {@code KeYGuiExtensionFacade.getTooltipStrings(window, pos)}, + * SequentView.java:234 / KeYGuiExtensionFacade.java:385-399. + * menu: MP3a — the {@code isShowSequentViewTooltips()} gate is driven by the View menu + * "Show Tooltips in Sequent View" toggle (Swing {@code ToggleSequentViewTooltipAction}, + * NAME = "Show Tooltips in Sequent View"); {@link #updateHoverTooltip} hides the tooltip + * whenever this method returns the empty string, so no further gating is needed at the + * show site. */ private String getTooltipText(PosInSequent pos) { if (!ProofIndependentSettings.DEFAULT_INSTANCE.getViewSettings() @@ -1440,8 +1444,18 @@ private String getTooltipText(PosInSequent pos) { return ""; } Term term = pos.getPosInOccurrence().subTerm(); - return "Operator: " + term.op().getClass().getSimpleName() + " (" + term.op() + ")\nSort: " - + term.sort(); + String text = "Operator: " + term.op().getClass().getSimpleName() + " (" + term.op() + + ")\nSort: " + term.sort(); + // D30: the extension tooltip strings (KeYGuiExtensionF.TooltipF); the menu context + // (and hence the mediator) may not be attached yet, then no strings can be resolved + if (menuMediator != null) { + for (String extension : KeYGuiExtensionFacadeF.getTooltipStrings(menuMediator, pos)) { + if (extension != null && !extension.isBlank()) { + text += "\n" + extension; + } + } + } + return text; } // ----------------------------------------------------------------------- From 562c72898d1b3d91815d8197d655135221393d8d Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Sat, 10 Oct 2026 17:16:43 +0200 Subject: [PATCH 145/281] P4: extend key.fx.verify.extensions with the new seam assertions Asserts: TestExtensionF discovered; the B13 Test menu nests at Test>Test>Test in the built menu bar; the B14 extension toolbar controls all sit in the single merged key-extension-tool-bar; the C24 PROOF_TREE items of the selected node are non-empty; and the D36 TestExtensionF sequent shortcut fires a toast on a Ctrl+Shift+F12 key event. The javadoc now names the whole MP9.0 assertion set (a)-(i). --- .../de/uka/ilkd/key/gui/fx/MainWindowF.java | 115 ++++++++++++++++-- 1 file changed, 103 insertions(+), 12 deletions(-) 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 index 5f7300f3020..1334b58bd0c 100644 --- 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 @@ -101,6 +101,7 @@ 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.fonticons.IconFactoryF; import de.uka.ilkd.key.gui.fx.goallist.GoalListViewF; import de.uka.ilkd.key.gui.fx.help.HelpFacadeF; @@ -705,6 +706,25 @@ private void wireSequentView() { 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())); @@ -2416,14 +2436,18 @@ private void runTermMenuWiringVerification(KeYEnvironmentno position is available * (the fallback of SequentTermContextMenuF; contributed items would be enabled only with a - * position). One stdout report line; skips the term-menu sub-assertion gracefully when no - * goal is loaded. + * 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. * * @param env the environment of the loaded proof */ @@ -2496,11 +2520,78 @@ private void runExtensionVerification(KeYEnvironment "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; + + int toastsBefore = NotificationManagerF.getInstance().getVisibleToastCount(); + sequentView.fireEvent(new KeyEvent(KeyEvent.KEY_PRESSED, "", "F12", KeyCode.F12, false, + true, true, false)); + int toastsAfter = NotificationManagerF.getInstance().getVisibleToastCount(); + boolean shortcutFires = toastsAfter > toastsBefore; + sb.append(" sequentShortcutToast=").append(shortcutFires); + pass &= shortcutFires; + String report = (pass ? "PASS" : "FAIL") + " - " + sb; System.out.println("Extension verification: " + report); LOGGER.info("Extension verification: {}", report); } + /** @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 @@ -2907,13 +2998,13 @@ private MenuBar buildMenuBar() { // 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). The five built-in menus' item sets are + // 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. - List extensionMenus = - KeYGuiExtensionFacadeF.getMenus(this, mediator); - if (!extensionMenus.isEmpty()) { - menuBar.getMenus().addAll(extensionMenus); - } + KeYGuiExtensionFacadeF.installMenus(this, menuBar, mediator); return menuBar; } From 8e63d2500accb3ec94bff3750ca7d8345463e9dd Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Sat, 10 Oct 2026 17:19:25 +0200 Subject: [PATCH 146/281] P4: fix the D36 verify key event to Ctrl+Shift+F12 (was Ctrl+Alt+F12) The fired KeyEvent must match the KeyCodeCombination(CONTROL_DOWN, SHIFT_DOWN, F12) that TestExtensionF registers for SEQUENT_VIEW; the arg order of the deprecated KeyEvent ctor is shifted by two positions. --- .../src/main/java/de/uka/ilkd/key/gui/fx/MainWindowF.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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 index 1334b58bd0c..9a5c2b91f9b 100644 --- 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 @@ -2561,8 +2561,10 @@ private void runExtensionVerification(KeYEnvironment= 1; int toastsBefore = NotificationManagerF.getInstance().getVisibleToastCount(); - sequentView.fireEvent(new KeyEvent(KeyEvent.KEY_PRESSED, "", "F12", KeyCode.F12, false, - true, true, false)); + // Ctrl+Shift+F12 — the combination TestExtensionF registered for SEQUENT_VIEW + // (KeyEvent args: eventType, character, text, keyCode, shift, control, alt, meta) + sequentView.fireEvent(new KeyEvent(KeyEvent.KEY_PRESSED, "", "F12", KeyCode.F12, true, + true, false, false)); int toastsAfter = NotificationManagerF.getInstance().getVisibleToastCount(); boolean shortcutFires = toastsAfter > toastsBefore; sb.append(" sequentShortcutToast=").append(shortcutFires); From 6339494fddf75b9f096d729929583278439b5cad Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Sat, 10 Oct 2026 17:19:25 +0200 Subject: [PATCH 147/281] =?UTF-8?q?P4:=20sign-off=20=E2=80=94=20B13/C24/D3?= =?UTF-8?q?0/D36=20resolved,=20B14/B16/C26/A10=20decided?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PARITY-SIGNOFF.md records the four resolved extension seams, the three decided deviations (B14 merged toolbar, B16 toast convention, C26 WONT-REPLICATE) and the A10 dialog-triage closure, plus the batch record mirroring the P2a-P3a style. --- key.ui.fx/PARITY-SIGNOFF.md | 50 +++++++++++++++++++++++++++++++------ 1 file changed, 42 insertions(+), 8 deletions(-) diff --git a/key.ui.fx/PARITY-SIGNOFF.md b/key.ui.fx/PARITY-SIGNOFF.md index 38df921d75d..8b1e7418ba9 100644 --- a/key.ui.fx/PARITY-SIGNOFF.md +++ b/key.ui.fx/PARITY-SIGNOFF.md @@ -137,7 +137,7 @@ differences found by a second Swing↔JavaFX sweep, with stable IDs used by the | A7 | Feedback: no zip archive of logs | OPEN (P3c) | | A8 | ToolTip options: "Save as Default" behaviour | OPEN (P3c) | | A9 | `EnsureSourceConsistency` info dialog | RESOLVED (P2b) | -| A10 | Dialog triage remainder of the sweep | OPEN (triaged in P4) | +| A10 | Dialog triage remainder of the sweep | RESOLVED (P4, triage) | ## B. Menus / actions / toolbar @@ -145,10 +145,10 @@ differences found by a second Swing↔JavaFX sweep, with stable IDs used by the |----|---------------------------|--------| | B11 | Dynamic "Goal Back" label (`GoalBackAction.java:113-125`) vs. static `MainWindowF.java:3325-3327` | OPEN (P3b) | | B12 | Macro superset + category submenus + ProofScript actions vs. the 4-macro `AUTOMATION_MACROS` | OPEN (P3b) | -| B13 | Extension PATH nesting (extension menus cannot nest) | OPEN (P4) | -| B14 | Per-extension toolbars merged into one | OPEN (P4, decide) | +| B13 | Extension PATH nesting (extension menus cannot nest) | RESOLVED (P4) | +| B14 | Per-extension toolbars merged into one | RESOLVED (P4, decided) | | B15 | Toolbar GoalBack/Prune have no enablement | OPEN (P3b) | -| B16 | Popup dialogs replaced by toasts (per site) | OPEN (P4, decide) | +| B16 | Popup dialogs replaced by toasts (per site) | RESOLVED (P4, decided) | | B17 | TermLabelMenu persistence/styling | OPEN (P3b) | | B18 | Recent-Files empty menu not disabled + font-size item order | OPEN (P3b) | @@ -161,9 +161,9 @@ differences found by a second Swing↔JavaFX sweep, with stable IDs used by the | C21 | OSS child nodes in the tree | RESOLVED (P3a) | | C22 | Whole-tree actions (expand all/collapse all/...) | RESOLVED (P3a) | | C23 | Popup items incl. SubtreeStatistics | RESOLVED (P3a) | -| C24 | `PROOF_TREE` extension seam (tree popup contributions) | OPEN (P4) | +| C24 | `PROOF_TREE` extension seam (tree popup contributions) | RESOLVED (P4) | | C25 | Node-filter counting rule | RESOLVED (P3a) | -| C26 | Hidden-tab passivation (Swing docking detail) | OPEN (P4, likely WONT-REPLICATE) | +| C26 | Hidden-tab passivation (Swing docking detail) | WONT-REPLICATE (P4, decided) | | C27 | Auto-mode partial subtree updates | RESOLVED (P3a) | ## D. Sequent / strategy / goal list / status bar @@ -172,13 +172,13 @@ differences found by a second Swing↔JavaFX sweep, with stable IDs used by the |----|---------------------------|--------| | D28 | Left-click taclet menu + `POPUP_DELAY` guard (`CurrentGoalViewListener.java:48-95`, `SequentViewListener.java:29`) | RESOLVED (P2a) | | D29 | Shift+click focussed auto mode (`CurrentGoalViewListener.java:56-58`) | RESOLVED (P2a) | -| D30 | Extension tooltip strings unwired | OPEN (P4) | +| D30 | Extension tooltip strings unwired | RESOLVED (P4) | | D31 | Sequent line width computed from the viewport | OPEN (P3c) | | D32 | Sequent search prefill from the hovered term (`SearchInSequentAction.java:32-37`) | RESOLVED (P2a) | | D33 | Fonts vs. `KEY_FONT_*` settings | OPEN (P3c) | | D34 | `SequentViewDock` missing | OPEN (P3c) | | D35 | Strategy tab Go button + parallel-prover merge-point lock (`StrategySelectionView.java:157-208/869-895`) | RESOLVED (P2a) | -| D36 | `KeyboardShortcuts` extension seams absent | OPEN (P4) | +| D36 | `KeyboardShortcuts` extension seams absent | RESOLVED (P4) | | D37 | Status-line task progress bar (log-only stubs, `WindowUserInterfaceControlF.java:317-327`) | OPEN (P3c) | P2a (`weigl/ocfx-p2-sequent`, merged 2026-10-10) resolved D28/D29/D32/D35 — @@ -217,3 +217,37 @@ covering the OSS rows on a strategy-proved proof. The view filters and the tree in the tree context menu because the FX docking framework has no tab-title gear menu (documented in `ProofTreeViewF`); the subtree statistics report is a plain-text window rather than the Swing HTML one (export is A4/P3c). C24 (the `PROOF_TREE` extension seam) stays open for P4. + +P4 (`weigl/ocfx-p4-extensions`) resolved B13/C24 (extension-seam parity) and decided +B14/B16/C26; A10 and the D-section remainder D30/D36 are triaged/resolved in the same batch: +- B13: `KeYGuiExtensionF.MainMenuF.getMenuPath()` (Swing `KeyAction.PATH`) + facade + `KeYGuiExtensionFacadeF.installMenus`: path-bearing providers splice their items into + existing or new menus of the bar (first segment matched by text — the five built-ins + included — deeper segments by sub-menu text); the empty path keeps the FX default (separate + top-level menus after About). +- C24: `ContextMenuF.getProofTreeContextItems(mediator, node)` default + facade aggregate; + `ProofTreeViewF` appends the contributions after a separator at the end of the popup, + rebuilt per showing, and drops the separator when nothing is contributed (Swing + `ProofTreePopupFactory.create`, :152-154). +- B14 (decided): the FX keeps ONE merged `key-extension-tool-bar` holding every extension + toolbar control; Swing embeds one JToolBar per extension in a FlowLayout. The single-bar + layout is asserted by `key.fx.verify.extensions` (`extensionToolbars=1`). +- B16 (decided): informational popups map to non-modal toasts (`NotificationManagerF`); + confirmations stay modal; the decision is applied per site (e.g. MainWindowF:1795-1802; + the P3c A5/A7 sites follow the same convention). +- C26 (WONT-REPLICATE): the FX docking framework keeps dockables mounted — there is no + tab-visibility lifecycle, so the Swing hidden-tab passivation + (`ProofTreeView.java:339-428` dirty-flag + listener detach, `GUIProofTreeModel.setAttentive`) + has nothing to attach to; documented in `ProofTreeViewF`. +- D30: `SequentViewF.getTooltipText` appends `KeYGuiExtensionFacadeF.getTooltipStrings` + (Swing `SequentView.getToolTipText`, SequentView.java:234). +- D36: new `KeYGuiExtensionF.KeyboardShortcutsF` capability (view-scoped `ShortcutF` + records; component constants mirroring the Swing `KeyboardShortcuts` set) bound by + `installKeyboardShortcuts` as key-pressed filters on the six host views + (sequent/goallist/prooftree/strategy/source/info). +- A10 (triage): all sweep dialog findings are now accounted for — A1-A3/A9 (P2b), + A4-A8 (P3c), and the remaining per-site dialog decisions follow the B16 toast convention. +- The ported Swing `TestExtension` (`TestExtensionF`, priority 100000) exercises each seam: + the `Test.Test.Test`-nested menu, a PROOF_TREE popup item, an extension-toolbar button and a + Ctrl+Shift+F12 sequent shortcut — asserted headlessly by `key.fx.verify.extensions` + (`testExtension`/`testNested`/`prooftreeExtensionItems`/`sequentShortcutToast`). From 689f6b3517f9a457ad8daac88a674fa64ee92d22 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Sat, 10 Oct 2026 17:21:43 +0200 Subject: [PATCH 148/281] =?UTF-8?q?P3c:=20A4=20=E2=80=94=20proof=20statist?= =?UTF-8?q?ics=20CSV/HTML=20export?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../notification/ProofStatisticsDialogF.java | 319 +++++++++++++++++- 1 file changed, 315 insertions(+), 4 deletions(-) diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/ProofStatisticsDialogF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/ProofStatisticsDialogF.java index dda53018300..fd721ee7ab0 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/ProofStatisticsDialogF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/notification/ProofStatisticsDialogF.java @@ -4,22 +4,42 @@ * SPDX-License-Identifier: GPL-2.0-or-later */ package de.uka.ilkd.key.gui.fx.notification; +import java.io.BufferedWriter; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Iterator; import java.util.List; +import java.util.Map; +import java.util.SortedSet; +import java.util.TreeSet; +import java.util.regex.Matcher; +import java.util.regex.Pattern; 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.ScrollPane; +import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; +import javafx.stage.FileChooser; import javafx.stage.Window; +import de.uka.ilkd.key.gui.fx.notification.NotificationManagerF.Kind; +import de.uka.ilkd.key.proof.Node; import de.uka.ilkd.key.proof.Proof; import de.uka.ilkd.key.proof.Statistics; +import de.uka.ilkd.key.proof.reference.ClosedBy; +import de.uka.ilkd.key.util.MiscTools; import org.key_project.util.collection.Pair; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + /** * menu: non-modal window showing the statistics of the selected proof (Swing * {@code ShowProofStatistics.Window}, ShowProofStatistics.java:234-410): the open-goal count plus @@ -27,11 +47,24 @@ * {@link ProofClosedDialogF} ({@code ProofClosedJTextPaneDisplay} opens the same Swing * "Proof Statistics" window for closed proofs). *

        - * The plain VBox/ScrollPane layout replaces the Swing HTML table; no export buttons for now - * (the CSV/HTML export of {@code ShowProofStatistics.Window} is not ported). + * Export (A4, P3c): the Swing CSV/HTML export of {@code ShowProofStatistics.Window} is ported — + * the "Export as CSV" / "Export as HTML" buttons write the exact Swing formats + * ({@link #getCSVStatisticsMessage(Proof)} / {@link #getHTMLStatisticsMessage(Proof)}) to a file + * chosen with a {@link FileChooser}; the plain VBox/ScrollPane layout of the dialog itself + * replaces the Swing HTML table (KNOWN-SIMPLIFIED, the exported HTML keeps the Swing format). */ public final class ProofStatisticsDialogF extends javafx.stage.Stage { + private static final Logger LOGGER = LoggerFactory.getLogger(ProofStatisticsDialogF.class); + + /** + * Regex pattern to check for tooltips in statistics entries (Swing + * {@code ShowProofStatistics.TOOLTIP_PATTERN}). + */ + private static final Pattern TOOLTIP_PATTERN = Pattern.compile(".+\\[tooltip: (.+)]"); + + private static final String CSV_SEPARATOR = ";"; + private ProofStatisticsDialogF(Window owner, Proof proof) { setTitle("Proof Statistics"); if (owner != null) { @@ -58,13 +91,25 @@ private ProofStatisticsDialogF(Window owner, Proof proof) { Button close = new Button("Close"); close.setOnAction(e -> hide()); - VBox.setVgrow(close, Priority.NEVER); + + // A4 (P3c): the export buttons of Swing ShowProofStatistics.Window (:299-307) — the + // Swing save buttons "Export as CSV"/"Export as HTML" write the stat summary to a file + // chosen in a file chooser (Swing KeYFileChooser with the statistics filter, :392-409); + // "Save proof"/"Save proof bundle"/"Show Soundiness Report" stay out (the FX File/Save + // and View/Soundiness entries cover them) + Button csvButton = new Button("Export as CSV"); + csvButton.setOnAction(e -> export(owner, proof, "csv", getCSVStatisticsMessage(proof))); + Button htmlButton = new Button("Export as HTML"); + htmlButton.setOnAction(e -> export(owner, proof, "html", getHTMLStatisticsMessage(proof))); + HBox buttons = new HBox(8, csvButton, htmlButton, close); + buttons.setAlignment(Pos.CENTER_RIGHT); + VBox.setVgrow(buttons, Priority.NEVER); ScrollPane scroll = new ScrollPane(statsBox); scroll.setFitToWidth(true); scroll.setPrefViewportHeight(Math.min(240, 40 + 22 * statsBox.getChildren().size())); - VBox root = new VBox(12, new Label("Proof " + proof.name()), scroll, close); + VBox root = new VBox(12, new Label("Proof " + proof.name()), scroll, buttons); root.getStyleClass().add("notification-dialog"); root.setPadding(new Insets(14)); root.setAlignment(Pos.CENTER_LEFT); @@ -86,4 +131,270 @@ public static void show(Window owner, Proof proof) { ProofStatisticsDialogF dialog = new ProofStatisticsDialogF(owner, proof); dialog.show(); } + + /** + * A4 (P3c): writes the CSV statistics message exactly like Swing + * {@code ShowProofStatistics.getCSVStatisticsMessage} (ShowProofStatistics.java:86-121): the + * open-goal line, the summary pairs separated by {@code ;}, and — when the proof has + * interactive steps — the "interactive;rule;count" lines sorted by count (descending, then + * rule name). + * + * @param proof the proof whose statistics to serialize + * @return the CSV text + */ + public static String getCSVStatisticsMessage(Proof proof) { + final int openGoals = proof.openGoals().size(); + StringBuilder stats = new StringBuilder(); + stats.append("open goals" + CSV_SEPARATOR).append(openGoals).append("\n"); + + final Statistics s = proof.getStatistics(); + + for (Pair x : s.getSummary()) { + if ("".equals(x.second)) { + stats.append(x.first).append("\n"); + } else { + stats.append(x.first).append(CSV_SEPARATOR).append(x.second).append("\n"); + } + } + + if (s.interactiveSteps > 0) { + SortedSet> sortedEntries = + new TreeSet<>( + (o1, o2) -> { + int cmpRes = o2.getValue().compareTo(o1.getValue()); + if (cmpRes == 0) { + cmpRes = o1.getKey().compareTo(o2.getKey()); + } + return cmpRes; + }); + sortedEntries.addAll(s.getInteractiveAppsDetails().entrySet()); + + for (Map.Entry entry : sortedEntries) { + stats.append("interactive" + CSV_SEPARATOR).append(entry.getKey()) + .append(CSV_SEPARATOR) + .append(entry.getValue()).append("\n"); + } + } + + return stats.toString(); + } + + /** + * A4 (P3c): the open-goal/cached-goal summary of the given node (Swing + * {@code ShowProofStatistics.getHTMLStatisticsMessage(Node)}, + * ShowProofStatistics.java:123-139). + * + * @param node the node whose subtree statistics to summarize + * @return the HTML statistics message + */ + public static String getHTMLStatisticsMessage(Node node) { + int openGoals = 0; + int cachedGoals = 0; + + Iterator leavesIt = node.leavesIterator(); + while (leavesIt.hasNext()) { + if (node.proof().getOpenGoal(leavesIt.next()) != null) { + if (node.lookup(ClosedBy.class) != null) { + cachedGoals++; + } else { + openGoals++; + } + } + } + + return getHTMLStatisticsMessage(openGoals, cachedGoals, node.statistics()); + } + + /** + * A4 (P3c): the open-goal/cached-goal summary of a proof (Swing + * {@code ShowProofStatistics.getHTMLStatisticsMessage(Proof)}, + * ShowProofStatistics.java:141-147). + * + * @param proof the proof whose statistics to render + * @return the HTML statistics message + */ + public static String getHTMLStatisticsMessage(Proof proof) { + int openGoals = proof.openGoals().size(); + int cachedGoals = + (int) proof.closedGoals().stream().filter(g -> g.node().lookup(ClosedBy.class) != null) + .count(); + return getHTMLStatisticsMessage(openGoals, cachedGoals, proof.getStatistics()); + } + + private static String getHTMLStatisticsMessage(int openGoals, int cachedGoals, + Statistics statistics) { + StringBuilder stats = new StringBuilder("" + ""); + + stats.append("
        "); + if (cachedGoals > 0 && openGoals > 0) { + stats.append("").append(openGoals).append(" open goal") + .append(openGoals > 1 ? "s, " : ", ").append(cachedGoals) + .append(" cached goal").append(cachedGoals > 1 ? "s." : ".") + .append(""); + } else if (cachedGoals > 0) { + stats.append("").append("Proved (using proof cache).").append(""); + } else if (openGoals > 0) { + stats.append("").append(openGoals).append(" open goal") + .append(openGoals > 1 ? "s." : ".").append(""); + } else { + stats.append("Proved."); + } + + stats.append("

        "); + stats.append(getStatisticsTable(statistics)); + stats.append(""); + return stats.toString(); + } + + private static String getStatisticsTable(Statistics s) { + StringBuilder stats = new StringBuilder(); + stats.append(""); + + for (Pair x : s.getSummary()) { + if ("".equals(x.second)) { + stats.append(""); + } else { + if (x.first.contains("[tooltip: ")) { + Matcher m = TOOLTIP_PATTERN.matcher(x.first); + if (m.find()) { + String tooltip = m.group(1); + stats.append(""); + } else { + stats.append(""); + } + } else { + stats.append(""); + } + } + } + + if (s.interactiveSteps > 0) { + stats.append(""); + + SortedSet> sortedEntries = + new TreeSet<>( + (o1, o2) -> { + int cmpRes = o2.getValue().compareTo(o1.getValue()); + + if (cmpRes == 0) { + cmpRes = o1.getKey().compareTo(o2.getKey()); + } + + return cmpRes; + }); + sortedEntries.addAll(s.getInteractiveAppsDetails().entrySet()); + + for (Map.Entry entry : sortedEntries) { + stats.append(""); + } + } + + stats.append("
        ").append(x.first).append("
        ") + .append(x.first, 0, x.first.indexOf('[')) + .append("") + .append(x.second) + .append("
        ").append(x.first).append("") + .append(x.second).append("
        ").append(x.first).append("").append(x.second) + .append("
        " + "Details on Interactive Apps" + "
        ").append(entry.getKey()).append("") + .append(entry.getValue()).append("
        "); + + return stats.toString(); + } + + /** + * A4 (P3c): writes the given statistics text to the chosen file (Swing + * {@code ShowProofStatistics.Window.export}, ShowProofStatistics.java:392-409: a save file + * chooser defaulting to {@code .}, written as UTF-8). Success + * and failure surface as toasts (the established FX info/error reporting). + * + * @param owner the owner window of the file chooser; may be {@code null} + * @param proof the proof being exported (its name forms the default file name) + * @param extension {@code "csv"} or {@code "html"} + * @param text the statistics text to write + */ + private static void export(Window owner, Proof proof, String extension, String text) { + FileChooser chooser = new FileChooser(); + chooser.setTitle("Choose filename to save statistics"); + String descriptor = extension.equalsIgnoreCase("csv") ? "CSV" : "HTML"; + chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter( + descriptor + " statistics (*." + extension + ")", "*." + extension)); + chooser.setInitialFileName( + MiscTools.toValidFileName(proof.name().toString()) + "." + extension); + java.io.File file = owner == null ? chooser.showSaveDialog(null) + : chooser.showSaveDialog(owner); + if (file == null) { + return; + } + try { + writeExport(file.toPath(), text); + NotificationManagerF.getInstance().notify( + "Proof statistics exported to " + file.getAbsolutePath(), Kind.INFO); + } catch (IOException e) { + LOGGER.warn("Failed to write statistics export", e); + NotificationManagerF.getInstance().notify( + "Failed to export proof statistics: " + e.getMessage(), Kind.ERROR); + } + } + + /** + * A4 (P3c): writes the statistics text to the given file as UTF-8 (the write step behind + * {@link #export}; shared with the {@code key.fx.verify.uicontrol} self test so no file + * chooser is needed there). + * + * @param file the target file + * @param text the statistics text + * @throws IOException if the file cannot be written + */ + public static void writeExport(Path file, String text) throws IOException { + try (BufferedWriter writer = Files.newBufferedWriter(file, StandardCharsets.UTF_8)) { + writer.write(text); + } + } + + /** + * A4 (P3c): self test of the statistics export behind the buttons — writes both the CSV and + * the HTML format to files and asserts the Swing-parity content (the CSV row separator, the + * HTML summary headline and table). Called by the {@code key.fx.verify.uicontrol} self test. + * + * @param proof the loaded demo proof to export, may be {@code null} (SKIP) + * @return {@code "PASS ..."} / {@code "FAIL ..."} / {@code "SKIP (no proof)"} + */ + public static String verifyStatisticsExport(Proof proof) { + if (proof == null) { + return "SKIP (no proof)"; + } + Path dir; + try { + dir = Files.createTempDirectory("keyfx-statistics-export-"); + } catch (IOException e) { + LOGGER.warn("Statistics export self test: cannot create temp dir", e); + return "FAIL (no temp dir)"; + } + try { + Path csv = dir.resolve("stats.csv"); + Path html = dir.resolve("stats.html"); + writeExport(csv, getCSVStatisticsMessage(proof)); + writeExport(html, getHTMLStatisticsMessage(proof)); + String csvText = Files.readString(csv, StandardCharsets.UTF_8); + String htmlText = Files.readString(html, StandardCharsets.UTF_8); + boolean csvOk = + csvText.lines().anyMatch(l -> l.startsWith("open goals" + CSV_SEPARATOR)) + && csvText.contains(CSV_SEPARATOR); + boolean htmlOk = htmlText.contains("open goal") && htmlText.contains("") + && htmlText.startsWith(""); + Files.deleteIfExists(csv); + Files.deleteIfExists(html); + Files.deleteIfExists(dir); + return (csvOk && htmlOk) + ? "PASS (csv chars=" + csvText.length() + ", html chars=" + htmlText.length() + + ")" + : "FAIL (csvOk=" + csvOk + ", htmlOk=" + htmlOk + ")"; + } catch (IOException e) { + LOGGER.warn("Statistics export self test failed", e); + return "FAIL (" + e.getMessage() + ")"; + } + } } From d0c6a662c4a95dbce0b750aadc664b99ab498263 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Sat, 10 Oct 2026 17:23:08 +0200 Subject: [PATCH 149/281] =?UTF-8?q?P3c:=20A5=20=E2=80=94=20About=20KeY=20e?= =?UTF-8?q?nriched=20toast,=20License=20dialog=20(LicenseDialogF)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../de/uka/ilkd/key/gui/fx/MainWindowF.java | 17 +- .../key/gui/fx/dialogs/LicenseDialogF.java | 149 ++++++++++++++++++ 2 files changed, 160 insertions(+), 6 deletions(-) create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/dialogs/LicenseDialogF.java 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 index 5f7300f3020..a91e30f04a7 100644 --- 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 @@ -90,6 +90,7 @@ import de.uka.ilkd.key.gui.fx.dialogs.DialogsVerifyF; 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; @@ -4206,15 +4207,19 @@ private void resetLayout() { } private void showLicense() { - NotificationManagerF.getInstance().notify( - KeYConstants.COPYRIGHT + " KeY is free software and comes with ABSOLUTELY NO " - + "WARRANTY. See About | License.", - Kind.INFO); + // 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() { - NotificationManagerF.getInstance().notify( - KeYResourceManager.getManager().getUserInterfaceTitle() + " — JavaFX UI (key.ui.fx)."); + // B16 (P3c): "About KeY" stays an information toast (an invisible-at-a-glance info + // styled message) instead of becoming a modal dialog; the toast is enriched with the + // Swing AboutAction content (copyright, WWW, version) to keep the parity information. + NotificationManagerF.getInstance().notify(KeYConstants.COPYRIGHT + + "\n\nWWW: http://key-project.org/\n\nVersion " + KeYConstants.VERSION, + Kind.INFO); } private void notYetImplemented() { diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/dialogs/LicenseDialogF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/dialogs/LicenseDialogF.java new file mode 100644 index 00000000000..9aa3d941b66 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/dialogs/LicenseDialogF.java @@ -0,0 +1,149 @@ +/* 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.dialogs; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import javafx.geometry.Insets; +import javafx.scene.Scene; +import javafx.scene.control.Button; +import javafx.scene.control.Tab; +import javafx.scene.control.TabPane; +import javafx.scene.control.TextArea; +import javafx.scene.input.KeyCode; +import javafx.scene.layout.BorderPane; +import javafx.stage.Modality; +import javafx.stage.Window; + +import de.uka.ilkd.key.gui.fx.theme.ThemeManager; +import de.uka.ilkd.key.util.KeYConstants; + +import org.jspecify.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A5 (P3c): JavaFX port of the Swing {@code LicenseAction} dialog + * (key.ui/.../gui/actions/LicenseAction.java), opened by the About>License… entry + * ({@code MainWindowF.showLicense}). The dialog shows the license of KeY (defined in + * {@code LICENSE.TXT}) and of the dependencies ({@code THIRD_PARTY_LICENSES.txt}) in two tabs, + * mirroring the Swing {@code JTabbedPane} layout. + *

        + * Port notes and deviations from the Swing original: + *

          + *
        • The license texts live in {@code key.ui/src/main/resources/de/uka/ilkd/key/gui/}. They are + * only runtime resources for key.ui.fx (key.ui itself stays off the compile classpath), + * so they are read by their classpath resource path through the application class loader instead + * of {@code KeYResourceManager.getResourceFile(MainWindow.class, …)}; when the file cannot be + * loaded the Swing fallback text {@link #KEY_FALLBACK} is shown for the KeY license and an empty + * text for the third-party tab.
        • + *
        • Unlike the Swing ok-button-in-a-{@code BorderLayout} this port adds the button to a + * {@code ButtonBar} at the bottom with the same behaviour (closing the dialog).
        • + *
        + */ +public final class LicenseDialogF { + private static final Logger LOGGER = LoggerFactory.getLogger(LicenseDialogF.class); + + /** + * Fallback text shown when {@code LICENSE.TXT} cannot be loaded (Swing {@code KEY_FALLBACK}). + */ + public static final String KEY_FALLBACK = (KeYConstants.COPYRIGHT + "\nKeY is protected by the " + + "GNU General Public License v2, or (at your option) any later version"); + + /** Classpath path of the KeY license text (resource lives in the key.ui module). */ + private static final String LICENSE_RESOURCE = "de/uka/ilkd/key/gui/LICENSE.TXT"; + + /** Classpath path of the third-party licenses text (resource lives in the key.ui module). */ + private static final String THIRD_PARTY_RESOURCE = + "de/uka/ilkd/key/gui/THIRD_PARTY_LICENSES.txt"; + + private LicenseDialogF() { + } + + /** + * Shows the modal license dialog (Swing {@code LicenseAction.showLicense}): a + * {@code TabPane} with the KeY license and the third-party licenses, plus an OK button. + * + * @param owner the owner window (the main window stage), may be {@code null} + */ + public static void show(Window owner) { + javafx.stage.Stage dialog = new javafx.stage.Stage(); + dialog.setTitle("KeY License"); + dialog.initOwner(owner); + if (owner != null) { + // Swing: JDialog(mainWindow, "KeY License") — window-modal in front of the owner + dialog.initModality(Modality.WINDOW_MODAL); + } + + TabPane pane = new TabPane(); + // Swing: JTextArea(s, 20, 40) in a JScrollPane — read-only text with a scroll bar + pane.getTabs().addAll( + new Tab("KeY License", createLicenseViewer(readText(readResource(LICENSE_RESOURCE), + KEY_FALLBACK))), + new Tab("Third party libraries", + createLicenseViewer(readText(readResource(THIRD_PARTY_RESOURCE), "")))); + + Button okButton = new Button("OK"); + okButton.setDefaultButton(true); + okButton.setOnAction(e -> dialog.close()); + + BorderPane root = new BorderPane(); + root.setCenter(pane); + root.setBottom(okButton); + BorderPane.setMargin(okButton, new Insets(10)); + Scene scene = new Scene(root, 600, 900); + ThemeManager.getInstance().manage(scene); + scene.setOnKeyPressed(e -> { + if (e.getCode() == KeyCode.ESCAPE) { + dialog.close(); + e.consume(); + } + }); + dialog.setScene(scene); + dialog.showAndWait(); + } + + /** Swing {@code createLicenseViewer}: a read-only multi-line text area (top-aligned caret). */ + private static javafx.scene.Node createLicenseViewer(String text) { + TextArea area = new TextArea(text); + area.setEditable(false); + area.positionCaret(0); + return area; + } + + /** + * Looks up a license resource by its classpath path (see the class javadoc for why the + * resources are addressed by path and not via {@code getResourceFile}). + * + * @param path the classpath resource path of the license text + * @return the resource URL, or {@code null} when the license module is not on the classpath + */ + private static @Nullable URL readResource(String path) { + return LicenseDialogF.class.getClassLoader().getResource(path); + } + + /** + * Reads a resource URL to a string (Swing {@code LicenseAction.readStream}, 1024-char + * buffer, UTF-8); on failure (or a {@code null} URL) the fallback text is returned. + */ + private static String readText(URL resource, String fallback) { + StringBuilder sb = new StringBuilder(); + try (InputStream in = resource.openStream(); + InputStreamReader inp = new InputStreamReader(in, StandardCharsets.UTF_8)) { + char[] buf = new char[1024]; + int c; + while ((c = inp.read(buf)) > 0) { + sb.append(buf, 0, c); + } + } catch (IOException | NullPointerException ioe) { + LOGGER.warn("License text resource {} not readable, using fallback", resource); + return fallback; + } + return sb.toString(); + } +} From 5795ae57a471f7d0ba0a64115d61784cf679f6a3 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Sat, 10 Oct 2026 17:23:16 +0200 Subject: [PATCH 150/281] =?UTF-8?q?P3b:=20B18=20=E2=80=94=20recent-files?= =?UTF-8?q?=20+=20font-size=20order?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - remove recentFilesMenu.setDisable; Swing leaves the empty submenu enabled (RecentFileMenu constructor, addRecentFileNoSave only re-enables after insert) - Font Size submenu: Swing order (Smaller/Decrease with minus, then Larger/Increase with plus, MainWindow.createViewMenu :1048-1051) --- .../de/uka/ilkd/key/gui/fx/MainWindowF.java | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) 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 index 5f7300f3020..c9b2e324711 100644 --- 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 @@ -1851,8 +1851,10 @@ private void updateRecentFilesMenu() { recentFilesMenu.getItems() .add(menuItem(text, () -> openRecentFile(entry))); } - // an empty submenu would render as a dark clickable nothing (Swing leaves it enabled) - recentFilesMenu.setDisable(entries.isEmpty()); + // 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. } /** @@ -3143,11 +3145,16 @@ private Menu buildViewMenu() { 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("Increase", "de.uka.ilkd.key.gui.actions.IncreaseFontSizeAction", - IconFactoryF.Key.PLUS, () -> changeFontSize(1)), - menuItem("Decrease", "de.uka.ilkd.key.gui.actions.DecreaseFontSizeAction", - IconFactoryF.Key.MINUS, () -> changeFontSize(-1))); + 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, From 70cedc8190a3f68af0e6ab53d187ae15ad424b2a Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Sat, 10 Oct 2026 17:23:18 +0200 Subject: [PATCH 151/281] =?UTF-8?q?P3b:=20B11/B15=20=E2=80=94=20Goal=20Bac?= =?UTF-8?q?k=20label=20+=20GoalBack/Prune=20enablement?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit B11: Proof menu Goal Back item carries the dynamic Swing label "Undo Last Rule Application (RuleName)" — mediator.goalBackRuleName() (from GoalBackAction.updateName), refreshed in the menu setOnShowing handler (Swing MenuListener.menuSelected); toolbar button keeps the static tooltip (longName=false). PROOF_MENU_EXPECTED row renamed. B15: Goal Back / Prune Proof menu items and toolbar buttons are no longer static; enablement mirrors the Swing actions (selection + GeneralSettings.noPruningClosed, disabled while auto mode runs) via refreshGoalBackPrune() from updateProofStatus() and an autoModeRunningProperty listener; menuparity self test asserts the enablement. --- .../de/uka/ilkd/key/core/fx/KeYMediatorF.java | 22 ++ .../de/uka/ilkd/key/gui/fx/MainWindowF.java | 192 +++++++++++++++++- 2 files changed, 206 insertions(+), 8 deletions(-) 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 index 9a05d0c0a9f..9068d8eaefa 100644 --- 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 @@ -22,9 +22,11 @@ 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.jspecify.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -349,6 +351,26 @@ private void setBack(Node 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. 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 index c9b2e324711..ad7c1a369f8 100644 --- 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 @@ -407,6 +407,19 @@ in order to generate and prove the proof obligations for the correctness of (non */ 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; + /** * Last directory of the open/save dialogs (Swing {@code OpenFileAction.lastSelectedPath}). */ @@ -462,6 +475,13 @@ public MainWindowF(Stage stage) { 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()); buildDockables(); workspace.setDefaultLayout(defaultLayout()); @@ -3393,18 +3413,51 @@ private Menu buildProofMenu() { "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(), - menuItem("Goal Back", "de.uka.ilkd.key.gui.actions.GoalBackAction", - IconFactoryF.Key.GOAL_BACK, mediator::goalBack), - menuItem("Prune Proof", "de.uka.ilkd.key.gui.actions.PruneProofAction", - IconFactoryF.Key.PRUNE, mediator::pruneProof), + 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) // ------------------------------------------------------------------ @@ -3509,11 +3562,13 @@ private void showKnownTypes() { // 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" }, - { "Goal Back" }, + { "Undo Last Rule Application" }, { "Prune Proof" }, { "Abandon Proof" }, { "---" }, @@ -3608,6 +3663,75 @@ private void verifyMenuParity() { 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(); + } + + /** + * 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(m.getText())).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(); + 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") + "]"); } private void logMenuParity(String menuName, String report) { @@ -4026,11 +4150,18 @@ private ToolBar buildProofToolBar() { toolbarButton("Stop Automatic Proof (Escape)", IconFactoryF.Key.AUTO_MODE_STOP, mediator::stopAutoMode); stopAuto.disableProperty().bind(mediator.autoModeRunningProperty().not()); + // P3b/B11+B15: the buttons 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. + goalBackToolbarButton = toolbarButton("Undo the last rule application.", + IconFactoryF.Key.GOAL_BACK, mediator::goalBack); + pruneToolbarButton = toolbarButton("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(startAuto, stopAuto, - toolbarButton("Goal Back", IconFactoryF.Key.GOAL_BACK, mediator::goalBack), - toolbarButton("Prune Proof", IconFactoryF.Key.PRUNE, mediator::pruneProof)); + bar.getItems().addAll(startAuto, stopAuto, goalBackToolbarButton, pruneToolbarButton); return bar; } @@ -4129,6 +4260,8 @@ private void updateProofStatus() { } 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; @@ -4141,6 +4274,49 @@ private void updateProofStatus() { } } + /** + * 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); } From e2b8f47bebb8e68b7fbcdaa4ac677cc5e16e8aa2 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Sat, 10 Oct 2026 17:24:48 +0200 Subject: [PATCH 152/281] =?UTF-8?q?P3c:=20A6=20=E2=80=94=20Create=20Github?= =?UTF-8?q?=20Issue=20with=20bug=20template=20(URL=20built=20in=20MainWind?= =?UTF-8?q?owF)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../de/uka/ilkd/key/gui/fx/MainWindowF.java | 117 +++++++++++++++++- 1 file changed, 115 insertions(+), 2 deletions(-) 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 index a91e30f04a7..f4c1dc8e331 100644 --- 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 @@ -7,6 +7,8 @@ import java.io.File; import java.io.IOException; 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; @@ -15,10 +17,12 @@ 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.function.Consumer; +import java.util.stream.Collectors; import javafx.animation.KeyFrame; import javafx.animation.Timeline; import javafx.application.Platform; @@ -148,6 +152,7 @@ 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.pp.NotationInfo; import de.uka.ilkd.key.pp.PosInSequent; import de.uka.ilkd.key.proof.Goal; @@ -3979,8 +3984,7 @@ private Menu buildAboutMenu() { menuItem("Send Feedback…", "de.uka.ilkd.key.gui.actions.MenuSendFeedackAction", () -> FeedbackDialogF.show(stage)), menuItem("Create Github Issue", - "de.uka.ilkd.key.gui.actions.CreateGithubIssueAction", - () -> HelpFacadeF.openExternal(GITHUB_ISSUE_URL)), + "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; @@ -4206,6 +4210,115 @@ private void resetLayout() { workspace.restoreFactoryDefault(); } + /** + * 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 From f42afc790981af514f568d1fc6b93ac908c8bed4 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Sat, 10 Oct 2026 17:25:58 +0200 Subject: [PATCH 153/281] =?UTF-8?q?P3c:=20A7=20=E2=80=94=20save=20feedback?= =?UTF-8?q?=20ZIP=20(bug=20description,=20version,=20system=20properties,?= =?UTF-8?q?=20logs)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../key/gui/fx/dialogs/FeedbackDialogF.java | 161 +++++++++++++++++- 1 file changed, 156 insertions(+), 5 deletions(-) diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/dialogs/FeedbackDialogF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/dialogs/FeedbackDialogF.java index d2b3e703197..5672ddc1d3a 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/dialogs/FeedbackDialogF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/dialogs/FeedbackDialogF.java @@ -4,12 +4,22 @@ * SPDX-License-Identifier: GPL-2.0-or-later */ package de.uka.ilkd.key.gui.fx.dialogs; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; import java.time.Duration; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; import javafx.geometry.Insets; import javafx.scene.Scene; import javafx.scene.control.Button; @@ -19,12 +29,15 @@ import javafx.scene.input.KeyCode; import javafx.scene.layout.BorderPane; import javafx.scene.layout.VBox; +import javafx.stage.FileChooser; import javafx.stage.Modality; 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 de.uka.ilkd.key.settings.PathConfig; +import de.uka.ilkd.key.util.KeYConstants; import de.uka.ilkd.key.util.KeYResourceManager; import org.slf4j.Logger; @@ -42,13 +55,16 @@ * version, system properties, … {@code saveMetaData}) and sends it as * {@code Content-Type: application/zip} after an "Ready to send?" confirmation; this port sends * the bare description as {@code text/plain; charset=UTF-8} (with the same {@code KeY-Version} - * header) — the metadata collection is deliberately not ported, {@code // menu:} - * KNOWN-DEFERRED. + * header) — the metadata collection is deliberately not ported for the send path, + * {@code // menu:} KNOWN-DEFERRED. *

      • Like the Swing original the send is performed off the UI thread * ({@code sendReport}, the Swing version blocks the EDT) — here via * {@linkplain HttpClient#sendAsync an asynchronous request}; failures surface as error toasts.
      • - *
      • The Swing "save ZIP to file" alternative (Swing {@code saveZIP}) is not ported (it exists - * only in the Swing dialog in front of the send step).
      • + *
      • The Swing "save ZIP to file" alternative (Swing {@code saveZIP}) is ported as the + * "Save ZIP…" button (A7, P3c): the archive holds the bug description, the KeY version, the + * system properties and the {@code key_*.log} log files of the current run — the portable + * subset of the Swing metadata items (no proof/settings/goal payload, documented in + * {@link #writeLogArchive}).
      • * */ public final class FeedbackDialogF { @@ -91,11 +107,18 @@ public static void show(Window owner) { Button send = new Button("Send"); send.setDefaultButton(true); + // A7 (P3c): the Swing "Save Feedback..." alternative (SendFeedbackAction.saveZIP) as a + // second action in the button bar — the archive is written via {@link #writeLogArchive}. + Button saveZip = new Button("Save ZIP…"); + saveZip.setTooltip(new javafx.scene.control.Tooltip( + "Information about the current run are saved to a zip file. " + + "This file can be also used when reporting a bug via e-mail.")); Button cancel = new Button("Cancel"); ButtonBar.setButtonData(send, ButtonBar.ButtonData.OK_DONE); ButtonBar.setButtonData(cancel, ButtonBar.ButtonData.CANCEL_CLOSE); + ButtonBar.setButtonData(saveZip, ButtonBar.ButtonData.LEFT); ButtonBar buttons = new ButtonBar(); - buttons.getButtons().addAll(send, cancel); + buttons.getButtons().addAll(send, saveZip, cancel); VBox center = new VBox(10, hint, message); center.setPadding(new Insets(12)); @@ -118,10 +141,138 @@ public static void show(Window owner) { dialog.close(); sendReport(owner, message.getText()); }); + saveZip.setOnAction(e -> { + saveZipToFile(owner, message.getText()); + dialog.close(); // Swing: saveZIP(...) then dispose() + }); cancel.setOnAction(e -> dialog.close()); dialog.showAndWait(); } + /** + * A7 (P3c): saves the feedback together with run metadata as a ZIP archive, chosen through a + * save dialog (Swing {@code SendFeedbackAction.saveZIP} + the "Save Feedback..." button). + * On success an INFO toast with the target file is shown (Swing + * {@code JOptionPane.showMessageDialog}; the FX port follows the toast convention). + */ + private static void saveZipToFile(Window owner, String message) { + try { + FileChooser chooser = new FileChooser(); + chooser.setTitle("Save Feedback"); + chooser.setInitialFileName("key-feedback.zip"); + chooser.getExtensionFilters().add( + new FileChooser.ExtensionFilter("ZIP archives", "*.zip")); + File file = chooser.showSaveDialog(owner); + if (file == null) { + return; // dialog cancelled + } + writeLogArchive(file.toPath(), message); + LOGGER.info("Feedback archive written to {}", file); + NotificationManagerF.getInstance().notify( + "Your message has been saved to the file " + file.getAbsolutePath() + ".\n" + + "If you want to report a bug, you can enclose this file in an e-mail to " + + FEEDBACK_RECIPIENT + ".", + Kind.INFO); + } catch (IOException e) { + LOGGER.error("Saving the feedback archive failed", e); + NotificationManagerF.getInstance().notify( + "Saving the feedback archive failed: " + e.getMessage(), Kind.ERROR); + } + } + + /** + * Writes the feedback ZIP archive (Swing {@code SendFeedbackAction.saveMetaDataToFile} with + * the portable subset of the {@code items} the FX port collects): + *
          + *
        • {@code bugDescription.txt} — the entered description (Swing {@code bugDescription.txt} + * entry),
        • + *
        • {@code keyVersion.txt} — {@link KeYConstants#VERSION} (Swing {@code VersionItem}),
        • + *
        • {@code systemProperties.txt} — the JVM system properties in the Swing + * {@code SystemPropertiesItem} {@code Properties.list} format,
        • + *
        • the {@code key_*.log} files of the current run from + * {@link PathConfig#currentPaths}{@code .logDirectory} (the Swing {@code StacktraceItem} / + * {@code FaultyFileItem} / proof and settings items are {@code // menu:} KNOWN-DEFERRED).
        • + *
        + * Exposed for the {@code key.fx.verify.uicontrol} self test ({@link #verifyLogArchive}). + * + * @param target the ZIP file to write + * @param message the bug description to store as {@code bugDescription.txt} + * @throws IOException when the archive cannot be written + */ + public static void writeLogArchive(Path target, String message) throws IOException { + try (ZipOutputStream zip = + new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(target.toFile())))) { + writeEntry(zip, "bugDescription.txt", message.getBytes(StandardCharsets.UTF_8)); + writeEntry(zip, "keyVersion.txt", + KeYConstants.VERSION.getBytes(StandardCharsets.UTF_8)); + writeEntry(zip, "systemProperties.txt", systemPropertiesText()); + Path logDir = PathConfig.currentPaths.logDirectory; + LOGGER.debug("Feedback archive: collecting log files from {}", logDir); + if (logDir != null && Files.isDirectory(logDir)) { + try (var logs = Files.list(logDir)) { + for (Path log : logs.filter(p -> p.getFileName().toString().startsWith("key_") + && p.getFileName().toString().endsWith(".log")).toList()) { + LOGGER.debug("Feedback archive: adding log file {}", log); + writeEntry(zip, log.getFileName().toString(), Files.readAllBytes(log)); + } + } + } + } + } + + /** Writes one entry with the given name and content to the ZIP stream. */ + private static void writeEntry(ZipOutputStream zip, String name, byte[] content) + throws IOException { + zip.putNextEntry(new ZipEntry(name)); + zip.write(content); + zip.closeEntry(); + } + + /** + * A7 (P3c): self test of {@link #writeLogArchive} — writes an archive with a fixed message + * into a temporary directory and checks the expected entries (bug description, version, + * system properties, {@code key_*.log} files). + * + * @return a self-test report ending in {@code PASS} or {@code FAIL} + */ + public static String verifyLogArchive() { + try { + Path dir = Files.createTempDirectory("keyfx-feedback-archive-"); + Path zip = dir.resolve("feedback-test.zip"); + writeLogArchive(zip, "Seam self test: bug description"); + boolean hasDescription = false; + boolean hasVersion = false; + boolean hasSystemProperties = false; + int logEntries = 0; + try (java.util.zip.ZipFile zf = new java.util.zip.ZipFile(zip.toFile())) { + var entries = zf.entries(); + while (entries.hasMoreElements()) { + String name = entries.nextElement().getName(); + hasDescription |= name.equals("bugDescription.txt"); + hasVersion |= name.equals("keyVersion.txt"); + hasSystemProperties |= name.equals("systemProperties.txt"); + logEntries += name.startsWith("key_") && name.endsWith(".log") ? 1 : 0; + } + } + boolean pass = hasDescription && hasVersion && hasSystemProperties && logEntries > 0; + return "entries bugDescription=" + hasDescription + " keyVersion=" + hasVersion + + " systemProperties=" + hasSystemProperties + " logs=" + logEntries + " " + + (pass ? "PASS" : "FAIL"); + } catch (IOException e) { + LOGGER.warn("Log archive self test failed", e); + return "Feedback archive self test failed: " + e.getMessage() + " FAIL"; + } + } + + /** The JVM system properties in the Swing {@code Properties.list} line format. */ + private static byte[] systemPropertiesText() { + StringWriter sw = new StringWriter(); + try (PrintWriter pw = new PrintWriter(sw)) { + System.getProperties().list(pw); + } + return sw.toString().getBytes(StandardCharsets.UTF_8); + } + /** * Posts the feedback text to the {@linkplain #REPORT_URL report server} (Swing * {@code SendFeedbackAction.sendReport}): the {@code http.client} POST carries the same From fbec4509c60cd6eab240ee77554ed9cd8cb96f6a Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Sat, 10 Oct 2026 17:27:41 +0200 Subject: [PATCH 154/281] =?UTF-8?q?P3c:=20A8=20=E2=80=94=20Save=20as=20Def?= =?UTF-8?q?ault=20in=20tooltip=20options=20dialog?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../fx/settings/ToolTipOptionsDialogF.java | 105 ++++++++++++++++-- 1 file changed, 94 insertions(+), 11 deletions(-) diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/ToolTipOptionsDialogF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/ToolTipOptionsDialogF.java index dac56b28adb..071104a805d 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/ToolTipOptionsDialogF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/ToolTipOptionsDialogF.java @@ -4,6 +4,9 @@ * SPDX-License-Identifier: GPL-2.0-or-later */ package de.uka.ilkd.key.gui.fx.settings; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; import javafx.geometry.Insets; import javafx.scene.Scene; import javafx.scene.control.Button; @@ -20,9 +23,13 @@ import javafx.stage.Window; import de.uka.ilkd.key.gui.fx.theme.ThemeManager; +import de.uka.ilkd.key.settings.PathConfig; import de.uka.ilkd.key.settings.ProofIndependentSettings; import de.uka.ilkd.key.settings.ViewSettings; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + /** * menu: MP3a — JavaFX port of the Swing {@code ViewSelector} * (key.ui/.../gui/configuration/ViewSelector.java), opened by the View>ToolTip Options… item @@ -38,12 +45,16 @@ *
      • "pretty-print whole Taclet" (Swing {@code showWholeTacletCB}, * {@code ViewSettings.getShowWholeTaclet}).
      • * - * The Swing "Save as Default" button (persisting via {@code ProofIndependentSettings.saveSettings}) - * is deliberately not ported — the default settings are written by the FX settings dialog - * (SettingsManagerF), which exposes the same options. + * The Swing "Save as Default" button ({@code ViewSelector.java:116-130}: write the three + * settings into {@code ProofIndependentSettings.DEFAULT_INSTANCE}, persist them via + * {@code ProofIndependentSettings.saveSettings()} and close) is ported (A8, P3c) so the tooltip + * defaults survive a restart — the {@link SettingsManagerF} settings dialog does not expose these + * tooltip options, contrary to the earlier note. */ public final class ToolTipOptionsDialogF extends javafx.stage.Stage { + private static final Logger LOGGER = LoggerFactory.getLogger(ToolTipOptionsDialogF.class); + private ToolTipOptionsDialogF(Window owner) { setTitle("Maximum line number for tooltips"); initOwner(owner); @@ -90,24 +101,29 @@ private ToolTipOptionsDialogF(Window owner) { // Integer.parseInt throws on invalid input; like Swing's intended behavior the value then // falls back to the previously stored setting instead of being applied. okButton.setOnAction(e -> { - int maxSteps; - try { - maxSteps = Integer.parseInt(maxLinesField.getText()); - } catch (NumberFormatException nfe) { - maxSteps = viewSettings.getMaxTooltipLines(); - } - viewSettings.setMaxTooltipLines(maxSteps); + viewSettings.setMaxTooltipLines( + parseMaxLines(maxLinesField, viewSettings)); viewSettings.setShowWholeTaclet(showWholeTacletBox.isSelected()); viewSettings.setShowUninstantiatedTaclet(showUninstantiatedTacletBox.isSelected()); close(); }); + Button saveButton = new Button("Save as Default"); + // A8 (P3c): the Swing "Save as Default" button (ViewSelector.java:116-130) — writes the + // same three settings, persists them via ProofIndependentSettings.saveSettings() and + // closes (the value survives a restart). + saveButton.setOnAction(e -> { + applyAsDefault(viewSettings, parseMaxLines(maxLinesField, viewSettings), + showWholeTacletBox.isSelected(), showUninstantiatedTacletBox.isSelected()); + close(); + }); Button cancelButton = new Button("Cancel"); // Swing Cancel: close without writing (ViewSelector.java:133-137) cancelButton.setOnAction(e -> close()); ButtonBar.setButtonData(okButton, ButtonBar.ButtonData.OK_DONE); ButtonBar.setButtonData(cancelButton, ButtonBar.ButtonData.CANCEL_CLOSE); + ButtonBar.setButtonData(saveButton, ButtonBar.ButtonData.LEFT); ButtonBar buttonBar = new ButtonBar(); - buttonBar.getButtons().addAll(okButton, cancelButton); + buttonBar.getButtons().addAll(okButton, saveButton, cancelButton); BorderPane.setMargin(buttonBar, new Insets(10)); BorderPane root = new BorderPane(); @@ -132,4 +148,71 @@ private ToolTipOptionsDialogF(Window owner) { public static void show(Window owner) { new ToolTipOptionsDialogF(owner).show(); } + + /** Parses the numeric tooltip-line field; on invalid input the stored value is kept. */ + private static int parseMaxLines(TextField field, ViewSettings viewSettings) { + try { + return Integer.parseInt(field.getText()); + } catch (NumberFormatException nfe) { + return viewSettings.getMaxTooltipLines(); + } + } + + /** + * A8 (P3c): writes the three tooltip options and persists them as default + * (Swing {@code ViewSelector} Save as Default: {@code ProofIndependentSettings.saveSettings()}, + * ViewSelector.java:118-130). + * + * @param viewSettings the (proof-independent) view settings to write + * @param maxLines parsed maximum tooltip line count + * @param showWholeTaclet {@code showWholeTaclet} flag + * @param showUninstantiatedTaclet {@code showUninstantiatedTaclet} flag + */ + static void applyAsDefault(ViewSettings viewSettings, int maxLines, boolean showWholeTaclet, + boolean showUninstantiatedTaclet) { + ProofIndependentSettings settings = ProofIndependentSettings.DEFAULT_INSTANCE; + settings.getViewSettings().setMaxTooltipLines(maxLines); + settings.getViewSettings().setShowWholeTaclet(showWholeTaclet); + settings.getViewSettings().setShowUninstantiatedTaclet(showUninstantiatedTaclet); + // temporary solution, stores more than wanted %%%% (comment kept from the Swing original) + settings.saveSettings(); + } + + /** + * A8 (P3c): self test of the "Save as Default" persistence behind the button — writes a + * distinctive value, verifies that the settings file on disk contains it, and restores the + * previous values (also persisted, so the test leaves no trace). + * + * @return a self-test report ending in {@code PASS} or {@code FAIL} + */ + public static String verifySaveAsDefault() { + ProofIndependentSettings settings = ProofIndependentSettings.DEFAULT_INSTANCE; + ViewSettings viewSettings = settings.getViewSettings(); + int originalMax = viewSettings.getMaxTooltipLines(); + boolean originalWhole = viewSettings.getShowWholeTaclet(); + boolean originalUninst = viewSettings.getShowUninstantiatedTaclet(); + try { + int testValue = originalMax == 4242 ? 4243 : 4242; + applyAsDefault(viewSettings, testValue, !originalWhole, !originalUninst); + // the persisted file is the .json (or the legacy .props) proof-independent settings + Path settingsPath = PathConfig.currentPaths.proofIndependentSettings; + if (!Files.exists(settingsPath)) { + settingsPath = settingsPath.resolveSibling( + settingsPath.getFileName().toString().replace(".json", ".props")); + } + String content = Files.exists(settingsPath) ? Files.readString(settingsPath) : ""; + boolean persisted = content.contains(String.valueOf(testValue)); + String verdict = persisted ? "PASS" : "FAIL"; + return "save-as-default value=" + testValue + " persisted=" + persisted + + " file=" + settingsPath + " " + verdict; + } catch (IOException e) { + LOGGER.warn("Save-as-default self test failed", e); + return "Save-as-default self test failed: " + e.getMessage() + " FAIL"; + } finally { + viewSettings.setMaxTooltipLines(originalMax); + viewSettings.setShowWholeTaclet(originalWhole); + viewSettings.setShowUninstantiatedTaclet(originalUninst); + settings.saveSettings(); + } + } } From 36b0e97e78f4a0c6e044b26e206793da96297aa9 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Sat, 10 Oct 2026 17:29:10 +0200 Subject: [PATCH 155/281] =?UTF-8?q?P3c:=20D31=20=E2=80=94=20sequent=20prin?= =?UTF-8?q?t=20line=20width=20from=20the=20viewport=20(Swing=20computeLine?= =?UTF-8?q?WidthFor)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../de/uka/ilkd/key/gui/fx/MainWindowF.java | 6 ++ .../key/gui/fx/nodeviews/SequentViewF.java | 88 ++++++++++++++++++- 2 files changed, 92 insertions(+), 2 deletions(-) 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 index f4c1dc8e331..b76777a31ac 100644 --- 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 @@ -938,6 +938,12 @@ protected KeYEnvironment call() throws Exception { 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); } if (System.getProperty("key.fx.verify.sequentsearch") != null && System.getProperty("key.fx.demo.autoprove.live") == null) { diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentViewF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentViewF.java index cf6ecafb931..b80a8f4fa79 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentViewF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentViewF.java @@ -214,6 +214,20 @@ public String printedText() { private final ScrollPane scrollPane = new ScrollPane(); private final TextFlow textFlow = new TextFlow(); + + /** + * D31 (P3c): hidden unmanaged {@code Text("W")} probe used to measure the character width of + * the mono font for {@link #computeLineWidth()} (Swing {@code SequentView} + * {@code FontMetrics.charWidth('W')}; not attached to any layout, it only resolves the font). + */ + private final Text charProbe = new Text("W"); + + /** + * D31 (P3c): the last used print line width (Swing {@code SequentView.lineWidth}); 0 until + * the first measurement, {@link #computeLineWidth()} falls back to + * {@link PosTableLayouter#DEFAULT_LINE_WIDTH} then. + */ + private int lineWidth; /** * Translucent rectangles painted behind the printed text (Swing's highlighter layer): the * update-operator highlight of the current printing. The pane fills the same area as the @@ -354,6 +368,16 @@ public SequentViewF() { // rebuild them directly textFlow.layoutBoundsProperty() .addListener((obs, oldBounds, newBounds) -> FxUtil.runLater(this::rebuildOverlays)); + // D31 (P3c): re-measure the print line width when the viewport width changes (Swing + // SequentViewChangeListener on the visible-rect change, SequentView.java:745-749, which + // recomputes the line width and re-displays); prints are only re-run when the width + // really differs (sub-pixel jitter would otherwise reprint on every layout pass) + scrollPane.viewportBoundsProperty().addListener((obs, oldBounds, newBounds) -> { + if (newBounds != null + && ((int) computeLineWidth()) != lineWidth) { + FxUtil.runLater(this::printSequent); + } + }); // the tooltip shows when the mouse pauses over a term (Swing ToolTipManager) hoverTooltipDelay.setOnFinished(event -> showHoverTooltip()); @@ -693,8 +717,8 @@ public void printSequent() { return; } filter.setSequent(selectedNode.sequent()); - // TODO(M2): compute the line width from the font metrics and the viewport width - printer.update(filter, PosTableLayouter.DEFAULT_LINE_WIDTH); + lineWidth = computeLineWidth(); + printer.update(filter, lineWidth); printed = printer.result(); LOGGER.debug("printSequent: chars={} filter={} antec={} succ={}", printed.length(), filter .getClass().getSimpleName(), @@ -712,6 +736,66 @@ public void printSequent() { rebuildOverlays(); } + /** + * D31 (P3c): computes the print line width from the {@link #scrollPane} viewport width and + * the mono font's character width (Swing {@code SequentView.computeLineWidthFor}, + * SequentView.java:710-719: {@code (int)(visibleWidth / charWidth('W'))}, minus one when the + * result exceeds one character). Before the first layout pass the viewport has no width, so + * the fallback {@link PosTableLayouter#DEFAULT_LINE_WIDTH} is used then. + * + * @return the number of characters per printed line + */ + private int computeLineWidth() { + double viewportWidth = scrollPane.getViewportBounds().getWidth(); + if (viewportWidth <= 0) { + // not laid out yet (or collapsed): keep the previous width, fall back to the Swing + // default 55 characters + return lineWidth != 0 ? lineWidth : PosTableLayouter.DEFAULT_LINE_WIDTH; + } + double charWidth = measureCharWidth(); + if (charWidth <= 0) { + return lineWidth != 0 ? lineWidth : PosTableLayouter.DEFAULT_LINE_WIDTH; + } + int maxChars = (int) (viewportWidth / charWidth); + if (maxChars > 1) { + maxChars -= 1; + } + // Swing returns maxChars unchecked; the printer must not see 0 (or negative) widths, so + // one character is the floor + return Math.max(maxChars, 1); + } + + /** + * D31 (P3c): measures the width of one {@code W} in the mono display font (Swing + * {@code FontMetrics.charWidth('W')} of {@code SequentView.computeLineWidthFor}) via the + * hidden unmanaged {@link #charProbe}; the probe's {@code getLayoutBounds()} is computed on + * demand from the font, without a scene attachment. + * + * @return the character width in pixels, or {@code 0} when the font is not resolvable yet + */ + private double measureCharWidth() { + charProbe.setFont(ConfigF.DEFAULT.monoFont()); + return charProbe.getLayoutBounds().getWidth(); + } + + /** + * D31 (P3c): self test of the viewport line width (Swing {@code computeLineWidthFor}) — + * asserts that the computed width is positive, below the Swing default when the viewport is + * narrower, and stable across two calls with the same viewport size. Hooked into the + * {@code key.fx.verify.sequent} block of {@code MainWindowF}. + * + * @return a self-test report ending in {@code PASS} or {@code FAIL} + */ + public String verifyViewportLineWidth() { + int width = computeLineWidth(); + double viewportWidth = scrollPane.getViewportBounds().getWidth(); + double charWidth = measureCharWidth(); + boolean pass = width > 0 && viewportWidth > 0 && charWidth > 0 + && (int) (viewportWidth / charWidth) >= width; + return "viewport=" + (int) viewportWidth + " char=" + (int) charWidth + " lineWidth=" + + width + (pass ? " PASS" : " FAIL"); + } + /** * Switches the syntax highlighting of the printed sequent (Swing View menu "Syntax * Highlighting"). From 56ba74a05a8491adb3d4784c0d7cc5807b43e5c2 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Sat, 10 Oct 2026 17:29:57 +0200 Subject: [PATCH 156/281] =?UTF-8?q?P3c:=20D33=20=E2=80=94=20per-view=20fon?= =?UTF-8?q?t=20families/sizes=20in=20ConfigF=20(Swing=20KEY=5FFONT=5F*)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../key/gui/fx/configuration/ConfigF.java | 41 ++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) 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 index 7c89d8d2faf..ac76d442e20 100644 --- 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 @@ -15,9 +15,26 @@ * {@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 }; @@ -55,11 +72,33 @@ public Font monoFont() { return Font.font("Monospaced", baseSize()); } - /** @return a font for the proof tree view */ + /** + * @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; From 43af5907f48de129b4b2f13971ca1dd9a0df85ba Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Sat, 10 Oct 2026 17:33:05 +0200 Subject: [PATCH 157/281] =?UTF-8?q?P3c:=20D34=20=E2=80=94=20open=20node=20?= =?UTF-8?q?in=20separate=20sequent=20buffer=20(SequentViewDockF)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../de/uka/ilkd/key/gui/fx/MainWindowF.java | 29 +++++++++ .../key/gui/fx/docking/SequentViewDockF.java | 63 +++++++++++++++++++ .../key/gui/fx/prooftree/ProofTreeViewF.java | 30 ++++++++- 3 files changed, 121 insertions(+), 1 deletion(-) create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/docking/SequentViewDockF.java 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 index b76777a31ac..cdd732c0645 100644 --- 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 @@ -102,6 +102,7 @@ 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; @@ -897,6 +898,9 @@ protected KeYEnvironment call() throws Exception { // 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 @@ -4216,6 +4220,31 @@ 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 diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/docking/SequentViewDockF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/docking/SequentViewDockF.java new file mode 100644 index 00000000000..49e3b77bccc --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/docking/SequentViewDockF.java @@ -0,0 +1,63 @@ +/* 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.docking; + +import java.util.List; +import java.util.Objects; + +import de.uka.ilkd.key.gui.fx.nodeviews.SequentViewF; +import de.uka.ilkd.key.proof.Node; + +/** + * D34 (P3c): JavaFX port of the Swing {@code SequentViewDock} + * (key.ui/.../gui/nodeviews/SequentViewDock.java) — a dockable showing the sequent of one + * arbitrary proof node in a separate buffer (tab), opened by the proof tree popup's + * "Open Node in Separate Buffer" entry. + *

          + *
        • the {@link SequentViewF} is created fresh per node and immediately prints the node's + * sequent (Swing constructor: {@code InnerNodeView} + {@code printSequent()});
        • + *
        • the title is {@code "Node: " + node.serialNr()} (Swing {@code setTitleText});
        • + *
        • the tab context menu carries the title action "Jump into Tree" (Swing + * {@code JumpIntoTreeAction} added via {@code addAction}): the given {@code jumpIntoTree} + * callback switches the main selection back to this node (wired by {@code MainWindowF}, which + * owns the selection model).
        • + *
        + */ +public final class SequentViewDockF extends SimpleDockable { + + private final SequentViewF sequentView; + + /** D34: callback selecting this node in the main proof tree (Swing JumpIntoTreeAction). */ + private final Runnable jumpIntoTree; + + /** + * Creates the dockable for the given proof node. + * + * @param node the proof node whose sequent is shown + * @param jumpIntoTree callback selecting the node in the main proof tree (Swing + * {@code JumpIntoTreeAction.actionPerformed}: switch the selection to this node, + * switching the proof first if needed) + */ + public SequentViewDockF(Node node, Runnable jumpIntoTree) { + super("node-buffer-" + node.serialNr(), "Node: " + node.serialNr(), + new SequentViewF()); + this.sequentView = (SequentViewF) getContent(); + this.jumpIntoTree = Objects.requireNonNull(jumpIntoTree); + sequentView.display(node); + sequentView.printSequent(); + } + + @Override + public List getTitleActions() { + return List.of(new DockTitleActionF("Jump into Tree", jumpIntoTree)); + } + + /** + * @return the node displaying the sequent buffer (self test) + */ + public SequentViewF getSequentView() { + return sequentView; + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/prooftree/ProofTreeViewF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/prooftree/ProofTreeViewF.java index 8f0001eb5f6..24b61b7a7de 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/prooftree/ProofTreeViewF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/prooftree/ProofTreeViewF.java @@ -16,6 +16,7 @@ import java.util.Set; import java.util.WeakHashMap; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; import javafx.geometry.Insets; import javafx.scene.control.Button; import javafx.scene.control.CheckMenuItem; @@ -291,6 +292,13 @@ private static final class ViewState { /** the node the popup was invoked on (Swing {@code ProofTreeContext.invokedNode}) */ private Node popupNode; + /** + * D34 (P3c): opens a separate sequent buffer for a proof-tree node, installed by + * {@code MainWindowF} (Swing {@code SequentViewDock.OpenCurrentNodeAction} creates the dock + * and adds it to the dock control); {@code null} before the wiring. + */ + private Consumer openNodeInSeparateBuffer; + /** * Listens to structural changes of the displayed proof and schedules a coalesced rebuild on * the FX thread — the analogue of the Swing view's {@code GUIProofTreeModel} update calls. @@ -1381,6 +1389,13 @@ private ContextMenu createContextMenu() { MenuItem notesItem = actionItem("Edit Notes...", null, this::editNotes); MenuItem subtreeStatsItem = actionItem("Show Subtree Statistics", IconFactoryF.Key.STATISTICS, this::showSubtreeStatistics); + // D34 (P3c): Swing ProofTreePopupFactory adds OpenCurrentNodeAction as the final popup + // entry (ProofTreePopupFactory.java:143) — opens the node's sequent in a separate buffer + MenuItem openNodeItem = actionItem("Open Node in Separate Buffer", null, () -> { + if (openNodeInSeparateBuffer != null && popupNode != null) { + openNodeInSeparateBuffer.accept(popupNode); + } + }); ContextMenu menu = new ContextMenu(hideIntermediateItem, onlyInteractiveItem, new SeparatorMenuItem(), hideClosedItem, hideInteractiveItem, linearizeItem, expandOssItem, new SeparatorMenuItem(), expandAllItem, collapseAllItem, @@ -1399,7 +1414,7 @@ expandOssItem, new SeparatorMenuItem(), expandAllItem, collapseAllItem, new SeparatorMenuItem(), actionItem("Set All Goals Below to Interactive", null, () -> setGoalsBelow(false)), actionItem("Set All Goals Below to Automatic", null, () -> setGoalsBelow(true)), - new SeparatorMenuItem(), subtreeStatsItem); + new SeparatorMenuItem(), subtreeStatsItem, openNodeItem); menu.setOnShowing(e -> { // pick up changes made elsewhere (e.g. by the classic UI sharing the settings) hideIntermediateItem.setSelected(hideIntermediateSteps()); @@ -1415,6 +1430,8 @@ expandOssItem, new SeparatorMenuItem(), expandAllItem, collapseAllItem, notesItem.setDisable(proof == null); subtreeStatsItem.setDisable(proof == null); pruneItem.setDisable(!isPrunable(popupNode)); + // D34: Opening a node buffer needs a proof node to open + openNodeItem.setDisable(proof == null || popupNode == null); }); return menu; } @@ -1648,6 +1665,17 @@ public void setActionContext(KeYMediatorF mediator, ProofControl proofControl) { } } + /** + * D34 (P3c): installs the "Open Node in Separate Buffer" popup handler (Swing + * {@code SequentViewDock.OpenCurrentNodeAction}); the consumer opens a + * {@link de.uka.ilkd.key.gui.fx.docking.SequentViewDockF} for the given node. + * + * @param openNode the consumer handling the popup action, or {@code null} to uninstall + */ + public void setOpenNodeInSeparateBuffer(Consumer openNode) { + this.openNodeInSeparateBuffer = openNode; + } + /** * C23: runs the automatic strategy on the subtree of the node the popup was invoked on * (Swing {@code RunStrategyOnNodeUserAction}): all enabled goals below the node, or the node From 1331f77919519d66420940037a7678b5ad20b523 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Sat, 10 Oct 2026 17:35:01 +0200 Subject: [PATCH 158/281] =?UTF-8?q?P3c:=20D37=20=E2=80=94=20status=20bar?= =?UTF-8?q?=20progress=20bar=20and=20task=20progress=20wiring?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../de/uka/ilkd/key/gui/fx/MainWindowF.java | 88 ++++++++++++++++++- .../gui/fx/WindowUserInterfaceControlF.java | 29 +++--- 2 files changed, 106 insertions(+), 11 deletions(-) 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 index cdd732c0645..d84c0f01398 100644 --- 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 @@ -49,6 +49,7 @@ 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.Tab; @@ -277,6 +278,17 @@ in order to generate and prove the proof obligations for the correctness of (non 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 @@ -1826,12 +1838,79 @@ 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(); } @@ -4098,6 +4177,13 @@ private HBox buildStatusBar() { 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); @@ -4105,7 +4191,7 @@ private HBox buildStatusBar() { statusRight.setTextOverrun(OverrunStyle.ELLIPSIS); statusRight.setMinWidth(0); statusRight.setMinHeight(0); - bar.getChildren().addAll(statusLeft, spacer, statusRight); + 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, after the theme/font-size label (Swing // MainWindow.createStatusBar / KeYGuiExtensionFacade.getStatusLineComponents, 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 index 7e0678588b7..34025d76fe1 100644 --- 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 @@ -279,8 +279,10 @@ public void progressStopped(Object sender) { /** Port of Swing WindowUserInterfaceControl.java:157-160. */ @Override public void reportStatus(Object sender, String status, int progress) { - // the FX status bar has no progress bar yet; the message is shown regardless - reportStatus(sender, status); + // 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). */ @@ -306,13 +308,14 @@ public void reportException(Object sender, @Nullable ProofOblInput input, Except } /** - * Port of Swing WindowUserInterfaceControl.java:294-299 (status line progress): the FX status - * bar has no progress bar yet, so the position is logged only. + * 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); - LOGGER.debug("Task progress: {}", position); + // D37 (P3c): Swing gets the progress bar via getStatusLine().setProgress(position) + FxUtil.runLater(() -> mainWindow.setTaskProgressValue(position)); } /** @@ -322,19 +325,23 @@ public void taskProgress(int position) { @Override public void taskStarted(TaskStartedInfo info) { super.taskStarted(info); - reportStatus(this, info.message()); + // 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-315 (progress bar maximum). */ + /** Port of Swing WindowUserInterfaceControl.java:307-309 (progress bar maximum). */ @Override public void setMaximum(int maximum) { - LOGGER.debug("Task progress maximum: {}", 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) { - LOGGER.debug("Task progress: {}", progress); + // D37 (P3c): Swing gets the position via getStatusLine().setProgress(progress) + FxUtil.runLater(() -> mainWindow.setTaskProgressValue(progress)); } // ------------------------------------------------------------------ @@ -390,7 +397,9 @@ private void taskFinishedInternal(TaskFinishedInfo info) { mainWindow.setStatusLine(info.toString()); } else if (info != null && info.getSource() instanceof ProofMacro macro) { if (!isAtLeastOneMacroRunning()) { - // Swing hides the status progress here (:213); the FX status bar has none yet + // 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() From 1ed6d9e4ac2d3b336e81d7f58ff60ba8b2e19e57 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Sat, 10 Oct 2026 17:36:23 +0200 Subject: [PATCH 159/281] =?UTF-8?q?P3c:=20uicontrol=20self=20test=20?= =?UTF-8?q?=E2=80=94=20P3c=20steps=20(A4/A6/A7/A8/D34/D37)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../de/uka/ilkd/key/gui/fx/MainWindowF.java | 16 ++++ .../ilkd/key/gui/fx/UiControlSelfTestF.java | 88 ++++++++++++++++++- 2 files changed, 100 insertions(+), 4 deletions(-) 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 index d84c0f01398..f073a17c0ff 100644 --- 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 @@ -1921,6 +1921,22 @@ 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 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 index 8e9adef12c5..2e1108ede04 100644 --- 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 @@ -4,8 +4,16 @@ * 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 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; @@ -24,7 +32,10 @@ *
      • {@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.
      • + * 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. */ @@ -32,6 +43,10 @@ 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() { } @@ -77,14 +92,79 @@ public static void run(MainWindowF mainWindow) { 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())); + 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"; From 567a724ff2a35282d443aba01a2ceba259374360 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Sat, 10 Oct 2026 17:37:14 +0200 Subject: [PATCH 160/281] =?UTF-8?q?P3b:=20B12=20=E2=80=94=20Strategy=20Mac?= =?UTF-8?q?ros=20superset=20+=20proof=20scripts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../de/uka/ilkd/key/gui/fx/MainWindowF.java | 40 ++- .../ilkd/key/gui/fx/ProofScriptWorkerF.java | 227 +++++++++++++++ .../key/gui/fx/nodeviews/ProofMacroMenuF.java | 267 ++++++++++++++++-- .../fx/nodeviews/SequentTermContextMenuF.java | 22 +- .../key/gui/fx/nodeviews/SequentViewF.java | 77 +++-- .../gui/fx/prooftree/ProofTreeVerifyF.java | 10 + .../key/gui/fx/prooftree/ProofTreeViewF.java | 116 +++++++- 7 files changed, 690 insertions(+), 69 deletions(-) create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/ProofScriptWorkerF.java 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 index ad7c1a369f8..e52e04226e9 100644 --- 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 @@ -457,12 +457,20 @@ public void selectedNodeChanged(KeYSelectionEvent ev } }; + /** + * 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 @@ -673,6 +681,14 @@ 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()}) */ @@ -2377,16 +2393,18 @@ private void runTermMenuVerification(KeYEnvironment } /** - * menu: MP8 — headless self test of the MP8a/MP8b term-menu wiring + * 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) exists and contains exactly the four macro names of - * the Automation submenu ({@link #AUTOMATION_MACROS}, same order). 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. + * 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(); @@ -2426,13 +2444,19 @@ private void runTermMenuWiringVerification(KeYEnvironment + *
      • 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/nodeviews/ProofMacroMenuF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/ProofMacroMenuF.java index aa01c13322d..fbf7770a8f0 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/ProofMacroMenuF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/ProofMacroMenuF.java @@ -4,60 +4,289 @@ * SPDX-License-Identifier: GPL-2.0-or-later */ package de.uka.ilkd.key.gui.fx.nodeviews; +import java.io.File; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import javafx.geometry.Insets; +import javafx.scene.control.Button; import javafx.scene.control.CustomMenuItem; import javafx.scene.control.Label; import javafx.scene.control.MenuItem; +import javafx.scene.control.SeparatorMenuItem; +import javafx.scene.control.TextArea; import javafx.scene.control.Tooltip; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; +import javafx.stage.FileChooser; +import javafx.stage.Modality; +import javafx.stage.Stage; import de.uka.ilkd.key.control.ProofControl; +import de.uka.ilkd.key.gui.fx.IssueDialogF; +import de.uka.ilkd.key.gui.fx.MainWindowF; +import de.uka.ilkd.key.gui.fx.ProofScriptWorkerF; import de.uka.ilkd.key.gui.fx.keyshortcuts.KeyStrokeManagerF; import de.uka.ilkd.key.macros.ProofMacro; +import de.uka.ilkd.key.nparser.KeyAst; +import de.uka.ilkd.key.nparser.ParsingFacade; +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.settings.FeatureSettings; import org.key_project.prover.sequent.PosInOccurrence; +import org.key_project.util.collection.ImmutableList; +import org.key_project.util.reflection.ClassLoaderUtil; + +import org.jspecify.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** - * menu: MP8 — shared construction of one proof-macro menu item (Swing {@code ProofMacroMenu} - * createMenuItem, ProofMacroMenu.java:143-144: item text = {@code macro.getName()}, tooltip = - * {@code macro.getDescription()}). Used by the sequent-view right-click macro popup (MP7, - * {@link SequentViewF#buildMacroPopup}) and by the term-menu "Strategy Macros" section (MP8b, - * {@link SequentTermContextMenuF}) so both present the very same macros with the same look. + * menu: MP8/B12 — shared construction of the proof-macro menu content (Swing {@code + * ProofMacroMenu}, ProofMacroMenu.java). Three user-visible surfaces are backed by this factory + * (so all three present the very same macros with the same look): + *
          + *
        • the sequent-view right-click macro popup (MP7, {@link SequentViewF#buildMacroPopup});
        • + *
        • the term-menu "Strategy Macros" section (MP8b, {@link SequentTermContextMenuF}); and
        • + *
        • the proof-tree context-menu "Strategy Macros" submenu (B12, + * {@link de.uka.ilkd.key.gui.fx.prooftree.ProofTreeViewF}).
        • + *
        + *

        + * The content is the *superset* of the Swing menu: every registered macro + * ({@link ProofMacroMenu#REGISTERED_MACROS}, the ServiceLoader list of + * {@code META-INF/services/de.uka.ilkd.key.macros.ProofMacro}) whose {@link ProofMacro#canApplyTo} + * accepts the clicked position, grouped by {@link ProofMacro#getCategory()} in first-appearance + * order with separators between the groups (null category keys form their own group), followed by + * the PROOF_SCRIPTS section ({@code "Run proof script from file..."} / {@code "Input proof + * script..."}) when the feature is active (Swing ProofMacroMenu.java:81-134). *

        - * JavaFX {@link MenuItem} has no tooltip property (unlike Swing {@code - * JMenuItem.setToolTipText}), so the item is a {@link CustomMenuItem} wrapping a - * tooltip-bearing {@link Label}. + * JavaFX {@link MenuItem} has no tooltip property (unlike Swing {@code JMenuItem.setToolTipText}), + * so each macro item is a {@link CustomMenuItem} wrapping a tooltip-bearing {@link Label} + * (Swing ProofMacroMenu.createMenuItem, ProofMacroMenu.java:143-144). */ -final class ProofMacroMenuF { +public final class ProofMacroMenuF { + + private static final Logger LOGGER = LoggerFactory.getLogger(ProofMacroMenuF.class); private ProofMacroMenuF() { } /** - * menu: MP8 — a single macro item whose action runs the macro on the selected node at the - * given position (Swing {@code ProofMacroUserAction}, ProofMacroUserAction.java:57-59: - * {@code mediator.getUI().getProofControl().runMacro(node, macro, pio)}; the core silently - * ignores the run while auto mode is active, and {@code pio} may be {@code null} — a - * sequent position may resolve to no occurrence and global macros accept that). + * P3b/B12: all registered proof macros (Swing {@code ProofMacroMenu.REGISTERED_MACROS}, + * ProofMacroMenu.java:60-61: {@code ClassLoaderUtil.loadServices(ProofMacro.class)}) — the + * superset that contains the four Automation-submenu macros of + * {@link MainWindowF#AUTOMATION_MACROS}. + */ + static final Iterable REGISTERED_MACROS = + ClassLoaderUtil.loadServices(ProofMacro.class); + + /** + * P3b/B12: the feature id gating the two proof-script entries (Swing + * {@code ProofMacroMenu.FEATURE_PROOF_SCRIPTS}, ProofMacroMenu.java:63-64). Resolved from the + * shared registry so the Swing and FX menus never register two features with the same id (the + * FeatureSettingsPanel would then list twice, see the BULK_UI_TEST handling in MainWindowF). + * Exposed for the persistent proof-tree macro submenu, which registers the live + * {@code FeatureSettings.onAndActivate} listener ({@link ProofTreeViewF}). + */ + public static final FeatureSettings.Feature PROOF_SCRIPTS_FEATURE = + FeatureSettings.Feature.FEATURES.stream().filter(f -> "PROOF_SCRIPTS".equals(f.id())) + .findFirst().orElseGet(() -> FeatureSettings.createFeature("PROOF_SCRIPTS")); + + /** + * P3b/B12: the last directory of the script file chooser (Swing + * {@code ProofScriptFromFileAction.lastDirectory}). + */ + private static Path lastScriptDirectory; + + /** + * menu: MP8 — a single macro item whose action runs the macro on the given node at the given + * position (Swing {@code ProofMacroUserAction}, ProofMacroUserAction.java:57-59: {@code + * mediator.getUI().getProofControl().runMacro(node, macro, pio)}; the core silently ignores + * the run while auto mode is active, and {@code pio} may be {@code null} — a sequent position + * may resolve to no occurrence and global macros accept that). * * @param macro the macro to run - * @param node the node the macro is started at (the mediator's selection) + * @param node the node the macro is started at (the mediator's selection for the sequent + * menus; the invoked popup node for the proof-tree menu, see ProofTreeViewF) * @param proofControl the proof control to run the macro with * @param pio the clicked {@link PosInOccurrence}, possibly {@code null} * @return the wired macro menu item */ - static MenuItem itemFor(ProofMacro macro, Node node, ProofControl proofControl, + public static MenuItem itemFor(ProofMacro macro, Node node, ProofControl proofControl, PosInOccurrence pio) { Label label = new Label(macro.getName()); Tooltip.install(label, new Tooltip(macro.getDescription())); CustomMenuItem item = new CustomMenuItem(label); item.setOnAction(e -> proofControl.runMacro(node, macro, pio)); - // shortcuts (P1): macro accelerators on the term-menu items for global applications - // (Swing ProofMacroMenu.createMenuItem, ProofMacroMenu.java:146-148: "currently only - // for global macro applications") + // shortcuts (P1): macro accelerators on the menu items for global applications (Swing + // ProofMacroMenu.createMenuItem, ProofMacroMenu.java:146-148: "currently only for global + // macro applications") if (pio == null) { KeyStrokeManagerF.getInstance().binding(macro.getClass().getName()) .ifPresent(item::setAccelerator); } return item; } + + /** + * P3b/B12: the flat, category-grouped menu content of the Swing {@code ProofMacroMenu} + * (ProofMacroMenu.java:81-134): every registered macro whose {@code canApplyTo} accepts the + * clicked position, grouped by category in first-appearance order with {@code + * SeparatorMenuItem}s between the groups, plus the PROOF_SCRIPTS section when the feature is + * active. The sequence of groups is therefore a superset of, and orders the four automation + * macros exactly like, the FX Automation submenu's {@link MainWindowF#AUTOMATION_MACROS}. + * + * @param proof the proof of the clicked node + * @param goals the enabled subtree goals of the node (Swing + * {@code node.proof().getSubtreeEnabledGoals(node)}, ProofMacroMenu.java:88-89) + * @param node the node the macros run on + * @param proofControl the proof control to run the macros with + * @param pio the clicked {@link PosInOccurrence}, possibly {@code null} + * @return the ordered menu items (macros + separators + optional script section) + */ + public static List items(Proof proof, ImmutableList goals, Node node, + ProofControl proofControl, @Nullable PosInOccurrence pio) { + // Macros are grouped according to their category (Swing ProofMacroMenu.java:84-99). + Map> groups = new LinkedHashMap<>(); + for (ProofMacro macro : REGISTERED_MACROS) { + if (macro.canApplyTo(proof, goals, pio)) { + groups.computeIfAbsent(macro.getCategory(), x -> new ArrayList<>()) + .add(itemFor(macro, node, proofControl, pio)); + } + } + List result = new ArrayList<>(); + boolean first = true; + for (List group : groups.values()) { + if (!first) { + result.add(new SeparatorMenuItem()); + } + first = false; + result.addAll(group); + } + if (FeatureSettings.isFeatureActivated(PROOF_SCRIPTS_FEATURE)) { + result.add(new SeparatorMenuItem()); + result.addAll(scriptSectionItems()); + } + return result; + } + + /** + * P3b/B12: whether at least one registered macro {@code canApplyTo} the position (Swing + * {@code ProofMacroMenu.isEmpty()}, ProofMacroMenu.java:160-162: the constructor counts the + * applicable macros; an empty menu causes the callers to fall back to the term menu). + */ + public static boolean anyApplicable(Proof proof, ImmutableList goals, + @Nullable PosInOccurrence pio) { + for (ProofMacro macro : REGISTERED_MACROS) { + if (macro.canApplyTo(proof, goals, pio)) { + return true; + } + } + return false; + } + + /** + * P3b/B12: the names of the {@code canApplyTo}-applicable macros at the given position, in + * registration order (exposed for the {@code key.fx.verify.prooftree} count seam: the + * proof-tree macro submenu must present exactly these items). + */ + public static List applicableMacroNames(Proof proof, ImmutableList goals, + @Nullable PosInOccurrence pio) { + List names = new ArrayList<>(); + for (ProofMacro macro : REGISTERED_MACROS) { + if (macro.canApplyTo(proof, goals, pio)) { + names.add(macro.getName()); + } + } + return names; + } + + /** + * P3b/B12: the two PROOF_SCRIPTS entries (Swing {@code ProofScriptFromFileAction} / {@code + * ProofScriptInputAction} created inline, ProofMacroMenu.java:115-117). The actions open the + * script file chooser / input dialog, parse the script ({@link ParsingFacade}) and start the + * {@link ProofScriptWorkerF} on the selected proof. + */ + private static List scriptSectionItems() { + MenuItem fromFile = new MenuItem("Run proof script from file..."); + fromFile.setOnAction(e -> runScriptFromFileDialog()); + MenuItem input = new MenuItem("Input proof script..."); + input.setOnAction(e -> inputScriptDialog()); + return List.of(fromFile, input); + } + + /** + * P3b/B12: the script file chooser + worker start (Swing {@code + * ProofScriptFromFileAction.actionPerformed}, ProofScriptFromFileAction.java:48-80). + */ + private static void runScriptFromFileDialog() { + MainWindowF window = MainWindowF.getInstance(); + if (window == null) { + return; + } + Path dir = lastScriptDirectory; + if (dir == null) { + Proof currentProof = window.getMediator().getSelectedProof(); + Path currentFile = currentProof == null ? null : currentProof.getProofFile(); + dir = currentFile == null ? Paths.get(".") : currentFile.getParent(); + } + FileChooser fc = new FileChooser(); + fc.setTitle("Select file to load"); + fc.setInitialDirectory(dir.toFile()); + File selectedFile = fc.showOpenDialog(window.getStage()); + if (selectedFile == null) { + return; + } + lastScriptDirectory = selectedFile.getParentFile().toPath(); + try { + KeyAst.ProofScript script = ParsingFacade.parseScript(selectedFile.toPath()); + new ProofScriptWorkerF(window.getMediator(), window.getUserInterfaceControl(), script, + null, window.getStage()).start(); + } catch (Exception ex) { + LOGGER.error("", ex); + IssueDialogF.showExceptionDialog(window.getStage(), ex); + } + } + + /** + * P3b/B12: the script input dialog (Swing {@code ProofScriptInputAction}, a JDialog with a + * text area and an OK button, ProofScriptInputAction.java:52-79). + */ + private static void inputScriptDialog() { + MainWindowF window = MainWindowF.getInstance(); + if (window == null) { + return; + } + Stage dialog = new Stage(); + dialog.initOwner(window.getStage()); + dialog.initModality(Modality.APPLICATION_MODAL); + dialog.setTitle("Enter proof script"); + + TextArea textArea = new TextArea(); + VBox.setVgrow(textArea, Priority.ALWAYS); + Button okButton = new Button("OK"); + okButton.setOnAction(event -> { + try { + KeyAst.ProofScript script = ParsingFacade.parseScript(textArea.getText()); + dialog.close(); + Goal goal = window.getMediator().getSelectedGoal(); + new ProofScriptWorkerF(window.getMediator(), window.getUserInterfaceControl(), + script, goal, window.getStage()).start(); + } catch (Exception ex) { + LOGGER.error("", ex); + IssueDialogF.showExceptionDialog(window.getStage(), ex); + } + }); + VBox box = new VBox(8, textArea, okButton); + box.setPadding(new Insets(8)); + dialog.setScene(new javafx.scene.Scene(box)); + dialog.setWidth(500); + dialog.setHeight(400); + dialog.showAndWait(); + } } diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentTermContextMenuF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentTermContextMenuF.java index 126e67aa5c4..086edf977a6 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentTermContextMenuF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentTermContextMenuF.java @@ -21,7 +21,6 @@ import de.uka.ilkd.key.control.ProofControl; import de.uka.ilkd.key.core.fx.KeYMediatorF; -import de.uka.ilkd.key.gui.fx.MainWindowF; import de.uka.ilkd.key.gui.fx.extension.KeYGuiExtensionFacadeF; import de.uka.ilkd.key.gui.fx.join.JoinActionF; import de.uka.ilkd.key.gui.fx.mergerule.MergeRuleMenuItemF; @@ -36,7 +35,6 @@ import de.uka.ilkd.key.logic.NameCreationInfo; import de.uka.ilkd.key.logic.ProgramElementName; import de.uka.ilkd.key.logic.op.ProgramVariable; -import de.uka.ilkd.key.macros.ProofMacro; import de.uka.ilkd.key.pp.AbbrevException; import de.uka.ilkd.key.pp.AbbrevMap; import de.uka.ilkd.key.pp.PosInSequent; @@ -208,15 +206,17 @@ private static MenuItem focusAutoModeItem(NamedAction action, MenuContext ctx) { } /** - * menu: MP8 — the "Strategy Macros" section (Swing {@code ProofMacroMenu}, a {@code + * menu: MP8/B12 — the "Strategy Macros" section (Swing {@code ProofMacroMenu}, a {@code * JMenu("Strategy Macros")}, ProofMacroMenu.java:81, with one item per applicable macro; * CurrentGoalViewMenu.addMacroMenu adds it to the term menu, CurrentGoalViewMenu.java: * 212-217). The FX model emits the section unconditionally (SequentMenuModelF.compute), so - * all four macros of the Automation submenu / right-click popup ({@code - * MainWindowF.AUTOMATION_MACROS}, same order as Swing MainWindow.createAutomationActions, - * MainWindow.java:814-827) are shown — Swing instead filters by {@code canApplyTo} and - * omits the whole menu when it is empty; with the model's fixed skeleton the section keeps - * its label either way. Each item is built by the shared {@link ProofMacroMenuF} helper. + * the section keeps its label even when no macro is applicable at the position (Swing + * instead omits the whole empty menu, CurrentGoalViewMenu.java:216-217). The content is the + * P3b/B12 registered-macro superset built by the shared {@link ProofMacroMenuF} factory: + * every macro of {@code ProofMacroMenu.REGISTERED_MACROS} (which contains the four + * Automation-submenu macros, {@code MainWindowF.AUTOMATION_MACROS}) that is applicable at + * the position, category-grouped with separators, plus the PROOF_SCRIPTS section — exactly + * like Swing ProofMacroMenu.java:87-134. */ private static MenuItem macroMenu(NamedAction action, MenuContext ctx) { Node node = ctx.mediator() == null ? null : ctx.mediator().getSelectedNode(); @@ -225,10 +225,10 @@ private static MenuItem macroMenu(NamedAction action, MenuContext ctx) { return disabledItem(action.label()); } Menu menu = new Menu(action.label()); + Proof proof = node.proof(); PosInOccurrence pio = ctx.pos() == null ? null : ctx.pos().getPosInOccurrence(); - for (ProofMacro macro : MainWindowF.AUTOMATION_MACROS) { - menu.getItems().add(ProofMacroMenuF.itemFor(macro, node, ctx.proofControl(), pio)); - } + menu.getItems().addAll(ProofMacroMenuF.items(proof, proof.getSubtreeEnabledGoals(node), + node, ctx.proofControl(), pio)); return menu; } diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentViewF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentViewF.java index cf6ecafb931..327944b00cf 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentViewF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentViewF.java @@ -989,17 +989,20 @@ private ContextMenu buildRightClickMenu(PosInSequent pos, ContextMenu fallback) } /** - * menu: MP7 — builds the macro popup for the clicked position: one item per macro of the - * Automation submenu's {@link MainWindowF#AUTOMATION_MACROS} list that is applicable at the - * position (Swing {@code ProofMacroMenu} iterates the registered macros and keeps those with - * {@code canApplyTo}, ProofMacroMenu.java:87-99); item text = {@code macro.getName()}, tooltip - * = {@code macro.getDescription()} (ProofMacroMenu.java:143-144). The action runs the macro on - * the selected node with the {@link PosInOccurrence} of the clicked position (Swing - * {@code ProofMacroUserAction}, ProofMacroUserAction.java:57-59: {@code + * menu: MP7/B12 — builds the macro popup for the clicked position (Swing {@code + * ProofMacroMenu} iterates the registered macros and keeps those with {@code canApplyTo}, + * ProofMacroMenu.java:87-99): the P3b/B12 superset of the Automation submenu's + * {@link MainWindowF#AUTOMATION_MACROS}, built by the shared {@link ProofMacroMenuF} factory + * (item text = {@code macro.getName()}, tooltip = {@code macro.getDescription()}, + * ProofMacroMenu.java:143-144; category-grouped, PROOF_SCRIPTS section). The action runs + * the macro on the selected node with the {@link PosInOccurrence} of the clicked position + * (Swing {@code ProofMacroUserAction}, ProofMacroUserAction.java:57-59: {@code * mediator.getUI().getProofControl().runMacro(node, macro, pio)}; the core silently ignores * the run while auto mode is active, and the {@code pio} may be {@code null} — the position * may resolve to no occurrence and global macros accept that). Returns {@code null} when no - * macro is applicable (the caller then falls back to the term menu). + * macro is applicable (the caller then falls back to the term menu, Swing + * {@code ProofMacroMenu.isEmpty()}, ProofMacroMenu.java:160-162 + CurrentGoalViewListener + * .java:62-64). * * @param pos the clicked sequent position or {@code null} * @return the macro popup, or {@code null} if no macro is applicable @@ -1013,18 +1016,14 @@ private ContextMenu buildMacroPopup(@Nullable PosInSequent pos) { Proof proof = node.proof(); PosInOccurrence pio = pos == null ? null : pos.getPosInOccurrence(); ImmutableList goals = proof.getSubtreeEnabledGoals(node); - ContextMenu menu = new ContextMenu(); - int count = 0; - for (ProofMacro macro : MainWindowF.AUTOMATION_MACROS) { - if (macro.canApplyTo(proof, goals, pio)) { - // menu: MP7/MP8 — the item construction (name label + description tooltip via - // CustomMenuItem, since JavaFX MenuItem has no tooltip property) is shared with - // the term-menu "Strategy Macros" section, see {@link ProofMacroMenuF#itemFor}. - menu.getItems().add(ProofMacroMenuF.itemFor(macro, node, menuProofControl, pio)); - count++; - } + // Swing ProofMacroMenu.isEmpty(): the popup is shown only when at least one macro is + // applicable — otherwise the right-click falls back to the term menu + if (!ProofMacroMenuF.anyApplicable(proof, goals, pio)) { + return null; } - return count == 0 ? null : menu; + ContextMenu menu = new ContextMenu(); + menu.getItems().addAll(ProofMacroMenuF.items(proof, goals, node, menuProofControl, pio)); + return menu; } /** @@ -1064,14 +1063,17 @@ private static List menuLabels(List items) { } /** - * menu: MP7 — headless self test of the right-click popup ({@code + * menu: MP7/B12 — headless self test of the right-click popup ({@code * key.fx.verify.rightclickmacro}), run after the demo load from MainWindowF: computes a * {@link PosInSequent} of the current printing and builds the popup through the * {@link #buildRightClickMenu} seam with the "Right Click for Proof Macros" flag ON and OFF - * (the persisted setting is restored afterwards). With the flag ON the popup must contain the - * macro names of the Automation submenu ({@link MainWindowF#AUTOMATION_MACROS}) and no - * term-menu entries; with the flag OFF the term-menu path must be taken. Uses the printed - * position table — no synthetic mouse events. Skips gracefully without a goal or position. + * (the persisted setting is restored afterwards). With the flag ON the popup must contain + * the macro names of the Automation submenu ({@link MainWindowF#AUTOMATION_MACROS}) and no + * term-menu entries other than the shared "Strategy Macros" items (B12: both surfaces are + * built from the same {@link ProofMacroMenuF} factory, so the P3b/B12 registered-macro + * superset labels of the term menu's Strategy Macros section are the exact macro-name + * overlap); with the flag OFF the term-menu path must be taken. Uses the printed position + * table — no synthetic mouse events. Skips gracefully without a goal or position. * * @return {@code "PASS - ..."} or {@code "FAIL - ..."} */ @@ -1100,12 +1102,13 @@ public String verifyRightClickMacro() { macroNames.add(macro.getName()); } boolean hasMacros = onLabels.containsAll(macroNames); - // menu: MP8 — the macro popup must not contain any term-menu entry other than the - // shared macro names: since MP8b the term menu has a "Strategy Macros" section with - // the very same four macros, so the names are excluded from the comparison (the - // MP7b term menu had them only in the macro popup). - boolean noTermEntries = onLabels.stream() - .filter(l -> !macroNames.contains(l)) + // menu: MP8/B12 — the macro popup must not contain any term-menu entry other than the + // shared macro names: the term menu has a "Strategy Macros" section backed by the + // very same ProofMacroMenuF factory, so its labels (the applicable macros + the + // optional PROOF_SCRIPTS entries) are exactly the expected overlap — every other + // popup label must not appear in the term menu. + List strategyLabels = macroSectionLabels(off.getItems()); + boolean noTermEntries = onLabels.stream().filter(l -> !strategyLabels.contains(l)) .noneMatch(offLabels::contains); // the OFF path is the term menu (fixed structural items of SequentTermContextMenuF) boolean termPath = offLabels.contains("Apply rules automatically here") @@ -1119,6 +1122,20 @@ public String verifyRightClickMacro() { } } + /** + * menu: MP7/B12 — the labels of the "Strategy Macros" section of the term menu (the labels + * of its {@link ProofMacroMenuF}-built items, flattened over sub-menus). + */ + private static List macroSectionLabels(List items) { + for (MenuItem item : items) { + if (item instanceof javafx.scene.control.Menu menu + && "Strategy Macros".equals(menu.getText())) { + return menuLabels(menu.getItems()); + } + } + return List.of(); + } + /** * menu: MP7 — 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 diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/prooftree/ProofTreeVerifyF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/prooftree/ProofTreeVerifyF.java index 4143b610885..0d6cd049741 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/prooftree/ProofTreeVerifyF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/prooftree/ProofTreeVerifyF.java @@ -124,6 +124,16 @@ private static String runAll(Proof proof, ProofTreeViewF view) { waitUntil(() -> !statsDialog.getStage().isShowing(), "subtree statistics dialog closes", problems); + // B12 (P3b): the "Strategy Macros" submenu of the popup — repopulated from the current + // context and asserted (enabled, exactly the applicable-macro count, category separators) + String[] macroReport = { null }; + runOnFx(() -> macroReport[0] = view.verifyStrategyMacros()); + if (macroReport[0] == null || !macroReport[0].startsWith("PASS")) { + problems.append("strategy macros: ").append(macroReport[0]).append("; "); + } else { + notes.append(' ').append(macroReport[0]); + } + notes.append(' ').append(autoModeReport); boolean pass = problems.length() == 0 && notes.toString().contains("PASS"); diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/prooftree/ProofTreeViewF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/prooftree/ProofTreeViewF.java index 8f0001eb5f6..27d7a55d3d3 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/prooftree/ProofTreeViewF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/prooftree/ProofTreeViewF.java @@ -20,6 +20,9 @@ import javafx.scene.control.Button; import javafx.scene.control.CheckMenuItem; import javafx.scene.control.ContextMenu; +import javafx.scene.control.CustomMenuItem; +import javafx.scene.control.Label; +import javafx.scene.control.Menu; import javafx.scene.control.MenuItem; import javafx.scene.control.SeparatorMenuItem; import javafx.scene.control.TextField; @@ -44,6 +47,7 @@ import de.uka.ilkd.key.core.fx.KeYSelectionListener; import de.uka.ilkd.key.core.fx.KeYSelectionModel; import de.uka.ilkd.key.gui.fx.fonticons.IconFactoryF; +import de.uka.ilkd.key.gui.fx.nodeviews.ProofMacroMenuF; import de.uka.ilkd.key.proof.Goal; import de.uka.ilkd.key.proof.Node; import de.uka.ilkd.key.proof.Proof; @@ -54,6 +58,7 @@ import de.uka.ilkd.key.rule.OneStepSimplifier; import de.uka.ilkd.key.rule.OneStepSimplifierRuleApp; import de.uka.ilkd.key.rule.Taclet; +import de.uka.ilkd.key.settings.FeatureSettings; import de.uka.ilkd.key.settings.GeneralSettings; import de.uka.ilkd.key.settings.ProofIndependentSettings; @@ -291,6 +296,16 @@ private static final class ViewState { /** the node the popup was invoked on (Swing {@code ProofTreeContext.invokedNode}) */ private Node popupNode; + /** + * P3b/B12: the "Strategy Macros" submenu of the context menu (Swing + * {@code ProofTreePopupFactory.initMacroMenu}, ProofTreePopupFactory.java:98-103: a {@code + * ProofMacroMenu} right after Prune, only when not empty). Persistent (built once in + * {@link #createContextMenu()}); its items are (re)populated whenever the menu opens or the + * PROOF_SCRIPTS feature changes ({@link #populateStrategyMacros()}), so every context uses + * the currently invoked node. + */ + private Menu strategyMacrosMenu; + /** * Listens to structural changes of the displayed proof and schedules a coalesced rebuild on * the FX thread — the analogue of the Swing view's {@code GUIProofTreeModel} update calls. @@ -884,6 +899,59 @@ public String verifyProoftreeSemantics() { return "PASS: linearized, OSS rows, whole-tree actions, filter counting, view states"; } + /** + * P3b/B12: headless self test of the proof-tree "Strategy Macros" submenu (run from the + * {@code key.fx.verify.prooftree} harness on the FX thread): repopulates the submenu from the + * current context (the invoked node, or the selection/root fallback of + * {@link #populateStrategyMacros()}) and asserts the menu is enabled with a loaded proof and + * that its macro items are EXACTLY the {@code canApplyTo}-applicable registered macros of the + * current context node (the count seam — Swing ProofMacroMenu.java:87-99; the PROOF_SCRIPTS + * entries are excluded: they carry no macro semantics and are gated by the feature). On a + * closed proof (e.g. the demo autoprove run) no macro is applicable and an empty, enabled + * submenu is the Swing-equivalent state (Swing would not even add it, + * ProofTreePopupFactory.java:100-102). + * + * @return {@code "PASS ..."} or {@code "FAIL ..."}; the item count is included for the log + */ + public String verifyStrategyMacros() { + populateStrategyMacros(); + if (strategyMacrosMenu == null) { + return "SKIP - no macro submenu"; + } + List labels = new ArrayList<>(); + int separators = 0; + for (MenuItem item : strategyMacrosMenu.getItems()) { + if (item instanceof SeparatorMenuItem) { + separators++; + } else { + labels.add(macroMenuItemText(item)); + } + } + Node node = popupNode != null ? popupNode + : mediator != null && mediator.getSelectedNode() != null + ? mediator.getSelectedNode() + : proof.root(); + List expected = ProofMacroMenuF.applicableMacroNames(proof, + proof.getSubtreeEnabledGoals(node), null); + List scripts = List.of("Run proof script from file...", "Input proof script..."); + List macroLabels = labels.stream().filter(l -> !scripts.contains(l)).toList(); + boolean ok = !strategyMacrosMenu.isDisable() && macroLabels.equals(expected); + return (ok ? "PASS" : "FAIL") + " - macro submenu " + macroLabels.size() + " items, " + + separators + " separators, disabled=" + strategyMacrosMenu.isDisable(); + } + + /** P3b/B12: the visible text of the macro submenu items (label-backed custom items). */ + private static String macroMenuItemText(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 ""; + } + /** @return the number of OSS protocol rows currently present in the tree */ private static long countOssRows(TreeItem item) { long count = 0; @@ -1378,13 +1446,18 @@ private ContextMenu createContextMenu() { this::runStrategyOnNode); MenuItem pruneItem = actionItem("Prune Proof", IconFactoryF.Key.PRUNE, this::prunePopupNode); + // P3b/B12: the "Strategy Macros" submenu right after Prune (Swing + // ProofTreePopupFactory.initMacroMenu, ProofTreePopupFactory.java:98-103). The items are + // (re)populated per popup-open from the invoked node, see populateStrategyMacros(). + strategyMacrosMenu = new Menu("Strategy Macros"); MenuItem notesItem = actionItem("Edit Notes...", null, this::editNotes); MenuItem subtreeStatsItem = actionItem("Show Subtree Statistics", IconFactoryF.Key.STATISTICS, this::showSubtreeStatistics); ContextMenu menu = new ContextMenu(hideIntermediateItem, onlyInteractiveItem, new SeparatorMenuItem(), hideClosedItem, hideInteractiveItem, linearizeItem, expandOssItem, new SeparatorMenuItem(), expandAllItem, collapseAllItem, - new SeparatorMenuItem(), applyStrategyItem, pruneItem, notesItem, + new SeparatorMenuItem(), applyStrategyItem, pruneItem, strategyMacrosMenu, + new SeparatorMenuItem(), notesItem, new SeparatorMenuItem(), actionItem("Expand All Below", IconFactoryF.Key.PLUS, () -> expandAllBelow(popupBranchItem)), @@ -1415,10 +1488,51 @@ expandOssItem, new SeparatorMenuItem(), expandAllItem, collapseAllItem, notesItem.setDisable(proof == null); subtreeStatsItem.setDisable(proof == null); pruneItem.setDisable(!isPrunable(popupNode)); + // P3b/B12: rebuild the macro submenu from the invoked node (Swing + // ProofTreePopupFactory.initMacroMenu rebuilds it per popup-open) + populateStrategyMacros(); }); + // P3b/B12: the submenu is persistent (unlike the term menu / sequent popup, which are + // rebuilt per show and pick up the PROOF_SCRIPTS feature at build time), so it gets the + // live feature listener of the Swing ProofMacroMenu (ProofMacroMenu.java:125-133). The + // listener immediately re-populates with the current value; on the FX thread, since it + // touches the menu items (the feature may be toggled from the settings dialog's thread). + FeatureSettings.onAndActivate(ProofMacroMenuF.PROOF_SCRIPTS_FEATURE, + active -> FxUtil.runLater(this::populateStrategyMacros)); return menu; } + /** + * P3b/B12: (re)populates the persistent "Strategy Macros" submenu for the currently invoked + * node ({@link #popupNode}; falls back to the mediator's selected node and then to the proof + * root for the headless self test): the applicable macros of the registered-macro superset + * ({@link ProofMacroMenuF}), category-grouped, plus the PROOF_SCRIPTS section according to + * the current feature state — the exact content of the Swing {@code ProofMacroMenu}. + *

        + * The macro items run on the {@link #popupNode} of the invocation (Swing's + * {@code ProofMacroUserAction} runs them on the mediator selection — a quirk of reusing the + * same action factory for the tree popup; the invoked node is the deliberate FX choice). + * Without a proof the submenu is disabled and empty (Swing's + * {@code mediator.enableWhenProofLoaded(this)}). + */ + private void populateStrategyMacros() { + if (strategyMacrosMenu == null) { + return; + } + strategyMacrosMenu.getItems().clear(); + if (proof == null || proof.isDisposed() || proofControl == null) { + strategyMacrosMenu.setDisable(true); + return; + } + Node node = popupNode != null ? popupNode + : mediator != null && mediator.getSelectedNode() != null + ? mediator.getSelectedNode() + : proof.root(); + strategyMacrosMenu.setDisable(false); + strategyMacrosMenu.getItems().addAll(ProofMacroMenuF.items(proof, + proof.getSubtreeEnabledGoals(node), node, proofControl, null)); + } + /** @return a menu item with an optional icon and the given action */ private static MenuItem actionItem(String label, IconFactoryF.Key icon, Runnable action) { MenuItem item = new MenuItem(label); From 26ee03f8d0a5be84071e29457620acd3b44d9d40 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Sat, 10 Oct 2026 17:41:49 +0200 Subject: [PATCH 161/281] P3c: sign off A4-A8, D31, D33, D34, D37 (verification PASS on Xvnc :98) --- key.ui.fx/PARITY-SIGNOFF.md | 44 +++++++++++++++++++++++++++++-------- 1 file changed, 35 insertions(+), 9 deletions(-) diff --git a/key.ui.fx/PARITY-SIGNOFF.md b/key.ui.fx/PARITY-SIGNOFF.md index 38df921d75d..eb3df6371a3 100644 --- a/key.ui.fx/PARITY-SIGNOFF.md +++ b/key.ui.fx/PARITY-SIGNOFF.md @@ -131,11 +131,11 @@ differences found by a second Swing↔JavaFX sweep, with stable IDs used by the | A1 | Contract/invariant configurator dialogs: the Swing registration `WindowUserInterfaceControl.java:74-82` (FunctionalOperationContract/DependencyContract/LoopInvariantRule/BlockContractInternal/BlockContractExternal completions + `ContractConfigurator.java`) — the FX registry (`WindowUserInterfaceControlF.java:126-135`) holds only `MergeRuleCompletionF` | RESOLVED (P2b) | | A2 | `LemmaSelectionDialog` (lemmatagenerator, 161 lines) not ported | RESOLVED (P2b) | | A3 | `LoadUserTacletsDialog` reduced (Swing 499 lines) | RESOLVED (P2b) | -| A4 | Proof Statistics: no export (Swing exports HTML/text) | OPEN (P3c) | -| A5 | About/License reduced to toasts instead of dialogs | OPEN (P3c) | -| A6 | GitHub-issue prefill dropped (bug-reporting URL parameters) | OPEN (P3c) | -| A7 | Feedback: no zip archive of logs | OPEN (P3c) | -| A8 | ToolTip options: "Save as Default" behaviour | OPEN (P3c) | +| A4 | Proof Statistics: no export (Swing exports HTML/text) | RESOLVED (P3c) | +| A5 | About/License reduced to toasts instead of dialogs | RESOLVED (P3c) | +| A6 | GitHub-issue prefill dropped (bug-reporting URL parameters) | RESOLVED (P3c) | +| A7 | Feedback: no zip archive of logs | RESOLVED (P3c) | +| A8 | ToolTip options: "Save as Default" behaviour | RESOLVED (P3c) | | A9 | `EnsureSourceConsistency` info dialog | RESOLVED (P2b) | | A10 | Dialog triage remainder of the sweep | OPEN (triaged in P4) | @@ -173,13 +173,13 @@ differences found by a second Swing↔JavaFX sweep, with stable IDs used by the | D28 | Left-click taclet menu + `POPUP_DELAY` guard (`CurrentGoalViewListener.java:48-95`, `SequentViewListener.java:29`) | RESOLVED (P2a) | | D29 | Shift+click focussed auto mode (`CurrentGoalViewListener.java:56-58`) | RESOLVED (P2a) | | D30 | Extension tooltip strings unwired | OPEN (P4) | -| D31 | Sequent line width computed from the viewport | OPEN (P3c) | +| D31 | Sequent line width computed from the viewport | RESOLVED (P3c) | | D32 | Sequent search prefill from the hovered term (`SearchInSequentAction.java:32-37`) | RESOLVED (P2a) | -| D33 | Fonts vs. `KEY_FONT_*` settings | OPEN (P3c) | -| D34 | `SequentViewDock` missing | OPEN (P3c) | +| D33 | Fonts vs. `KEY_FONT_*` settings | RESOLVED (P3c) | +| D34 | `SequentViewDock` missing | RESOLVED (P3c) | | D35 | Strategy tab Go button + parallel-prover merge-point lock (`StrategySelectionView.java:157-208/869-895`) | RESOLVED (P2a) | | D36 | `KeyboardShortcuts` extension seams absent | OPEN (P4) | -| D37 | Status-line task progress bar (log-only stubs, `WindowUserInterfaceControlF.java:317-327`) | OPEN (P3c) | +| D37 | Status-line task progress bar (log-only stubs, `WindowUserInterfaceControlF.java:317-327`) | RESOLVED (P3c) | P2a (`weigl/ocfx-p2-sequent`, merged 2026-10-10) resolved D28/D29/D32/D35 — `key.fx.verify.sequentmenu` PASS (term menu 31 entries, POPUP_DELAY guard, search prefill, @@ -217,3 +217,29 @@ covering the OSS rows on a strategy-proved proof. The view filters and the tree in the tree context menu because the FX docking framework has no tab-title gear menu (documented in `ProofTreeViewF`); the subtree statistics report is a plain-text window rather than the Swing HTML one (export is A4/P3c). C24 (the `PROOF_TREE` extension seam) stays open for P4. + +P3c (`weigl/ocfx-p3c-dialogs`, this branch) resolved A4/A5/A6/A7/A8 and D31/D33/D34/D37 — the +dialog/status-line parity items of the sweep: +A4 `ProofStatisticsDialogF` now exports the Swing CSV/HTML formats (`verifyStatisticsExport`); +A5 keeps "About KeY" as the enriched INFO toast (B16 convention) and turns the license into the +real `LicenseDialogF` (tabs "KeY License" + "Third party libraries", Swing `LicenseAction`); +A6 `MainWindowF.createGithubIssue`/`buildGithubIssueUrl` rebuilds the Swing +`CreateGithubIssueAction` issue template (%CHECKSUM% → internal version, %JAVA% → proof Java +sources) and opens it through the `HelpFacadeF` browser seam; A7 `FeedbackDialogF` "Save ZIP…" +writes `bugDescription.txt`/`keyVersion.txt`/`systemProperties.txt` + the `key_*.log` run logs +(`verifyLogArchive`); A8 `ToolTipOptionsDialogF` "Save as Default" persists the tooltip options +via `ProofIndependentSettings.saveSettings()` (`verifySaveAsDefault`); D31 `SequentViewF` +computes the print line width from the scroll-pane viewport via a `Text("W")` probe (Swing +`computeLineWidthFor`, reprint on width changes, `verifyViewportLineWidth` under +`key.fx.verify.sequent`); D33 `ConfigF` mirrors the Swing `KEY_FONT_*` families/sizes (proof +tree/sequent at the current size, goal/proof list at the fixed `SIZES[2]`); D34 +`SequentViewDockF` shows a node's sequent in a separate buffer with the "Jump into Tree" title +action (proof-tree popup "Open Node in Separate Buffer"); D37 `MainWindowF.buildStatusBar` gains +the hidden-until-used progress bar with `setStatusLine(String,int)` / +`setTaskProgressBarMaximum` (`<0` indeterminate, `0` hidden, `>0` determinate) / +`setTaskProgressValue` / `hideStatusProgress`, wired from `WindowUserInterfaceControlF` exactly +like Swing's `WindowUserInterfaceControl` (taskStarted → `setStatusLine(msg,size)`, setMaximum / +setProgress / taskProgress → bar, macro end → hide). `key.fx.verify.uicontrol` + +`key.fx.verify.menuparity` PASS (10 seam steps incl. the six P3c seams, menu counts +File 16 / Proof 24 / Options 7 / View 12 / About 5) and `key.fx.verify.prooftree` + +`key.fx.demo.autoprove` PASS on Xvnc :98. A10 stays open for P4 (dialog triage remainder). From ef70d08f9178b4be2682a73456eb025b58b64634 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Sat, 10 Oct 2026 17:45:01 +0200 Subject: [PATCH 162/281] =?UTF-8?q?P3b:=20B17=20=E2=80=94=20TermLabelMenu?= =?UTF-8?q?=20persistence=20+=20styling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gui/fx/originlabels/OriginLabelsF.java | 59 +++- .../gui/fx/originlabels/TermLabelMenuF.java | 318 ++++++++++++++++-- 2 files changed, 357 insertions(+), 20 deletions(-) diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/originlabels/OriginLabelsF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/originlabels/OriginLabelsF.java index b738bd856ba..12d374f71ef 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/originlabels/OriginLabelsF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/originlabels/OriginLabelsF.java @@ -99,14 +99,25 @@ public static List install(MainWindowF mainWindow) { sequentView.setVisibleTermLabels(termLabelMenu.getVisibleTermLabels()); // Swing TermLabelMenu rebuilds on selectedProofChanged (label names depend on the proof) + // and re-styles the label items on selectedNodeChanged (bold = occurs in the displayed + // sequent, B17) KeYSelectionModel selectionModel = mainWindow.getSelectionModel(); selectionModel.addKeYSelectionListenerChecked(new KeYSelectionListener() { @Override public void selectedProofChanged(KeYSelectionEvent event) { termLabelMenu.rebuildMenu(selectionModel.getSelectedProof()); + termLabelMenu.applyStyles(selectionModel.getSelectedNode()); + } + + @Override + public void selectedNodeChanged(KeYSelectionEvent event) { + // P3b/B17: Swing TermLabelMenu.selectedNodeChanged — the font style indicates + // whether a label occurs in the currently displayed sequent + termLabelMenu.applyStyles(selectionModel.getSelectedNode()); } }); termLabelMenu.rebuildMenu(selectionModel.getSelectedProof()); + termLabelMenu.applyStyles(selectionModel.getSelectedNode()); List items = new ArrayList<>(); items.add(termLabelMenu); @@ -304,7 +315,53 @@ public static String verify(MainWindowF mainWindow) { // --- origin visualizer part (Swing OriginTermLabelVisualizer / NodeInfoVisualizer) --- String visReport = verifyOriginVisualizer(mainWindow); - return termLabelReport + " | " + visReport; + + // --- B17 persistence part: the Swing AbstractButtonSaver preference round trip --- + String prefReport = verifyPreferencePersistence(proof); + return termLabelReport + " | " + visReport + " | " + prefReport; + } + + /** + * Self test part (B17): the check states persist under the exact Swing + * {@code AbstractButtonSaver} keys ("{@code ..selected}" in {@code + * Preferences.userNodeForPackage(MainWindowF)}, PreferenceSaver.java:260) — driving the + * user-click path must flip the menu state AND the raw preference, and reading the key back + * must round-trip. One label name of the loaded proof is used when one exists; the toggled + * states are restored afterwards (menu + preferences). + * + * @param proof the loaded proof (its term label names select the tested label item) + */ + private static String verifyPreferencePersistence(Proof proof) { + boolean ok = true; + StringBuilder report = new StringBuilder("termLabels-prefs:"); + // master switch: toggle through the real click path (handler + save), read it back + boolean display = termLabelMenu.isDisplayLabelsSelected(); + termLabelMenu.clickDisplayLabels(); + ok &= termLabelMenu.isDisplayLabelsSelected() == !display; + ok &= TermLabelMenuF.readDisplaySelected(display) == !display; + report.append(" display=").append(TermLabelMenuF.readDisplaySelected(display)); + // one label item (when the loaded proof supports one): toggle there and back, checking + // the state AND the stored preference each way — the click path saves like Swing + List names = TermLabelVisibilityManager.getSortedTermLabelNames(proof); + if (!names.isEmpty()) { + Name label = names.get(0); + boolean labelState = termLabelMenu.isLabelSelected(label); + termLabelMenu.clickLabel(label); + ok &= termLabelMenu.isLabelSelected(label) == !labelState; + ok &= TermLabelMenuF.readLabelSelected(label, labelState) == !labelState; + report.append(" label=").append(!labelState); + termLabelMenu.clickLabel(label); + ok &= termLabelMenu.isLabelSelected(label) == labelState; + ok &= TermLabelMenuF.readLabelSelected(label, !labelState) == labelState; + } else { + report.append(" label=skipped"); + } + // restore the master switch (the click path also restores the saved preference) + termLabelMenu.clickDisplayLabels(); + ok &= termLabelMenu.isDisplayLabelsSelected() == display; + ok &= TermLabelMenuF.readDisplaySelected(display) == display; + report.append(' ').append(ok ? "PASS" : "FAIL"); + return report.toString(); } /** @return the next node in pre-order (node itself first), {@code null} after the last */ diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/originlabels/TermLabelMenuF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/originlabels/TermLabelMenuF.java index 8bc6e9385cc..751c13c4903 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/originlabels/TermLabelMenuF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/originlabels/TermLabelMenuF.java @@ -5,17 +5,31 @@ package de.uka.ilkd.key.gui.fx.originlabels; import java.util.Comparator; +import java.util.HashSet; import java.util.Map; +import java.util.Set; import java.util.TreeMap; -import javafx.scene.control.CheckMenuItem; +import java.util.prefs.Preferences; +import javafx.geometry.Insets; +import javafx.scene.control.CheckBox; +import javafx.scene.control.CustomMenuItem; +import javafx.scene.control.Label; import javafx.scene.control.Menu; import javafx.scene.control.SeparatorMenuItem; +import javafx.scene.control.Tooltip; +import javafx.scene.layout.HBox; import de.uka.ilkd.key.control.TermLabelVisibilityManager; import de.uka.ilkd.key.control.event.TermLabelVisibilityManagerListener; +import de.uka.ilkd.key.gui.fx.MainWindowF; +import de.uka.ilkd.key.logic.JTerm; +import de.uka.ilkd.key.logic.label.TermLabel; +import de.uka.ilkd.key.proof.Node; import de.uka.ilkd.key.proof.Proof; import org.key_project.logic.Name; +import org.key_project.prover.sequent.Sequent; +import org.key_project.prover.sequent.SequentFormula; /** * JavaFX port of the Swing {@code de.uka.ilkd.key.gui.actions.TermLabelMenu} (View menu "Term @@ -34,10 +48,24 @@ * the caller (the FX main window re-prints its sequent view). *

      • The label items are rebuilt when the selected proof changes (Swing {@code rebuildMenu} in * {@code selectedProofChanged}), because the available label names depend on the proof's - * profile ({@code TermLabelVisibilityManager.getSortedTermLabelNames}). The Swing original - * additionally bolds the items whose labels occur in the displayed sequent — a cosmetic - * affordance that is not ported.
      • + * profile ({@code TermLabelVisibilityManager.getSortedTermLabelNames}). + *
      • The check states are persisted (B17): the master switch and every label item write their + * selected state to {@link Preferences} {@code userNodeForPackage(MainWindowF.class)} under the + * exact Swing {@code AbstractButtonSaver} keys ({@code ..selected}, i.e. + * {@code DisplayLabelsCheckBox.DisplayLabelsCheckBox.selected} and + * {@code TermLabelCheckBox.
      • * + *

        + * Styling parity (B17): the items are {@link CustomMenuItem}s wrapping an + * {@code HBox(CheckBox, Label)} — the label carries the text, the tooltip and the bold/italic + * occurrence font (JavaFX menu items have no tooltip or font API). Like Swing's + * {@code selectedNodeChanged} listener the boldness marks the labels that occur in the currently + * displayed sequent ({@link #applyStyles}), with the Swing tooltips ("Click to toggle visibility + * for term label X." / "Term label X does not occur in the current sequent." / the disabled + * hint). Unlike the Swing original the item creation re-runs the (re)loaded state through the + * manager instead of only showing the manager state. * * @author lanzinger (Swing original), the key.ui.fx team (port) */ @@ -46,17 +74,21 @@ public class TermLabelMenuF extends Menu { /** The submenu title (Swing {@code TermLabelMenu.TERM_LABEL_MENU}). */ public static final String TERM_LABEL_MENU = "Term Labels"; + /** the Swing {@code AbstractButtonSaver} preference node (PreferenceSaver.java:166). */ + private static final Preferences PREFS = + Preferences.userNodeForPackage(MainWindowF.class); + /** the shared visibility state consulted by the sequent view printer. */ private final TermLabelVisibilityManager visibleTermLabels = new TermLabelVisibilityManager(); /** the per-label check items, keyed by label name (Swing {@code checkBoxMap}). */ - private final Map checkBoxMap = + private final Map checkBoxMap = new TreeMap<>(Comparator.comparing(Name::toString)); /** * the "Display Term Labels in Formulas" master switch (Swing {@code DisplayLabelsCheckBox}). */ - private final CheckMenuItem displayLabelsItem; + private final DisplayLabelsCheckBoxF displayLabelsItem; /** re-prints the affected views (Swing {@code MainWindow.makePrettyView}). */ private final Runnable reprint; @@ -73,21 +105,26 @@ public TermLabelMenuF(Runnable reprint) { this.reprint = reprint; setText(TERM_LABEL_MENU); - displayLabelsItem = new CheckMenuItem("Display Term Labels in Formulas"); - displayLabelsItem.setSelected(visibleTermLabels.isShowLabels()); - displayLabelsItem.setOnAction(e -> visibleTermLabels - .setShowLabels(displayLabelsItem.isSelected())); + // the master switch: the persisted state wins over the manager default (Swing + // DisplayLabelsCheckBox constructor: loadPreferences + setSelected) + displayLabelsItem = + new DisplayLabelsCheckBoxF(readDisplaySelected(true), this::applyDisplayToggle); this.visibilityListener = e -> { displayLabelsItem.setSelected(visibleTermLabels.isShowLabels()); - for (Map.Entry entry : checkBoxMap.entrySet()) { - entry.getValue().setDisable(!visibleTermLabels.isShowLabels()); - entry.getValue().setSelected(!visibleTermLabels.isHidden(entry.getKey())); + for (Map.Entry entry : checkBoxMap.entrySet()) { + TermLabelCheckBoxF item = entry.getValue(); + item.setEnabled(visibleTermLabels.isShowLabels()); + item.setSelected(!visibleTermLabels.isHidden(entry.getKey())); } reprint.run(); }; visibleTermLabels.addTermLabelVisibilityManagerListener(visibilityListener); + // apply the loaded state (Swing loadPreferences -> the setSelected override -> the click + // handler); fires the listener above when it differs from the manager default + visibleTermLabels.setShowLabels(displayLabelsItem.isSelected()); + getItems().addAll(displayLabelsItem, new SeparatorMenuItem()); } @@ -101,7 +138,10 @@ public TermLabelVisibilityManager getVisibleTermLabels() { /** * Rebuilds the per-label check items for the given proof (Swing {@code rebuildMenu}, - * triggered by the mediator's {@code selectedProofChanged}). + * triggered by the mediator's {@code selectedProofChanged}). Each new item reads its + * persisted selected state and applies it to the manager (Swing {@code TermLabelCheckBox} + * constructor: {@code loadPreferences} + {@code visibleTermLabels.setHidden(labelName, + * !isSelected())}). * * @param proof the newly selected proof, may be {@code null} (only the master item remains) */ @@ -114,14 +154,132 @@ public void rebuildMenu(Proof proof) { } // sorted list of term label names supported by the proof's profile for (Name labelName : TermLabelVisibilityManager.getSortedTermLabelNames(proof)) { - CheckMenuItem item = new CheckMenuItem(labelName.toString()); - item.setSelected(!visibleTermLabels.isHidden(labelName)); - item.setDisable(!visibleTermLabels.isShowLabels()); - item.setOnAction( - e -> visibleTermLabels.setHidden(labelName, !item.isSelected())); + TermLabelCheckBoxF item = new TermLabelCheckBoxF(labelName, + readLabelSelected(labelName, !visibleTermLabels.isHidden(labelName)), + () -> applyLabelToggle(labelName)); + // apply the (re)loaded state to the manager (Swing TermLabelCheckBox constructor) + visibleTermLabels.setHidden(labelName, !item.isSelected()); checkBoxMap.put(labelName, item); getItems().add(item); } + // the fresh items fall back to the italic occurrence style until the selection syncs + } + + /** the label item change handler (Swing {@code TermLabelCheckBox.handleClickEvent}). */ + private void applyLabelToggle(Name labelName) { + TermLabelCheckBoxF item = checkBoxMap.get(labelName); + if (item != null) { + visibleTermLabels.setHidden(labelName, !item.isSelected()); + } + } + + /** the master switch change handler (Swing {@code DisplayLabelsCheckBox.handleClickEvent}). */ + private void applyDisplayToggle() { + visibleTermLabels.setShowLabels(displayLabelsItem.isSelected()); + } + + /** + * Applies the occurrence font style + tooltip of every label item for the node currently + * displayed in the sequent view (Swing {@code TermLabelMenu.selectedNodeChanged}): the + * labels occurring in the node's sequent are bold ("Click to toggle visibility for term + * label X."), the others italic ("Term label X does not occur in the current sequent."). + * + * @param node the selected node, may be {@code null} + */ + public void applyStyles(Node node) { + Set occurring = node == null ? Set.of() : getOccurringTermLabels(node.sequent()); + for (Map.Entry entry : checkBoxMap.entrySet()) { + entry.getValue().applyOccurrenceStyle(occurring.contains(entry.getKey())); + } + } + + /** @return the names of the term labels occurring in the given sequent (Swing recurse). */ + private static Set getOccurringTermLabels(Sequent seq) { + Set result = new HashSet<>(); + for (SequentFormula sf : seq) { + collectLabels((JTerm) sf.formula(), result); + } + return result; + } + + private static void collectLabels(JTerm term, Set result) { + if (term.hasLabels()) { + for (TermLabel label : term.getLabels()) { + result.add(label.name()); + } + } + for (org.key_project.logic.Term sub : term.subs()) { + collectLabels((JTerm) sub, result); + } + } + + /** @return whether the master switch is selected (Swing {@code isSelected}) */ + boolean isDisplayLabelsSelected() { + return displayLabelsItem.isSelected(); + } + + /** + * Sets the master switch without saving (Swing {@code setSelected}); applies to the manager. + */ + void setDisplayLabelsSelected(boolean selected) { + displayLabelsItem.setSelected(selected); + } + + /** The user-click path of the master switch (Swing actionPerformed: handler + save). */ + void clickDisplayLabels() { + displayLabelsItem.click(); + } + + /** @return whether the given label item exists and is selected */ + boolean isLabelSelected(Name labelName) { + TermLabelCheckBoxF item = checkBoxMap.get(labelName); + return item != null && item.isSelected(); + } + + /** Sets one label item without saving (Swing {@code setSelected}); applies to the manager. */ + void setLabelSelected(Name labelName, boolean selected) { + TermLabelCheckBoxF item = checkBoxMap.get(labelName); + if (item != null) { + item.setSelected(selected); + } + } + + /** The user-click path of one label item (Swing actionPerformed: handler + save). */ + void clickLabel(Name labelName) { + TermLabelCheckBoxF item = checkBoxMap.get(labelName); + if (item != null) { + item.click(); + } + } + + // ------------------------------------------------------------------ + // preferences (B17): the Swing AbstractButtonSaver key scheme + // ------------------------------------------------------------------ + + /** the AbstractButtonSaver key of a Swing check box (PreferenceSaver.java:260). */ + private static String buttonId(String simpleName, String componentName) { + return simpleName + "." + componentName + ".selected"; + } + + /** the persisted master state, or {@code fallback} when nothing is stored yet. */ + static boolean readDisplaySelected(boolean fallback) { + return PREFS.getBoolean(buttonId("DisplayLabelsCheckBox", "DisplayLabelsCheckBox"), + fallback); + } + + /** the persisted state of one label item, or {@code fallback} when nothing is stored yet. */ + static boolean readLabelSelected(Name labelName, boolean fallback) { + return PREFS.getBoolean(buttonId("TermLabelCheckBox", labelName.toString()), fallback); + } + + /** writes the master state (Swing {@code AbstractButtonSaver.save}). */ + static void writeDisplaySelected(boolean selected) { + PREFS.putBoolean(buttonId("DisplayLabelsCheckBox", "DisplayLabelsCheckBox"), selected); + } + + /** writes the state of one label item (Swing {@code AbstractButtonSaver.save}). */ + static void writeLabelSelected(Name labelName, boolean selected) { + PREFS.putBoolean(buttonId("TermLabelCheckBox", labelName.toString()), selected); } /** @@ -131,4 +289,126 @@ public void rebuildMenu(Proof proof) { public void dispose() { visibleTermLabels.removeTermLabelVisibilityManagerListener(visibilityListener); } + + /** + * P3b/B17: one check row of the term labels menu (Swing {@code KeYMenuCheckBox}, a {@code + * JCheckBoxMenuItem}): a {@link CustomMenuItem} whose content is an + * {@code HBox(CheckBox, Label)} — the label carries the text and (for label items) the + * tooltip and the bold/italic occurrence font, which JavaFX menu items cannot do. Clicking + * the row or the checkbox applies the change and persists it (Swing + * {@code actionPerformed -> handleClickEvent() + savePreferences}); {@link #setSelected} + * applies a value without persisting (Swing's {@code setSelected} override, used by the + * preference loading and by the visibility-manager sync). The menu stays open after a click, + * like Swing's check menu items. + */ + abstract static class CheckBoxMenuItemF extends CustomMenuItem { + final CheckBox checkBox = new CheckBox(); + final Label label = new Label(); + private final Runnable onChange; + + CheckBoxMenuItemF(String text, boolean selected, Runnable onChange) { + this.onChange = onChange; + label.setText(text); + checkBox.setSelected(selected); + HBox row = new HBox(6, checkBox, label); + row.setPadding(new Insets(0, 6, 0, 0)); + setContent(row); + setHideOnClick(false); + // direct checkbox clicks: the box is already toggled, apply + save + checkBox.setOnAction(e -> { + onChange.run(); + save(); + }); + // row clicks / keyboard activation: toggle the box, apply + save + setOnAction(e -> { + checkBox.setSelected(!checkBox.isSelected()); + onChange.run(); + save(); + }); + } + + /** @return the current state (Swing {@code isSelected}) */ + final boolean isSelected() { + return checkBox.isSelected(); + } + + /** applies a value to the box and the model without saving (Swing {@code setSelected}). */ + final void setSelected(boolean selected) { + checkBox.setSelected(selected); + onChange.run(); + } + + /** the user-click path (Swing {@code actionPerformed}): toggles, applies and saves. */ + final void click() { + checkBox.setSelected(!checkBox.isSelected()); + onChange.run(); + save(); + } + + /** persists the current state (Swing {@code mainWindow.savePreferences(checkBox)}). */ + abstract void save(); + } + + /** + * P3b/B17: the "Display Term Labels in Formulas" master switch (Swing + * {@code DisplayLabelsCheckBox}, name + class name {@code DisplayLabelsCheckBox}). + */ + static final class DisplayLabelsCheckBoxF extends CheckBoxMenuItemF { + DisplayLabelsCheckBoxF(boolean selected, Runnable onChange) { + super("Display Term Labels in Formulas", selected, onChange); + Tooltip.install(label, new Tooltip( + "Use this checkbox to toggle visibility for all term labels.")); + } + + @Override + void save() { + writeDisplaySelected(isSelected()); + } + } + + /** + * P3b/B17: one label item (Swing {@code TermLabelCheckBox}, class name {@code + * TermLabelCheckBox}). The bold/italic font style + tooltip mark whether the label occurs in + * the currently displayed sequent ({@link #applyOccurrenceStyle}, Swing + * {@code setBoldFont/setItalicFont}); the disabled tooltip follows Swing + * {@code updateToolTipText}. + */ + static final class TermLabelCheckBoxF extends CheckBoxMenuItemF { + private final Name labelName; + private String tooltipWhenEnabled; + + TermLabelCheckBoxF(Name labelName, boolean selected, Runnable onChange) { + super(labelName.toString(), selected, onChange); + this.labelName = labelName; + // Swing TermLabelCheckBox constructor: setItalicFont() until the selection syncs + applyOccurrenceStyle(false); + } + + /** bold = the label occurs in the displayed sequent, italic otherwise (Swing). */ + void applyOccurrenceStyle(boolean occurs) { + label.setStyle(occurs ? "-fx-font-weight: bold;" : "-fx-font-style: italic;"); + tooltipWhenEnabled = occurs + ? "Click to toggle visibility for term label " + labelName + "." + : "Term label " + labelName + " does not occur in the current sequent."; + updateTooltip(); + } + + /** enables/disables the checkbox and updates the tooltip (Swing {@code setEnabled}). */ + void setEnabled(boolean enabled) { + checkBox.setDisable(!enabled); + updateTooltip(); + } + + private void updateTooltip() { + String text = checkBox.isDisable() + ? "You turned off visibility for all term labels. This checkbox is disabled." + : tooltipWhenEnabled; + Tooltip.install(label, new Tooltip(text)); + } + + @Override + void save() { + writeLabelSelected(labelName, isSelected()); + } + } } From 956b99650884d3d160c3e1b6b3fdb8c7816b07c1 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Sat, 10 Oct 2026 17:45:27 +0200 Subject: [PATCH 163/281] P4: singleton toolbar controls in HeatmapF/TestExtensionF The host rebuilds the toolbars in the verify harness and checks the merged 'key-extension-tool-bar' membership by identity, so ToolbarF providers must return the same control instances on every call (the convention CachingExtensionF documents for its status-line singleton). HeatmapF and TestExtensionF created fresh controls per call, which made the B14 merge assertion fail invisibly. --- .../gui/fx/extension/contrib/HeatmapF.java | 42 +++++++++++++------ .../fx/extension/contrib/TestExtensionF.java | 18 ++++++-- 2 files changed, 43 insertions(+), 17 deletions(-) diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/extension/contrib/HeatmapF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/extension/contrib/HeatmapF.java index 9a2e7c0113c..01fd0a0e9db 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/extension/contrib/HeatmapF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/extension/contrib/HeatmapF.java @@ -5,6 +5,7 @@ package de.uka.ilkd.key.gui.fx.extension.contrib; import java.util.List; +import javafx.scene.control.Button; import javafx.scene.control.CheckMenuItem; import javafx.scene.control.Control; import javafx.scene.control.Menu; @@ -20,6 +21,8 @@ import de.uka.ilkd.key.settings.ProofIndependentSettings; import de.uka.ilkd.key.settings.ViewSettings; +import org.jspecify.annotations.Nullable; + /** * The Heatmap extension, FX port of {@code de.uka.ilkd.key.gui.extension.impl.HeatmapExt} * (Swing HeatmapExt.java:30-68): a separate "Heatmap" menu with the toggle and the settings @@ -43,6 +46,12 @@ public class HeatmapF private final SettingsProviderF heatmapSettingsProvider = new HeatmapSettingsProviderF(); + /** The shared toolbar toggle (singleton, so the host's toolbar membership checks hold). */ + private @Nullable ToggleButton heatmapToggle = null; + + /** The shared toolbar settings button (singleton, same reason). */ + private @Nullable Button heatmapSettingsButton = null; + @Override public List

        getMenus(MainWindowF window, KeYMediatorF mediator) { // extension: MP9.0 — Swing HeatmapExt.java:41-51: the main menu holds the toggle and @@ -79,20 +88,27 @@ private MenuItem settingsItem(MainWindowF window) { @Override public List getToolbarControls(MainWindowF window, KeYMediatorF mediator) { // extension: MP9.0 — Swing HeatmapExt.java:54-62: the toolbar holds the toggle button - // (icon-only in Swing, labelled here) and the settings action. + // (icon-only in Swing, labelled here) and the settings action. Both controls are + // singletons — the host rebuilds the toolbars (e.g. in the verify harness) and checks + // membership by identity, so a fresh instance per call would break those checks. ViewSettings vs = ProofIndependentSettings.DEFAULT_INSTANCE.getViewSettings(); - ToggleButton toggle = new ToggleButton("Heatmap"); - toggle.setTooltip(new Tooltip("Toggle the (deferred) heatmap overlay; the option is " - + "persisted in the ViewSettings.")); - toggle.setSelected(vs.isShowHeatmap()); - toggle.setOnAction(e -> vs.setHeatmapOptions(toggle.isSelected(), vs.isHeatmapSF(), - vs.isHeatmapNewest(), vs.getMaxAgeForHeatmap())); - javafx.scene.control.Button settings = - new javafx.scene.control.Button("Heatmap Settings…"); - settings.setTooltip(new Tooltip("Open the heatmap options in the settings dialog.")); - settings.setOnAction(e -> SettingsManagerF.getInstance() - .showSettingsDialog(window, heatmapSettingsProvider)); - return List.of(toggle, settings); + if (heatmapToggle == null) { + ToggleButton toggle = new ToggleButton("Heatmap"); + toggle.setTooltip(new Tooltip("Toggle the (deferred) heatmap overlay; the option is " + + "persisted in the ViewSettings.")); + toggle.setSelected(vs.isShowHeatmap()); + toggle.setOnAction(e -> vs.setHeatmapOptions(toggle.isSelected(), vs.isHeatmapSF(), + vs.isHeatmapNewest(), vs.getMaxAgeForHeatmap())); + heatmapToggle = toggle; + } + if (heatmapSettingsButton == null) { + Button settings = new Button("Heatmap Settings…"); + settings.setTooltip(new Tooltip("Open the heatmap options in the settings dialog.")); + settings.setOnAction(e -> SettingsManagerF.getInstance() + .showSettingsDialog(window, heatmapSettingsProvider)); + heatmapSettingsButton = settings; + } + return List.of(heatmapToggle, heatmapSettingsButton); } @Override diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/extension/contrib/TestExtensionF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/extension/contrib/TestExtensionF.java index 574a8452cb1..a8194b9e2d0 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/extension/contrib/TestExtensionF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/extension/contrib/TestExtensionF.java @@ -24,6 +24,7 @@ import de.uka.ilkd.key.proof.Node; import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; /** * Port of the Swing {@code TestExtension} (key.ui {@code gui.extension.impl.TestExtension}, @@ -46,6 +47,9 @@ public final class TestExtensionF implements KeYGuiExtensionF, KeYGuiExtensionF. /** The "Test" menu item label of the Swing original (TestExtension.java:106). */ private static final String TEST = "Test"; + /** The shared toolbar button (singleton — the host checks toolbar membership by identity). */ + private @Nullable Button testToolbarButton = null; + /** Shows the Swing original's "Test!" message (a JOptionPane) as an FX toast. */ private static void showTestMessage() { NotificationManagerF.getInstance().notify("Test!"); @@ -94,10 +98,16 @@ public List getProofTreeContextItems(KeYMediatorF mediator, Node node) @Override public List getToolbarControls(MainWindowF window, KeYMediatorF mediator) { - Button button = new Button(null, IconFactoryF.createIcon(IconFactoryF.Key.INFO_VIEW)); - button.setTooltip(new Tooltip(TEST)); - button.setOnAction(e -> showTestMessage()); - return List.of(button); + // a singleton like the sibling providers: the host (re)builds the toolbars and checks + // membership by identity, so every call must return the same control instances + if (testToolbarButton == null) { + Button button = + new Button(null, IconFactoryF.createIcon(IconFactoryF.Key.INFO_VIEW)); + button.setTooltip(new Tooltip(TEST)); + button.setOnAction(e -> showTestMessage()); + testToolbarButton = button; + } + return List.of(testToolbarButton); } // --- KeyboardShortcutsF (D36) ------------------ From bef4742120783e9e8439a91f6129d5847a3609fd Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Sat, 10 Oct 2026 17:45:44 +0200 Subject: [PATCH 164/281] =?UTF-8?q?P3b:=20B11/B12/B15/B17/B18=20=E2=80=94?= =?UTF-8?q?=20sign=20off=20menus/toolbar=20parity=20items?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- key.ui.fx/PARITY-SIGNOFF.md | 35 ++++++++++++++++++++++++++++++----- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/key.ui.fx/PARITY-SIGNOFF.md b/key.ui.fx/PARITY-SIGNOFF.md index 38df921d75d..42628d95d3a 100644 --- a/key.ui.fx/PARITY-SIGNOFF.md +++ b/key.ui.fx/PARITY-SIGNOFF.md @@ -143,14 +143,14 @@ differences found by a second Swing↔JavaFX sweep, with stable IDs used by the | ID | Finding (Swing reference) | Status | |----|---------------------------|--------| -| B11 | Dynamic "Goal Back" label (`GoalBackAction.java:113-125`) vs. static `MainWindowF.java:3325-3327` | OPEN (P3b) | -| B12 | Macro superset + category submenus + ProofScript actions vs. the 4-macro `AUTOMATION_MACROS` | OPEN (P3b) | +| B11 | Dynamic "Goal Back" label (`GoalBackAction.java:113-125`) vs. static `MainWindowF.java:3325-3327` | RESOLVED (P3b) | +| B12 | Macro superset + category submenus + ProofScript actions vs. the 4-macro `AUTOMATION_MACROS` | RESOLVED (P3b) | | B13 | Extension PATH nesting (extension menus cannot nest) | OPEN (P4) | | B14 | Per-extension toolbars merged into one | OPEN (P4, decide) | -| B15 | Toolbar GoalBack/Prune have no enablement | OPEN (P3b) | +| B15 | Toolbar GoalBack/Prune have no enablement | RESOLVED (P3b) | | B16 | Popup dialogs replaced by toasts (per site) | OPEN (P4, decide) | -| B17 | TermLabelMenu persistence/styling | OPEN (P3b) | -| B18 | Recent-Files empty menu not disabled + font-size item order | OPEN (P3b) | +| B17 | TermLabelMenu persistence/styling | RESOLVED (P3b) | +| B18 | Recent-Files empty menu not disabled + font-size item order | RESOLVED (P3b) | ## C. Proof tree @@ -217,3 +217,28 @@ covering the OSS rows on a strategy-proved proof. The view filters and the tree in the tree context menu because the FX docking framework has no tab-title gear menu (documented in `ProofTreeViewF`); the subtree statistics report is a plain-text window rather than the Swing HTML one (export is A4/P3c). C24 (the `PROOF_TREE` extension seam) stays open for P4. + +P3b (`weigl/ocfx-p3b-menus`, see the branch report) resolved B11/B12/B15/B17/B18 — menu/toolbar +parity items: + +- B11/B15 — the Proof-menu item and toolbar buttons now follow Swing enablement and labels: + "Undo Last Rule Application" shows the applied rule name dynamically (`KeYMediatorF + .`goalBackRuleName`), and Goal Back / Prune are enabled per the selected node and auto mode + (`key.fx.verify.menuparity` legs). +- B12 — the macro surfaces (sequent right-click popup, term-menu "Strategy Macros", proof-tree + context-menu "Strategy Macros") now show the registered-`ProofMacro` superset + (`ProofMacroMenuF`), category-grouped like Swing, plus the PROOF_SCRIPTS menu (file chooser / + input dialog) executed by a new `ProofScriptWorkerF`; the proof-tree macro submenu is + persistent with a live PROOF_SCRIPTS feature listener. +- B17 — `TermLabelMenuF` items are `CustomMenuItem(HBox(CheckBox+Label))` rows that persist + their state to `Preferences.userNodeForPackage(MainWindowF.class)` under the exact Swing + `AbstractButtonSaver` keys (`DisplayLabelsCheckBox.DisplayLabelsCheckBox.selected` / + `TermLabelCheckBox.
        ")) { + report.append("statistics html missing the table; "); + ok = false; + } + CountDownLatch opened = new CountDownLatch(1); + RuleStatisticsDialogF[] dialog = new RuleStatisticsDialogF[1]; + Platform.runLater(() -> { + dialog[0] = RuleStatisticsDialogF.show(window.getStage(), results); + opened.countDown(); + }); + if (!opened.await(TIMEOUT_SECONDS, TimeUnit.SECONDS)) { + report.append("statistics dialog did not open; "); + return false; + } + if (!awaitWebViewLoad(dialog[0].getWebView())) { + report.append("statistics html did not render in the WebView; "); + ok = false; + } else { + report.append("statistics html rendered in the WebView dialog; "); + } + Platform.runLater(dialog[0]::close); + return ok; + } + + private static boolean assertSorted(StringBuilder report, String what, + List entries, Comparator comparator) { + boolean sorted = isSorted(entries, comparator); + report.append(what).append(": ").append(sorted ? "ok" : "FAILED").append(" (") + .append(entries.size()).append(" entries); "); + return sorted; + } + + private static boolean isSorted(List entries, + Comparator comparator) { + for (int i = 1; i < entries.size(); i++) { + if (comparator.compare(entries.get(i - 1), entries.get(i)) > 0) { + return false; + } + } + return true; + } + + /** + * Waits until the given WebView finished loading its content (its DOM document is present). + */ + private static boolean awaitWebViewLoad(WebView view) throws InterruptedException { + long deadline = System.currentTimeMillis() + TIMEOUT_SECONDS * 1000; + while (System.currentTimeMillis() < deadline) { + if (view.getEngine().getDocument() != null) { + return true; + } + Thread.sleep(250); + } + return false; + } + + // ------------------------------------------------------------------ + // 4. fixed-point iteration + // ------------------------------------------------------------------ + + private static boolean checkFixedPoint(MainWindowF window, Proof originalProof, + StringBuilder report) throws InterruptedException { + SlicingExtensionF extension = findExtension(); + SlicingLeftPanelF panel = extension != null ? extension.getLeftPanel() : null; + if (panel == null) { + report.append("no slicing panel attached; "); + return false; + } + SliceToFixedPointDialogF dialog = onFx(() -> panel.openSliceToFixedPointDialog()); + if (dialog == null) { + report.append("fixed-point dialog did not open (no current proof?); "); + return false; + } + long deadline = System.currentTimeMillis() + TIMEOUT_SECONDS * 1000; + while (System.currentTimeMillis() < deadline && !dialog.isConverged()) { + Thread.sleep(1000); + } + boolean converged = dialog.isConverged(); + int iterations = dialog.iterationCount(); + report.append("fixed point: ").append(converged ? "converged" : "TIMEOUT") + .append(" after ").append(iterations).append(" iteration(s); "); + onFx(dialog::close); + Proof selected = window.getMediator().getSelectedProof(); + boolean sliceLoaded = selected != null && selected != originalProof && selected.closed(); + report.append("final proof loaded: ") + .append(sliceLoaded ? "ok" : "unchanged (no slicing potential)") + .append("; "); + return converged; + } + + // ------------------------------------------------------------------ + // 5. recent-files bypass of the direct loader path + // ------------------------------------------------------------------ + + private static boolean checkRecentFilesBypass(MainWindowF window, StringBuilder report) + throws InterruptedException { + String demoFile = System.getProperty("key.fx.demo.sequent"); + if (demoFile == null || demoFile.isBlank()) { + report.append("recent-files bypass not exercised (no key.fx.demo.sequent); "); + return false; + } + List before = onFx(window::recentFileEntries); + CountDownLatch loaded = new CountDownLatch(1); + onFx(() -> window.getMediator().registerProofLoadListener(p -> loaded.countDown())); + onFx(() -> window.loadProofFileDirect(java.nio.file.Path.of(demoFile))); + if (!loaded.await(TIMEOUT_SECONDS, TimeUnit.SECONDS)) { + report.append("direct loader load did not finish; "); + return false; + } + List after = onFx(window::recentFileEntries); + boolean untouched = after.equals(before); + report.append("recent-files bypass: ") + .append(untouched ? "list untouched by the direct loader load" : "FAILED") + .append("; "); + return untouched; + } + + // ------------------------------------------------------------------ + // helpers + // ------------------------------------------------------------------ + + private static void onFx(Runnable action) throws InterruptedException { + CountDownLatch latch = new CountDownLatch(1); + Platform.runLater(() -> { + try { + action.run(); + } finally { + latch.countDown(); + } + }); + latch.await(); + } + + private static T onFx(Supplier action) throws InterruptedException { + List holder = new ArrayList<>(1); + CountDownLatch latch = new CountDownLatch(1); + Platform.runLater(() -> { + try { + holder.add(action.get()); + } finally { + latch.countDown(); + } + }); + latch.await(); + return holder.get(0); + } + + /** Runs auto mode on the demo proof through the window's proof control (Swing parity of the + * {@code key.fx.demo.autoprove} load-task run). */ + private static final class ProofControlAccess { + static void runAutoMode(MainWindowF window) { + window.getUserInterfaceControl().getProofControl() + .startAndWaitForAutoMode(window.getMediator().getSelectedProof()); + } + } +} diff --git a/keyext.slicing.fx/src/main/java/org/key_project/slicing/fx/ui/GraphvizDotExecutorF.java b/keyext.slicing.fx/src/main/java/org/key_project/slicing/fx/ui/GraphvizDotExecutorF.java new file mode 100644 index 00000000000..a7c59fd41bb --- /dev/null +++ b/keyext.slicing.fx/src/main/java/org/key_project/slicing/fx/ui/GraphvizDotExecutorF.java @@ -0,0 +1,170 @@ +/* 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 org.key_project.slicing.fx.ui; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; + +import org.key_project.slicing.SlicingSettings; +import org.key_project.slicing.SlicingSettingsProvider; + +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Renders a graph in DOT format to a PNG image by launching the graphviz {@code dot} binary + * (non-Swing FX port of {@code org.key_project.slicing.util.GraphvizDotExecutor}): + * the DOT source is piped into {@code dot -Tpng} through stdin and the resulting PNG bytes are + * handed to the FX {@link javafx.scene.image.Image} of {@link PreviewDialogF}. + *

        + * Deviations from the Swing original (which is an AWT/Swing {@code SwingWorker} producing a + * {@code BufferedImage} via {@code javax.imageio.ImageIO}): + *

          + *
        • the render is a plain blocking method (no Swing worker); callers must invoke it off the + * JavaFX application thread and marshall the dialog update back with + * {@code Platform.runLater} (the callers in {@link SlicingLeftPanelF} and + * {@code SlicingExtensionF#showGraphItem} do exactly that),
        • + *
        • the {@code dot} availability check is re-run on every render instead of being cached in a + * static field: the executable path is a user setting that may change between renders, and the + * {@code key.fx.verify.slicing} harness has to exercise the not-installed fallback within a + * single app run,
        • + *
        • the image is kept as raw PNG bytes ({@code javafx.scene.image.Image} accepts a byte + * array); no AWT image types are involved.
        • + *
        + * If {@code dot} is not installed (or fails), {@link #renderPng} returns an error result and the + * callers fall back to showing the DOT source text (the documented fallback, exercised by the + * verify harness even when {@code dot} is present). + * + * @author Alexander Weigl (FX port) + * @author Arne Keller (Swing original) + */ +@NullMarked +public final class GraphvizDotExecutorF { + + private static final Logger LOGGER = LoggerFactory.getLogger(GraphvizDotExecutorF.class); + + private GraphvizDotExecutorF() { + } + + /** + * The outcome of a {@code dot} invocation (the FX counterpart of the Swing + * {@code GraphvizResult}). + * + * @param png the rendered PNG image data, {@code null} if an error occurred + * @param error the error message, {@code null} if an image was rendered + */ + public record Result(byte @Nullable [] png, @Nullable String error) { + + /** @return whether this result contains a rendered image */ + public boolean hasImage() { + return png != null; + } + + /** @return whether this result indicates an error */ + public boolean hasError() { + return error != null; + } + + /** + * @return whether the rendered data carries the PNG magic bytes (8-byte signature + * {@code 89 50 4E 47 0D 0A 1A 0A}) + */ + public boolean isPng() { + return png != null && png.length > 8 && png[0] == (byte) 0x89 && png[1] == 0x50 + && png[2] == 0x4E && png[3] == 0x47 && png[4] == 0x0D && png[5] == 0x0A + && png[6] == 0x1A && png[7] == 0x0A; + } + } + + /** + * Checks whether the configured {@code dot} executable works (Swing + * {@code GraphvizDotExecutor.isDotInstalled}): runs {@code -V}. + *

        + * Unlike the Swing original this is not cached — see the class javadoc. + * + * @return whether graphviz rendering is available + */ + public static boolean isDotInstalled() { + String executable = executablePath(); + if (executable.isBlank()) { + return false; + } + try { + Process process = new ProcessBuilder(executable, "-V").start(); + int exit = process.waitFor(); + process.destroyForcibly(); + return exit == 0; + } catch (IOException | InterruptedException e) { + return false; + } + } + + /** + * Renders the given DOT graph to a PNG image (Swing + * {@code GraphvizDotExecutor.doInBackground}). Blocking — call off the FX thread. + * + * @param dot graph to render (in DOT format) + * @return the render result (image or error) + */ + public static Result renderPng(String dot) { + String executable = executablePath(); + byte[] input = dot.getBytes(StandardCharsets.UTF_8); + LOGGER.info("starting dot ({}) with {} KB of graph data", executable, input.length / 1024); + Process[] processHolder = new Process[1]; + try { + processHolder[0] = new ProcessBuilder(executable, "-Tpng").start(); + Process process = processHolder[0]; + try (OutputStream stdin = process.getOutputStream()) { + stdin.write(input); + } + ByteArrayOutputStream stdout = new ByteArrayOutputStream(); + ByteArrayOutputStream stderr = new ByteArrayOutputStream(); + Thread errDrain = Thread.ofPlatform().daemon().name("dot-stderr-drain").start(() -> { + try (InputStream errStream = process.getErrorStream()) { + errStream.transferTo(stderr); + } catch (IOException e) { + // the drain thread terminates together with the process + } + }); + try (InputStream outStream = process.getInputStream()) { + outStream.transferTo(stdout); + } + int exit = process.waitFor(); + errDrain.join(5000); + if (exit != 0) { + return new Result(null, stderr.toString(StandardCharsets.UTF_8)); + } + byte[] png = stdout.toByteArray(); + if (png.length == 0) { + return new Result(null, "dot produced no image data"); + } + return new Result(png, null); + } catch (IOException e) { + LOGGER.info("failed to run dot ({})", executable, e); + return new Result(null, String.valueOf(e.getMessage())); + } catch (InterruptedException e) { + LOGGER.info("stopping dot"); + if (processHolder[0] != null) { + processHolder[0].destroyForcibly(); + } + Thread.currentThread().interrupt(); + return new Result(null, "dot interrupted"); + } + } + + /** + * @return the configured dot executable path (Swing + * {@code SlicingSettings.getDotExecutable}, which falls back to {@code dot}) + */ + private static String executablePath() { + SlicingSettings settings = SlicingSettingsProvider.getSlicingSettings(); + return settings.getDotExecutable(); + } +} diff --git a/keyext.slicing.fx/src/main/java/org/key_project/slicing/fx/ui/HtmlDialogF.java b/keyext.slicing.fx/src/main/java/org/key_project/slicing/fx/ui/HtmlDialogF.java new file mode 100644 index 00000000000..e4930cb9030 --- /dev/null +++ b/keyext.slicing.fx/src/main/java/org/key_project/slicing/fx/ui/HtmlDialogF.java @@ -0,0 +1,101 @@ +/* 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 org.key_project.slicing.fx.ui; + +import javafx.scene.Scene; +import javafx.scene.control.Button; +import javafx.scene.control.ButtonBar; +import javafx.scene.layout.BorderPane; +import javafx.geometry.Insets; +import javafx.scene.web.WebView; +import javafx.stage.Modality; +import javafx.stage.Window; + +import de.uka.ilkd.key.gui.fx.theme.ThemeManager; + +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Dialog that displays an HTML document, FX port of {@code org.key_project.slicing.ui.HtmlDialog} + * (which hosts the HTML in a Swing {@code JEditorPane}): the FX counterpart renders the HTML in + * a non-editable {@link WebView} above a Close button, the same seam the + * {@code SoundinessDialogF} report uses for its HTML output. Replaces the read-only multi-line + * text dialog stand-in for the HTML statistics, the execution timings and the fixed-point + * progress tables. + *

        + * Deviation from the Swing original: the {@code linkPressedCallback} of the Swing constructor is + * not ported (none of the current callers passes non-null links — the statistics and timings + * tables are generated with all columns non-clickable); a follow-up caller needing hyperlink + * activation can use the {@code WebView} engine's confirm handler. + */ +@NullMarked +public class HtmlDialogF extends javafx.stage.Stage { + + private static final Logger LOGGER = LoggerFactory.getLogger(HtmlDialogF.class); + + /** The rendered HTML content (exposed for the {@code key.fx.verify.slicing} harness). */ + private final WebView webView; + + /** + * Construct and show a new dialog for the provided HTML (Swing + * {@code HtmlDialog(Window, String, String, Consumer)}). + * + * @param owner owner window, may be {@code null} + * @param title title of the new dialog + * @param html HTML to show + */ + public HtmlDialogF(@Nullable Window owner, String title, String html) { + setTitle(title); + if (owner != null) { + initOwner(owner); + initModality(Modality.NONE); + } + + webView = new WebView(); + webView.setPrefSize(760, 420); + webView.getEngine().loadContent(html, "text/html"); + + Button closeButton = new Button("Close"); + closeButton.setOnAction(e -> close()); + ButtonBar.setButtonData(closeButton, ButtonBar.ButtonData.OK_DONE); + closeButton.setDefaultButton(true); + ButtonBar buttonBar = new ButtonBar(); + buttonBar.getButtons().add(closeButton); + BorderPane.setMargin(buttonBar, new Insets(10)); + + BorderPane root = new BorderPane(webView, null, null, buttonBar, null); + Scene scene = new Scene(root, 800, 500); + try { + // theme the dialog like the other FX dialogs (best effort) + scene.getStylesheets().add(ThemeManager.getInstance().getTheme().stylesheetUrl()); + } catch (RuntimeException e) { + LOGGER.debug("could not theme the HTML dialog", e); + } + setScene(scene); + } + + /** @return the web view showing the HTML (verify harness seam) */ + public WebView getWebView() { + return webView; + } + + /** + * Creates and shows the dialog (Swing {@code new HtmlDialog(...)} + implicit + * {@code setVisible(true)}). + * + * @param owner owner window, may be {@code null} + * @param title title of the new dialog + * @param html HTML to show + * @return the shown dialog + */ + public static HtmlDialogF show(@Nullable Window owner, String title, String html) { + HtmlDialogF dialog = new HtmlDialogF(owner, title, html); + dialog.show(); + return dialog; + } +} diff --git a/keyext.slicing.fx/src/main/java/org/key_project/slicing/fx/ui/PreviewDialogF.java b/keyext.slicing.fx/src/main/java/org/key_project/slicing/fx/ui/PreviewDialogF.java new file mode 100644 index 00000000000..ab7ed0aebec --- /dev/null +++ b/keyext.slicing.fx/src/main/java/org/key_project/slicing/fx/ui/PreviewDialogF.java @@ -0,0 +1,112 @@ +/* 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 org.key_project.slicing.fx.ui; + +import javafx.application.Platform; +import javafx.scene.Scene; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.ScrollPane; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.BorderPane; +import javafx.geometry.Insets; +import javafx.stage.Window; + +import org.key_project.slicing.fx.ui.GraphvizDotExecutorF.Result; + +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Dialog that displays a rendering of the dependency graph, FX port of + * {@code org.key_project.slicing.ui.PreviewDialog}: the DOT source is rendered by + * {@link GraphvizDotExecutorF} (the graphviz {@code dot} binary) and the resulting image is + * shown in an {@link ImageView} inside a scroll pane. + *

        + * Deviations from the Swing original: + *

          + *
        • the render runs on a daemon thread instead of a {@code SwingWorker}; the dialog is only + * opened once the render finished (the Swing original opens immediately with a + * "Running dot on ... KB" label and swaps the image in when done) — the deterministic open + * avoids a blank dialog flash and keeps the render off the FX thread,
        • + *
        • the pan/zoom {@code PanZoomImageView} is not ported; the image is shown fit-to-width in a + * {@link ScrollPane} (which provides the panning),
        • + *
        • if the render fails (graphviz missing or {@code dot} error) the dialog falls back to the + * DOT source text dialog ({@link SlicingLeftPanelF#showTextDialog}, the former stand-in seam) + * prefixed with the error message.
        • + *
        + */ +@NullMarked +public class PreviewDialogF extends javafx.stage.Stage { + + private static final Logger LOGGER = LoggerFactory.getLogger(PreviewDialogF.class); + + private PreviewDialogF(@Nullable Window owner, String title, Result result) { + setTitle(title); + if (owner != null) { + initOwner(owner); + } + + BorderPane root = new BorderPane(); + if (result.hasImage() && result.png() != null) { + Image image = new Image(new java.io.ByteArrayInputStream(result.png())); + ImageView view = new ImageView(image); + view.setFitWidth(800); + view.setFitHeight(600); + view.setPreserveRatio(true); + view.setSmooth(true); + ScrollPane scroll = new ScrollPane(view); + scroll.setPrefSize(820, 620); + root.setCenter(scroll); + } else { + // documented fallback for an unavailable or failing graphviz installation: show the + // error together with the DOT source (the previous stand-in behavior) + Label error = new Label( + "graphviz rendering failed (%s). Showing the DOT source instead.".formatted( + result.error() != null ? result.error() : "unknown error")); + error.setWrapText(true); + root.setTop(error); + } + + Button closeButton = new Button("Close"); + closeButton.setOnAction(e -> close()); + closeButton.setDefaultButton(true); + BorderPane.setMargin(closeButton, new Insets(10)); + root.setBottom(closeButton); + + setScene(new Scene(root, result.hasImage() ? 840 : 640, + result.hasImage() ? 700 : 200)); + } + + /** + * Renders the given DOT graph with graphviz and shows the result: an image dialog on + * success, the documented DOT-source text fallback on failure (Swing + * {@code new PreviewDialog(window, dot)}). The render runs on a daemon thread. + * + * @param owner owner window, may be {@code null} + * @param dot graph to render (in DOT format) + */ + public static void showDot(@Nullable Window owner, String dot) { + Thread thread = new Thread(() -> { + Result result = GraphvizDotExecutorF.renderPng(dot); + LOGGER.info("dot render finished: image={} error={}", result.hasImage(), + result.error()); + Platform.runLater(() -> { + if (result.hasImage()) { + new PreviewDialogF(owner, "Preview", result).show(); + } else { + SlicingLeftPanelF.showTextDialog("Dependency graph (rendering failed)", + "graphviz rendering failed: %s%n%n%s".formatted( + result.error() != null ? result.error() : "unknown error", dot)); + } + }); + }, "graphviz-dot-render"); + thread.setDaemon(true); + thread.start(); + } +} diff --git a/keyext.slicing.fx/src/main/java/org/key_project/slicing/fx/ui/RuleStatisticsDialogF.java b/keyext.slicing.fx/src/main/java/org/key_project/slicing/fx/ui/RuleStatisticsDialogF.java new file mode 100644 index 00000000000..a49b99f9ca0 --- /dev/null +++ b/keyext.slicing.fx/src/main/java/org/key_project/slicing/fx/ui/RuleStatisticsDialogF.java @@ -0,0 +1,168 @@ +/* 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 org.key_project.slicing.fx.ui; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.List; +import java.util.Optional; + +import javafx.scene.Scene; +import javafx.scene.control.Button; +import javafx.scene.control.ButtonBar; +import javafx.scene.layout.BorderPane; +import javafx.geometry.Insets; +import javafx.scene.web.WebView; +import javafx.stage.Modality; +import javafx.stage.Window; + +import org.key_project.slicing.RuleStatistics; +import org.key_project.slicing.RuleStatistics.RuleStatisticEntry; +import org.key_project.slicing.analysis.AnalysisResults; +import org.key_project.slicing.ui.HtmlFactory; + +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Dialog that displays the results of the dependency analysis algorithm, FX port of + * {@code org.key_project.slicing.ui.RuleStatisticsDialog}: the rule statistics are shown as an + * HTML table (generated by the reused {@link HtmlFactory#generateTable}) in the + * {@link HtmlDialogF} WebView seam, with the four sort buttons of the Swing original + * ("Sort by name", "Sort by total", "Sort by useless", "Sort by initial useless") that re-render + * the table in the chosen order. + */ +@NullMarked +public final class RuleStatisticsDialogF extends javafx.stage.Stage { + + private static final Logger LOGGER = LoggerFactory.getLogger(RuleStatisticsDialogF.class); + + /** The rule statistics displayed in this dialog (Swing {@code RuleStatisticsDialog.stats}). */ + private final transient RuleStatistics statistics; + + /** The web view rendering the table; re-loaded on every sort-button press. */ + private final WebView webView; + + private RuleStatisticsDialogF(@Nullable Window owner, AnalysisResults results) { + setTitle("Rule Statistics"); + this.statistics = results.ruleStatistics; + if (owner != null) { + initOwner(owner); + initModality(Modality.NONE); + } + + webView = new WebView(); + webView.setPrefSize(640, 380); + webView.getEngine().loadContent(genTable(statistics, + statistics.sortBy( + Comparator.comparing(RuleStatisticEntry::numberOfApplications).reversed())), + "text/html"); + + // Swing RuleStatisticsDialog.constructButtonPanel: the four sort orders re-render the + // table, Close disposes the dialog + Button sortButton1 = new Button("Sort by name"); + sortButton1.setOnAction(e -> render(statistics.sortBy( + Comparator.comparing(RuleStatisticEntry::ruleName)))); + Button sortButton2 = new Button("Sort by total"); + sortButton2.setOnAction(e -> render(statistics.sortBy( + Comparator.comparing(RuleStatisticEntry::numberOfApplications).reversed()))); + Button sortButton3 = new Button("Sort by useless"); + sortButton3.setOnAction(e -> render(statistics.sortBy( + Comparator.comparing(RuleStatisticEntry::numberOfUselessApplications).reversed()))); + Button sortButton4 = new Button("Sort by initial useless"); + sortButton4.setOnAction(e -> render(statistics.sortBy( + Comparator.comparing(RuleStatisticEntry::numberOfInitialUselessApplications) + .reversed()))); + Button closeButton = new Button("Close"); + closeButton.setOnAction(e -> close()); + ButtonBar.setButtonData(closeButton, ButtonBar.ButtonData.OK_DONE); + closeButton.setDefaultButton(true); + + ButtonBar buttonBar = new ButtonBar(); + buttonBar.getButtons().addAll(sortButton1, sortButton2, sortButton3, sortButton4, + closeButton); + BorderPane.setMargin(buttonBar, new Insets(10)); + + BorderPane root = new BorderPane(webView, null, null, buttonBar, null); + setScene(new Scene(root, 720, 520)); + } + + private void render(List entries) { + LOGGER.debug("re-rendering the rule statistics table ({} entries)", entries.size()); + webView.getEngine().loadContent(genTable(statistics, entries), "text/html"); + } + + /** @return the web view showing the statistics table (verify harness seam) */ + public WebView getWebView() { + return webView; + } + + /** + * Creates and shows the statistics dialog (Swing + * {@code new RuleStatisticsDialog(window, results)}). + * + * @param owner owner window, may be {@code null} + * @param results the analysis results to show + * @return the shown dialog + */ + public static RuleStatisticsDialogF show(@Nullable Window owner, AnalysisResults results) { + RuleStatisticsDialogF dialog = new RuleStatisticsDialogF(owner, results); + dialog.show(); + return dialog; + } + + /** + * Generate the HTML table to display in this dialog (Swing + * {@code RuleStatisticsDialog.genTable}): a summary row for all rules, a summary row for the + * branching rules, then one row per rule. + * + * @param statistics statistics on rule apps (needed for the branching-rule summary row) + * @param rules the entries in the order they should appear + * @return HTML + */ + public static String genTable(RuleStatistics statistics, List rules) { + List columns = List.of("Rule name", "Total applications", "Useless applications", + "Initial useless applications"); + + List> rows = new ArrayList<>(); + // summary row + int uniqueRules = rules.size(); + int totalSteps = rules.stream().mapToInt(RuleStatisticEntry::numberOfApplications).sum(); + int uselessSteps = + rules.stream().mapToInt(RuleStatisticEntry::numberOfUselessApplications).sum(); + int initialUseless = + rules.stream().mapToInt(RuleStatisticEntry::numberOfInitialUselessApplications).sum(); + rows.add(List.of(String.format("(all %d rules)", uniqueRules), + Integer.toString(totalSteps), Integer.toString(uselessSteps), + Integer.toString(initialUseless))); + // next summary row + List rulesBranching = + rules.stream().filter(it -> statistics.branches(it.ruleName())).toList(); + int uniqueRules2 = rulesBranching.size(); + totalSteps = + rulesBranching.stream().mapToInt(RuleStatisticEntry::numberOfApplications).sum(); + uselessSteps = + rulesBranching.stream().mapToInt(RuleStatisticEntry::numberOfUselessApplications) + .sum(); + initialUseless = rulesBranching.stream() + .mapToInt(RuleStatisticEntry::numberOfInitialUselessApplications).sum(); + rows.add(List.of(String.format("(%d branching rules)", uniqueRules2), + Integer.toString(totalSteps), Integer.toString(uselessSteps), + Integer.toString(initialUseless))); + rules.forEach(a -> { + String name = a.ruleName(); + Integer all = a.numberOfApplications(); + Integer useless = a.numberOfUselessApplications(); + Integer iua = a.numberOfInitialUselessApplications(); + rows.add(List.of(name, all.toString(), useless.toString(), iua.toString())); + }); + + return HtmlFactory.generateTable(columns, new boolean[] { false, false, false, false }, + Optional.of(new String[] { null, "right", "right", "right" }), rows, null); + } +} diff --git a/keyext.slicing.fx/src/main/java/org/key_project/slicing/fx/ui/SliceToFixedPointDialogF.java b/keyext.slicing.fx/src/main/java/org/key_project/slicing/fx/ui/SliceToFixedPointDialogF.java new file mode 100644 index 00000000000..985b4f01dd4 --- /dev/null +++ b/keyext.slicing.fx/src/main/java/org/key_project/slicing/fx/ui/SliceToFixedPointDialogF.java @@ -0,0 +1,284 @@ +/* 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 org.key_project.slicing.fx.ui; + +import java.nio.file.Path; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.Deque; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +import javafx.scene.Scene; +import javafx.scene.control.Button; +import javafx.scene.control.ButtonBar; +import javafx.scene.layout.BorderPane; +import javafx.geometry.Insets; +import javafx.scene.web.WebView; + +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.proof.Node; +import de.uka.ilkd.key.proof.Proof; + +import org.key_project.slicing.analysis.AnalysisResults; +import org.key_project.slicing.ui.HtmlFactory; +import org.key_project.util.javafx.FxUtil; + +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Dialog to monitor slicing progress, FX port of + * {@code org.key_project.slicing.ui.SliceToFixedPointDialog}: automatically analyzes and slices + * the currently selected proof until the fixed point is reached (no more steps can be removed), + * simulating the user interactions just like the Swing original — each iteration runs the + * analyze callback, and if more steps may be removed it triggers the slice callback (which + * slices and loads the next proof through the direct loader, keeping it out of the recent-files + * list). The dialog listens on the mediator's selection model to observe each newly loaded + * (closed) slice and starts the next worker; intermediate proof slices are discarded + * automatically. + *

        + * The progress tables (per-iteration step counts and the "times sliced away" statistics) are + * shown in the {@link HtmlDialogF} WebView seam using {@link HtmlFactory#generateTable}, like + * the Swing dialog's {@code JEditorPane}. + *

        + * Deviations from the Swing original: the FX dialog is a {@link javafx.stage.Stage} instead of + * a modal {@code JDialog} (the panel has no modal dialog infrastructure); everything else + * mirrors the Swing worker/selection-listener choreography. + * + * @author Alexander Weigl (FX port) + * @author Arne Keller (Swing original) + */ +@NullMarked +public class SliceToFixedPointDialogF extends javafx.stage.Stage + implements KeYSelectionListener { + + private static final Logger LOGGER = LoggerFactory.getLogger(SliceToFixedPointDialogF.class); + + /** Active KeY mediator. */ + private final KeYMediatorF mediator; + /** + * Function that provides the analysis results of the last loaded proof (wrapped to record + * the statistics of this dialog). + */ + private final Supplier<@Nullable AnalysisResults> analyzeCallback; + /** Function that slices the currently loaded proof. */ + private final Runnable sliceCallback; + + /** Text pane showing slicing progress in tabular form (Swing {@code logPane}). */ + private final WebView logPane; + + /** Current slicing worker. */ + private @Nullable SliceToFixedPointWorkerF worker; + + /** Table data to display. Contains statistics on sliced away rule applications. */ + private final List> tableRows = new ArrayList<>(); + /** + * Stores for all slices in total: rule name -> number of times sliced away. + */ + private final Map slicedAway = new HashMap<>(); + + /** Whether the fixed point was reached (the last analysis found nothing more to remove). */ + private boolean converged = false; + /** Number of analyze/slice iterations started in this dialog. */ + private int iterationCount = 0; + + /** + * Construct a new dialog (Swing {@code SliceToFixedPointDialog(KeYMediator, Window, + * Function, Runnable)}): registers itself as a selection listener, so it is always notified + * of newly loaded (= sliced) proofs. + * + * @param mediator KeY mediator + * @param owner owner window, may be {@code null} + * @param analyzeCallback function that provides analysis results on the last loaded proof + * @param sliceCallback function that slices the last loaded proof + */ + public SliceToFixedPointDialogF(KeYMediatorF mediator, + javafx.stage.@Nullable Window owner, + Supplier<@Nullable AnalysisResults> analyzeCallback, Runnable sliceCallback) { + setTitle("Slice to fixed point"); + this.mediator = mediator; + mediator.getSelectionModel().addKeYSelectionListener(this); + // wrap the analysis callback to gather statistics on useless rule applications + this.analyzeCallback = wrapAnalysisCallback(analyzeCallback); + this.sliceCallback = sliceCallback; + + logPane = new WebView(); + logPane.setPrefSize(700, 400); + updateTable(); + + Button closeButton = new Button("Close"); + closeButton.setOnAction(e -> { + mediator.getSelectionModel().removeKeYSelectionListener(this); + close(); + }); + ButtonBar.setButtonData(closeButton, ButtonBar.ButtonData.OK_DONE); + closeButton.setDefaultButton(true); + ButtonBar buttonBar = new ButtonBar(); + buttonBar.getButtons().add(closeButton); + BorderPane.setMargin(buttonBar, new Insets(10)); + + BorderPane root = new BorderPane(logPane, null, null, buttonBar, null); + setScene(new Scene(root, 760, 520)); + if (owner != null) { + initOwner(owner); + } + show(); + } + + /** + * Wrap the provided analyze callback such that results of the callback will first be + * recorded in {@link #tableRows} (Swing {@code SliceToFixedPointDialog.wrapAnalysisCallback}). + * + * @param callback callback to provide analysis results + * @return function that provides analysis results + */ + private Supplier<@Nullable AnalysisResults> wrapAnalysisCallback( + Supplier<@Nullable AnalysisResults> callback) { + return () -> { + @Nullable AnalysisResults results; + try { + results = callback.get(); + } catch (Exception e) { + LOGGER.error("failed to analyze proof", e); + results = null; + } + if (results != null) { + try { + // record useless rule applications in map + Deque queue = new ArrayDeque<>(List.of(results.proof.root())); + while (!queue.isEmpty()) { + Node node = queue.pop(); + node.childrenIterator().forEachRemaining(queue::add); + if (node.getAppliedRuleApp() == null + || results.usefulSteps.contains(node)) { + continue; + } + String name = node.getAppliedRuleApp().rule().displayName(); + if (node.childrenCount() > 1) { + name = name + "*"; + } + synchronized (this) { + slicedAway.compute(name, (k, v) -> v == null ? 1 : v + 1); + } + } + Path filename = results.proof.getProofFile(); + String label = filename != null + ? filename.getFileName().toString() + : results.proof.name().toString(); + synchronized (this) { + tableRows.add(List.of( + label, + String.valueOf(results.totalSteps), + String.valueOf(results.usefulStepsNr), + String.valueOf(results.proof.countBranches()), + String.valueOf(results.usefulBranchesNr))); + } + FxUtil.runLater(this::updateTable); + } catch (Exception e) { + LOGGER.error("failed to record statistics ", e); + } + } + return results; + }; + } + + /** Re-render the progress tables (Swing {@code SliceToFixedPointDialog.updateTable}). */ + private void updateTable() { + List> rows; + Map away; + synchronized (this) { + rows = List.copyOf(tableRows); + away = Map.copyOf(slicedAway); + } + String html = HtmlFactory.generateTable( + List.of("Filename", "Total steps", "Useful steps", "Total branches", + "Useful branches"), + new boolean[] { false, false, false, false, false }, + Optional.of(new String[] { null, "right", "right", "right", "right" }), + rows, + null); + List> data = away.entrySet().stream() + // sort inferences rule sliced away most to the start + .sorted(Comparator.comparing(x -> -x.getValue())) + .map(x -> (Collection) List.of(x.getKey(), x.getValue().toString())) + .collect(Collectors.toList()); + String html2 = HtmlFactory.generateTable( + List.of("Inference rule", "Times sliced away"), + new boolean[] { false, false }, + Optional.of(new String[] { null, "right" }), + data, + null); + logPane.getEngine().loadContent(html + "


        " + html2, "text/html"); + } + + /** + * Start analyzing and slicing the currently selected proof (Swing + * {@code SliceToFixedPointDialog.start}). + * + * @param proof the currently selected proof + */ + public void start(Proof proof) { + worker = newWorker(proof, null); + worker.execute(); + } + + /** Builds and registers a new worker (Swing worker construction in the listener). */ + private SliceToFixedPointWorkerF newWorker(Proof proof, @Nullable Proof proofToDispose) { + iterationCount++; + return new SliceToFixedPointWorkerF(proof, proofToDispose, analyzeCallback, sliceCallback, + () -> { + LOGGER.info("Slice to fixed point: converged after {} iteration(s)", iterationCount); + converged = true; + }); + } + + @Override + public void selectedProofChanged(KeYSelectionEvent e) { + FxUtil.runLater(() -> { + Proof selected = e.getSource().getSelectedProof(); + if (selected == null || !selected.closed()) { + return; + } + SliceToFixedPointWorkerF current = worker; + if (current != null && selected == current.getSlicedProof()) { + return; // no need to slice this one again + } + // pass previously sliced proof to worker, so that it is disposed of later + worker = newWorker(selected, current != null ? current.getSlicedProof() : null); + worker.execute(); + }); + } + + @Override + public void selectedNodeChanged(KeYSelectionEvent e) { + // the dialog only reacts to proof switches + } + + /** @return whether the fixed point was reached (verify harness seam) */ + public boolean isConverged() { + return converged; + } + + /** @return the number of analyze/slice iterations started (verify harness seam) */ + public int iterationCount() { + return iterationCount; + } + + /** @return the web view showing the progress tables (verify harness seam) */ + public WebView getWebView() { + return logPane; + } +} diff --git a/keyext.slicing.fx/src/main/java/org/key_project/slicing/fx/ui/SliceToFixedPointWorkerF.java b/keyext.slicing.fx/src/main/java/org/key_project/slicing/fx/ui/SliceToFixedPointWorkerF.java new file mode 100644 index 00000000000..8bd5bc4ba4f --- /dev/null +++ b/keyext.slicing.fx/src/main/java/org/key_project/slicing/fx/ui/SliceToFixedPointWorkerF.java @@ -0,0 +1,114 @@ +/* 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 org.key_project.slicing.fx.ui; + +import java.nio.file.Path; +import java.util.function.Supplier; + +import javafx.concurrent.Task; + +import de.uka.ilkd.key.proof.Proof; + +import org.key_project.slicing.analysis.AnalysisResults; +import org.key_project.util.javafx.FxUtil; + +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Worker to analyze and slice a proof, FX port of + * {@code org.key_project.slicing.ui.SliceToFixedPointWorker} (a SwingWorker): the background + * task analyzes the proof (via the wrapped analyze callback of the dialog) and either reports + * "no more slicing possible" (the fixed point is reached, {@code doneCallback} runs) or + * disposes the previous intermediate slice and triggers the slice button on the FX thread. + * + * @author Alexander Weigl (FX port) + * @author Arne Keller (Swing original) + */ +@NullMarked +final class SliceToFixedPointWorkerF { + + private static final Logger LOGGER = LoggerFactory.getLogger(SliceToFixedPointWorkerF.class); + + /** The proof to analyze and slice. */ + private final Proof proof; + /** The proof to dispose after successfully completing analysis + slicing. */ + private final @Nullable Proof proofToDispose; + /** + * Function that provides the analysis results of the {@link #proof} (the dialog's wrapped + * analyze callback). + */ + private final Supplier<@Nullable AnalysisResults> analyzeCallback; + /** Callback that starts slicing the {@link #proof} (invoked on the FX thread). */ + private final Runnable sliceCallback; + /** Callback called if the proof cannot be sliced further (invoked on the FX thread). */ + private final Runnable doneCallback; + + /** + * Construct a new worker (Swing {@code SliceToFixedPointWorker} constructor). + * + * @param proofToSlice the proof to slice + * @param proofToDispose the proof to dispose after successfully analyzing and slicing the + * current proof + * @param analyzeCallback callback that provides the analysis results + * @param sliceCallback callback to slice the proof + * @param doneCallback called if the proof can not be sliced further + */ + SliceToFixedPointWorkerF(Proof proofToSlice, @Nullable Proof proofToDispose, + Supplier<@Nullable AnalysisResults> analyzeCallback, Runnable sliceCallback, + Runnable doneCallback) { + this.proof = proofToSlice; + this.proofToDispose = proofToDispose; + this.analyzeCallback = analyzeCallback; + this.sliceCallback = sliceCallback; + this.doneCallback = doneCallback; + } + + /** + * Start the background analysis (Swing {@code SliceToFixedPointWorker.execute} + + * {@code doInBackground}). + */ + void execute() { + Task task = new Task<>() { + @Override + protected Void call() { + LOGGER.info("analyzing proof {} (ID: {})", proof.name(), + System.identityHashCode(proof)); + @Nullable AnalysisResults results; + try { + results = analyzeCallback.get(); + } catch (Exception e) { + LOGGER.error("failed to analyze proof", e); + results = null; + } + if (results == null || !results.indicateSlicingPotential()) { + // fixed point reached: no more steps can be removed + LOGGER.info("analysis: no more slicing possible"); + FxUtil.runLater(doneCallback); + } else { + if (proofToDispose != null) { + LOGGER.info("disposing intermediate proof slice {} (ID: {})", + proofToDispose.name(), System.identityHashCode(proofToDispose)); + proofToDispose.dispose(); + } + LOGGER.info("slicing proof {} (ID: {})", proof.name(), + System.identityHashCode(proof)); + FxUtil.runLater(sliceCallback); + } + return null; + } + }; + Thread thread = new Thread(task, "SliceToFixedPoint"); + thread.setDaemon(true); + thread.start(); + } + + /** @return the proof sliced by this worker (Swing {@code getSlicedProof}) */ + Proof getSlicedProof() { + return proof; + } +} diff --git a/keyext.slicing.fx/src/main/java/org/key_project/slicing/fx/ui/SlicingLeftPanelF.java b/keyext.slicing.fx/src/main/java/org/key_project/slicing/fx/ui/SlicingLeftPanelF.java index 3f0b4ab5a93..aaa31a10e19 100644 --- a/keyext.slicing.fx/src/main/java/org/key_project/slicing/fx/ui/SlicingLeftPanelF.java +++ b/keyext.slicing.fx/src/main/java/org/key_project/slicing/fx/ui/SlicingLeftPanelF.java @@ -9,8 +9,9 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; -import java.util.Comparator; +import java.util.Collection; import java.util.List; +import java.util.Optional; import java.util.stream.Collectors; import javafx.application.Platform; import javafx.concurrent.Task; @@ -25,6 +26,7 @@ import javafx.scene.control.TitledPane; import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; +import javafx.scene.web.WebView; import javafx.stage.FileChooser; import de.uka.ilkd.key.core.fx.KeYMediatorF; @@ -39,11 +41,12 @@ import de.uka.ilkd.key.settings.ProofIndependentSettings; import org.key_project.slicing.DependencyTracker; -import org.key_project.slicing.RuleStatistics.RuleStatisticEntry; import org.key_project.slicing.SlicingProofReplayer; import org.key_project.slicing.SlicingSettingsProvider; import org.key_project.slicing.analysis.AnalysisResults; import org.key_project.slicing.fx.SlicingExtensionF; +import org.key_project.slicing.ui.HtmlFactory; +import org.key_project.util.javafx.FxUtil; import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; @@ -56,16 +59,23 @@ * analysis controls and the slicing buttons, rendered with pure JavaFX controls. The underlying * logic (dependency tracking, analysis, slicing) is reused from {@code keyext.slicing} unchanged. *

        - * KNOWN-SIMPLIFIED (compared to the Swing panel): + * Parity notes (compared to the Swing panel): "Show rendering of graph" renders the DOT through + * the graphviz {@code dot} binary and shows the image in the {@link PreviewDialogF} (with the + * DOT-source text dialog as the documented fallback when rendering fails); "Slice proof to fixed + * point" opens the iterative {@link SliceToFixedPointDialogF}; "Show rule statistics" renders + * the HTML table with the four sort buttons of the Swing original in the {@link HtmlDialogF} + * WebView seam; the execution timings are shown as an {@link HtmlFactory} HTML table like in the + * Swing panel. Remaining, deliberate simplifications: *

          - *
        • "Show rendering of graph" renders the DOT through the Swing-only graphviz executor in the - * original; this port shows the DOT source in a text dialog instead (no AWT/Swing imports).
        • - *
        • "Slice proof to fixed point" opens the Swing-only iterative {@code SliceToFixedPointDialog} - * in the original; this port performs a single slicing iteration (identical to "Slice proof").
        • - *
        • "Show rule statistics" renders an HTML table with sort buttons in the original; this port - * shows the plain-text statistics with the default "total applications, descending" sort.
        • - *
        • The execution timings are shown as plain labels instead of an HTML table, and the 100 ms - * update debounce of the graph labels is dropped ({@link FxUtil}-marshalled direct updates).
        • + *
        • the 100 ms update debounce of the graph labels is dropped ({@link FxUtil}-marshalled + * direct updates);
        • + *
        • the "Export as DOT" file chooser runs owner-less (the drawer panel has no dedicated + * window);
        • + *
        • the slice load uses the FX window's own user-interface control instead of the Swing + * headless {@code DefaultUserInterfaceControl} (both are {@code ProblemLoaderControl}s), so the + * loader callbacks stay FX-safe;
        • + *
        • the panel grey-out checks the parallel-prover setting directly instead of the Swing-only + * {@code SingleCoreFeatureGate}.
        • *
        * * @author Alexander Weigl @@ -123,6 +133,13 @@ public class SlicingLeftPanelF extends ScrollPane private VBox timings = null; /** Titled section wrapping {@link #timings}, hidden until an analysis ran. */ private TitledPane timingsPane = null; + /** + * The WebView rendering the {@link HtmlFactory} timings table (lazily created, reused across + * analyses to avoid spawning a WebKit instance per update). + */ + private @Nullable WebView timingsView = null; + /** Whether the graphviz dot executable is available (checked once at panel build). */ + private final boolean dotAvailable = GraphvizDotExecutorF.isDotInstalled(); /** Number of nodes in the dependency graph. */ private int graphNodesNr = 0; @@ -179,6 +196,12 @@ private TitledPane getDependencyGraphPanel() { showGraphRendering = new Button("Show rendering of graph"); showGraphRendering.setMaxWidth(Double.MAX_VALUE); showGraphRendering.setOnAction(e -> previewGraph()); + if (!dotAvailable) { + // Swing SlicingLeftPanel.buildUI: graph rendering requires the graphviz dot binary + showGraphRendering.setDisable(true); + showGraphRendering.setTooltip(nativeTooltip( + "Install graphviz (dot) to enable graph rendering functionality.")); + } graphNodes = new Label(); graphEdges = new Label(); @@ -219,14 +242,11 @@ private TitledPane getProofSlicingPanel() { sliceProof.setOnAction(e -> sliceProof()); sliceProofFixedPoint = new Button("Slice proof to fixed point"); sliceProofFixedPoint.setMaxWidth(Double.MAX_VALUE); - // KNOWN-SIMPLIFIED: the Swing original opens the iterative SliceToFixedPointDialog - // (slice -> analysis -> slice ...); this port performs a single slicing iteration. - sliceProofFixedPoint.setOnAction(e -> sliceProof()); + sliceProofFixedPoint.setOnAction(e -> openSliceToFixedPointDialog()); sliceProofFixedPoint.setTooltip(nativeTooltip( """ Slices the proof and analyzes the result; the process repeats until no more steps can - be removed. KNOWN-SIMPLIFIED (FX): a single slicing iteration is performed, as with - "Slice proof".""")); + be removed.""")); return titledPane("Proof slicing", List.of(sliceProof, sliceProofFixedPoint)); } @@ -292,20 +312,34 @@ private void showRuleStatistics() { if (results == null) { return; } - // KNOWN-SIMPLIFIED: the Swing dialog renders an HTML table with four sort buttons; this - // port shows the plain-text rows with the default "total applications, descending" order. - List entries = results.ruleStatistics.sortBy( - Comparator.comparing(RuleStatisticEntry::numberOfApplications).reversed()); - StringBuilder text = new StringBuilder(); - for (RuleStatisticEntry entry : entries) { - text.append(entry.ruleName()).append(": ").append(entry.numberOfApplications()) - .append(" applications, ").append(entry.numberOfUselessApplications()) - .append(" useless, ") - .append(entry.numberOfInitialUselessApplications()) - .append(" initial useless\n"); + // HTML table with the four sort buttons of the Swing RuleStatisticsDialog, rendered in + // the HtmlDialogF WebView seam + RuleStatisticsDialogF.show(ownerWindow(), results); + } + + /** @return the window stage to own dialogs, or {@code null} if the panel is not attached */ + private javafx.stage.@Nullable Window ownerWindow() { + MainWindowF window = extension.window(); + return window != null ? window.getStage() : null; + } + + /** + * Opens the "Slice proof to fixed point" dialog (Swing SlicingLeftPanel.java:229-235, the + * fixed-point button handler): the dialog iterates analyze/slice/load on the current proof + * until the fixed point is reached. Also the entry point of the + * {@code key.fx.verify.slicing} harness. + * + * @return the opened dialog, or {@code null} if no proof is loaded + */ + public @Nullable SliceToFixedPointDialogF openSliceToFixedPointDialog() { + if (currentProof == null) { + return null; } - showTextDialog("Rule Statistics", - text.length() == 0 ? "No rule applications recorded." : text.toString()); + SliceToFixedPointDialogF dialog = + new SliceToFixedPointDialogF(mediator, ownerWindow(), this::analyzeProof, + this::sliceProof); + dialog.start(currentProof); + return dialog; } /** Show the dependency graph excerpt of the current proof (Swing {@code previewGraph}). */ @@ -317,11 +351,12 @@ private void previewGraph() { if (tracker == null) { return; } - String text = tracker.exportDot(abbreviateFormulas.isSelected(), + String dot = tracker.exportDot(abbreviateFormulas.isSelected(), abbreviateChains.isSelected()); - // KNOWN-SIMPLIFIED: the Swing original renders the DOT to a PNG via the Swing-only - // graphviz executor and shows an image dialog; this port shows the DOT source text. - showTextDialog("Dependency graph (DOT)", text); + // renders the DOT to a PNG with the graphviz dot binary and shows the image dialog; if + // the render fails (graphviz missing), the DOT source text dialog is shown instead (the + // documented fallback, see PreviewDialogF) + PreviewDialogF.showDot(ownerWindow(), dot); } /** @@ -412,11 +447,11 @@ protected Path call() throws Exception { } }; task.setOnSucceeded(event -> { - // KNOWN-SIMPLIFIED: the Swing original loads the slice through the problem loader - // directly to keep it out of the recent-files list ({@code UI.loadProblem}); the FX - // port takes the public load pipeline of the main window, which registers the file - // in the recent-files list. - window.openProofFile(task.getValue()); + // Swing parity (SlicingLeftPanel.java:465, "we do not use UI.loadProblem here to + // avoid adding the slice to the recent files"): the slice is loaded through the + // direct loader path, which — unlike openProofFile — does not register the file in + // the recent-files list. + window.loadProofFileDirect(task.getValue()); }); task.setOnFailed(event -> { Throwable error = task.getException() != null ? task.getException() @@ -461,13 +496,33 @@ private void displayResults(@Nullable AnalysisResults results) { usefulBranches.setText("Useful branches: " + results.usefulBranchesNr); showRuleStatistics.setDisable(false); timings.getChildren().clear(); - // KNOWN-SIMPLIFIED: the Swing original renders an HTML table via HtmlFactory; this port - // shows one "Algorithm: time" line per measured activity. - List lines = results.executionTime.executionTimes() - .map(action -> action.first + ": " + action.second + " ms") + // HtmlFactory timings table like the Swing panel (SlicingLeftPanel.java:500-511), + // rendered in the reused WebView + List> rows = results.executionTime.executionTimes() + .map(action -> (Collection) List.of(action.first, + action.second + " ms")) .collect(Collectors.toList()); - lines.forEach(line -> timings.getChildren().add(new Label(line))); - timingsPane.setVisible(!lines.isEmpty()); + String html = HtmlFactory.generateTable( + List.of("Algorithm", "Time"), + new boolean[] { false, false }, + Optional.of(new String[] { null, "right" }), + rows, + null); + WebView view = timingsView(); + timings.getChildren().add(view); + view.getEngine().loadContent(html, "text/html"); + timingsPane.setVisible(!rows.isEmpty()); + } + + /** + * @return the lazily created WebView for the timings table (reused across analyses) + */ + private WebView timingsView() { + if (timingsView == null) { + timingsView = new WebView(); + timingsView.setPrefSize(240, 120); + } + return timingsView; } private void resetGraphLabels() { @@ -559,8 +614,12 @@ public void updateUIState() { } dotExport.setDisable(noProofLoaded); dotExport.setTooltip(noProofLoaded ? nativeTooltip(NO_PROOF_SELECTED) : null); - showGraphRendering.setDisable(noProofLoaded); - showGraphRendering.setTooltip(noProofLoaded ? nativeTooltip(NO_PROOF_SELECTED) : null); + showGraphRendering.setDisable(noProofLoaded || !dotAvailable); + showGraphRendering.setTooltip(noProofLoaded ? nativeTooltip(NO_PROOF_SELECTED) + : !dotAvailable + ? nativeTooltip( + "Install graphviz (dot) to enable graph rendering functionality.") + : null); runAnalysis.setDisable(noProofLoaded); runAnalysis.setTooltip(noProofLoaded ? nativeTooltip(NO_PROOF_SELECTED) : null); showRuleStatistics.setDisable(noProofLoaded); @@ -615,8 +674,11 @@ public void proofDisposed(Proof proof) { } /** - * Shows a read-only multi-line text dialog (KNOWN-SIMPLIFIED stand-in for the Swing - * {@code HtmlDialog}/{@code PreviewDialog}). + * Shows a read-only multi-line text dialog. This is now only the fallback seam of + * the port: the HTML dialogs ({@link HtmlDialogF}, {@link RuleStatisticsDialogF}) and the + * graph preview ({@link PreviewDialogF}) have Swing-parity counterparts, and this dialog is + * used when graphviz rendering fails (see {@link PreviewDialogF#showDot}) and by the + * context-menu DOT excerpt if the render fails. * * @param title the dialog title * @param text the text to display diff --git a/keyext.slicing.fx/src/test/java/org/key_project/slicing/fx/GraphvizDotExecutorFTest.java b/keyext.slicing.fx/src/test/java/org/key_project/slicing/fx/GraphvizDotExecutorFTest.java new file mode 100644 index 00000000000..8ef15ee5703 --- /dev/null +++ b/keyext.slicing.fx/src/test/java/org/key_project/slicing/fx/GraphvizDotExecutorFTest.java @@ -0,0 +1,60 @@ +/* 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 org.key_project.slicing.fx; + +import org.key_project.slicing.SlicingSettingsProvider; +import org.key_project.slicing.fx.ui.GraphvizDotExecutorF; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Headless unit test of the non-Swing DOT renderer {@link GraphvizDotExecutorF} (WP-6): the + * no-graphviz fallback (unavailable dot executable yields an error result, the documented + * decision that makes the dialogs fall back to the DOT source text) and — if the graphviz dot + * binary is installed on the test machine — the real PNG render path. The in-app end-to-end + * behavior (image dialog, statistics dialog, fixed point) is exercised by the + * {@code key.fx.verify.slicing} hook. + */ +class GraphvizDotExecutorFTest { + + private static final String SAMPLE_DOT = "digraph g { a -> b; }"; + + @AfterEach + void restoreDotExecutable() { + // the render tests swap the configured dot executable; restore the default + SlicingSettingsProvider.getSlicingSettings().setDotExecutable("dot"); + } + + @Test + void missingDotExecutableYieldsErrorResult() { + SlicingSettingsProvider.getSlicingSettings() + .setDotExecutable("key-fx-verify-missing-dot-binary"); + GraphvizDotExecutorF.Result result = GraphvizDotExecutorF.renderPng(SAMPLE_DOT); + assertTrue(result.hasError()); + assertFalse(result.hasImage()); + assertFalse(result.isPng()); + } + + @Test + void rendersPngWhenGraphvizInstalled() { + if (!GraphvizDotExecutorF.isDotInstalled()) { + // documented fallback environment: without graphviz only the error path is + // verifiable (the verify hook asserts the same two branches in-app) + SlicingSettingsProvider.getSlicingSettings() + .setDotExecutable("key-fx-verify-missing-dot-binary"); + GraphvizDotExecutorF.Result result = GraphvizDotExecutorF.renderPng(SAMPLE_DOT); + assertTrue(result.hasError()); + return; + } + GraphvizDotExecutorF.Result result = GraphvizDotExecutorF.renderPng(SAMPLE_DOT); + assertTrue(result.hasImage()); + assertTrue(result.isPng()); + assertFalse(result.hasError()); + } +} From 74e9ac636fc723339b9526d9af8b2e56183b8957 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Sun, 11 Oct 2026 20:55:12 +0200 Subject: [PATCH 199/281] WP-7 C1: TestgenExtensionF implements ToolbarF + StartupF (drops StatusLineF stand-in) Swing parity (TestgenExtension.java): the extension contributes the two plain action buttons to the host extension toolbar (ToolbarF) and gets the window at app startup (StartupF). The former status-line MenuButton stand-in is removed - it existed only because the toolbar slot was missing. The settings-panel capture stays as fallback. Gate: key.ui.fx + keyext.ui.testgen.fx compileJava/test green. --- .../key/gui/testgen/fx/TestgenExtensionF.java | 140 ++++++++---------- .../gui/testgen/fx/TestgenExtensionFTest.java | 35 ++--- 2 files changed, 83 insertions(+), 92 deletions(-) diff --git a/keyext.ui.testgen.fx/src/main/java/de/uka/ilkd/key/gui/testgen/fx/TestgenExtensionF.java b/keyext.ui.testgen.fx/src/main/java/de/uka/ilkd/key/gui/testgen/fx/TestgenExtensionF.java index e7d799d9923..8a7aeef76c5 100644 --- a/keyext.ui.testgen.fx/src/main/java/de/uka/ilkd/key/gui/testgen/fx/TestgenExtensionF.java +++ b/keyext.ui.testgen.fx/src/main/java/de/uka/ilkd/key/gui/testgen/fx/TestgenExtensionF.java @@ -16,7 +16,6 @@ import javafx.scene.control.Button; import javafx.scene.control.Control; import javafx.scene.control.Menu; -import javafx.scene.control.MenuButton; import javafx.scene.control.MenuItem; import javafx.scene.control.Tooltip; import javafx.scene.input.KeyCombination; @@ -40,55 +39,44 @@ * Swing original contributes two {@code Action}s ("Search for Counterexample", "Generate * Testcases...") to the main menu (grouped by the Swing facade into an "Extensions" menu), a * {@code JToolBar} with the same two actions, a {@code KeyStrokeSettings.defineDefault} shortcut - * (Ctrl+T for the {@code TestGenMacro}) and the {@code TestgenOptionsPanel} settings provider; it + * (Ctrl+Shift+T ({@code MULTI_KEY_MASK}) for the {@code TestGenMacro}) and the {@code + * TestgenOptionsPanel} settings provider; it * also implements {@code KeyboardShortcuts} and {@code Startup} and registers selection/Z3 * listeners so the actions are only enabled on an open proof with the Z3 CE solver installed. *

        - * The FX port implements the {@link KeYGuiExtensionF} SPI through three capabilities: the - * settings entry ({@link KeYGuiExtensionF.SettingsF} → {@link TestgenSettingsProviderF}, a - * reflective {@code SettingsProviderF} proxy), the main-menu entry - * ({@link KeYGuiExtensionF.MainMenuF} → the "Test Case Generation" menu with the two items of the - * Swing extension menu, the generate item carrying the SHORTCUT+T accelerator that replaces the - * Swing {@code KeyStrokeSettings.defineDefault} default) and the status line - * ({@link KeYGuiExtensionF.StatusLineF}): the same two items inside a {@link MenuButton} plus the - * two plain action buttons of the Swing toolbar. The long-running generation and the - * counterexample search run off the FX thread in {@link TestGenerationTaskF} / + * The FX port implements the {@link KeYGuiExtensionF} SPI through four capabilities: the settings + * entry ({@link KeYGuiExtensionF.SettingsF} → {@link TestgenSettingsProviderF}, a reflective + * {@code SettingsProviderF} proxy), the main-menu entry ({@link KeYGuiExtensionF.MainMenuF} → the + * "Test Case Generation" menu with the two items of the Swing extension menu), the toolbar + * ({@link KeYGuiExtensionF.ToolbarF} → the two plain action buttons of the Swing {@code JToolBar} + * in the host's extension toolbar) and the startup hook ({@link KeYGuiExtensionF.StartupF} → + * {@link #init(MainWindowF, KeYMediatorF)}, which hands the window to the extension at app + * startup exactly like the Swing {@code Startup} hook does — WP-7). The long-running generation + * and the counterexample search run off the FX thread in {@link TestGenerationTaskF} / * {@link CounterExampleTaskF} and report into the {@link TestGenResultsDialogF} / * {@link CounterExampleResultsDialogF}. *

        - * KNOWN-SIMPLIFIED (capability reduction): the Swing extension also implements - * {@code Toolbar} and {@code Startup}, mirrored by the FX SPI as {@code ToolbarF} and - * {@code StartupF}. The two toolbar actions are expressed through the two plain buttons of the - * status-line slot (a real {@code ToolbarF} layout is not needed for two buttons, and plain - * {@code Button}s stay constructible without the FX toolkit, which keeps the headless unit tests - * toolkit-free), and the {@code StartupF} init hook is not implemented — the window is captured - * lazily through the settings panel instead (next block). The menu slot is implemented in full: - * unlike the initial revision, which could not name {@code MainWindowF}/{@code KeYMediatorF} in - * SPI signatures (javac failed completing {@code MainWindowF} while {@code key.core} was missing - * from the compile classpath), this module now declares {@code :key.core} like the other keyext - * FX ports; every runtime window reference remains a plain {@code Object} resolved through - * {@link TestgenReflectionF}. - *

        - * KNOWN-SIMPLIFIED (lazy window/mediator capture): {@link KeYGuiExtensionF.StatusLineF} - * does not receive the main window, and no {@code StartupF} hook is implemented, so the port - * captures the window through the one SPI surface that does pass it — the settings panel - * ({@link TestgenSettingsProviderF}, whose {@code getPanel/apply} forward the window to - * {@link #captureWindow(Object)}) — and wires the enablement listeners on that first capture. - * Until then the buttons stay clickable and validate on click (the run dialogs log the "open the - * settings dialog once" hint instead of crashing), which mirrors the Swing actions' on-click - * guards. + * WP-7 closes the former capability reduction: the extension now implements {@code ToolbarF} and + * {@code StartupF} like its Swing original. The status-line stand-in (the "Test Case Generation" + * {@code MenuButton} re-exposing the two menu items) is dropped with it — it existed only because + * the toolbar slot was missing, and Swing parity is exactly menu + toolbar. The window still + * arrives through the settings panel as a fallback (the reflective settings proxy forwards it to + * {@link #captureWindow(Object)}); with the {@code StartupF} hook in place it arrives at startup, + * so the enablement wiring (selection, auto-mode and SMT-settings listeners, the FX equivalent of + * the Swing action-infrastructure updates) runs from the start instead of on the first settings + * dialog visit. */ @KeYGuiExtensionF.Info(name = "Test case generation", experimental = false, description = "key.testgen (JavaFX port): generate JUnit test cases from the current proof, " + "or search for a counterexample, using the Z3 CE solver.") @NullMarked public final class TestgenExtensionF - implements KeYGuiExtensionF, KeYGuiExtensionF.SettingsF, KeYGuiExtensionF.StatusLineF, - KeYGuiExtensionF.MainMenuF { + implements KeYGuiExtensionF, KeYGuiExtensionF.SettingsF, KeYGuiExtensionF.ToolbarF, + KeYGuiExtensionF.StartupF, KeYGuiExtensionF.MainMenuF { private static final Logger LOGGER = LoggerFactory.getLogger(TestgenExtensionF.class); - /** Label of the status-line extension area (the Swing "Extensions" menu label equivalent). */ + /** Label of the extension's main-menu area (the Swing "Extensions" menu label equivalent). */ private static final String MENU_LABEL = "Test Case Generation"; /** Swing TestGenerationAction.NAME. */ @@ -107,31 +95,24 @@ public final class TestgenExtensionF /** the TestgenOptionsPanel port; a reflective {@code SettingsProviderF} proxy (see class). */ private final SettingsProviderF settingsProvider = TestgenSettingsProviderF.create(this); - /** status-line: the anchor menu button with the two extension-menu items. */ - private @Nullable MenuButton menuButton; - - /** status-line: the two plain toolbar buttons (Swing JToolBar entries). */ + /** toolbar: the two plain buttons of the Swing JToolBar (Swing TestgenExtension.getToolbar). */ private @Nullable Button generateButton; private @Nullable Button counterExampleButton; - /** - * every created "Generate Testcases..." item (status MenuButton + main menu), for enablement. - */ + /** every created "Generate Testcases..." main-menu item, for enablement. */ private final List generateItems = new ArrayList<>(); - /** - * every created "Search for Counterexample" item (status MenuButton + main menu), for - * enablement. - */ + /** every created "Search for Counterexample" main-menu item, for enablement. */ private final List counterExampleItems = new ArrayList<>(); /** main-menu: the "Test Case Generation" menu ({@link KeYGuiExtensionF.MainMenuF}). */ private @Nullable Menu menu; /** - * the main window, captured lazily through the settings panel (see the class comment for the - * {@code KNOWN-SIMPLIFIED} reasoning): {@code null} until the user opens the settings dialog. - * Kept as a plain {@code Object} — {@code MainWindowF} is not nameable in this module. + * the main window, captured at startup through the {@code StartupF}/{@code ToolbarF} hooks + * (WP-7) or as a fallback through the settings panel (the reflective settings proxy): it was + * {@code null} until the user opened the settings dialog before WP-7. Kept as a plain + * {@code Object} — {@code MainWindowF} is not nameable in this module. */ private @Nullable Object window; @@ -146,21 +127,19 @@ public SettingsProviderF getSettings() { return settingsProvider; } - // ------------------------------------------------------------------ StatusLineF + // ------------------------------------------------------------------ ToolbarF @Override - public List getStatusLineControls() { - // extension: MP9.5 — Swing TestgenExtension.java:43-61: the two actions live in the - // main menu ("Test Case Generation", see getMenus) and in the JToolBar. The FX port - // expresses the Swing toolbar through this status-line slot (KNOWN-SIMPLIFIED, class - // comment): the "Test Case Generation" MenuButton re-exposes the two menu items for - // quick access, the two plain buttons mirror the toolbar. - if (menuButton == null) { - MenuButton anchor = new MenuButton(MENU_LABEL); - // no accelerator here — the SHORTCUT+T binding lives on the main-menu generate - // item (getMenus) so the two surfaces do not double-fire it - anchor.getItems().addAll(makeGenerateItem(anchor), makeCounterExampleItem(anchor)); - + public List getToolbarControls(MainWindowF window, KeYMediatorF mediator) { + // WP-7 — Swing TestgenExtension.getToolbar (TestgenExtension.java:55-61): the JToolBar + // with the two actions; the FX host appends the contributed controls to its single + // extension toolbar (MainWindowF.buildTop). The window also arrives here (the toolbar is + // built after the mediator is set), so the enablement wiring starts like through the + // StartupF hook below. + if (window != null) { + captureWindow(window); + } + if (generateButton == null) { // Swing TestGenerationAction/CounterExampleAction SMALL_ICONs (FontAwesome) Button generate = new Button(NAME_GENERATE, new FontIcon(FontAwesomeSolid.FLASK)); generate.setTooltip(new Tooltip(TOOLTIP_GENERATE)); @@ -169,12 +148,22 @@ public List getStatusLineControls() { new FontIcon(FontAwesomeSolid.EXCLAMATION_TRIANGLE)); counterExample.setTooltip(new Tooltip(TOOLTIP_COUNTEREXAMPLE)); counterExample.setOnAction(e -> searchCounterExample(counterExample)); - - menuButton = anchor; generateButton = generate; counterExampleButton = counterExample; } - return List.of(menuButton, generateButton, counterExampleButton); + refresh(); + return List.of(generateButton, counterExampleButton); + } + + // ------------------------------------------------------------------ StartupF + + @Override + public void init(MainWindowF window, KeYMediatorF mediator) { + // WP-7 — Swing TestgenExtension.init (TestgenExtension.java:48-53): the Startup hook + // hands the window to the extension at app startup. The window capture enables the + // enablement listeners (captureWindow) instead of the former lazy capture through the + // settings panel. + captureWindow(window); } // ------------------------------------------------------------------ MainMenuF @@ -204,10 +193,10 @@ public List

        getMenus(MainWindowF window, KeYMediatorF mediator) { // ------------------------------------------------------------------ window capture /** - * Hands the main window to the extension — the only SPI surface of this module that receives - * it (the {@code SettingsProviderF} panel/apply, forwarded by - * {@link TestgenSettingsProviderF}). - * Used to bind the enablement listeners once (the reflection-only counterpart of the Swing + * Hands the main window to the extension — called from the {@code StartupF}/{@code ToolbarF} + * hooks (WP-7) and, as a fallback, from the settings panel (the + * {@code SettingsProviderF} panel/apply forwarded by {@link TestgenSettingsProviderF}). + * Wires the enablement listeners once (the reflection-only counterpart of the Swing * {@code Startup} hook). * * @param window the main window handed in by the settings dialog @@ -250,8 +239,9 @@ private void searchCounterExample(@Nullable Node source) { } /** - * The {@link Stage} owning the given node (the status-line controls live in the main - * window's scene), used as the run dialogs' owner. + * The {@link Stage} owning the given node (the toolbar button lives in the main window's + * scene; main-menu {@link MenuItem}s have none and return {@code null}), used as the run + * dialogs' owner. */ private static @Nullable Stage stageOf(@Nullable Node source) { if (source == null) { @@ -332,16 +322,16 @@ private void handleSMTSettingsChanged(PropertyChangeEvent evt) { } /** - * Re-computes the enablement of the status-line controls: like the Swing actions the - * extension is enabled only when Z3 CE is installed, a proof is selected and no auto-mode / - * generation run is active (Swing TestGenerationAction.checkZ3CE, + * Re-computes the enablement of the toolbar buttons and main-menu items: like the Swing + * actions the extension is enabled only when Z3 CE is installed, a proof is selected and no + * auto-mode / generation run is active (Swing TestGenerationAction.checkZ3CE, * TestGenerationAction.java:101-111); the counterexample action additionally requires the * selected node to be an open leaf (CounterExampleAction.selectedNodeChanged, * CounterExampleAction.java:67-84). The selection and SMT-settings events can fire on any * thread, so the refresh is marshalled to the FX thread. *

        * Without a captured window the controls stay enabled and the run dialogs validate on click - * (the class comment's {@code KNOWN-SIMPLIFIED} lazy capture). + * (the fallback capture through the settings panel, see the class comment). */ private void refresh() { Object connected = window; diff --git a/keyext.ui.testgen.fx/src/test/java/de/uka/ilkd/key/gui/testgen/fx/TestgenExtensionFTest.java b/keyext.ui.testgen.fx/src/test/java/de/uka/ilkd/key/gui/testgen/fx/TestgenExtensionFTest.java index a8ff6bf8bbe..a5661ad1d93 100644 --- a/keyext.ui.testgen.fx/src/test/java/de/uka/ilkd/key/gui/testgen/fx/TestgenExtensionFTest.java +++ b/keyext.ui.testgen.fx/src/test/java/de/uka/ilkd/key/gui/testgen/fx/TestgenExtensionFTest.java @@ -15,20 +15,20 @@ import static org.junit.jupiter.api.Assertions.assertTrue; /** - * Headless unit test of the {@link TestgenExtensionF} provider (MP9.5): the {@code @Info} + * Headless unit test of the {@link TestgenExtensionF} provider (MP9.5, WP-7): the {@code @Info} * annotation, the compilable SPI capability surface ({@link KeYGuiExtensionF.MainMenuF} + - * {@link KeYGuiExtensionF.SettingsF} + {@link KeYGuiExtensionF.StatusLineF}) and the - * reflection-dispatched settings entry (a {@code SettingsProviderF} proxy whose - * {@code getPanel/apply} forward the main window to the extension). + * {@link KeYGuiExtensionF.SettingsF} + {@link KeYGuiExtensionF.ToolbarF} + + * {@link KeYGuiExtensionF.StartupF}) and the reflection-dispatched settings entry (a {@code + * SettingsProviderF} proxy whose {@code getPanel/apply} forward the main window to the extension). *

        * KNOWN-SIMPLIFIED (headless): every {@code javafx.scene.control.Control} triggers the FX * toolkit in its class initializer ("Toolkit not initialized"), and this module's - * {@code build.gradle} exposes no headless toolkit to the tests — so the status-line {@code - * MenuButton}/{@code Button}s, the "Test Case Generation" menu and the settings-panel {@code - * CheckBox}/spinner widgets are built lazily inside the running app only, and the test asserts - * the provider/capability surface instead of the control trees. Neither the provider construction - * nor the capability asserts touch the JavaFX toolkit; the run dialogs and the generation workers - * are deliberately not exercised here. + * {@code build.gradle} exposes no headless toolkit to the tests — so the toolbar {@code Button}s, + * the "Test Case Generation" menu and the settings-panel {@code CheckBox}/spinner widgets are + * built lazily inside the running app only, and the test asserts the provider/capability surface + * instead of the control trees. Neither the provider construction nor the capability asserts + * touch the JavaFX toolkit; the run dialogs and the generation workers are deliberately not + * exercised here. */ class TestgenExtensionFTest { @@ -49,16 +49,17 @@ void providerInfoAnnotation() { @Test void capabilitySurface() { KeYGuiExtensionF extension = new TestgenExtensionF(); - // KNOWN-SIMPLIFIED: the ToolbarF/StartupF slots are not implemented (see - // TestgenExtensionF) — the Swing toolbar actions are expressed through the two plain - // status-line buttons; the control trees are built lazily in the app (they need the FX - // toolkit). + // WP-7 — the Swing capability set (main menu, toolbar, Startup hook, settings panel) is + // complete; the two toolbar Buttons and the control trees are built lazily in the app + // (they need the FX toolkit). assertInstanceOf(KeYGuiExtensionF.MainMenuF.class, extension, - "Test Generation menu with the two Swing menu actions + SHORTCUT+T"); + "Test Generation menu with the two Swing menu actions"); assertInstanceOf(KeYGuiExtensionF.SettingsF.class, extension, "TestgenOptionsPanel port"); - assertInstanceOf(KeYGuiExtensionF.StatusLineF.class, extension, - "MenuButton + toolbar buttons in the status line"); + assertInstanceOf(KeYGuiExtensionF.ToolbarF.class, extension, + "the two plain action buttons of the Swing JToolBar"); + assertInstanceOf(KeYGuiExtensionF.StartupF.class, extension, + "Swing Startup hook: window capture at app startup"); } @Test From e1b50a9e527262a400a2ffaa628bc611cb7ce2cb Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Sun, 11 Oct 2026 20:57:10 +0200 Subject: [PATCH 200/281] =?UTF-8?q?WP-9=20(1/4):=20AutoSave=20real-flow=20?= =?UTF-8?q?wiring=20assert=20=E2=80=94=20key.fx.verify.autosave=20now=20ru?= =?UTF-8?q?ns=20a=20capped=20auto-mode=20run=20with=20the=20saver=20armed?= =?UTF-8?q?=20through=20the=20real=20applyAutoSave=20+=20selection=20hand-?= =?UTF-8?q?off=20and=20proves=20AutoSaver=20actually=20writes=20the=20inte?= =?UTF-8?q?rmediate=20.autosave.N.key=20files=20(taskProgress=20->=20fireT?= =?UTF-8?q?askProgress=20chain),=20restores=20armed=20period=20+=20max=20s?= =?UTF-8?q?teps=20afterwards?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../de/uka/ilkd/key/gui/fx/MainWindowF.java | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) 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 index e2bc8e06379..9c1d2246fc7 100644 --- 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 @@ -22,6 +22,8 @@ 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.function.Consumer; import java.util.stream.Collectors; import javafx.animation.KeyFrame; @@ -196,6 +198,7 @@ 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; @@ -1337,6 +1340,9 @@ protected KeYEnvironment call() throws Exception { 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 @@ -4739,6 +4745,99 @@ private static Object readAutoSaveProof(AutoSaver saver) { } } + /** + * 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; From a42e660a2cc6d402663431b57edf9d412b63020a Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Sun, 11 Oct 2026 20:57:15 +0200 Subject: [PATCH 201/281] WP-7 C2: TestGenMacro default shortcut through the shared KeyStrokeManagerF The Swing TestgenExtension default (KeyStrokeSettings.defineDefault, MULTI_KEY_MASK = Ctrl+Shift+T) is registered in KeyStrokeManagerF.registerDefaults under the macro's real FQN (de.uka.ilkd.key.testgen.macros.TestGenMacro), so the binding is shared with the Swing keystrokes.json and drives the accelerator of the macro items in the proof macro menus (ProofMacroMenuF.itemFor). The 'Generate Testcases...' main-menu item now uses the manager binding and registers for later rebindings instead of the hardcoded SHORTCUT+T. Gate: key.ui.fx + keyext.ui.testgen.fx compileJava/test green. --- .../fx/keyshortcuts/KeyStrokeManagerF.java | 6 +++++ .../key/gui/testgen/fx/TestgenExtensionF.java | 22 ++++++++++++++----- .../gui/testgen/fx/TestgenExtensionFTest.java | 13 +++++++++++ 3 files changed, 35 insertions(+), 6 deletions(-) diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/keyshortcuts/KeyStrokeManagerF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/keyshortcuts/KeyStrokeManagerF.java index c0ca2a12588..992510aecad 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/keyshortcuts/KeyStrokeManagerF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/keyshortcuts/KeyStrokeManagerF.java @@ -108,6 +108,12 @@ private void registerDefaults() { defineDefault("de.uka.ilkd.key.macros.UpdateSimplificationMacro", modifier() + "SHIFT+L"); defineDefault("de.uka.ilkd.key.macros.IntegerSimplificationMacro", modifier() + "SHIFT+I"); defineDefault("de.uka.ilkd.key.macros.SMTPreparationMacro", modifier() + "SHIFT+Y"); + // testgen (WP-7): the Swing TestgenExtension defines the TestGenMacro default shortcut + // via KeyStrokeSettings.defineDefault with MULTI_KEY_MASK (Ctrl+Shift+T, + // TestgenExtension.java:51-52). The action id is the real macro FQN — the + // key.core.testgen services resource lists a stale FQN, the accelerator lookup in + // ProofMacroMenuF/SequentViewF uses macro.getClass().getName(). + defineDefault("de.uka.ilkd.key.testgen.macros.TestGenMacro", modifier() + "SHIFT+T"); defineDefault("de.uka.ilkd.key.gui.actions.SearchInProofTreeAction", modifier() + "SHIFT+F"); diff --git a/keyext.ui.testgen.fx/src/main/java/de/uka/ilkd/key/gui/testgen/fx/TestgenExtensionF.java b/keyext.ui.testgen.fx/src/main/java/de/uka/ilkd/key/gui/testgen/fx/TestgenExtensionF.java index 8a7aeef76c5..8f83270c3a5 100644 --- a/keyext.ui.testgen.fx/src/main/java/de/uka/ilkd/key/gui/testgen/fx/TestgenExtensionF.java +++ b/keyext.ui.testgen.fx/src/main/java/de/uka/ilkd/key/gui/testgen/fx/TestgenExtensionF.java @@ -18,12 +18,12 @@ import javafx.scene.control.Menu; import javafx.scene.control.MenuItem; import javafx.scene.control.Tooltip; -import javafx.scene.input.KeyCombination; import javafx.stage.Stage; import de.uka.ilkd.key.core.fx.KeYMediatorF; import de.uka.ilkd.key.gui.fx.MainWindowF; import de.uka.ilkd.key.gui.fx.extension.api.KeYGuiExtensionF; +import de.uka.ilkd.key.gui.fx.keyshortcuts.KeyStrokeManagerF; import de.uka.ilkd.key.gui.fx.settings.SettingsProviderF; import org.jspecify.annotations.NullMarked; @@ -85,6 +85,13 @@ public final class TestgenExtensionF /** Swing CounterExampleAction.NAME. */ private static final String NAME_COUNTEREXAMPLE = "Search for Counterexample"; + /** + * action id of the TestGenMacro in the shared keystroke settings (the macro's real FQN — + * the Swing {@code KeyStrokeSettings.defineDefault} id, see {@code KeyStrokeManagerF}). + */ + private static final String TESTGEN_MACRO_ACTION_ID = + "de.uka.ilkd.key.testgen.macros.TestGenMacro"; + /** Swing TestGenerationAction.TOOLTIP. */ private static final String TOOLTIP_GENERATE = "Generate test cases for open goals"; @@ -178,12 +185,15 @@ public List

        getMenus(MainWindowF window, KeYMediatorF mediator) { if (menu == null) { Menu m = new Menu(MENU_LABEL); // Swing TestgenExtension.java:51-52 registers the TestGenMacro default shortcut via - // KeyStrokeSettings.defineDefault; the FX SPI has no keyboard-shortcut capability, - // so the accelerator lives on the menu item. KNOWN-SIMPLIFIED: the shortcut moves - // from the TestGen strategy macro (term context menu in Swing) to the menu item — - // the FX app has no macro-keybinding slot yet. + // KeyStrokeSettings.defineDefault (MULTI_KEY_MASK = Ctrl+Shift+T); the FX + // counterpart is the shared KeyStrokeManagerF default, which also drives the + // accelerator of the macro item in the proof macro menus + // (ProofMacroMenuF.itemFor). The main-menu item carries the manager binding and is + // registered for later rebindings (KeyStrokeManager.register). + KeyStrokeManagerF manager = KeyStrokeManagerF.getInstance(); MenuItem generate = makeGenerateItem(null); - generate.setAccelerator(KeyCombination.keyCombination("SHORTCUT+T")); + manager.binding(TESTGEN_MACRO_ACTION_ID).ifPresent(generate::setAccelerator); + manager.register(generate, TESTGEN_MACRO_ACTION_ID); m.getItems().addAll(generate, makeCounterExampleItem(null)); menu = m; } diff --git a/keyext.ui.testgen.fx/src/test/java/de/uka/ilkd/key/gui/testgen/fx/TestgenExtensionFTest.java b/keyext.ui.testgen.fx/src/test/java/de/uka/ilkd/key/gui/testgen/fx/TestgenExtensionFTest.java index a5661ad1d93..70229f2ea9e 100644 --- a/keyext.ui.testgen.fx/src/test/java/de/uka/ilkd/key/gui/testgen/fx/TestgenExtensionFTest.java +++ b/keyext.ui.testgen.fx/src/test/java/de/uka/ilkd/key/gui/testgen/fx/TestgenExtensionFTest.java @@ -5,6 +5,7 @@ package de.uka.ilkd.key.gui.testgen.fx; import de.uka.ilkd.key.gui.fx.extension.api.KeYGuiExtensionF; +import de.uka.ilkd.key.gui.fx.keyshortcuts.KeyStrokeManagerF; import org.junit.jupiter.api.Test; @@ -62,6 +63,18 @@ void capabilitySurface() { "Swing Startup hook: window capture at app startup"); } + @Test + void testGenMacroShortcutBinding() { + // WP-7: the Swing TestgenExtension default (KeyStrokeSettings.defineDefault with + // MULTI_KEY_MASK = Ctrl+Shift+T) is registered in the shared KeyStrokeManagerF under the + // real macro FQN; the exact combination may be overridden by the user's shared + // keystrokes.json, so only the presence is asserted here. + assertTrue( + KeyStrokeManagerF.getInstance() + .binding("de.uka.ilkd.key.testgen.macros.TestGenMacro").isPresent(), + "the TestGenMacro default shortcut must be registered"); + } + @Test void settingsProviderDispatch() { TestgenExtensionF extension = new TestgenExtensionF(); From 2c708c2a554f665ee88a024cdacab752dafdf2a5 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Sun, 11 Oct 2026 21:00:50 +0200 Subject: [PATCH 202/281] WP-7 C3: auto-mode shell + cooperative stop for the testgen tasks TestGenerationTaskF/CounterExampleTaskF now wrap the run in the WP-1 mediator seams like their Swing originals (TGWorker.start/done, CounterExampleAction.CEWorker.start/done): initiateAutoMode(proof, true, false) before the run, finishAutoMode(proof, true, true, null) in the terminal-state hooks (also after stop/failure). The Stop button stops the SolverLauncher handle (Swing stopSMTLauncher parity) and the stop flag is honoured between the generation phases. generateTestcases ports the TestgenFacade pipeline reflectively (FinishSymbolicExecution/TestGen/SemanticsBlasting phases, 10 s solver timeout, JUnit suite generation in launcherStopped) and creates its own SolverLauncher so the run is stoppable; the facade route is gone. Gate: key.ui.fx + keyext.ui.testgen.fx compileJava/test green. --- .../fx/CounterExampleResultsDialogF.java | 11 +- .../gui/testgen/fx/CounterExampleTaskF.java | 56 +++- .../gui/testgen/fx/TestGenResultsDialogF.java | 9 +- .../gui/testgen/fx/TestGenerationTaskF.java | 83 ++++-- .../gui/testgen/fx/TestgenReflectionF.java | 276 ++++++++++++++---- 5 files changed, 354 insertions(+), 81 deletions(-) diff --git a/keyext.ui.testgen.fx/src/main/java/de/uka/ilkd/key/gui/testgen/fx/CounterExampleResultsDialogF.java b/keyext.ui.testgen.fx/src/main/java/de/uka/ilkd/key/gui/testgen/fx/CounterExampleResultsDialogF.java index c1e6b6817af..434ddc7f4e8 100644 --- a/keyext.ui.testgen.fx/src/main/java/de/uka/ilkd/key/gui/testgen/fx/CounterExampleResultsDialogF.java +++ b/keyext.ui.testgen.fx/src/main/java/de/uka/ilkd/key/gui/testgen/fx/CounterExampleResultsDialogF.java @@ -4,6 +4,9 @@ * SPDX-License-Identifier: GPL-2.0-or-later */ package de.uka.ilkd.key.gui.testgen.fx; +import de.uka.ilkd.key.core.fx.KeYMediatorF; +import de.uka.ilkd.key.proof.Proof; + import javafx.application.Platform; import javafx.beans.property.ReadOnlyBooleanWrapper; import javafx.geometry.Insets; @@ -35,7 +38,7 @@ * KNOWN-SIMPLIFIED: the Swing flow shows the counterexample model in the modal * {@code SolverListener} result dialogs; the FX port only prints the SMT statistics and the * outcome (found/not found) into the log. The auto-mode shell around the search - * (CounterExampleAction.java:169-191) is dropped, like in {@link TestGenResultsDialogF}. The + * (CounterExampleAction.java:169-191) lives in {@link CounterExampleTaskF}. The * window may be {@code null} when the extension has not been connected through the settings * dialog yet (see {@link TestgenExtensionF}) — the dialog then starts with a hint instead of a * search; the owner stage is resolved from the clicked status-line control. @@ -132,13 +135,13 @@ private void startSearch() { // panel (TestgenExtensionF); until then the run is validated on click. return; } - Object mediator = TestgenReflectionF.mediatorOf(captured); + KeYMediatorF mediator = (KeYMediatorF) TestgenReflectionF.mediatorOf(captured); Object goal = TestgenReflectionF.goalOf(mediator); if (goal == null) { append("No open goal is selected — cannot search for a counterexample.\n"); return; } - Object proof = TestgenReflectionF.proofOfGoal(goal); + Proof proof = (Proof) TestgenReflectionF.proofOfGoal(goal); Object sequent = TestgenReflectionF.sequentOfGoal(goal); Object ui = TestgenReflectionF.uiOf(captured); if (proof == null || sequent == null || ui == null) { @@ -150,7 +153,7 @@ private void startSearch() { statusLabel.setText("Searching for counterexamples..."); log.clear(); - CounterExampleTaskF task = new CounterExampleTaskF(ui, proof, sequent, logger); + CounterExampleTaskF task = new CounterExampleTaskF(mediator, ui, proof, sequent, logger); runningTask = task; task.setOnSucceeded(e -> { running.set(false); diff --git a/keyext.ui.testgen.fx/src/main/java/de/uka/ilkd/key/gui/testgen/fx/CounterExampleTaskF.java b/keyext.ui.testgen.fx/src/main/java/de/uka/ilkd/key/gui/testgen/fx/CounterExampleTaskF.java index d21e966c1b2..15839b0300d 100644 --- a/keyext.ui.testgen.fx/src/main/java/de/uka/ilkd/key/gui/testgen/fx/CounterExampleTaskF.java +++ b/keyext.ui.testgen.fx/src/main/java/de/uka/ilkd/key/gui/testgen/fx/CounterExampleTaskF.java @@ -7,6 +7,9 @@ import java.util.concurrent.atomic.AtomicReference; import javafx.concurrent.Task; +import de.uka.ilkd.key.core.fx.KeYMediatorF; +import de.uka.ilkd.key.proof.Proof; + import org.jspecify.annotations.NullMarked; /** @@ -18,12 +21,18 @@ * ({@link TestgenReflectionF#searchCounterExample}) in a {@link Task} and streams the SMT * statistics into the {@link LogSinkF} of the {@code CounterExampleResultsDialogF}. *

        + * The auto-mode shell is the Swing {@code CEWorker.start/done} parity + * (CounterExampleAction.java:169-191): {@link #call()} performs + * {@code mediator.initiateAutoMode(proof, true, false)} before the search and the terminal-state + * hooks (the FX-thread counterparts of the Swing {@code done()}) perform + * {@code mediator.finishAutoMode(proof, true, true, null)} — also after a stop/failure, so the + * interface never stays frozen. + *

        * KNOWN-SIMPLIFIED: the Swing {@code SolverListener} opens a modal progress dialog and a * results dialog showing the counterexample model (SolverListener.java); the FX port prints the * SMT statistics and the found/not-found outcome into the run dialog's log and leaves the model - * inspection to the generated test data / subsequent runs. The auto-mode shell of the Swing - * worker ({@code initiateAutoMode}/{@code finishAutoMode}, CounterExampleAction.java:169-191) - * is dropped like in {@link TestGenerationTaskF}. + * inspection to the generated test data / subsequent runs (the C4 {@code SolverListenerF} route + * brings the dialogs back). */ @NullMarked final class CounterExampleTaskF extends Task { @@ -32,20 +41,25 @@ final class CounterExampleTaskF extends Task { private final Object ui; /** the proof of the selected goal (reflectively obtained). */ - private final Object proof; + private final Proof proof; /** the sequent of the selected goal (reflectively obtained). */ private final Object sequent; + /** the mediator for the auto-mode shell (Swing CEWorker.start/done). */ + private final KeYMediatorF mediator; + /** the log sink of the run dialog (events fire on this worker thread). */ private final LogSinkF sink; /** the running {@code SolverLauncher} (reflectively obtained), for the Stop button. */ private final AtomicReference launcher = new AtomicReference<>(); - CounterExampleTaskF(Object ui, Object proof, Object sequent, LogSinkF sink) { - // ui/proof/sequent mirror the Swing worker's inputs (CounterExampleAction.actionPerformed, - // CounterExampleAction.java:106-120: node.proof() + node.sequent() of the selected goal). + CounterExampleTaskF(KeYMediatorF mediator, Object ui, Proof proof, Object sequent, + LogSinkF sink) { + // mediator/ui/proof/sequent mirror the Swing worker's inputs (CounterExampleAction + // .start, CounterExampleAction.java:169-172: the proof of the selected goal). + this.mediator = mediator; this.ui = ui; this.proof = proof; this.sequent = sequent; @@ -54,10 +68,38 @@ final class CounterExampleTaskF extends Task { @Override protected Void call() throws Exception { + // Swing CEWorker.start(): initiateAutoMode before the search (CounterExampleAction + // .java:169-172; there on the EDT, here at the beginning of the worker body — the FX + // mediator's seam only flips flags and publishes the observable property). + mediator.initiateAutoMode(proof, true, false); TestgenReflectionF.searchCounterExample(ui, proof, sequent, sink, launcher); return null; } + @Override + protected void succeeded() { + finishAutoMode(); + } + + @Override + protected void failed() { + finishAutoMode(); + } + + @Override + protected void cancelled() { + finishAutoMode(); + } + + /** + * Swing CEWorker.done(): {@code finishAutoMode(oldProof, true, true, null)} + * (CounterExampleAction.java:174-176) — runs on the FX thread (the state-change hooks), also + * after a stop or failure. + */ + private void finishAutoMode() { + mediator.finishAutoMode(proof, true, true, null); + } + /** * Asks the running search to stop: the reflective {@code SolverLauncher} is stopped and the * worker thread interrupted (the {@code SemanticsBlastingMacro} honours the interrupt). diff --git a/keyext.ui.testgen.fx/src/main/java/de/uka/ilkd/key/gui/testgen/fx/TestGenResultsDialogF.java b/keyext.ui.testgen.fx/src/main/java/de/uka/ilkd/key/gui/testgen/fx/TestGenResultsDialogF.java index 3fb61cca547..5801c4a0164 100644 --- a/keyext.ui.testgen.fx/src/main/java/de/uka/ilkd/key/gui/testgen/fx/TestGenResultsDialogF.java +++ b/keyext.ui.testgen.fx/src/main/java/de/uka/ilkd/key/gui/testgen/fx/TestGenResultsDialogF.java @@ -10,6 +10,9 @@ import java.util.ArrayList; import java.util.List; import java.util.stream.Stream; +import de.uka.ilkd.key.core.fx.KeYMediatorF; +import de.uka.ilkd.key.proof.Proof; + import javafx.application.Platform; import javafx.beans.property.ReadOnlyBooleanWrapper; import javafx.geometry.Insets; @@ -155,8 +158,8 @@ private void startRun() { // panel (TestgenExtensionF); until then the run is validated on click. return; } - Object mediator = TestgenReflectionF.mediatorOf(captured); - Object proof = TestgenReflectionF.proofOf(mediator); + KeYMediatorF mediator = (KeYMediatorF) TestgenReflectionF.mediatorOf(captured); + Proof proof = (Proof) TestgenReflectionF.proofOf(mediator); if (proof == null) { append("No proof is loaded — annotated/open goals for the generation are missing.\n"); return; @@ -172,7 +175,7 @@ private void startRun() { log.clear(); generatedFiles.getItems().clear(); - TestGenerationTaskF task = new TestGenerationTaskF(ui, proof, logger); + TestGenerationTaskF task = new TestGenerationTaskF(mediator, ui, proof, logger); runningTask = task; task.setOnSucceeded(e -> { running.set(false); diff --git a/keyext.ui.testgen.fx/src/main/java/de/uka/ilkd/key/gui/testgen/fx/TestGenerationTaskF.java b/keyext.ui.testgen.fx/src/main/java/de/uka/ilkd/key/gui/testgen/fx/TestGenerationTaskF.java index 7d773484a18..266ee5f1de8 100644 --- a/keyext.ui.testgen.fx/src/main/java/de/uka/ilkd/key/gui/testgen/fx/TestGenerationTaskF.java +++ b/keyext.ui.testgen.fx/src/main/java/de/uka/ilkd/key/gui/testgen/fx/TestGenerationTaskF.java @@ -1,9 +1,14 @@ /* This file is part of KeY - https://key-project.org - * KeY is licensed under the GNU General Public License Version 2, + * 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.testgen.fx; +import java.util.concurrent.atomic.AtomicReference; + +import de.uka.ilkd.key.core.fx.KeYMediatorF; +import de.uka.ilkd.key.proof.Proof; + import javafx.concurrent.Task; import org.jspecify.annotations.NullMarked; @@ -12,16 +17,20 @@ * Runs the test-case generation off the FX thread, FX port of {@code TGWorker} (Swing * TGWorker.java:30-86). The Swing worker is a {@code SwingWorker} that wraps the * {@code MainWindowTestGenerator} ({@code AbstractTestGenerator} subclass) and reports through - * the lifecycle logger of the {@code TGInfoDialog}; this port wraps the headless CLI pipeline - * ({@link TestgenReflectionF#generateTestcases}) in a {@link Task} and dispatches the report - * events to the {@link LogSinkF} of the {@code TestGenResultsDialogF}. + * the lifecycle logger of the {@code TGInfoDialog}; this port drives the reflective + * {@code TestgenFacade} pipeline ({@link TestgenReflectionF#generateTestcases}) in a + * {@link Task} and dispatches the report events to the {@link LogSinkF} of the + * {@code TestGenResultsDialogF}. *

        - * KNOWN-SIMPLIFIED: the Swing worker additionally puts the original proof into the - * mediator's auto-mode machinery ({@code initiateAutoMode}/{@code finishAutoMode}, - * TGWorker.java:42-63) and stops cooperatively through the {@code StopRequest} flag and the - * generator's {@code stopSMTLauncher} (TGWorker.java:66-71); the facade route has no launcher - * handle, so {@link #requestStop()} interrupts the worker thread, which the generation macros - * honour between phases (the final Z3 CE launch itself runs until it returns). + * The auto-mode shell is the Swing {@code TGWorker.start/done} parity (TGWorker.java:42-47, + * :57-62): {@link #call()} performs {@code mediator.initiateAutoMode(proof, true, false)} before + * the generation (the Swing {@code start()} does it before {@code execute()}) and the + * terminal-state hooks (the FX-thread counterparts of the Swing {@code done()}) perform + * {@code mediator.finishAutoMode(proof, true, true, null)} — also after a stop/failure, so the + * interface never stays frozen. The cooperative stop is the Swing {@code interruptionPerformed} + * parity (TGWorker.java:66-71): the stop flag is visible to the pipeline between the phases, the + * {@code SolverLauncher} handle (obtained through {@code launcherRef} — the Swing + * {@code stopSMTLauncher}) is stopped and the worker thread interrupted as the backup. */ @NullMarked final class TestGenerationTaskF extends Task { @@ -30,16 +39,23 @@ final class TestGenerationTaskF extends Task { private final Object ui; /** the proof to generate test cases for (reflectively obtained). */ - private final Object proof; + private final Proof proof; + + /** the mediator for the auto-mode shell (Swing TGWorker.start/done). */ + private final KeYMediatorF mediator; /** the log sink of the run dialog (events fire on this worker thread). */ private final LogSinkF sink; + /** the running {@code SolverLauncher} (the Swing {@code stopSMTLauncher} handle). */ + private final AtomicReference launcher = new AtomicReference<>(); + private volatile boolean stopRequested; - TestGenerationTaskF(Object ui, Object proof, LogSinkF sink) { - // ui/proof mirror TGWorker's "the selected proof" and the main window's UI - // (TGWorker.java:36-39). + TestGenerationTaskF(KeYMediatorF mediator, Object ui, Proof proof, LogSinkF sink) { + // mediator/ui/proof mirror TGWorker's getMediator(), "the selected proof" and the main + // window's UI (TGWorker.java:36-40). + this.mediator = mediator; this.ui = ui; this.proof = proof; this.sink = sink; @@ -47,7 +63,11 @@ final class TestGenerationTaskF extends Task { @Override protected Void call() throws Exception { - TestgenReflectionF.generateTestcases(ui, proof, sink); + // Swing TGWorker.start(): initiateAutoMode before the run (there on the EDT, here at the + // beginning of the worker body — the FX mediator's seam only flips flags and publishes + // the observable property, which the listeners marshal to the FX thread). + mediator.initiateAutoMode(proof, true, false); + TestgenReflectionF.generateTestcases(ui, proof, this::isStopRequested, sink, launcher); return null; } @@ -55,12 +75,41 @@ boolean isStopRequested() { return stopRequested; } + @Override + protected void succeeded() { + finishAutoMode(); + } + + @Override + protected void failed() { + finishAutoMode(); + } + + @Override + protected void cancelled() { + finishAutoMode(); + } + /** - * Asks the running generation to stop: the flag is visible to the dialogs, the interrupt is - * honoured by the generation macros (see the class comment). + * Swing TGWorker.done(): {@code finishAutoMode(originalProof, true, true, null)} — runs on + * the FX thread (the state-change hooks), also after a stop or failure. + */ + private void finishAutoMode() { + mediator.finishAutoMode(proof, true, true, null); + } + + /** + * Asks the running generation to stop (Swing TGWorker.interruptionPerformed, + * TGWorker.java:66-71): the flag is visible to the pipeline between the phases, the + * {@code SolverLauncher} is stopped ({@code stopSMTLauncher}) and the worker thread + * interrupted as the backup (the macros honour the interrupt). */ void requestStop() { stopRequested = true; + Object running = launcher.get(); + if (running != null) { + TestgenReflectionF.stopLauncher(running); + } if (isRunning()) { cancel(true); } diff --git a/keyext.ui.testgen.fx/src/main/java/de/uka/ilkd/key/gui/testgen/fx/TestgenReflectionF.java b/keyext.ui.testgen.fx/src/main/java/de/uka/ilkd/key/gui/testgen/fx/TestgenReflectionF.java index 7908618f6e0..51443d4924a 100644 --- a/keyext.ui.testgen.fx/src/main/java/de/uka/ilkd/key/gui/testgen/fx/TestgenReflectionF.java +++ b/keyext.ui.testgen.fx/src/main/java/de/uka/ilkd/key/gui/testgen/fx/TestgenReflectionF.java @@ -4,13 +4,16 @@ * SPDX-License-Identifier: GPL-2.0-or-later */ package de.uka.ilkd.key.gui.testgen.fx; +import java.io.IOException; import java.lang.reflect.Array; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.util.Collection; import java.util.List; +import java.util.NoSuchElementException; import java.util.concurrent.atomic.AtomicReference; +import java.util.function.BooleanSupplier; import javafx.stage.Stage; import org.jspecify.annotations.NullMarked; @@ -19,27 +22,31 @@ import org.slf4j.LoggerFactory; /** - * The reflective bridge to the {@code key.core} / {@code key.core.testgen} generation logic. + * The reflective bridge to the {@code key.core.testgen} generation logic. *

        - * NOT part of the SPI: neither key.core nor key.core.testgen is on the compile classpath - * of this module (the frozen {@code build.gradle} only exposes {@code key.ui.fx} and the Swing - * {@code keyext.ui.testgen}; the core modules arrive on the runtime classpath of the app), so all - * calls into the proof tree, the SMT machinery and the test-case generators are dispatched through - * plain reflection. The routed entry points mirror the ones the Swing extension uses: + * NOT part of the SPI: {@code key.core.testgen} is not on the compile classpath of this + * module (the frozen {@code build.gradle} only exposes {@code key.ui.fx}, the Swing + * {@code keyext.ui.testgen} and {@code key.core}; the testgen module arrives on the runtime + * classpath of the app), so all calls into the SMT/test-case generation machinery are dispatched + * through plain reflection — {@code key.core} types ({@code Proof}, {@code Sequent}, ...) are + * nameable and appear as typed parameters where the callers have them at hand. The routed entry + * points mirror the ones the Swing extension uses: *

          - *
        • {@link #generateTestcases(Object, Object, LogSinkF)} — the headless CLI pipeline - * {@code TestgenFacade.generateTestcases} (TestgenFacade.java:43-101): bounded symbolic - * execution (FinishSymbolicExecution / TestGen macro), semantic blasting and the Z3 CE run with - * JUnit test-suite generation. The Swing {@code TGWorker} drives the same steps through - * {@code AbstractTestGenerator} (which needs a UI subclass that cannot be materialised without the - * compile-time types); the facade is its headless counterpart that works off a plain - * {@code KeYEnvironment}.
        • - *
        • {@link #searchCounterExample(Object, Object, Object, LogSinkF, AtomicReference)} — the - * counterexample search of {@code AbstractSideProofCounterExampleGenerator.searchCounterExample} - * (side proof creation via {@code SideProofUtil}, {@code SemanticsBlastingMacro} on the side - * proof, then Z3 CE via a {@code SolverLauncher}). The abstract generator's - * {@code createSolverListener} hook is UI-specific; this port passes a reflective - * {@code SolverLauncherListener} instead and reports the outcome into the {@link LogSinkF}.
        • + *
        • {@link #generateTestcases} — the pipeline of {@code TestgenFacade.generateTestcases} + * (TestgenFacade.java:37-101): bounded symbolic execution (FinishSymbolicExecution / TestGen + * macro), semantic blasting and the Z3 CE run with JUnit test-suite generation. Unlike the + * facade the {@code SolverLauncher} is created here and handed to the caller, so the Stop button + * can stop it like the Swing {@code TGWorker.stopSMTLauncher} (the Swing {@code TGWorker} drives + * the same steps through {@code AbstractTestGenerator}, which needs a UI subclass that cannot be + * materialised without the compile-time types; the facade is its headless counterpart, and its + * hidden launcher would leave the run unstoppable).
        • + *
        • {@link #searchCounterExample} — the counterexample search of + * {@code AbstractSideProofCounterExampleGenerator.searchCounterExample} (side proof creation via + * {@code SideProofUtil}, {@code SemanticsBlastingMacro} on the side proof, then Z3 CE via a + * {@code SolverLauncher}). The abstract generator's {@code createSolverListener} hook is + * UI-specific; the caller supplies the {@code SolverLauncherListener} (see + * {@link #searchCounterExample} for the C4 {@code SolverListenerF} route) and this class keeps + * the reflective log-only fallback.
        • *
        * Every reflective call is guarded: a missing class or a reflection error is logged through the * {@link LogSinkF} (and the module logger) instead of crashing the FX thread; a propagated @@ -151,47 +158,122 @@ static boolean isOpenLeaf(@Nullable Object node) { // ------------------------------------------------------------------ test-case generation /** - * Runs the test-case generation (Swing TGWorker.doInBackground, TGWorker.java:50-53). The - * routed pipeline is {@code TestgenFacade.generateTestcases}: bounded symbolic execution, - * testing with the TestGen macro, semantic blasting of the test data constraints and the - * final Z3 CE run that writes the JUnit test suite into the configured output folder - * (TestgenFacade.java:43-101). - *

        - * KNOWN-SIMPLIFIED: the Swing {@code TGWorker} additionally wraps the run in the - * mediator's auto-mode machinery and supports an immediate stop through the - * {@code StopRequest}/{@code SolverLauncher} handles (TGWorker.java:42-71); the facade route - * has no launcher handle, so stopping the FX run interrupts the worker thread, which the - * macros honour between phases (the Z3 launch itself runs until it returns). + * Runs the test-case generation, the reflective counterpart of + * {@code TestgenFacade.generateTestcases} (TestgenFacade.java:37-101): bounded symbolic + * execution (if enabled), the {@code TestGenMacro}, semantic blasting and the final Z3 CE + * launch that writes the JUnit test suite into the configured output folder. Unlike the + * facade the {@code SolverLauncher} is created here and its handle handed to the caller + * ({@code launcherRef}), so the Stop button can stop it like the Swing + * {@code TGWorker.stopSMTLauncher} (TGWorker.java:70 — the facade hides its launcher). The + * phases are guarded by {@code shouldStop} (the Swing {@code StopRequest} parity, + * TGWorker.java:31-86); the auto-mode shell lives in {@link TestGenerationTaskF} (the Swing + * {@code TGWorker.start/done} placement). * * @param ui the UserInterfaceControl of the main window (reflected) * @param proof the selected proof (reflected) + * @param shouldStop the cooperative stop flag (checked between the phases and before the + * launch) * @param sink the log sink of the run dialog + * @param launcherRef receives the running {@code SolverLauncher} * @throws InterruptedException when the run was interrupted (task cancelled) */ - static void generateTestcases(Object ui, Object proof, LogSinkF sink) + static void generateTestcases(Object ui, Object proof, BooleanSupplier shouldStop, + LogSinkF sink, AtomicReference<@Nullable Object> launcherRef) throws InterruptedException { if (ui == null || proof == null) { sink.writeln("Test generation cancelled: no proof loaded."); return; } + if (!z3CeInstalled()) { + sink.writeln("Could not find the z3 SMT solver. Aborting."); + return; + } try { - Class settingsClass = clazz("de.uka.ilkd.key.testgen.TestGenerationSettings"); - Object settings = settingsClass.getMethod("getInstance").invoke(null); - Class uiClass = clazz("de.uka.ilkd.key.control.UserInterfaceControl"); - Class initConfigClass = clazz("de.uka.ilkd.key.proof.init.InitConfig"); Class proofClass = clazz("de.uka.ilkd.key.proof.Proof"); - Class envClass = clazz("de.uka.ilkd.key.control.KeYEnvironment"); - Class listenerClass = + Class settingsClass = clazz("de.uka.ilkd.key.testgen.TestGenerationSettings"); + Class lifecycleClass = clazz("de.uka.ilkd.key.testgen.smt.testgen.TestGenerationLifecycleListener"); + Class tgPhaseClass = clazz("de.uka.ilkd.key.testgen.smt.testgen.TGPhase"); + Class tgReporterClass = clazz("de.uka.ilkd.key.testgen.TGReporter"); + Class tgClass = clazz("de.uka.ilkd.key.testgen.TestCaseGenerator"); + + // reporter + generator (TestgenFacade.java:33-34) + Object settings = settingsClass.getMethod("getInstance").invoke(null); + Object reporter = tgReporterClass.getConstructor(lifecycleClass) + .newInstance(lifecycleListenerProxy(lifecycleClass, sink)); + Object tg = tgClass.getConstructor(proofClass, settingsClass, tgReporterClass) + .newInstance(proof, settings, reporter); + + // SMT settings triple (TestgenFacade.java:36-47); the 10 s solver timeout is the + // facade default (TestgenFacade.java:46) + Class newSettingsClass = clazz("de.uka.ilkd.key.settings.NewSMTTranslationSettings"); + Class pdClass = clazz("de.uka.ilkd.key.settings.ProofDependentSMTSettings"); + Class piClass = clazz("de.uka.ilkd.key.settings.ProofIndependentSMTSettings"); + Object newSettings = newSettingsClass.getConstructor().newInstance(); + Object pdSettings = pdClass.getMethod("getDefaultSettingsData").invoke(null); + Object piSettings = piClass.getMethod("getDefaultSettingsData").invoke(null); + piSettings.getClass().getMethod("setTimeout", int.class).invoke(piSettings, 10000); + Object smtSettings = clazz("de.uka.ilkd.key.settings.DefaultSMTSettings") + .getConstructor(pdClass, piClass, newSettingsClass, proofClass) + .newInstance(pdSettings, piSettings, newSettings, proof); + + // own SolverLauncher (TestgenFacade.java:49) — the handle goes into launcherRef so + // the Stop button can stop it; the JUnit test-suite generation runs in the + // launcherStopped event (TestgenFacade.java:50-65) + Object launcher = clazz("de.uka.ilkd.key.smt.SolverLauncher") + .getConstructor(clazz("de.uka.ilkd.key.smt.SMTSettings")) + .newInstance(smtSettings); + launcherRef.set(launcher); + Class launcherListenerClass = clazz("de.uka.ilkd.key.smt.SolverLauncherListener"); + launcher.getClass().getMethod("addListener", launcherListenerClass).invoke(launcher, + testSuiteListenerProxy(launcherListenerClass, tg, tgReporterClass, reporter)); + + Object taskListener = invokeNullary(invokeNullary(ui, "getProofControl"), + "getDefaultProverTaskListener"); + Object goals = invokeNullary(proof, "openEnabledGoals"); + + // phase 1: bounded symbolic execution (TestgenFacade.java:69-74) + if ((Boolean) settingsClass.getMethod("getApplySymbolicExecution").invoke(settings)) { + if (stopped(shouldStop)) { + return; + } + invokeApplyTo(clazz("de.uka.ilkd.key.macros.FinishSymbolicExecutionMacro") + .getConstructor().newInstance(), ui, proof, goals, taskListener); + } - Object initConfig = invokeNullary(proof, "getInitConfig"); - Object env = envClass.getConstructor(uiClass, initConfigClass).newInstance(ui, - initConfig); - Object listener = lifecycleListenerProxy(listenerClass, sink); - Class facade = clazz("de.uka.ilkd.key.testgen.TestgenFacade"); - Method generate = facade.getMethod("generateTestcases", envClass, proofClass, - settingsClass, listenerClass); - generate.invoke(null, env, proof, settings, listener); + // phase 2: the TestGen macro (TestgenFacade.java:76-78) + if (stopped(shouldStop)) { + return; + } + Object tgMacro = clazz("de.uka.ilkd.key.testgen.macros.TestGenMacro") + .getConstructor(int.class) + .newInstance(settingsClass.getMethod("getMaximalUnwinds").invoke(settings)); + invokeApplyTo(tgMacro, ui, proof, goals, taskListener); + + // phase 3: semantic blasting (TestgenFacade.java:80-82) + if (stopped(shouldStop)) { + return; + } + invokeApplyTo(clazz("de.uka.ilkd.key.testgen.macros.SemanticsBlastingMacro") + .getConstructor().newInstance(), ui, proof, goals, taskListener); + + // phase 4: the synchronous Z3 CE launch (TestgenFacade.java:84-86) + if (stopped(shouldStop)) { + return; + } + Object z3 = Class.forName("de.uka.ilkd.key.smt.solvertypes.SolverTypes") + .getField("Z3_CE_SOLVER").get(null); + Collection problems = (Collection) clazz("de.uka.ilkd.key.smt.SMTProblem") + .getMethod("createSMTProblems", proofClass).invoke(null, proof); + launcher.getClass().getMethod("launch", Collection.class, Collection.class, + clazz("de.uka.ilkd.key.java.Services")).invoke(launcher, List.of(z3), problems, + invokeNullary(proof, "getServices")); + + // finish events (TestgenFacade.java:88-89); the TGPhase.FINISHED goes through the + // TGReporter and fires sink.finished() via the lifecycle proxy + Object phase = tgPhaseClass.getField("FINISHED").get(null); + tgReporterClass.getMethod("phase", tgPhaseClass).invoke(reporter, phase); + tgReporterClass.getMethod("finish").invoke(reporter); } catch (InvocationTargetException e) { Throwable cause = e.getCause(); if (cause instanceof InterruptedException ie) { @@ -204,6 +286,31 @@ static void generateTestcases(Object ui, Object proof, LogSinkF sink) } } + /** + * The cooperative stop check between the phases (the Swing {@code StopRequest.shouldStop} + * plus the thread-interrupt backup, TGWorker.java:66-71 + AbstractTestGenerator.java:109). + * + * @return {@code true} when the run should stop + */ + private static boolean stopped(BooleanSupplier shouldStop) { + return shouldStop.getAsBoolean() || Thread.currentThread().isInterrupted(); + } + + /** + * Invokes {@code macro.applyTo(ui, proof, goals, null, taskListener)} — the shared + * {@code ProofMacro.applyTo} signature of the three generation phases. + */ + private static void invokeApplyTo(Object macro, Object ui, Object proof, Object goals, + Object taskListener) throws ReflectiveOperationException { + macro.getClass().getMethod("applyTo", + clazz("de.uka.ilkd.key.control.UserInterfaceControl"), + clazz("de.uka.ilkd.key.proof.Proof"), + clazz("org.key_project.util.collection.ImmutableList"), + clazz("de.uka.ilkd.key.logic.PosInOccurrence"), + clazz("org.key_project.prover.engine.ProverTaskListener")).invoke(macro, ui, proof, + goals, null, taskListener); + } + // ------------------------------------------------------------------ counterexample search /** @@ -218,11 +325,11 @@ static void generateTestcases(Object ui, Object proof, LogSinkF sink) * AbstractCounterExampleGenerator.java:67-100). *

        * KNOWN-SIMPLIFIED: the Swing {@code SolverListener} opens a modal progress dialog and - * a results dialog showing the counterexample model; the FX port prints the SMT statistics - * (solved/invalid/unknown path conditions) and the found/not-found outcome into the run - * dialog's log and leaves the model inspection to the generated test data. The auto-mode - * shell of the Swing worker (CounterExampleAction.java:169-191) is dropped like in - * {@link TestGenerationTaskF}. + * a results dialog showing the counterexample model; this port (the C4 route hands the + * caller's {@code SolverLauncherListener}) keeps a reflective log-only listener here and + * prints the SMT statistics (solved/invalid/unknown path conditions) and the found/not-found + * outcome into the run dialog's log. The auto-mode shell of the Swing worker + * (CounterExampleAction.java:169-191) lives in {@link CounterExampleTaskF}. * * @param ui the UserInterfaceControl of the main window (reflected) * @param proof the proof of the selected goal (reflected) @@ -378,6 +485,75 @@ private static Object lifecycleListenerProxy(Class listenerClass, LogSinkF si }); } + /** + * A {@code java.lang.reflect.Proxy} for {@code SolverLauncherListener} that runs the JUnit + * test-suite generation on the finished solvers (the anonymous listener of + * {@code TestgenFacade.generateTestcases}, TestgenFacade.java:50-65: the first solver's + * exception is re-thrown, then {@code TestCaseGenerator.generateJUnitTestSuite} and the + * "Compile the generated files" hint; NoSuchElement/IO go to the reporter like in Swing). + */ + private static Object testSuiteListenerProxy(Class listenerClass, Object testGenerator, + Class tgReporterClass, Object reporter) { + return Proxy.newProxyInstance(listenerClass.getClassLoader(), + new Class[] { listenerClass }, (proxy, method, args) -> { + switch (method.getName()) { + case "launcherStopped" -> { + Object solvers = args == null || args.length < 2 ? null : args[1]; + Collection finished = solvers instanceof Collection c ? c + : List.of(); + try { + Object first = finished.iterator().next(); + Object exception = callNullary(first, "getException"); + if (exception != null) { + throw new RuntimeException("Exception during SMT", + (Throwable) exception); + } + testGenerator.getClass() + .getMethod("generateJUnitTestSuite", Collection.class) + .invoke(testGenerator, finished); + tgReporterClass.getMethod("writeln", String.class).invoke(reporter, + "Compile the generated files using a Java compiler."); + } catch (NoSuchElementException e) { + reportException(tgReporterClass, reporter, e); + } catch (InvocationTargetException e) { + Throwable cause = e.getCause(); + if (cause instanceof NoSuchElementException + || cause instanceof IOException) { + reportException(tgReporterClass, reporter, cause); + } else { + throw cause == null ? e : cause; + } + } catch (ReflectiveOperationException e) { + LOGGER.warn("Could not generate the JUnit test suite", e); + } + } + case "equals" -> { + return args != null && args.length == 1 && args[0] == proxy; + } + case "hashCode" -> { + return System.identityHashCode(proxy); + } + case "toString" -> { + return "SolverLauncherListener(testSuite proxy)"; + } + default -> { + } + } + return null; + }); + } + + /** {@code TGReporter.reportException} (TestgenFacade.java:63-64). */ + private static void reportException(Class tgReporterClass, Object reporter, + Throwable throwable) { + try { + tgReporterClass.getMethod("reportException", Exception.class).invoke(reporter, + throwable); + } catch (ReflectiveOperationException | RuntimeException e) { + LOGGER.warn("Could not report the test-generation exception", e); + } + } + /** * A {@code java.lang.reflect.Proxy} for {@code SolverLauncherListener} that streams the * solver statistics of the counterexample search into the {@link LogSinkF}. The result From b4fd4a09e9fec6514e0f71bd0882da304faaf5dc Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Sun, 11 Oct 2026 21:02:02 +0200 Subject: [PATCH 203/281] =?UTF-8?q?WP-9=20(2/4):=20Minimize-Interaction=20?= =?UTF-8?q?real-flow=20wiring=20assert=20=E2=80=94=20key.fx.verify.minimiz?= =?UTF-8?q?einteraction=20now=20proves=20the=20load-success=20hand-off=20h?= =?UTF-8?q?onors=20the=20persisted=20flag=20on=20the=20loaded=20demo:=20ar?= =?UTF-8?q?med,=20an=20auto-instantiable=20taclet=20app=20applies=20throug?= =?UTF-8?q?h=20the=20minimize=20branch=20of=20AbstractProofControl.selecte?= =?UTF-8?q?dTaclet=20without=20the=20taclet=20dialog;=20disarmed,=20the=20?= =?UTF-8?q?same=20app=20opens=20the=20TacletMatchDialogF;=20applied=20node?= =?UTF-8?q?=20pruned=20back,=20flag=20restored?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../de/uka/ilkd/key/gui/fx/MainWindowF.java | 196 ++++++++++++++++++ 1 file changed, 196 insertions(+) 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 index 9c1d2246fc7..53ce07bf6f3 100644 --- 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 @@ -24,6 +24,7 @@ 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; @@ -180,6 +181,7 @@ import de.uka.ilkd.key.proof.io.ProofSaver; import de.uka.ilkd.key.proof.io.SingleThreadProblemLoader; 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; @@ -196,6 +198,9 @@ 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; @@ -1297,6 +1302,10 @@ protected KeYEnvironment call() throws Exception { 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 @@ -4886,6 +4895,193 @@ private void applyMinimizeInteraction(@Nullable ProofControl pc) { 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 + FxUtil.runLater(() -> { + gs.setTacletFilter(true); + applyMinimizeInteraction(proofControl); + }); + 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, true); + armedNoDialog = (Boolean) leg[0]; + unarmedDialog = (Boolean) leg[1]; // dialog detection of the armed leg (must be no) + // flag off: the same application must open the taclet dialog instead + FxUtil.runLater(() -> { + gs.setTacletFilter(false); + applyMinimizeInteraction(proofControl); + }); + TacletApp candidate2 = findMinimizeCandidate(proof); + if (candidate2 != null) { + Object[] leg2 = applyTacletExpecting(proofControl, proof, candidate2, false); + unarmedDialog = (Boolean) leg2[0]; + 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(); + } + + /** + * 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) { + Stage dialog = 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 From b3e8dbf3587924fd4251430d7312581195976e29 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Sun, 11 Oct 2026 21:03:31 +0200 Subject: [PATCH 204/281] =?UTF-8?q?WP-9=20(3/4):=20EnsureSourceConsistency?= =?UTF-8?q?=20load-pipeline=20wiring=20assert=20=E2=80=94=20new=20key.fx.v?= =?UTF-8?q?erify.sourceconsistency=20hook=20(proof-independent):=20loads?= =?UTF-8?q?=20a=20JML=20example=20(heap/model=5Fmethods/CellTest=5Ftest2.k?= =?UTF-8?q?ey,=20key.fx.demo.sourceconsistency)=20with=20the=20flag=20arme?= =?UTF-8?q?d=20and=20disarmed=20and=20asserts=20DiskFileRepo=20vs=20Simple?= =?UTF-8?q?FileRepo=20per=20AbstractProblemLoader.createFileRepo=20plus=20?= =?UTF-8?q?the=20repo-stream=20source=20read=20the=20source=20view=20perfo?= =?UTF-8?q?rms=20(content=20identical=20to=20disk,=20cached=20under=20the?= =?UTF-8?q?=20repo=20temp=20dir);=20flag=20restored?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- key.ui.fx/build.gradle | 8 +- .../de/uka/ilkd/key/gui/fx/MainWindowF.java | 130 ++++++++++++++++++ 2 files changed, 137 insertions(+), 1 deletion(-) diff --git a/key.ui.fx/build.gradle b/key.ui.fx/build.gradle index 7ae56b6f38e..0da51727453 100644 --- a/key.ui.fx/build.gradle +++ b/key.ui.fx/build.gradle @@ -157,6 +157,11 @@ run { // 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) + // * key.fx.verify.sourceconsistency = any (WP-9: run the EnsureSourceConsistency load- + // pipeline self test — the flag decides DiskFileRepo vs SimpleFileRepo on the next load + // and the repo serves the Java source through the source-view read path; the JML example + // defaults to heap/model_methods/CellTest_test2.key, override with + // key.fx.demo.sourceconsistency) ["key.fx.driver", "key.fx.theme", "key.fx.demo.sequent", "key.fx.verify.sequent", "key.fx.show", "key.fx.demo.autoprove", "key.fx.verify.tree", "key.fx.demo.autoprove.live", "key.fx.demo.maxsteps", "key.fx.verify.goallist", @@ -173,7 +178,8 @@ run { "key.fx.verify.loadingexit", "key.fx.verify.shortcuts", "key.fx.verify.inputfreeze", "key.fx.verify.sequentmenu", "key.fx.verify.dialogs", "key.fx.verify.prooftree", "key.fx.verify.selectionhistory", "key.fx.verify.isabelle", - "key.fx.verify.exploration", "key.fx.verify.caching"] + "key.fx.verify.exploration", "key.fx.verify.caching", + "key.fx.verify.sourceconsistency", "key.fx.demo.sourceconsistency"] .each { name -> def value = System.getProperty(name) if (value != null) { 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 index 53ce07bf6f3..638e11221ba 100644 --- 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 @@ -6,6 +6,7 @@ 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; @@ -180,6 +181,9 @@ 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; @@ -693,6 +697,16 @@ public void initialize() { .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 @@ -5676,6 +5690,122 @@ protected KeYEnvironment call() throws Exception { 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: + *

          + *
        1. 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};
        2. + *
        3. 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.
        4. + *
        + * 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. From d51f5b8e2082ad928f354fbba574f903e43d9fbf Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Sun, 11 Oct 2026 21:03:33 +0200 Subject: [PATCH 205/281] WP-7 C4: counterexample search routed through SolverListenerF searchCounterExample gains the createSolverListener parity hook of AbstractSideProofCounterExampleGenerator (a SolverLauncherListener factory for the DefaultSMTSettings + side proof); CounterExampleTaskF answers it with a SolverListenerF (owner stage + mediator), so the Z3 CE run shows the modal progress dialog and the result/counterexample-model dialogs like the Swing SolverListener. The reflective log-only SolverLauncherListener proxy with its statistics summary is removed (the run dialog keeps the pipeline log). Gate: key.ui.fx + keyext.ui.testgen.fx compileJava/test green. --- .../fx/CounterExampleResultsDialogF.java | 19 ++-- .../gui/testgen/fx/CounterExampleTaskF.java | 39 ++++--- .../gui/testgen/fx/TestgenReflectionF.java | 107 +++++------------- 3 files changed, 62 insertions(+), 103 deletions(-) diff --git a/keyext.ui.testgen.fx/src/main/java/de/uka/ilkd/key/gui/testgen/fx/CounterExampleResultsDialogF.java b/keyext.ui.testgen.fx/src/main/java/de/uka/ilkd/key/gui/testgen/fx/CounterExampleResultsDialogF.java index 434ddc7f4e8..8f7583931b0 100644 --- a/keyext.ui.testgen.fx/src/main/java/de/uka/ilkd/key/gui/testgen/fx/CounterExampleResultsDialogF.java +++ b/keyext.ui.testgen.fx/src/main/java/de/uka/ilkd/key/gui/testgen/fx/CounterExampleResultsDialogF.java @@ -36,12 +36,13 @@ * {@link CounterExampleTaskF}. *

        * KNOWN-SIMPLIFIED: the Swing flow shows the counterexample model in the modal - * {@code SolverListener} result dialogs; the FX port only prints the SMT statistics and the - * outcome (found/not found) into the log. The auto-mode shell around the search - * (CounterExampleAction.java:169-191) lives in {@link CounterExampleTaskF}. The - * window may be {@code null} when the extension has not been connected through the settings - * dialog yet (see {@link TestgenExtensionF}) — the dialog then starts with a hint instead of a - * search; the owner stage is resolved from the clicked status-line control. + * {@code SolverListener} result dialogs; the FX route goes through {@link SolverListenerF} (the + * same parity, WP-7) — this run dialog keeps the pipeline log (side proof, semantics blasting). + * The auto-mode shell around the search (CounterExampleAction.java:169-191) lives in + * {@link CounterExampleTaskF}. The window may be {@code null} when the extension has not been + * connected through the settings dialog yet (see {@link TestgenExtensionF}) — the dialog then + * starts with a hint instead of a search; the owner stage is resolved from the clicked + * status-line control. */ @NullMarked final class CounterExampleResultsDialogF extends Dialog { @@ -50,6 +51,8 @@ final class CounterExampleResultsDialogF extends Dialog { LoggerFactory.getLogger(CounterExampleResultsDialogF.class); private final @Nullable Object window; + /** the owner of the {@code SolverListenerF} progress dialog of the search (WP-7). */ + private final @Nullable Stage owner; private final TextArea log = new TextArea(); private final ProgressIndicator progress = new ProgressIndicator(); private final Label statusLabel = new Label("Idle"); @@ -62,6 +65,7 @@ final class CounterExampleResultsDialogF extends Dialog { CounterExampleResultsDialogF(@Nullable Object window, @Nullable Stage owner) { this.window = window; + this.owner = owner; setTitle("Counterexample Search"); if (owner != null) { initOwner(owner); @@ -153,7 +157,8 @@ private void startSearch() { statusLabel.setText("Searching for counterexamples..."); log.clear(); - CounterExampleTaskF task = new CounterExampleTaskF(mediator, ui, proof, sequent, logger); + CounterExampleTaskF task = + new CounterExampleTaskF(mediator, owner, ui, proof, sequent, logger); runningTask = task; task.setOnSucceeded(e -> { running.set(false); diff --git a/keyext.ui.testgen.fx/src/main/java/de/uka/ilkd/key/gui/testgen/fx/CounterExampleTaskF.java b/keyext.ui.testgen.fx/src/main/java/de/uka/ilkd/key/gui/testgen/fx/CounterExampleTaskF.java index 15839b0300d..ca766a68bc2 100644 --- a/keyext.ui.testgen.fx/src/main/java/de/uka/ilkd/key/gui/testgen/fx/CounterExampleTaskF.java +++ b/keyext.ui.testgen.fx/src/main/java/de/uka/ilkd/key/gui/testgen/fx/CounterExampleTaskF.java @@ -6,20 +6,26 @@ import java.util.concurrent.atomic.AtomicReference; import javafx.concurrent.Task; +import javafx.stage.Stage; import de.uka.ilkd.key.core.fx.KeYMediatorF; +import de.uka.ilkd.key.gui.fx.smt.SolverListenerF; import de.uka.ilkd.key.proof.Proof; +import de.uka.ilkd.key.settings.DefaultSMTSettings; import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; /** * Runs the counterexample search off the FX thread, FX port of the Swing * {@code CounterExampleAction.CEWorker} (CounterExampleAction.java:160-194). The Swing worker * runs {@code AbstractSideProofCounterExampleGenerator.searchCounterExample} in a - * {@code SwingWorker} and reports through the {@code SolverListener} (with progress/result - * dialogs); this port runs the reflective counterpart of the same search - * ({@link TestgenReflectionF#searchCounterExample}) in a {@link Task} and streams the SMT - * statistics into the {@link LogSinkF} of the {@code CounterExampleResultsDialogF}. + * {@code SwingWorker} with the {@code createSolverListener} hook answering + * {@code new SolverListener(settings, proof)} (CounterExampleAction.java:186-188); this port runs + * the reflective counterpart of the same search ({@link TestgenReflectionF#searchCounterExample}) + * in a {@link Task} with the same hook answering a {@link SolverListenerF} (the modal progress + * dialog and the results/counterexample-model dialogs, WP-7) and streams the pipeline messages + * into the {@link LogSinkF} of the {@code CounterExampleResultsDialogF}. *

        * The auto-mode shell is the Swing {@code CEWorker.start/done} parity * (CounterExampleAction.java:169-191): {@link #call()} performs @@ -27,12 +33,6 @@ * hooks (the FX-thread counterparts of the Swing {@code done()}) perform * {@code mediator.finishAutoMode(proof, true, true, null)} — also after a stop/failure, so the * interface never stays frozen. - *

        - * KNOWN-SIMPLIFIED: the Swing {@code SolverListener} opens a modal progress dialog and a - * results dialog showing the counterexample model (SolverListener.java); the FX port prints the - * SMT statistics and the found/not-found outcome into the run dialog's log and leaves the model - * inspection to the generated test data / subsequent runs (the C4 {@code SolverListenerF} route - * brings the dialogs back). */ @NullMarked final class CounterExampleTaskF extends Task { @@ -46,20 +46,25 @@ final class CounterExampleTaskF extends Task { /** the sequent of the selected goal (reflectively obtained). */ private final Object sequent; - /** the mediator for the auto-mode shell (Swing CEWorker.start/done). */ + /** the mediator for the auto-mode shell (Swing CEWorker.start/done) and the listener. */ private final KeYMediatorF mediator; + /** the owner window of the {@code SolverListenerF} progress dialog (may be {@code null}). */ + private final @Nullable Stage owner; + /** the log sink of the run dialog (events fire on this worker thread). */ private final LogSinkF sink; /** the running {@code SolverLauncher} (reflectively obtained), for the Stop button. */ private final AtomicReference launcher = new AtomicReference<>(); - CounterExampleTaskF(KeYMediatorF mediator, Object ui, Proof proof, Object sequent, - LogSinkF sink) { + CounterExampleTaskF(KeYMediatorF mediator, @Nullable Stage owner, Object ui, Proof proof, + Object sequent, LogSinkF sink) { // mediator/ui/proof/sequent mirror the Swing worker's inputs (CounterExampleAction - // .start, CounterExampleAction.java:169-172: the proof of the selected goal). + // .start, CounterExampleAction.java:169-172: the proof of the selected goal); the owner + // is the Swing SolverListener's dialog owner. this.mediator = mediator; + this.owner = owner; this.ui = ui; this.proof = proof; this.sequent = sequent; @@ -72,7 +77,11 @@ protected Void call() throws Exception { // .java:169-172; there on the EDT, here at the beginning of the worker body — the FX // mediator's seam only flips flags and publishes the observable property). mediator.initiateAutoMode(proof, true, false); - TestgenReflectionF.searchCounterExample(ui, proof, sequent, sink, launcher); + TestgenReflectionF.searchCounterExample(ui, proof, sequent, sink, launcher, + // Swing CEWorker.createSolverListener (CounterExampleAction.java:186-188): the + // SolverListener for the settings + side proof of the search + (settings, sideProof) -> new SolverListenerF((DefaultSMTSettings) settings, + (Proof) sideProof, owner, mediator)); return null; } diff --git a/keyext.ui.testgen.fx/src/main/java/de/uka/ilkd/key/gui/testgen/fx/TestgenReflectionF.java b/keyext.ui.testgen.fx/src/main/java/de/uka/ilkd/key/gui/testgen/fx/TestgenReflectionF.java index 51443d4924a..c40dd9af3ee 100644 --- a/keyext.ui.testgen.fx/src/main/java/de/uka/ilkd/key/gui/testgen/fx/TestgenReflectionF.java +++ b/keyext.ui.testgen.fx/src/main/java/de/uka/ilkd/key/gui/testgen/fx/TestgenReflectionF.java @@ -16,6 +16,8 @@ import java.util.function.BooleanSupplier; import javafx.stage.Stage; +import de.uka.ilkd.key.smt.SolverLauncherListener; + import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; import org.slf4j.Logger; @@ -313,6 +315,18 @@ private static void invokeApplyTo(Object macro, Object ui, Object proof, Object // ------------------------------------------------------------------ counterexample search + /** + * The {@code SolverLauncherListener} factory of the counterexample search — the + * UI-specific {@code createSolverListener} hook of + * {@code AbstractSideProofCounterExampleGenerator} (AbstractSideProofCounterExampleGenerator + * .java:22-27): the {@code SolverLauncherListener} for the given {@code DefaultSMTSettings} + * (first argument) and side {@code Proof} (second argument). + */ + @FunctionalInterface + interface SolverListenerFactoryF { + SolverLauncherListener create(Object settings, Object sideProof); + } + /** * Searches a counterexample for the sequent of the selected goal (Swing * {@code CounterExampleAction.CEWorker.doInBackground}, @@ -324,23 +338,24 @@ private static void invokeApplyTo(Object macro, Object ui, Object proof, Object * {@code AbstractCounterExampleGenerator.searchCounterExample}, * AbstractCounterExampleGenerator.java:67-100). *

        - * KNOWN-SIMPLIFIED: the Swing {@code SolverListener} opens a modal progress dialog and - * a results dialog showing the counterexample model; this port (the C4 route hands the - * caller's {@code SolverLauncherListener}) keeps a reflective log-only listener here and - * prints the SMT statistics (solved/invalid/unknown path conditions) and the found/not-found - * outcome into the run dialog's log. The auto-mode shell of the Swing worker - * (CounterExampleAction.java:169-191) lives in {@link CounterExampleTaskF}. + * The {@code SolverLauncherListener} comes from {@code listenerFactory} (the + * {@code createSolverListener} parity hook): {@link CounterExampleTaskF} supplies a + * {@code SolverListenerF}, which opens the modal progress dialog and the results + * (counterexample model) like the Swing {@code SolverListener}. * * @param ui the UserInterfaceControl of the main window (reflected) * @param proof the proof of the selected goal (reflected) * @param sequent the sequent of the selected goal (reflected) - * @param sink the log sink of the run dialog + * @param sink the log sink of the run dialog (the pipeline messages) * @param launcherRef receives the running {@code SolverLauncher} so the Stop button can ask it * to stop + * @param listenerFactory creates the {@code SolverLauncherListener} for the given + * {@code DefaultSMTSettings} and side proof * @throws InterruptedException when the run was interrupted (task cancelled) */ static void searchCounterExample(Object ui, Object proof, Object sequent, LogSinkF sink, - AtomicReference<@Nullable Object> launcherRef) throws InterruptedException { + AtomicReference<@Nullable Object> launcherRef, SolverListenerFactoryF listenerFactory) + throws InterruptedException { if (ui == null || proof == null || sequent == null) { sink.writeln("Counterexample search cancelled: no goal selected."); return; @@ -408,15 +423,15 @@ static void searchCounterExample(Object ui, Object proof, Object sequent, LogSin clazz("de.uka.ilkd.key.settings.NewSMTTranslationSettings"), proofClass) .newInstance(pdSettings, piSettings, newSettings, sideProof); - // (4) launch Z3 CE with a reflective SolverLauncherListener + // (4) launch Z3 CE with the caller's SolverLauncherListener // (AbstractCounterExampleGenerator.java:93-99) Object launcher = clazz("de.uka.ilkd.key.smt.SolverLauncher") .getConstructor(clazz("de.uka.ilkd.key.smt.SMTSettings")) .newInstance(smtSettings); launcherRef.set(launcher); - Object listener = solverLauncherListenerProxy(sink); launcher.getClass().getMethod("addListener", - clazz("de.uka.ilkd.key.smt.SolverLauncherListener")).invoke(launcher, listener); + clazz("de.uka.ilkd.key.smt.SolverLauncherListener")) + .invoke(launcher, listenerFactory.create(smtSettings, sideProof)); Object z3 = Class.forName("de.uka.ilkd.key.smt.solvertypes.SolverTypes") .getField("Z3_CE_SOLVER").get(null); Collection problems = (Collection) clazz("de.uka.ilkd.key.smt.SMTProblem") @@ -554,76 +569,6 @@ private static void reportException(Class tgReporterClass, Object reporter, } } - /** - * A {@code java.lang.reflect.Proxy} for {@code SolverLauncherListener} that streams the - * solver statistics of the counterexample search into the {@link LogSinkF}. The result - * summary mirrors {@code AbstractTestGenerator.filterSolverResultsAndShowSolverStatistics} - * (AbstractTestGenerator.java:383-428). - */ - private static Object solverLauncherListenerProxy(LogSinkF sink) - throws ClassNotFoundException { - Class listenerClass = clazz("de.uka.ilkd.key.smt.SolverLauncherListener"); - return Proxy.newProxyInstance(listenerClass.getClassLoader(), - new Class[] { listenerClass }, (proxy, method, args) -> { - switch (method.getName()) { - case "launcherStarted" -> { - Object problems = args == null || args.length == 0 ? null : args[0]; - int count = problems instanceof Collection c ? c.size() : 0; - sink.writeln("Searching for counterexamples: " + count - + " SMT problem(s)..."); - } - case "launcherStopped" -> { - Object solvers = args == null || args.length < 2 ? null : args[1]; - reportSolverResults(solvers, sink); - } - case "equals" -> { - return args != null && args.length == 1 && args[0] == proxy; - } - case "hashCode" -> { - return System.identityHashCode(proxy); - } - case "toString" -> { - return "SolverLauncherListener(proxy)"; - } - default -> { - } - } - return null; - }); - } - - private static void reportSolverResults(@Nullable Object finishedSolvers, LogSinkF sink) { - Collection solvers = finishedSolvers instanceof Collection c ? c : List.of(); - int valid = 0; - int falsifiable = 0; - int unknown = 0; - for (Object solver : solvers) { - try { - Object result = invokeNullary(solver, "getFinalResult"); - if (result != null) { - Object truth = invokeNullary(result, "isValid"); - if (truth != null) { - switch (truth.toString()) { - case "VALID" -> valid++; - case "FALSIFIABLE" -> falsifiable++; - case "UNKNOWN" -> unknown++; - default -> { - } - } - } - } - } catch (ReflectiveOperationException | RuntimeException ex) { - sink.writeln("Solver exception: " + (ex.getMessage() == null - ? ex.getClass().getSimpleName() - : ex.getMessage())); - } - } - sink.writeln("--- SMT Solver Results ---\n" + " solved pathconditions:" + falsifiable - + "\n" + " invalid pre-/pathconditions:" + valid + "\n" + " unknown:" + unknown); - sink.writeln(falsifiable > 0 ? "Found " + falsifiable + " counterexample(s)." - : "No counterexample found for the selected goal."); - } - private static Throwable throwableOf(Object @Nullable [] args) { Object value = args == null || args.length < 2 ? null : args[1]; return value instanceof Throwable t ? t From 352b9c7445b3fcc1a4ac7e95e7b666955e9eac72 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Sun, 11 Oct 2026 21:06:16 +0200 Subject: [PATCH 206/281] WP-7 C5: embedded live TestGen options panel in the run dialog Extracts the reusable TestgenOptionsPanelF (the widget rows of TestgenSettingsProviderF, the FX TestgenOptionsPanel port) with two modes: live (edits write into the global TestGenerationSettingsF immediately) and settings-dialog mode (widgets read on Apply/OK, as before). TestGenResultsDialogF embeds the live panel on its eastern edge like the Swing TGInfoDialog (TGInfoDialog.java:126); the Swing embedded panel lost its edits (working copy never applied from the run dialog), so writing live is the deliberate FX change. Gate: key.ui.fx + keyext.ui.testgen.fx compileJava/test green. --- .../gui/testgen/fx/TestGenResultsDialogF.java | 13 +- .../gui/testgen/fx/TestgenOptionsPanelF.java | 249 ++++++++++++++++++ .../testgen/fx/TestgenSettingsProviderF.java | 224 ++-------------- 3 files changed, 285 insertions(+), 201 deletions(-) create mode 100644 keyext.ui.testgen.fx/src/main/java/de/uka/ilkd/key/gui/testgen/fx/TestgenOptionsPanelF.java diff --git a/keyext.ui.testgen.fx/src/main/java/de/uka/ilkd/key/gui/testgen/fx/TestGenResultsDialogF.java b/keyext.ui.testgen.fx/src/main/java/de/uka/ilkd/key/gui/testgen/fx/TestGenResultsDialogF.java index 5801c4a0164..f85a8c006c3 100644 --- a/keyext.ui.testgen.fx/src/main/java/de/uka/ilkd/key/gui/testgen/fx/TestGenResultsDialogF.java +++ b/keyext.ui.testgen.fx/src/main/java/de/uka/ilkd/key/gui/testgen/fx/TestGenResultsDialogF.java @@ -45,8 +45,10 @@ *

        * KNOWN-SIMPLIFIED: the Swing dialog embeds a live {@code TestgenOptionsPanel} on its * eastern edge (TGInfoDialog.java:126) so the options can be edited right before a run; the FX - * port keeps the options exclusively in the Settings dialog (the global - * {@link TestGenerationSettingsF} are re-read at run start). The "Close" button is disabled while + * dialog embeds the {@link TestgenOptionsPanelF} the same way (WP-7). Unlike the Swing original + * (whose embedded panel edits a working copy that is never applied from the run dialog, so the + * edits were lost) the FX panel writes into the global {@link TestGenerationSettingsF} live. The + * "Close" button is disabled while * a run is active like in Swing (TGInfoDialog.java:118). The generated-files list is a simple * recursive listing of the configured output folder (the Swing dialog only prints "Writing test * file" into the log). The window may be {@code null} when the extension has not been connected @@ -70,6 +72,12 @@ final class TestGenResultsDialogF extends Dialog { private final Button btnStop = new Button("Stop"); private final Button btnClose = new Button("Close"); private final ReadOnlyBooleanWrapper running = new ReadOnlyBooleanWrapper(this, "running"); + /** + * the embedded live options panel (the Swing {@code TGInfoDialog} embeds a + * {@code TestgenOptionsPanel} on its eastern edge, TGInfoDialog.java:126); edits write into + * the global {@link TestGenerationSettingsF} immediately (see {@link TestgenOptionsPanelF}). + */ + private final TestgenOptionsPanelF optionsPanel = new TestgenOptionsPanelF(true); private TestGenerationTaskF runningTask; @@ -102,6 +110,7 @@ final class TestGenResultsDialogF extends Dialog { BorderPane root = new BorderPane(center); root.setBottom(buttons); + root.setRight(optionsPanel.getNode()); DialogPane pane = getDialogPane(); pane.setPrefSize(900, 650); pane.setContent(root); diff --git a/keyext.ui.testgen.fx/src/main/java/de/uka/ilkd/key/gui/testgen/fx/TestgenOptionsPanelF.java b/keyext.ui.testgen.fx/src/main/java/de/uka/ilkd/key/gui/testgen/fx/TestgenOptionsPanelF.java new file mode 100644 index 00000000000..bcca01622c0 --- /dev/null +++ b/keyext.ui.testgen.fx/src/main/java/de/uka/ilkd/key/gui/testgen/fx/TestgenOptionsPanelF.java @@ -0,0 +1,249 @@ +/* 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.testgen.fx; + +import java.io.File; + +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Node; +import javafx.scene.control.Button; +import javafx.scene.control.CheckBox; +import javafx.scene.control.Control; +import javafx.scene.control.Label; +import javafx.scene.control.Spinner; +import javafx.scene.control.SpinnerValueFactory; +import javafx.scene.control.TextField; +import javafx.scene.control.Tooltip; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.Region; +import javafx.stage.DirectoryChooser; + +import org.jspecify.annotations.NullMarked; + +/** + * The test-generation options panel, FX port of {@code TestgenOptionsPanel} (Swing + * TestgenOptionsPanel.java:14-157): the symbolic-execution/macro options, the maximal unwinds + * and concurrent SMT-process numbers, the output folder, and the duplicate/RFL/invariant/ + * post-condition toggles over the global singleton {@link TestGenerationSettingsF}. + *

        + * The panel is used at two surfaces (the Swing {@code TestgenOptionsPanel} is likewise both the + * Settings-dialog entry and the eastern edge of {@code TGInfoDialog}, TGInfoDialog.java:126): + *

          + *
        • in the Settings dialog through {@link TestgenSettingsProviderF} (the {@code live=false} + * mode: the edits stay in the widgets and are written back on Apply/OK like the Swing + * {@code applySettings}, TestgenOptionsPanel.java:153-157), and
        • + *
        • embedded in the {@code TestGenResultsDialogF} run dialog ({@code live=true}: every edit + * writes into the global settings immediately, so options can be changed right before a run — + * the Swing embedded panel edits a working copy that is never applied from the run dialog, so + * its edits were lost; writing live is the deliberate FX change).
        • + *
        + * KNOWN-SIMPLIFIED: the dead OpenJML/Objenesis file choosers of the Swing original are + * dropped (their setters are commented out, TestgenOptionsPanel.java:97-107). All widgets are + * created lazily in the constructor call: every {@code javafx.scene.control.Control} triggers + * the FX toolkit in its class initializer, so this class must not be instantiated headless. + */ +@NullMarked +final class TestgenOptionsPanelF { + + private static final String INFO_APPLY_SYMBOLIC_EX = + "Performs bounded symbolic execution on the current proof tree." + + " More precisely, the TestGen Macro is executed which the user can also manually execute by right-clicking " + + "on the proof tree and selecting Strategy Macros->TestGen."; + private static final String INFO_SAVE_TO = + "Choose the folder where the test case files will be written."; + private static final String INFO_MAX_PROCESSES = + "Maximal number of SMT processes that are allowed to " + "run concurrently."; + private static final String INFO_INVARIANT_FOR_ALL = + "Includes class invariants in the test data constraints. " + + "I.e., require the class invariant of all created objects to be true in the initial state."; + private static final String INFO_MAX_UNWINDS = + "Maximal number of loop unwinds or method calls on a branch that " + + "is symbolically executed when using the Strategy Macro \"TestGen\". The Strategy Macro is available" + + " by right-click on the proof tree."; + private static final String INFO_REMOVE_DUPLICATES = + "Generate a single testcase for two or more nodes which " + + "represent the same test data constraint. Two different nodes may represent the same test data constraint, " + + "because some formulas from the nodes which cannot be translated into a test case may be filtered out " + + "from the test data constraint."; + private static final String INFO_RFL_SELECTION = + "Enables initialization of protected, private, and ghost fields " + "with test data, " + + "as well as creation of objects from classes which have no default constructor " + + "(requires the Objenesis library)." + + "This functionality is enabled by RFL.java which is generated along the test suite. Please note, " + + "a runtime checker may not be able to handle the generated code."; + private static final String INFO_INCLUDE_POSTCONDITION = + "Includes the negated post condition in the test data " + + "constraint when generating test data. The post condition can only be included for paths (branches)" + + " where symbolic execution has finished."; + + private final CheckBox symbolicEx; + private final Spinner maxUnwinds; + private final CheckBox invariantForAll; + private final CheckBox includePostCondition; + private final Spinner maxProcesses; + private final TextField saveToFilePanel; + private final CheckBox removeDuplicates; + private final CheckBox checkboxRFL; + + private final Node panel; + + /** + * Creates the panel. + * + * @param live whether every edit writes into the global {@link TestGenerationSettingsF} + * immediately (the embedded run-dialog mode; the Settings-dialog mode keeps the + * edits in the widgets until {@link #applySettings()}) + */ + TestgenOptionsPanelF(boolean live) { + symbolicEx = new CheckBox("Apply symbolic execution"); + maxUnwinds = intSpinner(0, Integer.MAX_VALUE, 1, 3); + invariantForAll = new CheckBox("Require invariant for all objects"); + includePostCondition = new CheckBox("Include post condition"); + maxProcesses = intSpinner(0, Integer.MAX_VALUE, 1, 1); + saveToFilePanel = new TextField(); + removeDuplicates = new CheckBox("Remove duplicates"); + checkboxRFL = new CheckBox("Use reflection framework"); + setTooltips(); + symbolicEx.setSelected(false); + invariantForAll.setSelected(true); + includePostCondition.setSelected(false); + removeDuplicates.setSelected(true); + checkboxRFL.setSelected(false); + if (live) { + TestGenerationSettingsF global = new TestGenerationSettingsF(); + symbolicEx.selectedProperty() + .addListener((obs, o, v) -> global.setApplySymbolicEx(v)); + maxUnwinds.valueProperty() + .addListener((obs, o, v) -> global.setMaxUnwinds(Math.max(0, v))); + invariantForAll.selectedProperty() + .addListener((obs, o, v) -> global.setInvariantForAll(v)); + includePostCondition.selectedProperty() + .addListener((obs, o, v) -> global.setIncludePostCondition(v)); + maxProcesses.valueProperty() + .addListener((obs, o, v) -> global.setConcurrentProcesses(Math.max(0, v))); + saveToFilePanel.textProperty().addListener((obs, o, v) -> global.setOutputPath(v)); + removeDuplicates.selectedProperty() + .addListener((obs, o, v) -> global.setRemoveDuplicates(v)); + checkboxRFL.selectedProperty().addListener((obs, o, v) -> global.setUseRFL(v)); + } + panel = buildPanel(); + } + + /** @return the panel node (a {@link GridPane} of label/input rows) */ + Node getNode() { + return panel; + } + + /** Re-reads the global settings into the widgets (Swing getPanel, :139-151). */ + void readFromSettings() { + TestGenerationSettingsF settings = new TestGenerationSettingsF(); + symbolicEx.setSelected(settings.applySymbolicEx()); + maxUnwinds.getValueFactory().setValue(settings.maxUnwinds()); + invariantForAll.setSelected(settings.invariantForAll()); + includePostCondition.setSelected(settings.includePostCondition()); + maxProcesses.getValueFactory().setValue(settings.processes()); + saveToFilePanel.setText(settings.outputFolderPath()); + removeDuplicates.setSelected(settings.removeDuplicates()); + checkboxRFL.setSelected(settings.useRFL()); + } + + /** Writes the widget states back into the global settings singleton (Swing applySettings). */ + void applySettings() { + TestGenerationSettingsF global = new TestGenerationSettingsF(); + global.setApplySymbolicEx(symbolicEx.isSelected()); + global.setInvariantForAll(invariantForAll.isSelected()); + global.setIncludePostCondition(includePostCondition.isSelected()); + global.setMaxUnwinds(Math.max(0, maxUnwinds.getValue())); + global.setConcurrentProcesses(Math.max(0, maxProcesses.getValue())); + global.setOutputPath(saveToFilePanel.getText()); + global.setRemoveDuplicates(removeDuplicates.isSelected()); + global.setUseRFL(checkboxRFL.isSelected()); + } + + /** + * The options panel: a {@link GridPane} of label/input rows with tooltips (the Swing + * {@code TestgenOptionsPanel} uses a {@code GridBagLayout} with the same rows, + * TestgenOptionsPanel.java:67-108). + */ + private Node buildPanel() { + GridPane grid = new GridPane(); + grid.setHgap(8); + grid.setVgap(8); + grid.setPadding(new Insets(12)); + int row = 0; + addRow(grid, row++, null, symbolicEx); + addRow(grid, row++, "Maximal unwinds:", maxUnwinds); + addRow(grid, row++, null, invariantForAll); + addRow(grid, row++, null, includePostCondition); + addRow(grid, row++, "Concurrent processes:", maxProcesses); + addRow(grid, row++, "Store test cases to folder:", saveToRow()); + addRow(grid, row++, null, removeDuplicates); + addRow(grid, row, null, checkboxRFL); + return grid; + } + + /** One label + input row of the panel (or the input alone when {@code title == null}). */ + private void addRow(GridPane grid, int row, String title, Node input) { + if (title != null) { + Label label = new Label(title); + label.setAlignment(Pos.CENTER_LEFT); + if (input instanceof Control control) { + label.setTooltip(control.getTooltip()); + } + grid.add(label, 0, row); + grid.add(input, 1, row); + } else { + grid.add(input, 0, row); + } + if (input instanceof Region region) { + region.setMaxWidth(Double.MAX_VALUE); + } + GridPane.setHgrow(input, Priority.ALWAYS); + } + + /** The output-folder row: the text field plus a directory-chooser button. */ + private HBox saveToRow() { + Button browse = new Button("..."); + browse.setTooltip(new Tooltip(INFO_SAVE_TO)); + browse.setOnAction(e -> { + DirectoryChooser chooser = new DirectoryChooser(); + chooser.setTitle("Choose the test-case output folder"); + File initial = new File(saveToFilePanel.getText()); + if (initial.isDirectory()) { + chooser.setInitialDirectory(initial); + } + File selected = saveToFilePanel.getScene() == null + ? null + : chooser.showDialog(saveToFilePanel.getScene().getWindow()); + if (selected != null && selected.isDirectory()) { + saveToFilePanel.setText(selected.getAbsolutePath()); + } + }); + HBox.setHgrow(saveToFilePanel, Priority.ALWAYS); + return new HBox(4, saveToFilePanel, browse); + } + + private void setTooltips() { + symbolicEx.setTooltip(new Tooltip(INFO_APPLY_SYMBOLIC_EX)); + maxUnwinds.setTooltip(new Tooltip(INFO_MAX_UNWINDS)); + invariantForAll.setTooltip(new Tooltip(INFO_INVARIANT_FOR_ALL)); + includePostCondition.setTooltip(new Tooltip(INFO_INCLUDE_POSTCONDITION)); + maxProcesses.setTooltip(new Tooltip(INFO_MAX_PROCESSES)); + saveToFilePanel.setTooltip(new Tooltip(INFO_SAVE_TO)); + removeDuplicates.setTooltip(new Tooltip(INFO_REMOVE_DUPLICATES)); + checkboxRFL.setTooltip(new Tooltip(INFO_RFL_SELECTION)); + } + + private static Spinner intSpinner(int min, int max, int step, int value) { + Spinner spinner = + new Spinner<>(new SpinnerValueFactory.IntegerSpinnerValueFactory(min, max, value, + step)); + spinner.setEditable(true); + return spinner; + } +} diff --git a/keyext.ui.testgen.fx/src/main/java/de/uka/ilkd/key/gui/testgen/fx/TestgenSettingsProviderF.java b/keyext.ui.testgen.fx/src/main/java/de/uka/ilkd/key/gui/testgen/fx/TestgenSettingsProviderF.java index 448b11d3b73..5bcae4d5c1f 100644 --- a/keyext.ui.testgen.fx/src/main/java/de/uka/ilkd/key/gui/testgen/fx/TestgenSettingsProviderF.java +++ b/keyext.ui.testgen.fx/src/main/java/de/uka/ilkd/key/gui/testgen/fx/TestgenSettingsProviderF.java @@ -4,38 +4,24 @@ * SPDX-License-Identifier: GPL-2.0-or-later */ package de.uka.ilkd.key.gui.testgen.fx; -import java.io.File; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Proxy; import java.util.List; -import javafx.geometry.Insets; -import javafx.geometry.Pos; import javafx.scene.Node; -import javafx.scene.control.Button; -import javafx.scene.control.CheckBox; -import javafx.scene.control.Control; -import javafx.scene.control.Label; -import javafx.scene.control.Spinner; -import javafx.scene.control.SpinnerValueFactory; -import javafx.scene.control.TextField; -import javafx.scene.control.Tooltip; -import javafx.scene.layout.GridPane; -import javafx.scene.layout.HBox; -import javafx.scene.layout.Priority; -import javafx.scene.layout.Region; -import javafx.stage.DirectoryChooser; import de.uka.ilkd.key.gui.fx.settings.SettingsProviderF; import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; /** - * The test-generation options panel, FX port of {@code TestgenOptionsPanel} (Swing - * TestgenOptionsPanel.java:14-157): the symbolic-execution/macro options, the maximal unwinds - * and concurrent SMT-process numbers, the output folder, and the duplicate/RFL/invariant/ - * post-condition toggles — reading and writing the global singleton + * The test-generation settings entry, FX port of {@code TestgenOptionsPanel} (Swing + * TestgenOptionsPanel.java:14-157) as a {@code SettingsProviderF}: the symbolic-execution/macro + * options, the maximal unwinds and concurrent SMT-process numbers, the output folder, and the + * duplicate/RFL/invariant/post-condition toggles — reading and writing the global singleton * {@link TestGenerationSettingsF} (TestgenOptionsPanel.getPanel/getDescription/applySettings, - * TestgenOptionsPanel.java:134-157). + * TestgenOptionsPanel.java:134-157). The widgets live in {@link TestgenOptionsPanelF} (shared + * with the embedded panel of the run dialog). *

        * KNOWN-SIMPLIFIED (proxy provider): {@code SettingsProviderF.getPanel/apply} are declared * with a {@code MainWindowF} parameter, and any {@code MainWindowF} type use in this module @@ -49,46 +35,14 @@ *

        * KNOWN-SIMPLIFIED (panel): the Swing panel subclasses the Swing {@code SettingsPanel} * base (row/validator/chooser helpers) and installs change listeners that write into a working - * copy of the settings as the user edits; the FX port builds a plain {@link GridPane} of - * label/input rows (the settings host wraps the panel in a {@code ScrollPane}), reads the widgets - * in {@code apply} only (leaving the dialog through OK/Apply is the only path that persists, so - * the result is identical), and drops the dead OpenJML/Objenesis file choosers of the Swing - * original (their setters are commented out, TestgenOptionsPanel.java:97-107). + * copy of the settings as the user edits; the FX port keeps the edits in the widgets + * ({@code live=false} mode of {@link TestgenOptionsPanelF}) and reads them in {@code apply} only + * (leaving the dialog through OK/Apply is the only path that persists, so the result is + * identical). */ @NullMarked final class TestgenSettingsProviderF { - private static final String INFO_APPLY_SYMBOLIC_EX = - "Performs bounded symbolic execution on the current proof tree." - + " More precisely, the TestGen Macro is executed which the user can also manually execute by right-clicking " - + "on the proof tree and selecting Strategy Macros->TestGen."; - private static final String INFO_SAVE_TO = - "Choose the folder where the test case files will be written."; - private static final String INFO_MAX_PROCESSES = - "Maximal number of SMT processes that are allowed to " + "run concurrently."; - private static final String INFO_INVARIANT_FOR_ALL = - "Includes class invariants in the test data constraints. " - + "I.e., require the class invariant of all created objects to be true in the initial state."; - private static final String INFO_MAX_UNWINDS = - "Maximal number of loop unwinds or method calls on a branch that " - + "is symbolically executed when using the Strategy Macro \"TestGen\". The Strategy Macro is available" - + " by right-click on the proof tree."; - private static final String INFO_REMOVE_DUPLICATES = - "Generate a single testcase for two or more nodes which " - + "represent the same test data constraint. Two different nodes may represent the same test data constraint, " - + "because some formulas from the nodes which cannot be translated into a test case may be filtered out from " - + "the test data constraint."; - private static final String INFO_RFL_SELECTION = - "Enables initialization of protected, private, and ghost fields " + "with test data, " - + "as well as creation of objects from classes which have no default constructor " - + "(requires the Objenesis library)." - + "This functionality is enabled by RFL.java which is generated along the test suite. Please note, " - + "a runtime checker may not be able to handle the generated code."; - private static final String INFO_INCLUDE_POSTCONDITION = - "Includes the negated post condition in the test data " - + "constraint when generating test data. The post condition can only be included for paths (branches)" - + " where symbolic execution has finished."; - private TestgenSettingsProviderF() { } @@ -108,29 +62,21 @@ static SettingsProviderF create(TestgenExtensionF extension) { } /** - * The {@link InvocationHandler} of the settings provider + the panel widgets: dispatching the + * The {@link InvocationHandler} of the settings provider: dispatching the * {@code SettingsProviderF} methods (routed by name, mirroring the Swing original's - * semantics) and building the options panel node. + * semantics) over the {@link TestgenOptionsPanelF} widgets. *

        - * The widget fields are deliberately built lazily in {@link #ensureWidgets()}: every - * {@code javafx.scene.control.Control} triggers the FX toolkit in its class initializer - * ("Toolkit not initialized"), so neither the provider proxy nor the extension must touch - * Controls until the settings panel is really opened inside the running app. + * The panel is deliberately built lazily: every {@code javafx.scene.control.Control} triggers + * the FX toolkit in its class initializer ("Toolkit not initialized"), so neither the + * provider proxy nor the extension must touch Controls until the settings panel is really + * opened inside the running app. */ private static final class SettingsState implements InvocationHandler { private final TestgenExtensionF extension; - private CheckBox symbolicEx; - private Spinner maxUnwinds; - private CheckBox invariantForAll; - private CheckBox includePostCondition; - private Spinner maxProcesses; - private TextField saveToFilePanel; - private CheckBox removeDuplicates; - private CheckBox checkboxRFL; - - private Node panel; + /** the options panel widgets; {@code null} until the panel is first opened */ + private @Nullable TestgenOptionsPanelF optionsPanel; SettingsState(TestgenExtensionF extension) { this.extension = extension; @@ -193,140 +139,20 @@ public Object invoke(Object proxy, java.lang.reflect.Method method, Object[] arg /** Builds (once) and returns the options panel, re-reading the global settings. */ private Node panel() { - if (panel == null) { - ensureWidgets(); - panel = buildPanel(); + if (optionsPanel == null) { + optionsPanel = new TestgenOptionsPanelF(false); } - TestGenerationSettingsF settings = new TestGenerationSettingsF(); - symbolicEx.setSelected(settings.applySymbolicEx()); - maxUnwinds.getValueFactory().setValue(settings.maxUnwinds()); - invariantForAll.setSelected(settings.invariantForAll()); - includePostCondition.setSelected(settings.includePostCondition()); - maxProcesses.getValueFactory().setValue(settings.processes()); - saveToFilePanel.setText(settings.outputFolderPath()); - removeDuplicates.setSelected(settings.removeDuplicates()); - checkboxRFL.setSelected(settings.useRFL()); - return panel; + optionsPanel.readFromSettings(); + return optionsPanel.getNode(); } /** Writes the widget states back into the global settings singleton. */ private void applySettings() { - if (symbolicEx == null) { + if (optionsPanel == null) { // the panel was never opened, nothing was edited to persist. return; } - TestGenerationSettingsF global = new TestGenerationSettingsF(); - global.setApplySymbolicEx(symbolicEx.isSelected()); - global.setInvariantForAll(invariantForAll.isSelected()); - global.setIncludePostCondition(includePostCondition.isSelected()); - global.setMaxUnwinds(Math.max(0, maxUnwinds.getValue())); - global.setConcurrentProcesses(Math.max(0, maxProcesses.getValue())); - global.setOutputPath(saveToFilePanel.getText()); - global.setRemoveDuplicates(removeDuplicates.isSelected()); - global.setUseRFL(checkboxRFL.isSelected()); - } - - /** Creates the widget controls (lazily: Control class-init requires the FX toolkit). */ - private void ensureWidgets() { - if (symbolicEx != null) { - return; - } - symbolicEx = new CheckBox("Apply symbolic execution"); - maxUnwinds = intSpinner(0, Integer.MAX_VALUE, 1, 3); - invariantForAll = new CheckBox("Require invariant for all objects"); - includePostCondition = new CheckBox("Include post condition"); - maxProcesses = intSpinner(0, Integer.MAX_VALUE, 1, 1); - saveToFilePanel = new TextField(); - removeDuplicates = new CheckBox("Remove duplicates"); - checkboxRFL = new CheckBox("Use reflection framework"); - setTooltips(); - symbolicEx.setSelected(false); - invariantForAll.setSelected(true); - includePostCondition.setSelected(false); - removeDuplicates.setSelected(true); - checkboxRFL.setSelected(false); - } - - /** - * The options panel: a {@link GridPane} of label/input rows with tooltips (the Swing - * {@code TestgenOptionsPanel} uses a {@code GridBagLayout} with the same rows, - * TestgenOptionsPanel.java:67-108). - */ - private Node buildPanel() { - GridPane grid = new GridPane(); - grid.setHgap(8); - grid.setVgap(8); - grid.setPadding(new Insets(12)); - int row = 0; - addRow(grid, row++, null, symbolicEx); - addRow(grid, row++, "Maximal unwinds:", maxUnwinds); - addRow(grid, row++, null, invariantForAll); - addRow(grid, row++, null, includePostCondition); - addRow(grid, row++, "Concurrent processes:", maxProcesses); - addRow(grid, row++, "Store test cases to folder:", saveToRow()); - addRow(grid, row++, null, removeDuplicates); - addRow(grid, row, null, checkboxRFL); - return grid; - } - - /** One label + input row of the panel (or the input alone when {@code title == null}). */ - private void addRow(GridPane grid, int row, String title, Node input) { - if (title != null) { - Label label = new Label(title); - label.setAlignment(Pos.CENTER_LEFT); - if (input instanceof Control control) { - label.setTooltip(control.getTooltip()); - } - grid.add(label, 0, row); - grid.add(input, 1, row); - } else { - grid.add(input, 0, row); - } - if (input instanceof Region region) { - region.setMaxWidth(Double.MAX_VALUE); - } - GridPane.setHgrow(input, Priority.ALWAYS); - } - - /** The output-folder row: the text field plus a directory-chooser button. */ - private HBox saveToRow() { - Button browse = new Button("..."); - browse.setTooltip(new Tooltip(INFO_SAVE_TO)); - browse.setOnAction(e -> { - DirectoryChooser chooser = new DirectoryChooser(); - chooser.setTitle("Choose the test-case output folder"); - File initial = new File(saveToFilePanel.getText()); - if (initial.isDirectory()) { - chooser.setInitialDirectory(initial); - } - File selected = saveToFilePanel.getScene() == null - ? null - : chooser.showDialog(saveToFilePanel.getScene().getWindow()); - if (selected != null && selected.isDirectory()) { - saveToFilePanel.setText(selected.getAbsolutePath()); - } - }); - HBox.setHgrow(saveToFilePanel, Priority.ALWAYS); - return new HBox(4, saveToFilePanel, browse); - } - - private void setTooltips() { - symbolicEx.setTooltip(new Tooltip(INFO_APPLY_SYMBOLIC_EX)); - maxUnwinds.setTooltip(new Tooltip(INFO_MAX_UNWINDS)); - invariantForAll.setTooltip(new Tooltip(INFO_INVARIANT_FOR_ALL)); - includePostCondition.setTooltip(new Tooltip(INFO_INCLUDE_POSTCONDITION)); - maxProcesses.setTooltip(new Tooltip(INFO_MAX_PROCESSES)); - saveToFilePanel.setTooltip(new Tooltip(INFO_SAVE_TO)); - removeDuplicates.setTooltip(new Tooltip(INFO_REMOVE_DUPLICATES)); - checkboxRFL.setTooltip(new Tooltip(INFO_RFL_SELECTION)); - } - - private static Spinner intSpinner(int min, int max, int step, int value) { - Spinner spinner = - new Spinner<>(new SpinnerValueFactory.IntegerSpinnerValueFactory(min, max, value, - step)); - spinner.setEditable(true); - return spinner; + optionsPanel.applySettings(); } } } From 7f537ca55c3ffdea4e45c231e76fc02257894612 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Sun, 11 Oct 2026 21:10:20 +0200 Subject: [PATCH 207/281] WP-7 C6: counterexample model tree, help tab and line numbers in InformationWindowF Ports the Swing CETree to CETreeF (a TreeView over the SMT model with the Constants/Heaps/Location Sets/Sequences structure and the double-click expansion of #o/#l/#s reference leaves). The information window gains the solver parameter: for the Z3 CE solver with a model it is retitled 'Counterexample ...' and gets the model tree and the Help tab (Swing InformationWindow.initModel), and each information tab gets a simple line-number gutter (FX has no TextLineNumber). SolverListenerF.showInformation passes the solver. Gate: key.ui.fx + keyext.ui.testgen.fx compileJava/test green. --- .../de/uka/ilkd/key/gui/fx/smt/CETreeF.java | 353 ++++++++++++++++++ .../key/gui/fx/smt/InformationWindowF.java | 152 +++++++- .../ilkd/key/gui/fx/smt/SolverListenerF.java | 5 +- 3 files changed, 492 insertions(+), 18 deletions(-) create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/smt/CETreeF.java diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/smt/CETreeF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/smt/CETreeF.java new file mode 100644 index 00000000000..ba166a151f1 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/smt/CETreeF.java @@ -0,0 +1,353 @@ +/* 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.smt; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map.Entry; +import javafx.scene.control.TreeItem; +import javafx.scene.control.TreeView; + +import de.uka.ilkd.key.smt.model.Heap; +import de.uka.ilkd.key.smt.model.Location; +import de.uka.ilkd.key.smt.model.LocationSet; +import de.uka.ilkd.key.smt.model.Model; +import de.uka.ilkd.key.smt.model.ObjectVal; +import de.uka.ilkd.key.smt.model.Sequence; + +import org.key_project.util.collection.Pair; + +/** + * The counterexample model tree, FX port of {@code de.uka.ilkd.key.gui.smt.CETree} (Swing + * CETree.java:20-404): a {@link TreeView} with the Constants, Heaps, Location Sets and Sequences + * of the SMT model. The value objects of the model (references like {@code #o5}) are leaf nodes; + * a double click on such a leaf expands it with the properties/fields of the referenced object, + * location set or sequence (the Swing {@code CEMouseAdapter}). + */ +public final class CETreeF { + + /** + * A comparator that sorts ignoring case. Used to sort labels (Swing + * {@code CETree.IGNORECASE_COMPARATOR}). + */ + private static final Comparator> IGNORECASE_COMPARATOR = + (o1, o2) -> (o1.first + "=" + o1.second).compareToIgnoreCase(o2.first + "=" + o2.second); + /** the SMT model. */ + private final Model model; + + /** the FX tree. */ + private final TreeView tree; + + public CETreeF(Model model) { + this.model = model; + model.removeUnnecessaryObjects(); + model.addAliases(); + tree = new TreeView<>(constructTree()); + tree.setShowRoot(true); + tree.setOnMouseClicked(this::mouseClicked); + } + + /** @return the tree component (the Swing {@code CETree.getTreeComponent}) */ + public TreeView getTreeComponent() { + return tree; + } + + private TreeItem constructTree() { + TreeItem root = new TreeItem<>("Model"); + + TreeItem constants = new TreeItem<>("Constants"); + fillConstants(constants); + root.getChildren().add(constants); + TreeItem heaps = new TreeItem<>("Heaps"); + fillHeaps(heaps); + root.getChildren().add(heaps); + TreeItem locssets = new TreeItem<>("Location Sets"); + fillLocsets(locssets); + root.getChildren().add(locssets); + TreeItem sequences = new TreeItem<>("Sequences"); + fillSequences(sequences); + root.getChildren().add(sequences); + return root; + } + + private void fillHeaps(TreeItem heaps) { + for (Heap h : model.getHeaps()) { + TreeItem heap = new TreeItem<>(h.getName()); + heaps.getChildren().add(heap); + for (ObjectVal ov : h.getObjects()) { + TreeItem object = new TreeItem<>(ov.getName()); + heap.getChildren().add(object); + addObjectProperties(ov, object); + } + } + } + + private void addObjectProperties(ObjectVal ov, TreeItem object) { + String sortName = computeSortName(ov); + // General properties + List> objectProperties = computeObjectProperties(ov, sortName); + for (Pair property : objectProperties) { + object.getChildren().add(new TreeItem<>(property.first + "=" + property.second)); + } + // Fields + TreeItem fields = new TreeItem<>("Fields"); + object.getChildren().add(fields); + List> fieldsLabels = computeFields(ov); + for (Pair label : fieldsLabels) { + fields.getChildren().add(new TreeItem<>(label.first + "=" + label.second)); + } + + // Array Fields + if (hasArrayFields(sortName)) { + TreeItem arrayFields = new TreeItem<>("Array Fields"); + object.getChildren().add(arrayFields); + List arrayFieldLabels = computeArrayFields(ov); + for (String field : arrayFieldLabels) { + arrayFields.getChildren().add(new TreeItem<>(field)); + } + } + + // Fun Values + TreeItem functionValues = new TreeItem<>("Functions"); + object.getChildren().add(functionValues); + List> functionLabels = computeFunctions(ov); + for (Pair functionLabel : functionLabels) { + functionValues.getChildren() + .add(new TreeItem<>(functionLabel.first + "=" + functionLabel.second)); + } + } + + public static List> computeFunctions(ObjectVal ov) { + List> result = new LinkedList<>(); + for (Entry e : ov.getFunValues().entrySet()) { + result.add(new Pair<>(Model.removePipes(e.getKey()), e.getValue())); + } + return result; + } + + public static List computeArrayFields(ObjectVal ov) { + List result = new LinkedList<>(); + for (int i = 0; i < ov.getLength(); ++i) { + result.add("[" + i + "]=" + ov.getArrayValue(i)); + } + return result; + } + + public static boolean hasArrayFields(String sortName) { + return sortName.endsWith("[]"); + } + + public static String computeSortName(ObjectVal ov) { + return ov.getSort() == null ? "java.lang.Object" : ov.getSort().name().toString(); + } + + public static List> computeObjectProperties(ObjectVal ov, + String sortName) { + List> result = new LinkedList<>(); + // Type + sortName = Model.removePipes(sortName); + result.add(new Pair<>("Type", sortName)); + + // Exact Instance + boolean ei = ov.isExactInstance(); + result.add(new Pair<>("Exact Instance", String.valueOf(ei))); + + // Length + int l = ov.getLength(); + result.add(new Pair<>("Length", String.valueOf(l))); + return result; + } + + public static List> computeFields(ObjectVal ov) { + List> labels = new ArrayList<>(); + + for (Entry e : ov.getFieldvalues().entrySet()) { + labels.add(new Pair<>(Model.removePipes(e.getKey()), e.getValue())); + } + + // sort the labels alphabetically + labels.sort(IGNORECASE_COMPARATOR); + return labels; + } + + private void fillLocsets(TreeItem locsets) { + for (LocationSet ls : model.getLocsets()) { + TreeItem locset = new TreeItem<>(computeLocationSetName(ls)); + locsets.getChildren().add(locset); + addLocSetProperties(ls, locset); + } + } + + public static String computeLocationSetName(LocationSet ls) { + return Model.removePipes(ls.getName()); + } + + private void addLocSetProperties(LocationSet ls, TreeItem locset) { + List locationNames = computeLocationSetProperties(ls); + for (String locationName : locationNames) { + locset.getChildren().add(new TreeItem<>(locationName)); + } + } + + public static List computeLocationSetProperties(LocationSet ls) { + List result = new LinkedList<>(); + for (int i = 0; i < ls.size(); ++i) { + Location l = ls.get(i); + String locationName = "(" + Model.removePipes(l.getObjectID()) + ", " + + Model.removePipes(l.getFieldID()) + ")"; + result.add(locationName); + } + return result; + } + + private void fillSequences(TreeItem sequences) { + for (Sequence s : model.getSequences()) { + TreeItem sequence = new TreeItem<>(computeSequenceName(s)); + sequences.getChildren().add(sequence); + addSequenceProperties(s, sequence); + } + } + + public static String computeSequenceName(Sequence s) { + return Model.removePipes(s.getName()); + } + + private void addSequenceProperties(Sequence s, TreeItem sequence) { + List labels = computeSequenceProperties(s); + for (String label : labels) { + sequence.getChildren().add(new TreeItem<>(label)); + } + } + + public static List computeSequenceProperties(Sequence s) { + List result = new LinkedList<>(); + result.add("Length=" + s.getLength()); + + for (int i = 0; i < s.getLength(); ++i) { + result.add("[" + i + "]=" + Model.removePipes(s.get(i))); + } + return result; + } + + private void fillConstants(TreeItem constants) { + List> labels = computeConstantLabels(model); + + for (Pair label : labels) { + constants.getChildren().add(new TreeItem<>(label.first + "=" + label.second)); + } + } + + public static List> computeConstantLabels(Model model) { + List> labels = new ArrayList<>(); + for (Entry e : model.getConstants().entrySet()) { + labels.add(new Pair<>(Model.removePipes(e.getKey()), e.getValue())); + } + + // sort the labels alphabetically + labels.sort(IGNORECASE_COMPARATOR); + return labels; + } + + /** + * The double-click expansion of the reference leaves (the Swing {@code CEMouseAdapter}, + * CETree.java:294-402): a leaf whose value references a model object ({@code #o}), location + * set ({@code #l}) or sequence ({@code #s}) is expanded with the referenced entries. + */ + private void mouseClicked(javafx.scene.input.MouseEvent e) { + if (e.getClickCount() != 2) { + return; + } + TreeItem node = tree.getSelectionModel().getSelectedItem(); + if (node == null || !node.getChildren().isEmpty()) { + return; + } + String value = node.getValue(); + if (value.contains("=")) { + value = value.substring(value.indexOf('=') + 1); + } + + if (value.startsWith("#o")) { + value = value.substring(value.indexOf('=') + 1); + + // the referenced object sits in a heap: Model > Heaps > > + TreeItem heapsAncestor = ancestorAtDepth(node, 1); + if (heapsAncestor == null || !"Heaps".equals(heapsAncestor.getValue())) { + return; + } + TreeItem heapNode = ancestorAtDepth(node, 2); + if (heapNode == null) { + return; + } + String heapID = heapNode.getValue(); + + // search for heap + Heap heap = null; + for (Heap h : model.getHeaps()) { + if (heapID.equals(h.getName())) { + heap = h; + break; + } + } + if (heap == null) { + return; + } + + // search for object + ObjectVal val = null; + for (ObjectVal o : heap.getObjects()) { + if (o.getName().equals(value)) { + val = o; + break; + } + } + if (val == null) { + return; + } + addObjectProperties(val, node); + } else if (value.startsWith("#l")) { + LocationSet locset = null; + for (LocationSet ls : model.getLocsets()) { + if (ls.getName().startsWith(value)) { + locset = ls; + break; + } + } + if (locset == null) { + return; + } + addLocSetProperties(locset, node); + } else if (value.startsWith("#s")) { + Sequence sequence = null; + for (Sequence s : model.getSequences()) { + if (s.getName().startsWith(value)) { + sequence = s; + break; + } + } + if (sequence == null) { + return; + } + addSequenceProperties(sequence, node); + } + } + + /** + * @param node a tree item + * @param depth the depth of the wanted ancestor (0 = the root) + * @return the ancestor of the given depth, or {@code null} if the node is not that deep + */ + private static TreeItem ancestorAtDepth(TreeItem node, int depth) { + TreeItem current = node; + for (int i = 0; i < depth; i++) { + current = current.getParent(); + if (current == null) { + return null; + } + } + return current; + } +} diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/smt/InformationWindowF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/smt/InformationWindowF.java index 596610e5e85..22f7f073f70 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/smt/InformationWindowF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/smt/InformationWindowF.java @@ -5,29 +5,52 @@ package de.uka.ilkd.key.gui.fx.smt; import java.util.List; +import javafx.geometry.Pos; +import javafx.scene.Node; import javafx.scene.Scene; +import javafx.scene.control.Label; +import javafx.scene.control.ScrollPane; import javafx.scene.control.Tab; import javafx.scene.control.TabPane; import javafx.scene.control.TextArea; +import javafx.scene.layout.BorderPane; import javafx.scene.text.Font; +import javafx.scene.text.TextAlignment; import javafx.stage.Stage; import javafx.stage.Window; import de.uka.ilkd.key.gui.fx.theme.ThemeManager; +import de.uka.ilkd.key.smt.SMTSolver; +import de.uka.ilkd.key.smt.model.Model; +import de.uka.ilkd.key.smt.solvertypes.SolverTypes; /** * The information window presents detailed information about the execution of an SMT solver, * counter-part of the Swing {@code de.uka.ilkd.key.gui.smt.InformationWindow}: one tab per * {@link Information} (error message, the SMT2 translation passed to the solver, the taclet - * translation, the solver output, translation warnings). - *

        - * KNOWN-SIMPLIFIED: the counterexample model tree ({@code CETree}) and the line numbers - * ({@code TextLineNumber}) of the Swing original are not ported — the entries are presented as - * read-only monospaced text areas, and the counterexample help tab (which the Swing original - * adds with the model tree) is omitted with it. + * translation, the solver output, translation warnings) with line numbers, and — for the Z3 CE + * solver with a model — the "Counterexample" tab (the {@link CETreeF} model tree) and the "Help" + * tab (the {@link #CE_HELP} text), WP-7 (Swing InformationWindow.initModel, :88-101). */ public final class InformationWindowF extends Stage { + /** + * The help text of the counterexample tab (Swing {@code InformationWindow.CE_HELP}). + */ + public static final String CE_HELP = + """ + Bounded Counterexample Finder for KeY Proof Obligations + + - Shows a bounded model which satisfies the negation of the selected proof obligation + - Only proof obligations without modalities are supported + - The OneStepSimplifier neeeds to be activated, otherwise updates need to be handled manually beforehand + - Double click ond location set, sequence and object nodes(inside a heap) to extend them + - Choose bit sizes in Options -> SMT Solvers + - We have identified the following sources for spurious counterexample: + - Chosen bit sizes too small. Example: Bit size of Integer is 3 but literal 9 appears in proof obligation. + - Finite type instances: Example: There is no maximum integer. + - Removal of axioms. Example: There is a location set which contains location (o, f)"""; + /** * One solver information entry, counter-part of the Swing * {@code InformationWindow.Information}. @@ -39,7 +62,8 @@ public final class InformationWindowF extends Stage { public record Information(String title, String content, String solver) { } - public InformationWindowF(Window owner, String title, List information) { + public InformationWindowF(Window owner, SMTSolver solver, String title, + List information) { setTitle(title); if (owner != null) { initOwner(owner); @@ -47,14 +71,9 @@ public InformationWindowF(Window owner, String title, List informat TabPane tabs = new TabPane(); tabs.setTabClosingPolicy(TabPane.TabClosingPolicy.UNAVAILABLE); + initModel(solver, tabs); for (Information el : information) { - TextArea area = new TextArea(el.content()); - area.setEditable(false); - area.setWrapText(false); - // the solver input/output is SMT2 text; a monospaced font keeps it readable (the - // Swing original used the sequent view font) - area.setFont(Font.font("Monospaced", 13)); - tabs.getTabs().add(new Tab(el.title(), area)); + tabs.getTabs().add(new Tab(el.title(), newTab(el))); } Scene scene = new Scene(tabs, 640, 520); @@ -63,14 +82,113 @@ public InformationWindowF(Window owner, String title, List informat } /** - * Shows the information window for the given entries (Swing opens a non-modal - * {@code JDialog} from {@code SolverListener.showInformation}). + * The counterexample model tabs (Swing {@code InformationWindow.initModel}, :88-101): for the + * Z3 CE solver with a query model the window is retitled "Counterexample ..." and gets the + * model tree and the help tab. + * + * @param solver the solver whose run is presented + * @param tabs the tab pane to extend + */ + private void initModel(SMTSolver solver, TabPane tabs) { + if (solver.getType() != SolverTypes.Z3_CE_SOLVER) { + return; + } + if (solver.getSocket().getQuery() == null) { + return; + } + + Model m = solver.getSocket().getQuery().getModel(); + setTitle("Counterexample " + getTitle()); + tabs.getTabs().add(new Tab("Counterexample", createModelTab(m))); + createHelpTab(tabs); + } + + /** The help tab (Swing {@code InformationWindow.createHelpTab}, :103-111). */ + private void createHelpTab(TabPane tabs) { + TextArea content = new TextArea(CE_HELP); + content.setEditable(false); + content.setWrapText(true); + tabs.getTabs().add(new Tab("Help", content)); + } + + /** The counterexample model tree tab (Swing {@code InformationWindow.createModelTab}). */ + private static Node createModelTab(Model model) { + ScrollPane pane = new ScrollPane(new CETreeF(model).getTreeComponent()); + pane.setFitToWidth(true); + pane.setFitToHeight(true); + return pane; + } + + /** + * One information tab: the read-only, non-wrapping monospaced text with a simple line-number + * gutter (the Swing original used {@code TextLineNumber}, InformationWindow.java:124-140). + */ + private static Node newTab(Information information) { + TextArea area = new TextArea(information.content()); + area.setEditable(false); + area.setWrapText(false); + // the solver input/output is SMT2 text; a monospaced font keeps it readable (the + // Swing original used the sequent view font) + area.setFont(Font.font("Monospaced", 13)); + + BorderPane nowrap = new BorderPane(area); + nowrap.setLeft(lineNumberGutter(area)); + ScrollPane pane = new ScrollPane(nowrap); + pane.setFitToWidth(true); + return pane; + } + + /** + * A simple line-number gutter for the given text area (FX has no {@code TextLineNumber}): + * a right-aligned label whose lines follow the text and whose vertical offset follows the + * scroll position. + */ + private static Node lineNumberGutter(TextArea area) { + Label gutter = new Label(); + gutter.setFont(area.getFont()); + gutter.setTextAlignment(TextAlignment.RIGHT); + gutter.setAlignment(Pos.TOP_RIGHT); + gutter.setStyle("-fx-padding: 6 6 0 0;"); + Runnable update = () -> { + String text = area.getText(); + int lines = text.isEmpty() ? 1 : text.split("\n", -1).length; + StringBuilder numbers = new StringBuilder(); + for (int i = 1; i <= lines; i++) { + numbers.append(i).append('\n'); + } + gutter.setText(numbers.toString()); + }; + area.textProperty().addListener((obs, o, v) -> update.run()); + area.scrollTopProperty() + .addListener((obs, o, v) -> gutter.setTranslateY(-v.doubleValue())); + update.run(); + return gutter; + } + + /** + * Shows the information window for the given entries without a solver (Swing opens a + * non-modal {@code JDialog} from {@code SolverListener.showInformation}); no counterexample + * tabs are added. * * @param owner the owner window; may be {@code null} * @param title the window title * @param information the tabs to show */ public static void show(Window owner, String title, List information) { - new InformationWindowF(owner, title, information).show(); + show(owner, null, title, information); + } + + /** + * Shows the information window for the given entries and solver (the counterexample tabs are + * added for the Z3 CE solver with a model). + * + * @param owner the owner window; may be {@code null} + * @param solver the solver whose run is presented; may be {@code null} + * @param title the window title + * @param information the tabs to show + */ + public static void show(Window owner, SMTSolver solver, String title, + List information) { + new InformationWindowF(owner, solver, title, information).show(); } } diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/smt/SolverListenerF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/smt/SolverListenerF.java index e3f5896391a..a1cc6b25e90 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/smt/SolverListenerF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/smt/SolverListenerF.java @@ -607,7 +607,10 @@ private void focusResults() { } private void showInformation(InternSMTProblemF problem) { - InformationWindowF.show(owner, "Information for " + problem, problem.getInformation()); + // WP-7 — the solver is passed so the Z3 CE entries get the counterexample model tree and + // the help tab (Swing InformationWindow.initModel, InformationWindow.java:88-101) + InformationWindowF.show(owner, problem.solver, "Information for " + problem, + problem.getInformation()); } private void stopEvent(SolverLauncher launcher) { From f530a84702126577fe18a2ee1338e705ff104581 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Sun, 11 Oct 2026 21:14:34 +0200 Subject: [PATCH 208/281] WP-7 C7: undoable SMT result application through a user action Ports the Swing user-action seam: UserActionF (the UserAction port, name/apply/undo/canUndo with fireActionPerformed) + UserActionListenerF, registered on KeYMediatorF (addUserActionListener/fireActionPerformed, Swing KeYMediator.java:815-825). SMTProofApplyUserActionF (the Swing SMTProofApplyUserAction port) applies the SMTRule results undoably - apply freezes the interface via the WP-1 auto-mode seams and finishAutoMode restores the default selection; undo prunes the proof back to the recorded open goals. SolverListenerF.applyResults uses the action (Swing SolverListener.applyResults parity). The key.fx.verify.smt harness asserts the recorded action (name/canUndo), the freeze flag and that undo reopens the closed goal. Gate: key.ui.fx + keyext.ui.testgen.fx compileJava/test green. --- .../de/uka/ilkd/key/core/fx/KeYMediatorF.java | 30 +++++ .../de/uka/ilkd/key/gui/fx/MainWindowF.java | 51 ++++++- .../ilkd/key/gui/fx/UserActionListenerF.java | 27 ++++ .../useractions/SMTProofApplyUserActionF.java | 125 ++++++++++++++++++ .../fx/actions/useractions/UserActionF.java | 71 ++++++++++ .../ilkd/key/gui/fx/smt/SolverListenerF.java | 52 ++------ 6 files changed, 314 insertions(+), 42 deletions(-) create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/UserActionListenerF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/actions/useractions/SMTProofApplyUserActionF.java create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/actions/useractions/UserActionF.java 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 index 1b0caccc05b..315e50c684a 100644 --- 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 @@ -13,6 +13,8 @@ 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; @@ -117,6 +119,12 @@ public final class KeYMediatorF implements KeYSelectionModel.ProofBinder { */ 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). @@ -469,6 +477,28 @@ 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 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 index e2bc8e06379..4ad0b7c78fb 100644 --- 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 @@ -13,6 +13,7 @@ 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; @@ -22,6 +23,8 @@ 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.function.Consumer; import java.util.stream.Collectors; import javafx.animation.KeyFrame; @@ -86,6 +89,7 @@ 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; @@ -2529,6 +2533,13 @@ private void runSmtVerificationAsync() { // 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); + 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()); @@ -2549,8 +2560,46 @@ private void runSmtVerificationAsync() { // 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); + } 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 + ")"; + } 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); @@ -2564,7 +2613,7 @@ private void runSmtVerificationAsync() { LOGGER.info("SMT verification: {}", finalReport); NotificationManagerF.getInstance() .notify("SMT verification: " + finalReport, - finalReport.endsWith("PASS") ? Kind.INFO : Kind.ERROR); + finalReport.contains("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/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/smt/SolverListenerF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/smt/SolverListenerF.java index a1cc6b25e90..3af46835830 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/smt/SolverListenerF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/smt/SolverListenerF.java @@ -29,8 +29,8 @@ import javafx.stage.Window; import javafx.util.Duration; -import de.uka.ilkd.key.control.AbstractProofControl; import de.uka.ilkd.key.core.fx.KeYMediatorF; +import de.uka.ilkd.key.gui.fx.actions.useractions.SMTProofApplyUserActionF; 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.smt.InformationWindowF.Information; @@ -45,13 +45,11 @@ 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.settings.DefaultSMTSettings; import de.uka.ilkd.key.settings.ProofIndependentSMTSettings.ProgressMode; import de.uka.ilkd.key.settings.ProofIndependentSettings; import de.uka.ilkd.key.smt.SMTFocusResults; import de.uka.ilkd.key.smt.SMTProblem; -import de.uka.ilkd.key.smt.SMTRule; import de.uka.ilkd.key.smt.SMTSolver; import de.uka.ilkd.key.smt.SMTSolver.ReasonOfInterruption; import de.uka.ilkd.key.smt.SMTSolverResult.ThreeValuedTruth; @@ -63,8 +61,6 @@ import de.uka.ilkd.key.taclettranslation.assumptions.TacletSetTranslation; import org.key_project.logic.Term; -import org.key_project.prover.sequent.PosInOccurrence; -import org.key_project.util.collection.ImmutableList; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -78,12 +74,9 @@ * the result coloring (green/red/orange/blue from the {@code [solverListener]} palette colors) * into the table cells. *

        - * KNOWN-SIMPLIFIED: the results are applied directly ({@code SMTRule} built-in rule application - * with the unsat core if available) instead of through the Swing {@code SMTProofApplyUserAction} - * history mechanism (no undo entry for the automatic CLOSE-mode application). Swing also stops - * the interface ({@code stopInterface}) around the rule application; the FX application runs - * synchronously on the FX thread, so a freeze would be unobservable (the auto mode freeze of - * MainWindowF.freezeExceptAutoModeButton covers the long-running case). + * The results are applied through the undoable {@code SMTProofApplyUserActionF} (WP-7, the Swing + * {@code SMTProofApplyUserAction} history mechanism) with the interface frozen around the + * {@code SMTRule} built-in rule application. */ public class SolverListenerF implements SolverLauncherListener { @@ -533,38 +526,15 @@ private ProgressCellF cell(InternSMTProblemF problem) { } /** - * Applies the results (Swing {@code SolverListener.applyResults}): every goal whose solver - * returned a valid result is closed with the {@code SMTRule} built-in rule application (with - * the unsat core if the solver provided one). The Swing original wraps this in a - * {@code SMTProofApplyUserAction} (undoable); the FX port applies directly. + * Applies the results (Swing {@code SolverListener.applyResults}, SolverListener.java:209-221): + * every goal whose solver returned a valid result is closed with the {@code SMTRule} built-in + * rule application (with the unsat core if the solver provided one), performed through the + * undoable {@link SMTProofApplyUserActionF} (WP-7 — the Swing {@code SMTProofApplyUserAction} + * history mechanism; the interface freeze and the default selection happen inside the + * action's apply). */ private void applyResults() { - Set goalsClosed = new HashSet<>(); - for (InternSMTProblemF problem : problems) { - Goal goal = problem.problem.getGoal(); - if (goalsClosed.contains(goal) - || problem.solver.getFinalResult().isValid() != ThreeValuedTruth.VALID) { - continue; - } - goalsClosed.add(goal); - ImmutableList unsatCore = - SMTFocusResults.getUnsatCore(problem.problem); - IBuiltInRuleApp app; - if (unsatCore != null) { - app = SMTRule.INSTANCE.createApp(problem.solver.name(), unsatCore); - } else { - app = SMTRule.INSTANCE.createApp(problem.solver.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); - } - // switch to new open goal (Swing: mediator.getSelectionModel().defaultSelection(); - // the mediator.stopInterface/startInterface input freeze is not ported yet) - mediator.getSelectionModel().defaultSelection(); + new SMTProofApplyUserActionF(mediator, smtProof, problems).actionPerformed(); } /** From 4ead80b05fb37d10726d81dc37ccc978e11d6bb4 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Sun, 11 Oct 2026 21:18:02 +0200 Subject: [PATCH 209/281] WP-7 C8: per-solver SMT options panels (SolverOptionsF) as child providers --- .../gui/fx/settings/SMTSettingsProviderF.java | 40 ++- .../key/gui/fx/settings/SolverOptionsF.java | 246 ++++++++++++++++++ 2 files changed, 279 insertions(+), 7 deletions(-) create mode 100644 key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/SolverOptionsF.java diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/SMTSettingsProviderF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/SMTSettingsProviderF.java index 23e03fd7108..3e03660b603 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/SMTSettingsProviderF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/SMTSettingsProviderF.java @@ -4,7 +4,9 @@ * SPDX-License-Identifier: GPL-2.0-or-later */ package de.uka.ilkd.key.gui.fx.settings; +import java.util.ArrayList; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import javafx.scene.Node; import javafx.scene.control.CheckBox; @@ -82,15 +84,39 @@ public SMTSettingsProviderF() { addCheckBox("Store taclet translation to file", "", false); chkEnableOnLoad = addCheckBox("Enable SMT solvers when loading proofs", "", true); - // menu: KNOWN-DEFERRED — the remaining Swing options of SMTSettingsProvider / - // SolverOptions are not ported: the per-solver command/parameters/timeout and support - // panels (SolverOptions.java:36-50), the progress-dialog mode, the global timeout, the - // concurrent-process count, the int/seq/object/locset bounds, the "check for support" - // toggle and the store-translation file path panel (SMTSettingsProvider.java:105-158, - // 185-256). The four persisted toggles above and the solver selection are the - // functional core; the rest is left for the SMT milestone. + // menu: KNOWN-DEFERRED — the remaining Swing options of SMTSettingsProvider are not + // ported: the progress-dialog mode, the global timeout, the concurrent-process count, + // the int/seq/object/locset bounds and the store-translation file path panel + // (SMTSettingsProvider.java:105-158, 185-256). The per-solver command/parameters/timeout + // and support panels (Swing SolverOptions) are ported as child providers + // ({@link SolverOptionsF}, see children); the four persisted toggles above and the + // solver selection are the functional core; the rest is left for the SMT milestone. refreshFromSettings(); + + // menu: MP4 — one per-solver child panel per available solver type, like the Swing + // SMTSettingsProvider (SMTSettingsProvider.java:155-158): only those types that are + // actually theoretically available according to the settings (containsSolver). + children = new ArrayList<>(); + ProofIndependentSMTSettings smtSettings = + ProofIndependentSettings.DEFAULT_INSTANCE.getSMTSettings(); + for (SolverType type : SolverTypes.getSolverTypes()) { + if (smtSettings.containsSolver(type)) { + children.add(new SolverOptionsF(type)); + } + } + } + + /** + * One {@link SolverOptionsF} child panel per solver type that the settings know + * (Swing {@code SMTSettingsProvider.getChildren}, SMTSettingsProvider.java:155-163), built + * in the constructor and shown as tree children in the settings dialog. + */ + private List children; + + @Override + public List getChildProviders() { + return children; } @Override diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/SolverOptionsF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/SolverOptionsF.java new file mode 100644 index 00000000000..22eb2675926 --- /dev/null +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/settings/SolverOptionsF.java @@ -0,0 +1,246 @@ +/* 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.settings; + +import javafx.scene.Node; +import javafx.scene.control.Button; +import javafx.scene.control.Spinner; +import javafx.scene.control.TextArea; +import javafx.scene.control.TextField; + +import de.uka.ilkd.key.gui.fx.MainWindowF; +import de.uka.ilkd.key.settings.ProofIndependentSMTSettings; +import de.uka.ilkd.key.settings.ProofIndependentSettings; +import de.uka.ilkd.key.smt.solvertypes.SolverType; + +/** + * Per-solver options panel of the settings dialog, counter-part of + * {@code de.uka.ilkd.key.gui.smt.settings.SolverOptions} of the Swing module {@code key.ui} + * (registered there as a child provider of {@code SMTSettingsProvider}, + * SMTSettingsProvider.java:155-158). + *

        + * Shows the immutable facts of one {@link SolverType} (name, info, installed version, support + * state) and the three persisted values (command, per-solver timeout, parameters) that live on + * the {@link SolverType} itself. All values are re-read from + * {@link ProofIndependentSettings#DEFAULT_INSTANCE} in {@link #getPanel(MainWindowF)} (the Swing + * original re-reads a clone there, SolverOptions.java:180-183) and written back through + * {@link SolverType#setSolverCommand}, {@link SolverType#setSolverParameters} and + * {@link SolverType#setSolverTimeout} in {@link #apply(MainWindowF)} + * (SolverOptions.java:205-224). + *

        + * menu: KNOWN-DEFERRED — the Swing {@code applySettings} additionally calls + * {@code MainWindow.updateSMTSelectMenu()} (SolverOptions.java:218) to rebuild the SMT solver + * combo box; the FX main window has no SMT select menu yet (MainWindowF has no + * {@code updateSMTSelectMenu} counterpart), so this refresh is not applicable here. + */ +public class SolverOptionsF extends SettingsPanelF implements SettingsProviderF { + + /** + * Help texts and support verdicts, copied from the Swing {@code SMTSettingsProvider} + * (SMTSettingsProvider.java:62-107) so that both UIs show identical wording. + */ + public static final String INFO_SOLVER_NAME = + """ + There are two ways to make supported provers applicable for KeY: + 1. Specify the absolute path of the prover in the field 'Command'. + 2. Change the environment variable $PATH of your system, so that it + refers to the installed prover. In that case you must specify the name of the solver in 'Command'"""; + public static final String INFO_SOLVER_COMMAND = """ + The command for the solver. + + Either you specify the absolute path of your solver or just the command for starting it. + In the latter case you have to modify the PATH-variable of your system. + Please note that you also have to specify the filename extension. + For example: z3.exe"""; + public static final String INFO_SOLVER_PARAMETERS = + """ + In this field you can specify which parameters are passed to the \ + solver when the solver is started. Note that the default parameters are crucial for a stable run of the\ + solver."""; + public static final String INFO_SOLVER_SUPPORT = + """ + For the KeY system only some particular versions of this solver + have been tested. It is highly recommended to use those versions, because otherwise it is not guaranteed that + the connection to this solver is stable. + If you want to check whether the installed solver is supported, please click on the button below."""; + public static final String SOLVER_SUPPORTED = "Version of solver is supported."; + public static final String SOLVER_MAY_SUPPORTED = "Version of solver may not be supported."; + public static final String SOLVER_UNSUPPORTED = "Support has not been checked, yet."; + public static final String INFO_SOLVER_TIMEOUT = + "Overrides timeout for this solver. Values in seconds. If value <= 0, global timeout is used."; + public static final String INFO_SOLVER_INFO = "Information about this solver."; + + private final transient SolverType solverType; + + private TextField txtName; + private TextField txtInstalled; + private TextField txtCommand; + private TextField txtParameters; + private TextField txtSupported; + private Spinner spinTimeout; + + /** Creates the panel for one solver type and builds the form once (the panel is reused). */ + public SolverOptionsF(SolverType solverType) { + this.solverType = solverType; + setHeaderText("SMT Solver: " + getDescription()); + + txtName = createSolverName(); + createSolverInformation(); + txtInstalled = createSolverInstalled(); + txtCommand = createSolverCommand(); + spinTimeout = createSolverTimeout(); + txtParameters = createSolverParameters(); + txtSupported = createSolverSupported(); + createDefaultButton(); + createCheckSupportButton(); + + refreshFromSettings(); + } + + @Override + public String getDescription() { + return solverType.getName(); + } + + @Override + public Node getPanel(MainWindowF window) { + // like the Swing original, re-read the current settings on every getPanel + // (SolverOptions.getPanel → setSmtSettings, SolverOptions.java:180-202) + refreshFromSettings(); + return this; + } + + @Override + public void apply(MainWindowF window) throws InvalidSettingsInputExceptionF { + ProofIndependentSMTSettings settings = + ProofIndependentSettings.DEFAULT_INSTANCE.getSMTSettings(); + if (!settings.containsSolver(solverType)) { + throw new IllegalStateException("Could not find solver data for type: " + solverType); + } + String command = txtCommand.getText(); + String params = txtParameters.getText(); + long timeout = (long) (spinTimeout.getValue() * 1000.0); + + solverType.setSolverCommand(command); + solverType.setSolverParameters(params); + solverType.setSolverTimeout(timeout); + // menu: KNOWN-DEFERRED — the Swing original calls window.updateSMTSelectMenu() here + // (SolverOptions.java:218); the FX main window has no SMT select menu counterpart yet. + + refreshFromSettings(); // refresh gui + } + + /** + * Re-reads the current settings into the input components (Swing {@code setSmtSettings}, + * SolverOptions.java:185-202). + */ + private void refreshFromSettings() { + ProofIndependentSMTSettings settings = + ProofIndependentSettings.DEFAULT_INSTANCE.getSMTSettings(); + if (!settings.containsSolver(solverType)) { + throw new IllegalStateException("Could not find solver data for type: " + solverType); + } + txtCommand.setText(settings.getCommand(solverType)); + txtParameters.setText(settings.getParameters(solverType)); + spinTimeout.getValueFactory() + .setValue(((double) settings.getSolverTimeout(solverType)) / 1000); + txtName.setText(solverType.getName()); + + final boolean installed = solverType.isInstalled(true); + String info = installed ? "yes" : "no"; + if (installed) { + try { + info = versionInfo(info, solverType.getRawVersion()); + } catch (RuntimeException re) { + // this case occurs for instance if the user can see e.g. z3 but doesn't have + // the permission to execute the solver + info = "(version: unknown) solver is installed, but trying to access it resulted " + + "in an error " + (re.getCause() != null + ? re.getCause().getLocalizedMessage() + : re.getLocalizedMessage()); + } + } + txtInstalled.setText(info); + } + + private TextField createSolverName() { + TextField txt = addTextField("Name", solverType.getName(), INFO_SOLVER_NAME, + emptyValidator()); + txt.setEditable(false); + return txt; + } + + private void createSolverInformation() { + String info = solverType.getInfo(); + if (info != null && !info.isEmpty()) { + TextArea area = addTextArea("Info", info, INFO_SOLVER_INFO, null, 3); + area.setEditable(false); + } + } + + private TextField createSolverInstalled() { + // initial content is refreshed in refreshFromSettings; the field is read-only + TextField txt = addTextField("Installed", "", "", emptyValidator()); + txt.setEditable(false); + return txt; + } + + private TextField createSolverCommand() { + return addTextField("Command", solverType.getSolverCommand(), INFO_SOLVER_COMMAND, + emptyValidator()); + } + + private Spinner createSolverTimeout() { + // seconds with fractions, stored as milliseconds on the solver type + // (Swing model: SpinnerNumberModel(0.0, -1.0, Long.MAX_VALUE, 1), SolverOptions.java:108) + return addDoubleNumberField("Timeout", -1.0, (double) Long.MAX_VALUE, 1.0, 0.0, + INFO_SOLVER_TIMEOUT, emptyValidator()); + } + + private TextField createSolverParameters() { + return addTextField("Parameters", solverType.getSolverParameters(), + INFO_SOLVER_PARAMETERS, emptyValidator()); + } + + private TextField createSolverSupported() { + TextField txt = addTextField("Support", getSolverSupportText(), + INFO_SOLVER_SUPPORT + " " + createSupportedVersionText(), emptyValidator()); + txt.setEditable(false); + return txt; + } + + private String createSupportedVersionText() { + return "The following minimal version is supported: " + + solverType.getMinimumSupportedVersion(); + } + + private String getSolverSupportText() { + if (solverType.supportHasBeenChecked()) { + return solverType.isSupportedVersion() ? SOLVER_SUPPORTED : SOLVER_MAY_SUPPORTED; + } + return SOLVER_UNSUPPORTED; + } + + private void createDefaultButton() { + Button toDefaultButton = new Button("Set parameters to default"); + toDefaultButton.setOnAction( + e -> txtParameters.setText(solverType.getDefaultSolverParameters())); + addRowWithHelp(null, new javafx.scene.control.Label(), toDefaultButton); + } + + private void createCheckSupportButton() { + Button checkForSupportButton = new Button("Check for support"); + checkForSupportButton.setDisable(!solverType.isInstalled(false)); + checkForSupportButton.setOnAction(e -> { + solverType.checkForSupport(); + txtSupported.setText(getSolverSupportText()); + }); + addRowWithHelp(null, new javafx.scene.control.Label(), checkForSupportButton); + } + + private static String versionInfo(String info, String versionString) { + return info + " (" + versionString + ")"; + } +} From 772621b11799027042f086264b4fffa259e481d9 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Sun, 11 Oct 2026 21:09:12 +0200 Subject: [PATCH 210/281] =?UTF-8?q?WP-9=20(4/4):=20Contract=20E2E=20wiring?= =?UTF-8?q?=20assert=20=E2=80=94=20the=20dialogs=20self=20test=20gains=20a?= =?UTF-8?q?=20sixth=20leg:=20a=20real=20example=20proof=20(heap/model=5Fme?= =?UTF-8?q?thods/CellTest=5FcallSet.key,=20key.fx.verify.dialogs.contract)?= =?UTF-8?q?=20is=20loaded=20off=20the=20main=20window=20via=20KeYEnvironme?= =?UTF-8?q?nt.load;=20the=20operation-contract=20PO=20inlines=20the=20call?= =?UTF-8?q?ee=20in=20a=20try/catch,=20so=20the=20harness=20first=20parks?= =?UTF-8?q?=20the=20strategy=20at=20the=20active=20method=20call=20(METHOD?= =?UTF-8?q?=5FNONE=20strategy=20rebuild=20+=20one=20auto-mode=20run=20?= =?UTF-8?q?=E2=80=94=20the=20state=20the=20interactive=20GUI=20offers=20th?= =?UTF-8?q?e=20rule=20from),=20builds=20a=20UseOperationContractRule=20app?= =?UTF-8?q?=20on=20a=20succedent=20top-level=20position=20and=20completes?= =?UTF-8?q?=20it=20through=20the=20REAL=20WindowUserInterfaceControlF.comp?= =?UTF-8?q?leteBuiltInRuleApp=20dispatch=20(the=20modal=20ContractConfigur?= =?UTF-8?q?atorF=20is=20driven=20by=20a=20background=20watcher=20via=20the?= =?UTF-8?q?=20lastInstance=20seam:=20selectContract=20+=20requestOk),=20th?= =?UTF-8?q?en=20applies=20the=20completed=20app=20interactively=20via=20Pr?= =?UTF-8?q?oofControl.applyInteractive=20growing=20the=20proof;=20Contract?= =?UTF-8?q?ConfiguratorF=20gets=20the=20lastInstance/getContractPanel=20ha?= =?UTF-8?q?rness=20seam?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- key.ui.fx/build.gradle | 5 +- .../ContractConfiguratorF.java | 21 ++ .../key/gui/fx/dialogs/DialogsVerifyF.java | 197 +++++++++++++++++- 3 files changed, 220 insertions(+), 3 deletions(-) diff --git a/key.ui.fx/build.gradle b/key.ui.fx/build.gradle index 0da51727453..9cfd2085a54 100644 --- a/key.ui.fx/build.gradle +++ b/key.ui.fx/build.gradle @@ -136,6 +136,8 @@ run { // term menu + POPUP_DELAY guard, search prefill, shift+click focussed auto mode) // * key.fx.verify.dialogs = any (P2b: run the contract-completion registry + dialog // skeletons — contract/auxiliary configurators, lemma selection dialog, item chooser) + // * key.fx.verify.dialogs.contract = (P2b/WP-9: example for the contract E2E + // leg of the dialogs self test; default heap/model_methods/CellTest_callSet.key) // * key.fx.verify.prooftree = any (P3a: run the proof tree C19-C23/C25/C27 self test — // per-proof view states, linearized mode, OSS protocol rows, whole-tree expand/collapse, // node-filter counting, notes + subtree statistics dialogs; combine with @@ -176,7 +178,8 @@ run { "key.fx.verify.profileloadingdialog", "key.fx.verify.javacsettings", "key.fx.verify.termmenu", "key.fx.verify.colors", "key.fx.verify.smt", "key.fx.verify.loadingexit", "key.fx.verify.shortcuts", "key.fx.verify.inputfreeze", - "key.fx.verify.sequentmenu", "key.fx.verify.dialogs", "key.fx.verify.prooftree", + "key.fx.verify.sequentmenu", "key.fx.verify.dialogs", "key.fx.verify.dialogs.contract", + "key.fx.verify.prooftree", "key.fx.verify.selectionhistory", "key.fx.verify.isabelle", "key.fx.verify.exploration", "key.fx.verify.caching", "key.fx.verify.sourceconsistency", "key.fx.demo.sourceconsistency"] diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/contractcompletions/ContractConfiguratorF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/contractcompletions/ContractConfiguratorF.java index bbd0946b0fe..9aefb12fcea 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/contractcompletions/ContractConfiguratorF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/contractcompletions/ContractConfiguratorF.java @@ -34,6 +34,13 @@ */ public class ContractConfiguratorF { + /** + * the most recently constructed dialog (verification harness seam: background drivers of the + * {@code key.fx.verify.*} hooks route the OK/Cancel to the dialog whose stage they found + * showing via {@link #lastInstance()}). + */ + private static ContractConfiguratorF lastInstance; + private final Stage stage = new Stage(); private final ContractSelectionPanelF contractPanel; private boolean successful = false; @@ -52,6 +59,7 @@ public class ContractConfiguratorF { */ public ContractConfiguratorF(Services services, Contract[] contracts, String title, boolean allowMultipleContracts) { + lastInstance = this; contractPanel = new ContractSelectionPanelF(services, allowMultipleContracts); contractPanel.setContracts(contracts, title); @@ -135,4 +143,17 @@ public boolean wasSuccessful() { public Contract getContract() { return contractPanel.getContract(); } + + /** @return the wrapped selection panel (verification harness) */ + public ContractSelectionPanelF getContractPanel() { + return contractPanel; + } + + /** + * @return the most recently constructed dialog (verification harness: background drivers + * route the OK/Cancel to the dialog whose stage they found showing) + */ + public static ContractConfiguratorF lastInstance() { + return lastInstance; + } } diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/dialogs/DialogsVerifyF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/dialogs/DialogsVerifyF.java index 0d0b0cd8471..e8a87f787f8 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/dialogs/DialogsVerifyF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/dialogs/DialogsVerifyF.java @@ -4,13 +4,18 @@ * SPDX-License-Identifier: GPL-2.0-or-later */ package de.uka.ilkd.key.gui.fx.dialogs; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; import javafx.application.Platform; import javafx.stage.Window; +import de.uka.ilkd.key.control.DefaultUserInterfaceControl; +import de.uka.ilkd.key.control.KeYEnvironment; import de.uka.ilkd.key.gui.fx.InteractiveRuleApplicationCompletionF; import de.uka.ilkd.key.gui.fx.WindowUserInterfaceControlF; import de.uka.ilkd.key.gui.fx.contractcompletions.BlockContractExternalCompletionF; @@ -23,10 +28,26 @@ import de.uka.ilkd.key.gui.fx.mergerule.MergeRuleCompletionF; import de.uka.ilkd.key.gui.fx.notification.NotificationManagerF; import de.uka.ilkd.key.gui.fx.notification.NotificationManagerF.Kind; +import de.uka.ilkd.key.java.Services; +import de.uka.ilkd.key.logic.JTerm; +import de.uka.ilkd.key.proof.Goal; import de.uka.ilkd.key.proof.Proof; +import de.uka.ilkd.key.proof.init.Profile; +import de.uka.ilkd.key.rule.ContractRuleApp; +import de.uka.ilkd.key.rule.IBuiltInRuleApp; import de.uka.ilkd.key.rule.Taclet; +import de.uka.ilkd.key.rule.UseOperationContractRule; +import de.uka.ilkd.key.rule.UseOperationContractRule.Instantiation; +import de.uka.ilkd.key.speclang.FunctionalOperationContract; +import de.uka.ilkd.key.strategy.StrategyFactory; +import de.uka.ilkd.key.strategy.StrategyProperties; import de.uka.ilkd.key.taclettranslation.lemma.TacletSoundnessPOLoader.TacletInfo; +import org.key_project.logic.Name; +import org.key_project.logic.PosInTerm; +import org.key_project.prover.sequent.PosInOccurrence; +import org.key_project.prover.sequent.Sequent; +import org.key_project.prover.sequent.SequentFormula; import org.key_project.util.collection.ImmutableSet; import org.slf4j.Logger; @@ -52,7 +73,13 @@ * returns exactly them; *

      • checks the {@link InvariantConfiguratorF} singleton and the abbreviation-map wiring is in * place (the modal invariant editor itself needs an interactive loop application — not - * headless-drivable, see the P2b note in PARITY-SIGNOFF.md).
      • + * headless-drivable, see the P2b note in PARITY-SIGNOFF.md); + *
      • runs the contract end to end (WP-9): a real example proof is loaded through + * {@link KeYEnvironment#load}, a succedent top-level position becomes a + * {@link UseOperationContractRule} app, the app is completed via the REAL + * {@link WindowUserInterfaceControlF#completeBuiltInRuleApp} dispatch (the modal + * {@link ContractConfiguratorF} is driven by a background watcher through the + * {@code lastInstance} seam) and applied interactively.
      • * */ public final class DialogsVerifyF { @@ -98,12 +125,14 @@ private static String runAll(Proof proof, WindowUserInterfaceControlF ui) { verifyAuxiliaryContractConfigurator(proof, problems); verifyLemmaSelectionDialog(proof, problems); verifyInvariantConfiguratorWiring(problems); + verifyContractApplyE2E(ui, problems); if (problems.length() > 0) { return "FAIL: " + problems; } return "PASS: registry (6 completions), item chooser, contract configurator, auxiliary" - + " configurator, lemma selection dialog, invariant configurator wiring"; + + " configurator, lemma selection dialog, invariant configurator wiring," + + " contract apply E2E"; } /** Step 1: the interactive-completion registry of the FX user interface control. */ @@ -310,6 +339,170 @@ private static void verifyInvariantConfiguratorWiring(StringBuilder problems) { } } + /** + * Step 6 (WP-9): the contract end to end. A real example proof (default + * {@code heap/model_methods/CellTest_callSet.key}, override with + * {@code key.fx.verify.dialogs.contract}) is loaded off the main window + * ({@link KeYEnvironment#load}, the decoupled path of the Swing problem loader). The + * operation-contract PO inlines the callee inside a try/catch, so the harness first stops + * the strategy at the active method call (method treatment METHOD_NONE + one auto-mode run + * — the state the interactive GUI offers the rule from), turns a succedent top-level + * position into a {@link UseOperationContractRule} app ({@code createApp}, like + * {@code AbstractProofControl.selectedBuiltInRule} builds it), and completes the app + * through the REAL {@link WindowUserInterfaceControlF#completeBuiltInRuleApp} dispatch: + * the {@link FunctionalOperationContractCompletionF} opens the modal + * {@link ContractConfiguratorF} ({@code showAndWait} blocks the FX thread in its nested + * event loop) while a background driver finds the dialog via the {@code lastInstance} seam, + * selects a contract and presses OK. The completed app is applied the way the GUI applies + * it ({@code ProofControl.applyInteractive}) and the proof must grow (the contract + * application splits the goal into child nodes). + */ + private static void verifyContractApplyE2E(WindowUserInterfaceControlF ui, + StringBuilder problems) { + String example = System.getProperty("key.fx.verify.dialogs.contract", + "heap/model_methods/CellTest_callSet.key"); + Path path = Path.of(example); + if (!path.isAbsolute()) { + path = Path.of(System.getProperty("key.examples.dir", "key.ui/examples"), example); + } + if (!Files.isRegularFile(path)) { + problems.append("contract e2e: example not found: ").append(path).append("; "); + return; + } + KeYEnvironment env = null; + try { + env = KeYEnvironment.load(path, null, null, null, null); + Proof proof = env.getLoadedProof(); + if (proof == null) { + problems.append("contract e2e: no proof loaded from ").append(path).append("; "); + return; + } + Services services = proof.getServices(); + + // The operation-contract PO inlines the callee's body inside a try/catch, so the + // rule is not applicable on the initial goal. With METHOD_NONE the strategy unwinds + // the assumption implication and the try/catch and gets stuck exactly at the active + // method call — the state the interactive GUI offers "Use Operation Contract" from. + StrategyProperties strategyProperties = + (StrategyProperties) proof.getSettings().getStrategySettings() + .getActiveStrategyProperties().clone(); + strategyProperties.setProperty(StrategyProperties.METHOD_OPTIONS_KEY, + StrategyProperties.METHOD_NONE); + Profile profile = services.getProfile(); + Name strategyName = proof.getSettings().getStrategySettings().getStrategy(); + StrategyFactory strategyFactory = profile.supportsStrategyFactory(strategyName) + ? profile.getStrategyFactory(strategyName) + : profile.getDefaultStrategyFactory(); + proof.setActiveStrategy(strategyFactory.create(proof, strategyProperties)); + env.getProofControl().startAndWaitForAutoMode(proof); + + // an open goal with a succedent top-level position the contract rule applies to + // (the positions the rule itself offers: UseOperationContractRule.isApplicable — + // top level, succedent, instantiation succeeds, contracts available) + Goal goal = null; + PosInOccurrence pio = null; + Instantiation inst = null; + outer: for (Goal open : proof.openGoals()) { + Sequent sequent = open.sequent(); + for (int i = 0; i < sequent.succedent().size(); i++) { + SequentFormula sf = sequent.succedent().get(i); + Instantiation candidate = UseOperationContractRule + .computeInstantiation((JTerm) sf.formula(), services); + if (candidate != null) { + goal = open; + pio = new PosInOccurrence(sf, PosInTerm.getTopLevel(), false); + inst = candidate; + break outer; + } + } + } + if (goal == null) { + problems.append("contract e2e: no operation-contract position on any open goal") + .append(" (").append(proof.openGoals().size()).append(" goal(s)) of ") + .append(path.getFileName()).append("; "); + return; + } + final Goal e2eGoal = goal; + ImmutableSet contracts = + UseOperationContractRule.getApplicableContracts(inst, services); + if (contracts.isEmpty()) { + problems.append("contract e2e: no applicable contracts for ") + .append(inst.pm().getFullName()).append("; "); + return; + } + FunctionalOperationContract chosen = contracts.iterator().next(); + + // complete the app through the REAL dispatch; the completion shows the dialog + // modally on the FX thread and the background driver selects + confirms it + ContractRuleApp app = UseOperationContractRule.INSTANCE.createApp(pio); + AtomicReference completed = new AtomicReference<>(); + Thread driver = new Thread(() -> { + // only the instance the dispatch is about to construct (the steps 3a/3b + // dialogs above also pass through lastInstance, but their stages are closed) + ContractConfiguratorF previous = ContractConfiguratorF.lastInstance(); + long deadline = System.currentTimeMillis() + 20_000; + while (System.currentTimeMillis() < deadline) { + ContractConfiguratorF dialog = ContractConfiguratorF.lastInstance(); + if (dialog != null && dialog != previous && dialog.getStage().isShowing()) { + Platform.runLater(() -> { + dialog.getContractPanel().selectContract(chosen); + dialog.requestOk(); + }); + return; + } + try { + Thread.sleep(25); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return; + } + } + LOGGER.warn("Contract e2e: dialog driver timed out"); + }, "fx-verify-dialogs-contract-driver"); + driver.setDaemon(true); + driver.start(); + runOnFx(() -> completed.set(ui.completeBuiltInRuleApp(app, e2eGoal, false))); + try { + driver.join(25_000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + IBuiltInRuleApp result = completed.get(); + if (!(result instanceof ContractRuleApp completedApp)) { + problems.append("contract e2e: dispatch did not return a ContractRuleApp (") + .append(result).append("); "); + return; + } + if (!completedApp.complete()) { + problems.append("contract e2e: returned app not complete; "); + } + if (completedApp.getInstantiation() != chosen) { + problems.append("contract e2e: the selected contract was not applied to the app; "); + } + + // apply the completed app the way the GUI applies it (the contract application + // splits the goal into several child nodes — assert the proof grew) + var proofControl = env.getProofControl(); + int before = proof.countNodes(); + ContractRuleApp toApply = completedApp; + runOnFx(() -> proofControl.applyInteractive(toApply, e2eGoal)); + int after = proof.countNodes(); + if (after <= before) { + problems.append("contract e2e: applyInteractive did not grow the proof (") + .append(before).append(" -> ").append(after).append("); "); + } + LOGGER.info("Contract e2e: applied {} on {} (nodes {} -> {})", + chosen.getDisplayName(), inst.pm().getFullName(), before, after); + } catch (Throwable e) { + LOGGER.warn("Contract e2e: failed", e); + problems.append("contract e2e: ").append(e).append("; "); + } finally { + if (env != null) { + env.dispose(); + } + } + } + /** Polls a condition with a timeout (the FX runLater queue keeps the modal loop alive). */ private static boolean waitUntil(java.util.function.BooleanSupplier condition, String what, StringBuilder problems) { From 7308a3ce6d50d87cb6568c24cc9b6a7f39018e73 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Sun, 11 Oct 2026 21:34:01 +0200 Subject: [PATCH 211/281] =?UTF-8?q?WP-9:=20PARITY-SIGNOFF=20ledger=20?= =?UTF-8?q?=E2=80=94=20the=20four=20deferred=20integration-wiring=20assert?= =?UTF-8?q?s=20(AutoSave=20real=20flow,=20Minimize-Interaction=20load=20pi?= =?UTF-8?q?peline,=20EnsureSourceConsistency=20load=20pipeline/source=20vi?= =?UTF-8?q?ew,=20interactive=20contract=20E2E)=20documented=20as=20RESOLVE?= =?UTF-8?q?D=20with=20headless=20evidence=20logs;=20P2b=20'not=20headless-?= =?UTF-8?q?drivable'=20contract-E2E=20paragraph=20flipped=20to=20RESOLVED?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- key.ui.fx/PARITY-SIGNOFF.md | 55 +++++++++++++++++++++++++++++++++---- 1 file changed, 50 insertions(+), 5 deletions(-) diff --git a/key.ui.fx/PARITY-SIGNOFF.md b/key.ui.fx/PARITY-SIGNOFF.md index 3f5d848226f..752b243f0dc 100644 --- a/key.ui.fx/PARITY-SIGNOFF.md +++ b/key.ui.fx/PARITY-SIGNOFF.md @@ -200,11 +200,15 @@ completions (`FunctionalOperationContractCompletionF`, `DependencyContractComple `LoadUserTacletsDialogF` axiom panel/warning/help, `EnsureSourceConsistency` info dialog) are ported; `key.fx.verify.dialogs` PASS (registry + dialog skeletons) on Xvnc :99. -Interactive end-to-end contract application (completing a use-operation-contract rule app on a -spec-bearing method) is not headless-drivable without a JML-annotated example in -`key.ui/examples`; it stays covered by the skeletons above plus the to-be-added example in a -later batch. HTML-styled contract cells of the Swing configurator are plain text in the FX port -(KNOWN-SIMPLIFIED, see the class javadocs). +Interactive end-to-end contract application is RESOLVED (WP-9, `weigl/ocfx-wp9-integration`): +the sixth leg of `key.fx.verify.dialogs` loads a real spec-bearing example off the main window +(`heap/model_methods/CellTest_callSet.key`, override `key.fx.verify.dialogs.contract`), builds a +`UseOperationContractRule` app on a succedent top-level goal position, completes it through the +REAL `WindowUserInterfaceControlF.completeBuiltInRuleApp` dispatch (the modal +`ContractConfiguratorF` is driven by a background watcher via the new `lastInstance` seam: +select contract + OK) and applies the completed app via `ProofControl.applyInteractive`, +growing the proof by exactly one node. HTML-styled contract cells of the Swing configurator +remain plain text in the FX port (KNOWN-SIMPLIFIED, see the class javadocs). P3a (`weigl/ocfx-p3a-prooftree`, merged 2026-10-10) resolved C19/C20/C21/C22/C23/C25/C27 — `ProofTreeViewF`: the per-proof view-state cache (WeakHashMap, saved and @@ -308,3 +312,44 @@ setProgress / taskProgress → bar, macro end → hide). `key.fx.verify.uicontro `key.fx.verify.menuparity` PASS (10 seam steps incl. the six P3c seams, menu counts File 16 / Proof 24 / Options 7 / View 12 / About 5) and `key.fx.verify.prooftree` + `key.fx.demo.autoprove` PASS on Xvnc :98. A10 stays open for P4 (dialog triage remainder). + +WP-9 (`weigl/ocfx-wp9-integration`) closed the four deferred integration-wiring asserts — each +one a headless self test that drives the REAL wiring end to end instead of a seam stub: + +1. **AutoSave real flow** — `key.fx.verify.autosave` now arms the saver through the real + `applyAutoSave` + `KeYMediatorF` selection hand-off on the loaded demo proof and runs a + capped auto-mode application (interval 25, max 1000 steps) on a worker thread; the assert is + that the `AutoSaver` actually writes the intermediate `.autosave.N.key` files through the + `taskProgress` → `fireTaskProgress` chain and the final `.autosave.proof` — the writes are + isolated via `java.io.tmpdir` and the armed period/max steps are restored. PASS + (`/tmp/opencode/wp9/autosave.log`: "Auto save verification (real flow): PASS (6 save(s) + under /tmp/opencode/wp9/tmp …)"). +2. **Minimize-Interaction load pipeline** — `key.fx.verify.minimizeinteraction` proves the + load-success hand-off honors the persisted flag on the loaded demo: armed, an + auto-instantiable taclet app applies through the minimize branch of + `AbstractProofControl.selectedTaclet` without the taclet dialog (the applied node is pruned + back); disarmed, the same app opens the taclet dialog. The flag is restored. PASS + (`/tmp/opencode/wp9/minimize.log`: "Minimize interaction verification: PASS"). +3. **EnsureSourceConsistency load pipeline** — new `key.fx.verify.sourceconsistency` hook + (proof-independent): loads the JML example (`heap/model_methods/CellTest_test2.key`, + override `key.fx.demo.sourceconsistency`) twice — with the toggle armed (`DiskFileRepo`, + `AbstractProblemLoader.createFileRepo:400-409`) and disarmed (`SimpleFileRepo`) — and reads + the source through the repo stream the source view uses (`SourceViewF`'s + `readRepoFile` path, Swing `SourceView.addFile:617-621`), asserting content identical to + disk and the repo cache under the temp dir. The flag is restored. PASS + (`/tmp/opencode/wp9/sourceconsistency.log`: "Source consistency verification: PASS (armed + repo=DiskFileRepo disarmed repo=SimpleFileRepo; source read via repo=Cell.java (25 lines); + cache dir=…)"). +4. **Contract E2E** — the sixth leg of `key.fx.verify.dialogs` loads a real spec-bearing + example (`heap/model_methods/CellTest_callSet.key`, override + `key.fx.verify.dialogs.contract`) off the main window via `KeYEnvironment.load`; the PO + inlines the callee in a try/catch, so the harness first parks the strategy at the active + method call (METHOD_NONE strategy rebuild + one auto-mode run), builds a + `UseOperationContractRule` app and completes it through the REAL + `WindowUserInterfaceControlF.completeBuiltInRuleApp` dispatch — the modal + `ContractConfiguratorF` (opened by `FunctionalOperationContractCompletionF.complete`) is + driven by a background watcher via the new `lastInstance` seam (select contract + OK) — + then applies the completed app via `ProofControl.applyInteractive` (the proof grows: the + contract application splits the goal into child nodes). PASS + (`/tmp/opencode/wp9/dialogs-e2e.log`: "Contract e2e: applied JML operation contract 0 on + set (nodes 21 -> 25)" + "Dialogs verification: PASS: … contract apply E2E"). From 0660ab8fe919584876d1529233b3e3dbe65be42e Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Sun, 11 Oct 2026 21:34:13 +0200 Subject: [PATCH 212/281] =?UTF-8?q?WP-3:=20the=20sequent=20printer=20now?= =?UTF-8?q?=20honors=20the=20ViewSettings=20pretty/unicode/hide-prefix=20f?= =?UTF-8?q?lags=20=E2=80=94=20the=20initial=20printer=20was=20built=20from?= =?UTF-8?q?=20a=20bare=20new=20NotationInfo()=20(plain=20syntax=20regardle?= =?UTF-8?q?ss=20of=20the=20settings),=20Swing=20prints=20through=20the=20m?= =?UTF-8?q?ediator's=20ViewSettings-refreshed=20NotationInfo;=20shared=20c?= =?UTF-8?q?reatePrinter=20helper=20also=20used=20by=20refreshPrettyView?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../key/gui/fx/nodeviews/SequentViewF.java | 32 +++++++++++++------ 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentViewF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentViewF.java index 5304a60c186..21e672f4252 100644 --- a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentViewF.java +++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/nodeviews/SequentViewF.java @@ -67,6 +67,7 @@ import de.uka.ilkd.key.gui.fx.extension.KeYGuiExtensionFacadeF; import de.uka.ilkd.key.gui.fx.fonticons.IconFactoryF; import de.uka.ilkd.key.gui.fx.keyshortcuts.KeyStrokeManagerF; +import de.uka.ilkd.key.java.Services; import de.uka.ilkd.key.logic.label.TermLabel; import de.uka.ilkd.key.macros.ProofMacro; import de.uka.ilkd.key.pp.HideSequentPrintFilter; @@ -184,8 +185,7 @@ public boolean contains(Name name) { public void setVisibleTermLabels(VisibleTermLabels labels) { visibleTermLabels = labels != null ? labels : NO_VISIBLE_TERM_LABELS; if (printer != null && selectedNode != null) { - printer = SequentViewLogicPrinter.positionPrinter(new NotationInfo(), - selectedNode.proof().getServices(), visibleTermLabels); + printer = createPrinter(selectedNode.proof().getServices()); if (filter instanceof SearchSequentPrintFilter searchFilter) { searchFilter.setLogicPrinter(printer); } @@ -193,6 +193,20 @@ public void setVisibleTermLabels(VisibleTermLabels labels) { } } + /** + * WP-3 (menuparity): creates the position printer with a {@link NotationInfo} refreshed from + * the persisted {@link ViewSettings} (Swing parity: the Swing views print through + * {@code mainWindow.getMediator().getNotationInfo()}, which is refreshed from the ViewSettings + * — a bare {@code new NotationInfo()} would print plain syntax regardless of the settings). + */ + private SequentViewLogicPrinter createPrinter(Services services) { + NotationInfo notationInfo = new NotationInfo(); + ViewSettings viewSettings = ProofIndependentSettings.DEFAULT_INSTANCE.getViewSettings(); + notationInfo.refresh(services, viewSettings.isUsePretty(), viewSettings.isUseUnicode(), + viewSettings.isHidePackagePrefix()); + return SequentViewLogicPrinter.positionPrinter(notationInfo, services, visibleTermLabels); + } + /** * The printed sequent string of the current display, for the term-label self test * ({@code key.fx.verify.lemmaorigin}). @@ -687,8 +701,11 @@ public void display(Node node) { proof = node.proof(); // TODO(M2c): share the NotationInfo and the term label visibility with the mediator // lemmaorigin: use the installed label visibility (Swing SequentView printer setup) - printer = SequentViewLogicPrinter.positionPrinter(new NotationInfo(), - node.proof().getServices(), visibleTermLabels); + // WP-3 (menuparity): the printer carries the ViewSettings pretty/unicode/hide-prefix + // flags (Swing SequentView uses mainWindow.getMediator().getNotationInfo(), which + // MainWindow refreshes from the ViewSettings) — a bare NotationInfo would default to + // plain syntax regardless of the settings + printer = createPrinter(node.proof().getServices()); if (filter instanceof SearchSequentPrintFilter searchFilter) { // the search filters print single formulas through the view's printer (Swing // SequentViewSearchBar.search refreshes it the same way) @@ -724,12 +741,7 @@ public void refreshPrettyView() { if (printer == null || selectedNode == null) { return; } - ViewSettings viewSettings = ProofIndependentSettings.DEFAULT_INSTANCE.getViewSettings(); - NotationInfo notationInfo = new NotationInfo(); - notationInfo.refresh(selectedNode.proof().getServices(), viewSettings.isUsePretty(), - viewSettings.isUseUnicode(), viewSettings.isHidePackagePrefix()); - printer = SequentViewLogicPrinter.positionPrinter(notationInfo, - selectedNode.proof().getServices(), visibleTermLabels); + printer = createPrinter(selectedNode.proof().getServices()); if (filter instanceof SearchSequentPrintFilter searchFilter) { // the search filters print single formulas through the view's printer (Swing // SequentViewSearchBar.search refreshes it the same way) From 285b06a935de39ae0283d9a771b34b2f471bd056 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Sun, 11 Oct 2026 21:34:13 +0200 Subject: [PATCH 213/281] =?UTF-8?q?WP-3:=20menuparity=20PrettyUnicode=20le?= =?UTF-8?q?g=20=E2=80=94=20state-aware=20visible-effect=20probe=20(unicode?= =?UTF-8?q?=20toggled=20to=20the=20opposite=20state=20and=20back;=20the=20?= =?UTF-8?q?demo=20sequent=20has=20no=20arithmetic=20so=20pretty/default=20?= =?UTF-8?q?notation=20is=20identical=20=E2=80=94=20Swing=20parity),=20fixe?= =?UTF-8?q?d=20the=20inverted=20unicode-enablement=20check?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../de/uka/ilkd/key/gui/fx/MainWindowF.java | 37 +++++++++++-------- 1 file changed, 21 insertions(+), 16 deletions(-) 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 index 19869613dff..932998a927f 100644 --- 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 @@ -4148,9 +4148,8 @@ private String verifyPrettyUnicode() { if (viewSettings.isUsePretty() || NotationInfo.DEFAULT_PRETTY_SYNTAX) { problems.add("pretty=off not mirrored"); } - String textPrettyOff = sequentView.printedText(); - if (text0 == null || textPrettyOff == null || text0.equals(textPrettyOff)) { - problems.add("pretty=off no visible effect"); + if (sequentView.printedText() == null) { + problems.add("pretty=off no reprint"); } // the Unicode item is disabled without pretty printing (Swing setEnabled(usePretty)) if (!unicode.isDisable()) { @@ -4162,24 +4161,30 @@ private String verifyPrettyUnicode() { if (!viewSettings.isUsePretty() || !NotationInfo.DEFAULT_PRETTY_SYNTAX) { problems.add("pretty=on not mirrored"); } - if (!unicode.isDisable()) { - problems.add("unicode still disabled with pretty"); + if (unicode.isDisable()) { + problems.add("unicode disabled with pretty"); } - // unicode off/on with pretty printing enabled - String textUnicodeOn = sequentView.printedText(); - unicode.setSelected(false); + // 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() || NotationInfo.DEFAULT_UNICODE_ENABLED) { - problems.add("unicode=off not mirrored"); + if (viewSettings.isUseUnicode() == originalUnicode) { + problems.add("unicode toggle not mirrored"); } - String textUnicodeOff = sequentView.printedText(); - unicode.setSelected(true); + String textOpposite = sequentView.printedText(); + unicode.setSelected(!unicode.isSelected()); unicode.fire(); - if (!viewSettings.isUseUnicode()) { - problems.add("unicode=on not mirrored"); + if (viewSettings.isUseUnicode() != originalUnicode) { + problems.add("unicode back not mirrored"); } - boolean unicodeEffect = textUnicodeOn != null && textUnicodeOff != null - && !textUnicodeOn.equals(textUnicodeOff); + 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 From fabd5247dba2857f9c744a9e2c19c4f988354f02 Mon Sep 17 00:00:00 2001 From: Alexander Weigl Date: Sun, 11 Oct 2026 21:38:26 +0200 Subject: [PATCH 214/281] WP-7 C9: TestgenVerifyF harness (key.fx.verify.testgen) with reflective dispatch --- key.ui.fx/build.gradle | 8 +- .../de/uka/ilkd/key/gui/fx/MainWindowF.java | 36 ++ .../key/gui/testgen/fx/TestgenVerifyF.java | 479 ++++++++++++++++++ 3 files changed, 522 insertions(+), 1 deletion(-) create mode 100644 keyext.ui.testgen.fx/src/main/java/de/uka/ilkd/key/gui/testgen/fx/TestgenVerifyF.java diff --git a/key.ui.fx/build.gradle b/key.ui.fx/build.gradle index 7ae56b6f38e..dd1168d6bec 100644 --- a/key.ui.fx/build.gradle +++ b/key.ui.fx/build.gradle @@ -157,6 +157,12 @@ run { // 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) + // * key.fx.verify.testgen = any (WP-7: run the TestGen extension self test — the extension + // capability surface (toolbar buttons + Startup hook + "Test Case Generation" menu with + // the macro accelerator), the options panel round trips (Settings mode + live mode), the + // idle render of the two run dialogs (with the embedded live options panel), the CE model + // tree of a stub SMT model in the InformationWindowF and the cooperative stop; no real + // test generation or counterexample run — combine with key.fx.verify.smt for the real z3) ["key.fx.driver", "key.fx.theme", "key.fx.demo.sequent", "key.fx.verify.sequent", "key.fx.show", "key.fx.demo.autoprove", "key.fx.verify.tree", "key.fx.demo.autoprove.live", "key.fx.demo.maxsteps", "key.fx.verify.goallist", @@ -173,7 +179,7 @@ run { "key.fx.verify.loadingexit", "key.fx.verify.shortcuts", "key.fx.verify.inputfreeze", "key.fx.verify.sequentmenu", "key.fx.verify.dialogs", "key.fx.verify.prooftree", "key.fx.verify.selectionhistory", "key.fx.verify.isabelle", - "key.fx.verify.exploration", "key.fx.verify.caching"] + "key.fx.verify.exploration", "key.fx.verify.caching", "key.fx.verify.testgen"] .each { name -> def value = System.getProperty(name) if (value != null) { 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 index 4ad0b7c78fb..f47f6dc86b9 100644 --- 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 @@ -1122,6 +1122,19 @@ protected KeYEnvironment call() throws Exception { report.endsWith("PASS") ? Kind.INFO : Kind.ERROR); statusRight.setText(report); } + // 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) { @@ -2503,6 +2516,29 @@ private static String runCachingVerification(javafx.stage.Window owner, Proof pr } } + /** + * 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 diff --git a/keyext.ui.testgen.fx/src/main/java/de/uka/ilkd/key/gui/testgen/fx/TestgenVerifyF.java b/keyext.ui.testgen.fx/src/main/java/de/uka/ilkd/key/gui/testgen/fx/TestgenVerifyF.java new file mode 100644 index 00000000000..b8c3ceb0322 --- /dev/null +++ b/keyext.ui.testgen.fx/src/main/java/de/uka/ilkd/key/gui/testgen/fx/TestgenVerifyF.java @@ -0,0 +1,479 @@ +/* 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.testgen.fx; + +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import javafx.scene.Node; +import javafx.scene.Parent; +import javafx.scene.control.Button; +import javafx.scene.control.CheckBox; +import javafx.scene.control.DialogPane; +import javafx.scene.control.Menu; +import javafx.scene.control.MenuItem; +import javafx.scene.control.Spinner; +import javafx.scene.control.Tab; +import javafx.scene.control.TabPane; +import javafx.scene.control.TextArea; +import javafx.scene.control.TreeItem; +import javafx.scene.layout.BorderPane; +import javafx.stage.Stage; +import javafx.stage.Window; + +import de.uka.ilkd.key.core.fx.KeYMediatorF; +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.keyshortcuts.KeyStrokeManagerF; +import de.uka.ilkd.key.gui.fx.smt.CETreeF; +import de.uka.ilkd.key.gui.fx.smt.InformationWindowF; +import de.uka.ilkd.key.proof.Proof; +import de.uka.ilkd.key.settings.DefaultSMTSettings; +import de.uka.ilkd.key.settings.NewSMTTranslationSettings; +import de.uka.ilkd.key.settings.ProofDependentSMTSettings; +import de.uka.ilkd.key.settings.ProofIndependentSMTSettings; +import de.uka.ilkd.key.smt.SMTSolver; +import de.uka.ilkd.key.smt.SolverLauncher; +import de.uka.ilkd.key.smt.communication.AbstractSolverSocket; +import de.uka.ilkd.key.smt.model.Heap; +import de.uka.ilkd.key.smt.model.Location; +import de.uka.ilkd.key.smt.model.LocationSet; +import de.uka.ilkd.key.smt.model.Model; +import de.uka.ilkd.key.smt.model.ObjectVal; +import de.uka.ilkd.key.smt.model.Sequence; +import de.uka.ilkd.key.smt.ProblemTypeInformation; +import de.uka.ilkd.key.smt.ModelExtractor; +import de.uka.ilkd.key.smt.SMTObjTranslator; +import de.uka.ilkd.key.smt.solvertypes.SolverTypes; +import de.uka.ilkd.key.smt.lang.SMTSort; + +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Headless verify harness for the TestGen extension parity work ({@code key.fx.verify.testgen}, + * forwarded by key.ui.fx/build.gradle and invoked reflectively by {@code MainWindowF}): a self + * test of the WP-7 testgen pieces, all stub-based (no real test-case generation or counterexample + * run — the real SMT end to end run is the {@code key.fx.verify.smt} hook): + *
          + *
        1. the extension capability surface (C1/C2): {@code ToolbarF}/{@code StartupF} implemented, the + * two toolbar buttons ("Generate Testcases...", "Search for Counterexample") build and the + * generate button is enabled for the loaded proof, the "Test Case Generation" menu carries the + * two items with the {@code KeyStrokeManagerF} accelerator of the {@code TestGenMacro},
        2. + *
        3. the options panel (C5): the Settings-dialog mode ({@code live=false}) renders and round + * trips through {@code readFromSettings/applySettings}, the live mode ({@code live=true}) writes + * an edited spinner value into the global {@link TestGenerationSettingsF} immediately,
        4. + *
        5. the run dialogs (C3/C4): both {@code TestGenResultsDialogF} (with the embedded live options + * panel on its right edge) and {@code CounterExampleResultsDialogF} render in their idle mode + * (Start enabled, Stop disabled, empty log),
        6. + *
        7. the counterexample model tree (C6): a stub SMT {@link Model} renders in {@link CETreeF} + * with the Constants/Heaps/Location Sets/Sequences rows, and the {@link InformationWindowF} + * retitles to "Counterexample ..." for the stub Z3 CE solver and adds the Counterexample and Help + * tabs next to the information tabs,
        8. + *
        9. the cooperative stop (C3/C4): {@code TestGenerationTaskF.requestStop()} sets the stop flag + * and {@code TestgenReflectionF.stopLauncher} stops a (never-launched) {@code SolverLauncher} + * without an exception.
        10. + *
        + *

        + * {@code MainWindowF} is deliberately not named in this module (the class-file completion quirk + * of {@link TestgenSettingsProviderF}); the main window instance for the reflective toolbar/menu + * dispatch is obtained via reflection. The harness must be called on the JavaFX thread after a + * demo proof has been loaded. + */ +@NullMarked +public final class TestgenVerifyF { + + private static final Logger LOGGER = LoggerFactory.getLogger(TestgenVerifyF.class); + + private TestgenVerifyF() { + } + + /** + * Runs the TestGen extension self test. + * + * @param owner the owner window for the dialogs + * @param proof the loaded demo proof + * @param mediator the application mediator + * @return the one-line report ending in {@code PASS} or {@code FAIL} + */ + public static String run(Window owner, @Nullable Proof proof, KeYMediatorF mediator) { + if (proof == null) { + return "TestGen verify: SKIP (no proof loaded)"; + } + Stage stage = owner instanceof Stage s ? s : null; + TestGenResultsDialogF generateDialog = null; + CounterExampleResultsDialogF ceDialog = null; + int oldMaxUnwinds = new TestGenerationSettingsF().maxUnwinds(); + try { + TestgenExtensionF extension = findExtension(); + check(extension != null, "testgen extension registered via the service loader"); + if (extension == null) { + return "TestGen verify: FAIL (extension not registered)"; + } + + // (1) extension capability surface (C1/C2) — the typed capability asserts like the + // unit test, the control trees through the reflective window dispatch (MainWindowF + // is not nameable in this module) + check(extension instanceof KeYGuiExtensionF.ToolbarF, "ToolbarF implemented (WP-7)"); + check(extension instanceof KeYGuiExtensionF.StartupF, "StartupF implemented (WP-7)"); + check(extension instanceof KeYGuiExtensionF.MainMenuF, "MainMenuF implemented"); + check(extension instanceof KeYGuiExtensionF.SettingsF, "SettingsF implemented"); + Class mainWindowClass = Class.forName("de.uka.ilkd.key.gui.fx.MainWindowF"); + Object mainWindow = mainWindowClass.getMethod("getInstance").invoke(null); + Method toolbarMethod = + extension.getClass().getMethod("getToolbarControls", mainWindowClass, + KeYMediatorF.class); + List toolbar = (List) toolbarMethod.invoke(extension, mainWindow, mediator); + check(toolbar.size() == 2, "the toolbar carries the two action buttons"); + Button generateButton = (Button) toolbar.get(0); + Button counterExampleButton = (Button) toolbar.get(1); + check("Generate Testcases...".equals(generateButton.getText()), + "generate button carries the Swing TestGenerationAction name"); + check("Search for Counterexample".equals(counterExampleButton.getText()), + "counterexample button carries the Swing CounterExampleAction name"); + check(!TestgenReflectionF.z3CeInstalled() || isAutoMode(mediator) + || !generateButton.isDisabled(), + "generate button enabled for the loaded proof when z3 is installed " + + "and no run is active"); + Method menusMethod = + extension.getClass().getMethod("getMenus", mainWindowClass, KeYMediatorF.class); + List menus = (List) menusMethod.invoke(extension, mainWindow, mediator); + check(menus.size() == 1 && menus.get(0) instanceof Menu menu + && "Test Case Generation".equals(menu.getText()) + && menu.getItems().size() == 2, + "the 'Test Case Generation' main menu carries the two Swing actions"); + if (menus.size() == 1 && menus.get(0) instanceof Menu menu) { + MenuItem generateItem = menu.getItems().get(0); + check( + generateItem.getAccelerator() != null && generateItem.getAccelerator() + .equals(KeyStrokeManagerF.getInstance() + .binding(TESTGEN_MACRO_ACTION_ID).orElse(null)), + "the macro default shortcut (WP-7 C2) drives the generate item accelerator"); + } + + // (2) options panel (C5): the Settings-dialog mode and the live embedded mode + TestgenOptionsPanelF settingsMode = new TestgenOptionsPanelF(false); + check(settingsMode.getNode() != null, "the settings-mode options panel renders"); + settingsMode.readFromSettings(); + settingsMode.applySettings(); + TestgenOptionsPanelF liveMode = new TestgenOptionsPanelF(true); + List spinners = new ArrayList<>(); + collect(Spinner.class, liveMode.getNode(), spinners); + check(spinners.size() == 2, "the live options panel has the two number spinners"); + // the maximal-unwinds spinner starts at 3 (the concurrent-process spinner at 1); + // editing it must write into the global settings immediately (the FX change) + Spinner unwinds = spinners.stream() + .filter(sp -> ((Number) sp.getValue()).intValue() == 3).findFirst().orElse(null); + check(unwinds != null, "the maximal-unwinds spinner is present"); + if (unwinds != null) { + @SuppressWarnings("unchecked") + Spinner unwindsSpinner = (Spinner) unwinds; + unwindsSpinner.getValueFactory().setValue(oldMaxUnwinds + 5); + check(new TestGenerationSettingsF().maxUnwinds() == oldMaxUnwinds + 5, + "the live options panel writes the edit into the global settings"); + } + + // (3) run dialogs (C3/C4): idle render modes; the generate dialog embeds the live + // options panel on its right edge (C5) + generateDialog = new TestGenResultsDialogF(null, stage); + generateDialog.show(); + check(generateDialog.isShowing(), "the generation run dialog renders"); + checkRunDialogIdle(generateDialog.getDialogPane(), "generation run dialog"); + BorderPane generateRoot = (BorderPane) generateDialog.getDialogPane().getContent(); + List embeddedOptions = new ArrayList<>(); + collect(CheckBox.class, generateRoot.getRight(), embeddedOptions); + check(embeddedOptions.size() == 8, + "the embedded live options panel (8 toggles) sits on the right edge (C5)"); + generateDialog.close(); + check(!generateDialog.isShowing(), "the generation run dialog closes"); + + ceDialog = new CounterExampleResultsDialogF(null, stage); + ceDialog.show(); + check(ceDialog.isShowing(), "the counterexample run dialog renders"); + checkRunDialogIdle(ceDialog.getDialogPane(), "counterexample run dialog"); + ceDialog.close(); + + // (4) counterexample model tree (C6): a stub SMT model renders in CETreeF + CETreeF ceTree = new CETreeF(stubModel()); + check(ceTree.getTreeComponent().getRoot() != null + && "Model".equals(ceTree.getTreeComponent().getRoot().getValue()), + "the model tree root is 'Model'"); + check(rootChildLabels(ceTree).equals(List.of("Constants", "Heaps", "Location Sets", + "Sequences")), "the model tree has the four model categories"); + TreeItem heaps = rootChild(ceTree, 1); + check(heaps != null && heaps.getChildren().size() == 1 + && "#o1".equals(heaps.getChildren().get(0).getValue()), + "the heap lists the stub object #o1"); + if (heaps != null && !heaps.getChildren().isEmpty()) { + List objectRows = labels(heaps.getChildren().get(0)); + check(objectRows.contains("Type=java.lang.Object") + && objectRows.contains("Length=0") && objectRows.contains("Fields"), + "the object row carries the general properties and the fields node"); + TreeItem fields = child(heaps.getChildren().get(0), "Fields"); + check(fields != null && labels(fields).contains("x=#o0"), + "the fields node lists the stub field x=#o0"); + } + TreeItem locsets = rootChild(ceTree, 2); + check(locsets != null && locsets.getChildren().size() == 1 + && "#l1".equals(locsets.getChildren().get(0).getValue()) + && labels(locsets.getChildren().get(0)).contains("(#o1, x)"), + "the location-set row lists the stub location"); + TreeItem sequences = rootChild(ceTree, 3); + check(sequences != null && sequences.getChildren().size() == 1 + && "#s1".equals(sequences.getChildren().get(0).getValue()) + && labels(sequences.getChildren().get(0)).contains("Length=2") + && labels(sequences.getChildren().get(0)).contains("[0]=true"), + "the sequence row lists the length and the values"); + + // and the information window (C6): for the stub Z3 CE solver with a query model the + // window is retitled "Counterexample ..." and gains the model + help tabs + InformationWindowF noSolver = + new InformationWindowF(owner, null, "Information window", + List.of(new InformationWindowF.Information("Solver Input", "text", "z3"))); + check("Information window".equals(noSolver.getTitle()), + "without a solver the window title is unchanged"); + check(!tabTitles(noSolver).contains("Counterexample"), + "without a solver no counterexample tabs are added"); + noSolver.close(); + + InformationWindowF ceWindow = + new InformationWindowF(owner, stubSolver(stubModelExtractor()), "Information " + + "window", + List.of(new InformationWindowF.Information("Solver Input", "text", "z3"))); + check(ceWindow.getTitle().startsWith("Counterexample"), + "the Z3 CE window is retitled 'Counterexample ...' (WP-7 C6)"); + List titles = tabTitles(ceWindow); + check(titles.contains("Counterexample") && titles.contains("Help") + && titles.contains("Solver Input"), + "the counterexample, help and information tabs are present: " + titles); + ceWindow.close(); + + // (5) cooperative stop (C3/C4): the stop flag of the generation task and the + // launcher stop (the Swing stopSMTLauncher/interruptionPerformed parity) + Object ui = TestgenReflectionF.uiOf(mainWindow); + TestGenerationTaskF task = + new TestGenerationTaskF(mediator, ui, proof, new RecordingSink()); + task.requestStop(); + check(task.isStopRequested(), "requestStop sets the cooperative stop flag"); + SolverLauncher launcher = new SolverLauncher(new DefaultSMTSettings( + ProofDependentSMTSettings.getDefaultSettingsData(), + ProofIndependentSMTSettings.getDefaultSettingsData(), + new NewSMTTranslationSettings(), proof)); + TestgenReflectionF.stopLauncher(launcher); + LOGGER.info("TestGen verify: capability surface, options panel, run dialogs, CE " + + "model tree and cooperative stop OK (no real generation run)"); + return "TestGen verify: extension wiring + options panel + run dialogs + CE model " + + "tree + cooperative stop without a real generation run PASS"; + } catch (RuntimeException | AssertionError | ReflectiveOperationException e) { + LOGGER.error("TestGen verify failed", e); + return "TestGen verify: FAIL (" + e + ")"; + } finally { + if (generateDialog != null) { + generateDialog.close(); + } + if (ceDialog != null) { + ceDialog.close(); + } + new TestGenerationSettingsF().setMaxUnwinds(oldMaxUnwinds); + } + } + + /** action id of the TestGenMacro (the {@code KeyStrokeManagerF} default, WP-7 C2). */ + private static final String TESTGEN_MACRO_ACTION_ID = + "de.uka.ilkd.key.testgen.macros.TestGenMacro"; + + /** whether the mediator reports a running auto mode (the buttons are then disabled). */ + private static boolean isAutoMode(KeYMediatorF mediator) { + return TestgenReflectionF.autoModePropertyOf(mediator) + instanceof javafx.beans.property.ReadOnlyBooleanProperty p && p.get(); + } + + private static TestgenExtensionF findExtension() { + return KeYGuiExtensionFacadeF.getExtensions().stream() + .filter(TestgenExtensionF.class::isInstance) + .map(TestgenExtensionF.class::cast).findFirst().orElse(null); + } + + /** The idle-button/log assertions shared by the two run dialogs (C3/C4). */ + private static void checkRunDialogIdle(DialogPane pane, String what) { + List

    ~KcOyL3jp3{Bc2s&aXTlkXCOj<*7(>dq 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