FX TextField Disabled Node Events - java

I am well aware of the Java FX Node API which states:
A disabled Node does not receive mouse or key events.
So, I am trying to come up with a work around. In our old Swing application we used to allow users to double click on a disabled web/email field (Formatted TextField) to open a link to the page or their native mail client. I am hoping to simulate this behavior in FX. Instead of calling:
setDisable(true);
I am now calling:
setEditable(false);
The only remaining issue is that I would like to style the Node as if it were disabled, or at minimum disable text selection.
Is there a simple way to get the exact style of the Node when it is disabled, or will I need to create my own CSS class? (Unfortunately my CSS knowledge is relatively weak).

I can't see an "easy" way to do this with CSS, without replicating the default rules for a disabled text field (but maybe someone else has a trick for that).
Here's a completely different approach, though. When the text field is disabled, it doesn't receive mouse events, so any mouse events will just "drop through" to the node below it in Z-order. So if you wrap the text field in some pane, and register a mouse handler with the pane, that mouse handler will get invoked if the text field is disabled.
SSCCE:
import javafx.application.Application;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.CheckBox;
import javafx.scene.control.TextField;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class DIsabledTextFieldEventTest extends Application {
#Override
public void start(Stage primaryStage) {
TextField textField = new TextField();
StackPane textFieldHolder = new StackPane(textField);
CheckBox disableTextField = new CheckBox("Disable text field");
textField.disableProperty().bind(disableTextField.selectedProperty());
textFieldHolder.setOnMouseClicked(e -> {
if (e.getClickCount() == 2) {
System.out.println("Double click on disabled text field!");
}
});
VBox root = new VBox(10, disableTextField, textFieldHolder);
root.setAlignment(Pos.CENTER);
Scene scene = new Scene(root, 350, 120);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}

Is there a simple way to get the exact style of the Node when it is disabled, or will I need to create my own CSS class?
If you do the below to your non-editable text field, it looks exactly like a disabled text field (tested on my MacBook).
textField.setStyle("-fx-opacity:0.5");

Related

How to modify JavaX Button (if decorated with CSS) so it gets darker when needed?

Modify the color of a JavaFX button so it gets darker when I pass the mouse, but when it is decorated with CSS:
When you modify the color of a Button in JavaFX, you lose the effect that makes it darker when the mouse passes. So seeing that we get 2 functions to do some action when the mouse gets over the button setOnMouseEntered/setOnMouseExited I can just get the color and use the function .darker()like this:
private Color last_color = null;
public void change_color(Button button) {
Color color;
if (last_color == null) {
color = (Color)button.getBackground().getFills().get(0).getFill();
last_color = color;
color = color.darker();
} else {
color = last_color;
last_color = null;
}
button.setBackground(new Background(new BackgroundFill(color, CornerRadii.EMPTY, Insets.EMPTY)));
}
This actually works, except when the color of the buttons is in CSS, looks like the CSS style is beyong the background color set with Java code.
The buttons are probably going to be styled with CSS, and I would like to have a generic function to work in any Button. Is possible to get the color from CSS and modify it?
If not, how could I make the Button darker when the mouse is over it?
To change the color of the button on hover, you can add the corresponding css in the CSS file.
.myButton:hover{
-fx-color: derive(-fx-base, -45%);
}
But if you say, you need to do some calculations, or other logic to determine the color, then you see the below approach.The below approach is to give you some idea only. THIS IS NOT A SOLUTION. I always recommend to get things done using CSS.
The key point to note is : the background of a button is made up of many fills. You need to know which fill you need to modify.
import javafx.application.Application;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import java.util.ArrayList;
import java.util.List;
public class ButtonBackgroundDemo extends Application {
public static void main(String[] args) {
launch(args);
}
BackgroundFill lastFill;
#Override
public void start(Stage primaryStage) {
Button button = new Button("Api Test");
button.addEventFilter(MouseEvent.MOUSE_ENTERED, e->changeColor(button, e));
button.addEventFilter(MouseEvent.MOUSE_EXITED, e->changeColor(button, e));
Button button2 = new Button("Css Test");
button2.getStyleClass().add("myButton");
VBox root = new VBox(button,button2);
root.setAlignment(Pos.CENTER);
root.setSpacing(20);
Scene scene = new Scene(root, 200, 200);
scene.getStylesheets().add(getClass().getResource("button.css").toExternalForm());
primaryStage.setScene(scene);
primaryStage.show();
}
/**
* I really think this is an overkill. Instead, use CSS implementation
*/
private void changeColor(Button button, MouseEvent e) {
int fillSize = button.getBackground().getFills().size();
List<BackgroundFill> fills = new ArrayList<>();
button.getBackground().getFills().forEach(fills::add);
if(e.getEventType() == MouseEvent.MOUSE_ENTERED){
lastFill = fills.get(fillSize-1);
fills.remove(lastFill);
// Use your logic to darken the lastFill.getFill(). This may be Linear-Gradient as well. so get the correct color from gradient as well.
BackgroundFill fill = new BackgroundFill(Color.DARKGREY, lastFill.getRadii(),lastFill.getInsets());
fills.add(fill);
}else {
fills.remove(fillSize-1);
fills.add(lastFill);
}
button.setBackground(new Background(fills,null));
}
}
And in the button.css:
.myButton:hover{
-fx-color: derive(-fx-base, -45%);
}

Access a JavaFX TextArea within Tab returned from a TabPane

I would like to access a text area placed within a Tab. All the Tabs in the TabPane will have a TextArea. Unfortunately Tab is not an interface and there does not seem a way to change the type held inside of the TabPane so I can not see a way to make code know that there is going to be a TextArea inside of the generic tab without keeping a separate list of them somewhere outside. TabPane can only return a Tab and tab does not distinguish that it holds a TextArea and even if I make an extension of Tab and give it to the TabPane it will still only return a Tab. Keeping an outside list seems really hacky and I dont think that would ever be the intended design. So what am I missing here. I am not using FXML.
You could create your own extension of a Tab whose content is a TextArea and provide a getTextArea() method for it.
That way, you can know each Tab will have a TextArea and you can manipulate it however you would like.
Below is a simple application to demonstrate:
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
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.layout.VBox;
import javafx.stage.Stage;
public class Main extends Application {
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) {
// Simple UI
VBox root = new VBox(10);
root.setAlignment(Pos.CENTER);
root.setPadding(new Insets(10));
// Create the TabPane
TabPane tabPane = new TabPane();
// Create some TextAreaTabs to put into the TabPane
tabPane.getTabs().addAll(
new TextAreaTab("Tab 1", "This is tab 1!"),
new TextAreaTab("Tab 2", "This is tab 2!"),
new TextAreaTab("Tab 3", "This is tab 3!")
);
// A button to get the text from each tab
Button button = new Button("Print Text");
// Set the action for the button to loop through all the tabs in the TabPane and print the contents of its TextArea
button.setOnAction(e -> {
for (Tab tab : tabPane.getTabs()) {
// You'll need to cast the Tab to a TextAreaTab in order to access the getTextArea() method
System.out.println(tab.getText() + ": " +
((TextAreaTab) tab).getTextArea().getText());
}
});
// Add the TabPane and button to the root layout
root.getChildren().addAll(tabPane, button);
// Show the stage
primaryStage.setScene(new Scene(root));
primaryStage.setWidth(300);
primaryStage.setHeight(300);
primaryStage.show();
}
}
// This is the custom Tab that allows you to set its content to a TextArea
class TextAreaTab extends Tab {
private TextArea textArea = new TextArea();
public TextAreaTab(String tabTitle, String textAreaContent) {
// Create the tab with the title provided
super(tabTitle);
// Set the Tab to be unclosable
setClosable(false);
// Set the text for the TextArea
textArea.setText(textAreaContent);
// Set the TextArea as the content of this tab
setContent(textArea);
}
public TextArea getTextArea() {
return textArea;
}
}
A simpler option would be to simply cast the getContent() method for each Tab to a TextArea within your loop:
((Text Area) tab.getContent()).getText()
I find the first option to be more readable and flexible as you can configure all your tabs to be identical within one location.

