79284047

Date: 2024-12-16 08:36:37
Score: 2.5
Natty:
Report link

As @user5182503 (Pavel K. ?) observed, in JavaFX 9+, access to the package containing the required Property Bundle is disallowed.

However, there is a new URL Scheme jrt: to read Content from the Runtime.

Here is an answer using that new functionality.

It was written and tested under Windows 11 Pro with the Zulu JDK FX 17 runtime from Azul Systems Inc. and is based on the answer submitted by @Silvio Barbieri.

Hope you like it:

package com.stackoverflow.q71053358;

import static javafx.scene.control.ScrollPane.ScrollBarPolicy.AS_NEEDED;

import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.time.ZonedDateTime;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.PropertyResourceBundle;
import java.util.ResourceBundle;
import java.util.StringJoiner;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import javafx.application.Application;
import javafx.application.Platform;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.ChoiceDialog;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.Control;
import javafx.scene.control.MenuItem;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.SeparatorMenuItem;
import javafx.scene.control.TextArea;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.stage.Stage;

/**
 * Example for
 * <a href="https://stackoverflow.com/questions/71053358/">Stackoverflow Question 71053358</a>
 * <br><br>
 * Tested with Zulu JavaFX JDK 17.
 * <br><br>
 * Demonstrates use of the <code>jrt:</code> URL Scheme to access
 * Properties in Packages that in recent JDK's are not accessible.
 */
public class EmulateDefaultContextMenu extends Application {

    private static final class JrtURL {

        private static final String JAVA_RUNTIME_SCHEME = "jrt:";

        private        final URL    url;

        public JrtURL(final String module, final String package_, final String member) throws MalformedURLException {
            this.url = new URL(new StringJoiner("/")
                    .add(JAVA_RUNTIME_SCHEME)
                    .add(module)
                    .add(package_)
                    .add(member)
                    .toString());
        }

        public InputStream openStream() throws IOException {
            return this.url.openStream();
        }
    }

    private static final class Key {

        public final String key;

        public Key(final String... keyParts) {
            this.key = Stream.of(keyParts).collect(Collectors.joining());
        }
        public String lookupString(final ResourceBundle bundle) {
            return bundle.getString(this.key);
        }
    }

    public  static enum Ability {
        ENABLED,
        DISABLED;

        public boolean isEnabled()  {return this == ENABLED;}
        public boolean isDisabled() {return this == DISABLED;}
    }

    private static enum LogSeverity {
        ERROR,  // <- High Severity
        WARN,
        INFO,
        DEBUG,
        TRACE;  // <- Low Severity
    }

    private static final String   TEXT_AREA_MODULE     = "javafx.controls";
    private static final String   TEXT_AREA_PKG        = "com/sun/javafx/scene/control/skin/resources";
    private static final String   TEXT_AREA_PROPS      = "controls.properties";
    private static final String   TEXT_AREA_PROPS_DE   = "controls_de.properties";

    private static final String   TEXT_AREA_MENU       = "TextInputControl.menu.";

    private static final Key      TEXT_AREA_UNDO       = new Key(TEXT_AREA_MENU, "Undo");
    private static final Key      TEXT_AREA_REDO       = new Key(TEXT_AREA_MENU, "Redo");
    private static final Key      TEXT_AREA_CUT        = new Key(TEXT_AREA_MENU, "Cut");
    private static final Key      TEXT_AREA_COPY       = new Key(TEXT_AREA_MENU, "Copy");
    private static final Key      TEXT_AREA_PASTE      = new Key(TEXT_AREA_MENU, "Paste");
    private static final Key      TEXT_AREA_DELETE     = new Key(TEXT_AREA_MENU, "DeleteSelection");
    private static final Key      TEXT_AREA_SELECT_ALL = new Key(TEXT_AREA_MENU, "SelectAll");

    private        final TextArea logTextArea          = new TextArea();

