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);
}
}