Trying to select programatically in JavaFX is not working

I am in the process of teaching myself JavaFX. Coming from the Swing world there are a lot of similarities between the 2. Especially event processing. Part of my process is to try and mimic an existing application as closely as possible. One of the things I am doing is creating a dialog that will allow the user to select a font to use. There is a text field for them to type in the font name and a list where they can scroll and select one. When they start typing the list will automatically scroll to through the list to start matching what the user is typing. I am also trying to populate the text field with the currently matched font name and then highlight the portion that the user has not typed yet so they can continue to type until the correct match is found.
For example if the user types the letter 't' on Windows the first font found is Tahoma. So the text field will be set to Tahoma and the carat will be positioned right after the 'T' and the 'ahoma' will be highlighted. What happens instead is that the field is populated with Tahoma and the carat is positioned at the end and nothing is highlighted. So it is like it is ignoring the 2 lines of code for positioning and highlighting or the event processor is causing my calls to JavaFX libraries to be run out of order.
I think this may be a bug with JavaFX but it could also be my misunderstanding of the event system. Please let me know which one and why.
Here is a complete sample code showing the problem. Just start typing in the text field to try it out.
package test;
import javafx.application.Application;
import javafx.beans.value.ChangeListener;
import javafx.scene.Scene;
import javafx.scene.control.TextField;
import javafx.scene.layout.BorderPane;
import javafx.scene.text.Font;
import javafx.stage.Stage;
public class TestTyping extends Application {
ChangeListener<String> textChange;
#Override
public void start(Stage primaryStage) throws Exception {
BorderPane root = new BorderPane();
TextField text = new TextField();
root.setTop(text);
Scene scene = new Scene(root);
primaryStage.setScene(scene);
primaryStage.show();
textChange = (observable, oldValue, newValue) -> {
text.textProperty().removeListener(textChange);
for (String family : Font.getFamilies()) {
if (family.equalsIgnoreCase(newValue) || family.toLowerCase().startsWith(newValue.toLowerCase())) {
text.setText(family);
text.positionCaret(newValue.length());
text.selectEnd();
break;
}
}
text.textProperty().addListener(textChange);
};
text.textProperty().addListener(textChange);
}
public static void main(String... args) {
launch(args);
}
}
Wrap caret position and select end into Platform.runLater. The problem is in events order. I don't know correct details about this issue so I will not provide you a detailed answer, only solution.
Platform.runLater(()-> {
text.positionCaret(newValue.length());
text.selectEnd();
});
Here's an alternative approach entirely, which uses a TextFormatter to modify changes to the text. The advantage here is that it doesn't rely on the "timing" of various property changes with respect to event handling, which is not documented and thus could possibly change in later JavaFX versions. It also avoids the slightly ugly "remove the listener and add it back" idiom.
import java.util.function.UnaryOperator;
import javafx.application.Application;
import javafx.beans.value.ChangeListener;
import javafx.scene.Scene;
import javafx.scene.control.TextField;
import javafx.scene.control.TextFormatter;
import javafx.scene.control.TextFormatter.Change;
import javafx.scene.layout.BorderPane;
import javafx.scene.text.Font;
import javafx.stage.Stage;
public class TestTyping extends Application {
ChangeListener<String> textChange;
#Override
public void start(Stage primaryStage) throws Exception {
BorderPane root = new BorderPane();
TextField text = new TextField();
root.setTop(text);
Scene scene = new Scene(root);
primaryStage.setScene(scene);
primaryStage.show();
UnaryOperator<Change> filter = c -> {
// for delete, move the caret, or change selection, don't modify anything...
if (c.getText().isEmpty()) {
return c ;
}
for (String family : Font.getFamilies()) {
if (family.toLowerCase().startsWith(c.getControlNewText().toLowerCase())) {
c.setText(family.substring(c.getRangeStart(), family.length()));
c.setAnchor(c.getControlNewText().length());
break ;
}
}
return c ;
};
text.setTextFormatter(new TextFormatter<String>(filter));
}
public static void main(String... args) {
launch(args);
}
}