    @Override
    public void start(final Stage primaryStage) throws Exception {
        /*
         * Set up Logging ScrollPane...
         */
        final var logScrollPane = new ScrollPane(logTextArea);

        logTextArea.setStyle   ("-fx-font-family: 'monospaced'");
        logTextArea.setEditable(false); // Side-effect.: CTRL-A, CTRL-C & CTRL-X are ignored

        logTextArea.addEventFilter(KeyEvent.KEY_PRESSED, e -> {

            if (e.isShortcutDown()) { // (CTRL on Win, META on Mac)

                if (e.getCode() == KeyCode.Y     // Suppress CTRL-Y
                ||  e.getCode() == KeyCode.Z) {  // Suppress CTRL-Z
                    e.consume();
                }
            }
        });

        logScrollPane.setHbarPolicy (AS_NEEDED);
        logScrollPane.setVbarPolicy (AS_NEEDED);
        logScrollPane.setFitToHeight(true);
        logScrollPane.setFitToWidth (true);

        /*
         * Generate the Context Menu...
         */
        try {
            final var jrtURL        = new JrtURL(TEXT_AREA_MODULE, TEXT_AREA_PKG, TEXT_AREA_PROPS);
            final var jrtURL_de     = new JrtURL(TEXT_AREA_MODULE, TEXT_AREA_PKG, TEXT_AREA_PROPS_DE);

            final var nullBundle    = getNullBundle();                          // Failing-all-else.: use Key as Title
            final var bundle_en     = getPropertyBundle(jrtURL,    nullBundle); // Fallback to English Titles
            final var bundle        = getPropertyBundle(jrtURL_de, bundle_en);  // German Titles, if available

            final var contextMenu   = newContextMenu(logTextArea);
            /*
             * For completeness, the following Items are ALL those that would be generated for a fully-enabled TextArea.
             * As our TextArea is not editable and CTRL-Y & CTRL-Z are ignored, some are superfluous.
             * The superfluous are assigned to a null Context Menu (i.e. none) & will therefore not appear.
             * Nevertheless, the Listeners for the full functionality are included.
             */
            final var itemUndo      = addMenuItem (null,        bundle, TEXT_AREA_UNDO,       Ability.DISABLED, e -> logTextArea.undo());
            final var itemRedo      = addMenuItem (null,        bundle, TEXT_AREA_REDO,       Ability.DISABLED, e -> logTextArea.redo());
            final var itemCut       = addMenuItem (null,        bundle, TEXT_AREA_CUT,        Ability.DISABLED, e -> logTextArea.cut());
            final var itemCopy      = addMenuItem (contextMenu, bundle, TEXT_AREA_COPY,       Ability.DISABLED, e -> logTextArea.copy());
            ;                         addMenuItem (null,        bundle, TEXT_AREA_PASTE,      Ability.ENABLED,  e -> logTextArea.paste());
            final var itemDelete    = addMenuItem (null,        bundle, TEXT_AREA_DELETE,     Ability.DISABLED, e -> deleteSelectedText());
            ;                         addSeparator(null);
            final var itemSelectAll = addMenuItem (contextMenu, bundle, TEXT_AREA_SELECT_ALL, Ability.DISABLED, e -> logTextArea.selectAll());
            ;                         addSeparator(contextMenu);
            ;                         addSeparator(contextMenu);
            ;                         addMenuItem (contextMenu,         "Change Log Level",   Ability.ENABLED,  e -> changeLogThreshold());

            logTextArea.undoableProperty() .addListener((obs, oldValue, newValue) -> itemUndo.setDisable(!newValue));
            logTextArea.redoableProperty() .addListener((obs, oldValue, newValue) -> itemRedo.setDisable(!newValue));
            logTextArea.selectionProperty().addListener((obs, oldValue, newValue) -> {
                itemCut      .setDisable(newValue.getLength() == 0);
                itemCopy     .setDisable(newValue.getLength() == 0);
                itemDelete   .setDisable(newValue.getLength() == 0);
                itemSelectAll.setDisable(newValue.getLength() == newValue.getEnd());
            });
        } catch (final IOException e) {
            e.printStackTrace();
        }

        /*
         * Set the Scene...
         */
        primaryStage.setTitle("Question 71053358");
        primaryStage.setScene(new Scene(logScrollPane, 480, 320));
        primaryStage.show();

        /*
         * Generate some Content every now-and-again...
         */
        final Runnable runnable  = () -> {
            Platform.runLater(() -> logTextArea.appendText(ZonedDateTime.now().toString() + '\n'));
        };
        Executors.newScheduledThreadPool(1).scheduleAtFixedRate(runnable, 2, 9, TimeUnit.SECONDS);
    }