Why does disabled JavaFX TextArea has a different color than TextField

I am restyling a JavaFX application, but I have a problem with the :disabled styles. When I try to change the -fx-text-fill and -fx-opacity settings, textareas still get a slightly lighter text color than textfields. This is the style I got right now:
/*** Text Fields and Areas ***/
.text-field,
.text-area {
-fx-text-fill: #000;
-fx-opacity: 1.0;
}
.text-field:disabled,
.text-area:disabled {
-fx-text-fill: #000;
-fx-opacity: 0.5;
}
This is how the disabled components look in the program:
Screenshot from the JavaFX application
As you can see, the text color of the TextField is #7a7a7a which is 50% of #000. The TextArea however appear to have the color #c7c7c7 which is 25% of #000.
Does anyone know how I can get the same disabled color for textareas as for textfields?
What's going on
IMO the current behaviour is a bug and should be filed at http://bugreport.java.com (I have done this, unfortunately the Java bug reporting system does not provide any way to track this bug report unless it is accepted by the JavaFX team).
The issue is that the opacity modifier for the text in the text area is applied twice. The default TextArea skin is implemented as a TextArea control node, with an internal ScrollPane in it and, when the TextArea is disabled, the opacity of both is set to 0.4, so the text (and scroll bars in the scroll pane) have the disability opacity fading effect applied twice (which is wrong). You can see this by inspecting a disabled TextArea control in ScenicView.
Workaround
Explicitly set the disabled scroll-pane's opacity to 1 when it is contained within a text-input control.
.text-input > .scroll-pane:disabled {
-fx-opacity: 1;
}
Sample app:
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import org.scenicview.ScenicView;
public class DisabilityAssistance extends Application {
#Override
public void start(Stage stage) throws Exception{
TextArea area = new TextArea("Text Area");
area.setDisable(true);
TextField field = new TextField("Text Field");
field.setDisable(true);
Scene scene = new Scene(new VBox(10, area, field));
stage.setScene(scene);
stage.show();
scene.getStylesheets().add(getClass().getResource(
"disability.css"
).toURI().toURL().toExternalForm());
ScenicView.show(stage.getScene());
}
public static void main(String[] args) {
launch(args);
}
}
Output (without CSS work-around applied):
Output (with CSS work-around applied):