    private static final PropertyResourceBundle getPropertyBundle(final JrtURL jrtURL, final ResourceBundle parentBundle) throws IOException {

        try (final var inputStream = jrtURL.openStream())
        {
            return new PropertyResourceBundle(inputStream) {
                {
                    this.setParent(parentBundle /* (may be null) */);
                }
            };
        }
    }

    private static final ResourceBundle getNullBundle() {
        return new       ResourceBundle() {
            @Override
            protected Object handleGetObject(final String key) {
                return key;
            }
            @Override
            public Enumeration<String> getKeys() {
                return Collections.emptyEnumeration();
            }
        };
    }

    private static ContextMenu newContextMenu(final Control control) {

        final ContextMenu      contextMenu = new ContextMenu();

        control.setContextMenu(contextMenu);

        return                 contextMenu;
    }

    private static MenuItem addMenuItem(final ContextMenu parent, final ResourceBundle  bundle,  final Key titleKey, final Ability ability, final EventHandler<ActionEvent> handler) {
        return              addMenuItem(                  parent, titleKey.lookupString(bundle),                                   ability,                                 handler);
    }

    private static MenuItem addMenuItem(final ContextMenu parent,                                final String title, final Ability ability, final EventHandler<ActionEvent> handler) {

        final var                 child = new MenuItem(title);
        ;                         child.setDisable (ability.isDisabled());
        ;                         child.setOnAction(handler);

        if (parent != null) {
            parent.getItems().add(child);
        }

        return                    child;
    }

    private static SeparatorMenuItem addSeparator(final ContextMenu parent) {

        final var                 child = new SeparatorMenuItem();

        if (parent != null) {
            parent.getItems().add(child);
        }

        return                    child;
    }

    private void deleteSelectedText() {

        final var range = logTextArea.getSelection();

        if (range.getLength() == 0) {
            return;
        }
        final var                 text    = logTextArea.getText();
        final var                 newText = text.substring(0, range.getStart()) + text.substring(range.getEnd());

        logTextArea.setText      (newText);
        logTextArea.positionCaret(range.getStart());
    }

    private void changeLogThreshold() {

        final var header  =
                """
                Only messages with a Severity
                greater than or equal to the Threshold
                will be logged.
                """;

        final var choices = Arrays.asList(LogSeverity.values());

        final var chooser = new ChoiceDialog<LogSeverity>(LogSeverity.INFO, choices);
        ;         chooser.setTitle      ("Log Level");
        ;         chooser.setContentText("Threshold.:");
        ;         chooser.setHeaderText (header);

        ;         chooser.showAndWait().ifPresent(choice -> logTextArea.appendText("-> " + choice + '\n'));
    }

    public static void main(final String[] args) {
        launch(args);
    }
}
Reasons:
  • Blacklisted phrase (1): stackoverflow
  • Blacklisted phrase (1): Stackoverflow
  • Long answer (-1):
  • Has code block (-0.5):
  • Contains question mark (0.5):
  • User mentioned (1): @user5182503
  • User mentioned (0): @Silvio
  • Low reputation (0.5):
Posted by: Dave The Dane