JavaFX ScrollBar setOnMousePressed not working

really liking JavaFX but have come across this problem and wondered if it was a bug.
The ScrollBar.setOnMousePressed() doesn't seem to fire when it has been initialised with a handler. The code below demonstrates the problem:-
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ScrollBar;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class Play extends Application {
public static void main(String[] args) {
launch(args);
}
private static int cnt;
#Override
public void start(Stage primaryStage) {
primaryStage.setTitle("Bug?");
Button btn = new Button("This text will get replaced by the event handlers");
ScrollBar scrollBar = new ScrollBar();
// When pressing and releasing the ScrollBar thumb, we only get decrements
// If you replace the ScrollBar with say a Button, then the code below works as you might expect.
scrollBar.setOnMousePressed( event -> btn.setText("X" + cnt++));
scrollBar.setOnMouseReleased( event -> btn.setText("X" + cnt--));
VBox root = new VBox();
root.getChildren().add(btn);
root.getChildren().add(scrollBar);
primaryStage.setScene(new Scene(root, 350, 250));
primaryStage.show();
}
}
Note, Im running on JDK 1.8.0_66 64 Bit on Microsoft Windows 10.
A simple workaround, as suggested by James_D, is to use EventFilters instead of setOnMousePressed(), as follows:-
So,
scrollBar.addEventFilter(MouseEvent.MOUSE_PRESSED,
event -> btn.setText("X" + cnt++));
instead of
scrollBar.setOnMousePressed( event -> btn.setText("X" + cnt++));
I believe .setOnMousePressed() should work, but doesn't because of a bug in the library. I've raised with oracle and will update the answer once oracle clarifies.

Categories