Implementing Fluent Design's reveal highlight effect in JavaFX - java

I'd like to implement the reveal highlight effect for JavaFX that can be seen in various parts of Windows 10, particularly the Settings and Calculator apps.
The effect seems to be composed for two parts, a border highlight (seen here) and a background highlight (seen here, though admittedly looks better in person due to compression).
My first instinct was to see if this could be done in some sort of pixel shader but after googling around for that it seems JavaFX does provide a public API for anything like that?
Would it be possible to create this effect without resorting to a canvas and drawing the whole UI by hand?

First I'd like to say I have no idea how Windows implements that style. But one idea I had is to have multiple layers:
A black background.
A circle with a radial gradient going from white to transparent that moves with the mouse.
A region with a black background and a shape that has holes wherever the option nodes are.
The option nodes with a layered background.
When the mouse is not hovering:
Transparent background with no insets.
Black background with a slight inset.
When the mouse is hovering:
Low opacity white background with no insets.
Black background with a slight inset.
White-to-transparent radial gradient background that's centered on the mouse.
Unfortunately that means a lot of the styling has to be done in the code even though I'd prefer to put most of it in CSS. Here's a proof-of-concept I quickly mocked up. It's not fully functional but shows the look you want is possible.
OptionsPane.java
import javafx.beans.InvalidationListener;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Bounds;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.geometry.VPos;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.TilePane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.paint.RadialGradient;
import javafx.scene.paint.Stop;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Rectangle;
import javafx.scene.shape.Shape;
import javafx.scene.text.Font;
public class OptionsPane extends Region {
public static class Option {
private final String title;
private final String subtitle;
private final Node graphic;
public Option(String title, String subtitle, Node graphic) {
this.title = title;
this.subtitle = subtitle;
this.graphic = graphic;
}
public String getTitle() {
return title;
}
public String getSubtitle() {
return subtitle;
}
public Node getGraphic() {
return graphic;
}
}
private final ObservableList<Option> options = FXCollections.observableArrayList();
private final TilePane topTiles = new TilePane();
private final Region midCover = new Region();
private final Circle underGlow = new Circle();
public OptionsPane() {
setBackground(new Background(new BackgroundFill(Color.BLACK, null, null)));
underGlow.setManaged(false);
underGlow.setRadius(75);
underGlow.visibleProperty().bind(hoverProperty());
underGlow.setFill(
new RadialGradient(
0, 0,
0.5, 0.5,
1.0,
true,
null,
new Stop(0.0, Color.WHITE),
new Stop(0.35, Color.TRANSPARENT)));
addEventFilter(
MouseEvent.MOUSE_MOVED,
e -> {
underGlow.setCenterX(e.getX());
underGlow.setCenterY(e.getY());
});
midCover.setBackground(new Background(new BackgroundFill(Color.BLACK, null, null)));
topTiles.setMinSize(0, 0);
topTiles.setVgap(20);
topTiles.setHgap(20);
topTiles.setPadding(new Insets(20));
topTiles.setPrefTileWidth(250);
topTiles.setPrefTileHeight(100);
topTiles.setPrefColumns(3);
options.addListener(
(InvalidationListener)
obs -> {
topTiles.getChildren().clear();
options.forEach(opt -> topTiles.getChildren().add(createOptionRegion(opt)));
});
getChildren().addAll(underGlow, midCover, topTiles);
}
public final ObservableList<Option> getOptions() {
return options;
}
#Override
protected void layoutChildren() {
double x = getInsets().getLeft();
double y = getInsets().getTop();
double w = getWidth() - getInsets().getRight() - x;
double h = getHeight() - getInsets().getBottom() - y;
layoutInArea(midCover, x, y, w, h, -1, HPos.CENTER, VPos.CENTER);
layoutInArea(topTiles, x, y, w, h, -1, HPos.CENTER, VPos.CENTER);
Shape coverShape = new Rectangle(x, y, w, h);
for (Node optionNode : topTiles.getChildren()) {
Bounds b = optionNode.getBoundsInParent();
Rectangle rect = new Rectangle(b.getMinX(), b.getMinY(), b.getWidth(), b.getHeight());
coverShape = Shape.subtract(coverShape, rect);
}
midCover.setShape(coverShape);
}
private Region createOptionRegion(Option option) {
Label titleLabel = new Label(option.getTitle());
titleLabel.setTextFill(Color.WHITE);
titleLabel.setFont(Font.font("System", 13));
Label subtitleLabel = new Label(option.getSubtitle());
subtitleLabel.setTextFill(Color.DARKGRAY);
subtitleLabel.setFont(Font.font("System", 10));
VBox textBox = new VBox(5, titleLabel, subtitleLabel);
HBox.setHgrow(textBox, Priority.ALWAYS);
HBox container = new HBox(10, textBox);
container.setPadding(new Insets(10));
if (option.getGraphic() != null) {
container.getChildren().add(0, option.getGraphic());
}
setNonHoverBackground(container);
container
.hoverProperty()
.addListener(
(obs, ov, nv) -> {
if (!nv) {
setNonHoverBackground(container);
}
});
container.setOnMouseMoved(e -> setHoverBackground(container, e.getX(), e.getY()));
return container;
}
private void setNonHoverBackground(Region region) {
BackgroundFill fill1 = new BackgroundFill(Color.TRANSPARENT, null, null);
BackgroundFill fill2 = new BackgroundFill(Color.BLACK, null, new Insets(2));
region.setBackground(new Background(fill1, fill2));
}
private void setHoverBackground(Region region, double x, double y) {
RadialGradient gradient =
new RadialGradient(
0, 0,
x, y,
400,
false,
null,
new Stop(0.0, new Color(1, 1, 1, 0.2)),
new Stop(0.35, Color.TRANSPARENT));
BackgroundFill fill1 = new BackgroundFill(new Color(1, 1, 1, 0.3), null, null);
BackgroundFill fill2 = new BackgroundFill(Color.BLACK, null, new Insets(2));
BackgroundFill fill3 = new BackgroundFill(gradient, null, null);
region.setBackground(new Background(fill1, fill2, fill3));
}
}
Main.java
import java.util.ArrayList;
import java.util.List;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
public class Main extends Application {
#Override
public void start(Stage primaryStage) {
OptionsPane pane = new OptionsPane();
List<OptionsPane.Option> options = new ArrayList<>();
for (int i = 0; i < 10; i++) {
Rectangle graphic = new Rectangle(20, 20, Color.DARKGRAY);
options.add(
new OptionsPane.Option("Option Title #" + (i + 1), "Description #" + (i + 1), graphic));
}
pane.getOptions().addAll(options);
primaryStage.setScene(new Scene(pane));
primaryStage.show();
}
}
And this is what it looks like:
It's not exactly the same but you can experiment yourself and change things as you want.

Related

How can I create a feathered brush with JavaFX?

So I'm trying to create a very basic photo editor program in Java, using JavaFX. I got a brush and eraser working pretty well so far the following way:
package application;
import java.io.File;
import javax.imageio.ImageIO;
import javafx.application.Platform;
import javafx.embed.swing.SwingFXUtils;
import javafx.fxml.FXML;
import javafx.geometry.Point2D;
import javafx.scene.canvas.*;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.paint.Color;
import javafx.scene.shape.*;
public class EditorController {
private boolean eraser = false;
#FXML
private Canvas canvas;
#FXML
private ColorPicker colorPicker;
#FXML
private TextField brushSize;
#FXML
private TextField selectedTool;
private Point2D last = null;
public void initialize() {
GraphicsContext gc = canvas.getGraphicsContext2D();
canvas.setOnMouseReleased(e -> {last = null;});
canvas.setOnMouseClicked(e -> {
if (!eraser) {
double size = Double.parseDouble(brushSize.getText());
float mouseX = (float) e.getX();
float mouseY = (float) e.getY();
gc.fillOval(mouseX-(size/2), mouseY-(size/2), size, size);
}
});
canvas.setOnMouseDragged(e -> {
System.out.println(eraser);
double size = Double.parseDouble(brushSize.getText());
gc.setLineCap(StrokeLineCap.ROUND);
gc.setLineWidth(size);
float mouseX = (float) e.getX();
float mouseY = (float) e.getY();
if (last != null && !eraser) {
gc.strokeLine(last.getX(), last.getY(), mouseX, mouseY);
} else if (eraser) {
gc.clearRect(mouseX, mouseY, size, size);
}
last = new Point2D(mouseX, mouseY);
});
}
public void onSave() {
try {
Image snapshot = canvas.snapshot(null, null);
ImageIO.write(SwingFXUtils.fromFXImage(snapshot, null), "png", new File("paint.png"));
} catch (Exception e) {
System.out.println("Failed to save image: " + e);
}
}
public void onLoad() {
// not implemented yet
}
// not implemented yet
public void onUndo() { }
public void onRedo() { }
public void onSmaller() { }
public void onBigger() { }
public void onResetView() { }
public void onFitView() { }
public void onFillView() { }
public void onNewLayer() { }
public void onDeleteLayer() { }
public void onDuplicateLayer() { }
public void onGroupLayers() { }
public void onMergeLayers() { }
public void onAddMask() { }
public void onBrush() { eraser = false; selectedTool.setText("Brush"); }
public void onEraser() { eraser = true; selectedTool.setText("Eraser"); }
public void onExit() {
Platform.exit();
}
}
Now I want to have a feather/hardness value for the brush (like in photoshop) where I can draw a softer-looking line, but I'm not sure how to achieve it with JavaFX? Are there any tools within it for things like this?
So with a visual example: the brush on the left would be a feathered brush, the one on the right isn't (and that's what I have currently)
Simple drawing app.
It uses a radial gradient rendered to an image that is drawn on the canvas, but you could just draw the gradient directly onto the canvas. A gradient is a paint so you can set it directly as an argument to setFill on a graphics context.
The solution in my example probably won't exactly give you the solution you are looking for, but perhaps you could tweak it for what you need.
It was a quick app I put together for demo purposes, it could be structured better if a more functional drawing app was required.
The code which creates the "feathered brush" is based on a gradient:
private RadialGradient createSoftBrushGradient(Color primaryColor) {
return new RadialGradient(
0, 0,
.5, .5,
.5,
true,
CycleMethod.NO_CYCLE,
new Stop(0, primaryColor),
new Stop(1, Color.TRANSPARENT)
);
}
import javafx.application.Application;
import javafx.beans.property.*;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.SnapshotParameters;
import javafx.scene.canvas.*;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.*;
import javafx.scene.paint.*;
import javafx.scene.shape.Circle;
import javafx.stage.Stage;
public class SprayPaint extends Application {
private final IntegerProperty brushDiameter = new SimpleIntegerProperty();
private final ObjectProperty<Image> brushImage = new SimpleObjectProperty<>();
private final ToggleGroup brushHardnessSelection = new ToggleGroup();
private final RadioButton hardBrushSelection = new RadioButton();
private final RadioButton softBrushSelection = new RadioButton();
private final Circle hardBrush = new Circle();
private final Circle softBrush = new Circle();
private final SnapshotParameters snapshotParams = new SnapshotParameters();
private final Canvas canvas = new Canvas(600, 450);
#Override
public void start(Stage stage) {
snapshotParams.setFill(Color.TRANSPARENT);
Pane controls = createControls();
StackPane canvasHolder = new StackPane(canvas);
canvasHolder.setStyle("-fx-border-color: gray;");
canvasHolder.setMaxSize(Pane.USE_PREF_SIZE, Pane.USE_PREF_SIZE);
VBox layout = new VBox(
10,
controls,
canvasHolder
);
layout.setPadding(new Insets(10));
stage.setScene(new Scene(layout));
stage.show();
enableDrawing(canvas);
}
private void enableDrawing(Canvas canvas) {
EventHandler<MouseEvent> drawHandler = event -> {
Image brush = snapshotBrushImage();
GraphicsContext gc = canvas.getGraphicsContext2D();
gc.drawImage(
brush,
event.getX() - brushDiameter.doubleValue() / 2,
event.getY() - brushDiameter.doubleValue() / 2
);
};
canvas.setOnMousePressed(drawHandler);
canvas.setOnMouseDragged(drawHandler);
}
private Image snapshotBrushImage() {
if (brushImage.get() == null) {
if (hardBrushSelection == brushHardnessSelection.getSelectedToggle()) {
brushImage.set(snapshot(hardBrush));
} else { // soft brush selected
brushImage.set(snapshot(softBrush));
}
}
return brushImage.get();
}
private Image snapshot(Circle brushNode) {
return brushNode.snapshot(snapshotParams, null);
}
private Pane createControls() {
hardBrush.radiusProperty().bind(
brushDiameter.divide(2.0)
);
softBrush.radiusProperty().bind(
brushDiameter.divide(2.0)
);
hardBrushSelection.getStyleClass().addAll("toggle-button", "left-pill");
hardBrushSelection.getStyleClass().remove( "radio-button");
StackPane hardBrushGraphic = new StackPane(hardBrush);
hardBrushGraphic.setMinSize(40, 40);
hardBrushSelection.setGraphic(hardBrushGraphic);
hardBrushSelection.setToggleGroup(brushHardnessSelection);
softBrushSelection.getStyleClass().addAll( "toggle-button", "right-pill");
softBrushSelection.getStyleClass().remove( "radio-button");
StackPane softBrushGraphic = new StackPane(softBrush);
softBrushGraphic.setMinSize(40, 40);
softBrushSelection.setGraphic(softBrushGraphic);
softBrushSelection.setToggleGroup(brushHardnessSelection);
hardBrushSelection.setSelected(true);
HBox brushSelectionPanel = new HBox(hardBrushSelection, softBrushSelection);
Slider brushDiameterSlider = new Slider(8, 40, 20);
brushDiameterSlider.setMajorTickUnit(4);
brushDiameterSlider.setMinorTickCount(0);
brushDiameterSlider.setShowTickMarks(true);
brushDiameter.setValue((int) Math.round(brushDiameterSlider.getValue()));
brushDiameterSlider.valueProperty().addListener((observable, oldValue, newValue) ->
brushDiameter.setValue((int) Math.round(newValue.doubleValue()))
);
Label diameterLabel = new Label();
diameterLabel.textProperty().bind(
brushDiameter.asString()
);
ColorPicker colorPicker = new ColorPicker();
hardBrush.fillProperty().bind(
colorPicker.valueProperty()
);
colorPicker.valueProperty().addListener((observable, oldColor, newColor) ->
softBrush.setFill(
createSoftBrushGradient(newColor)
)
);
colorPicker.setValue(Color.NAVY);
brushDiameter.addListener((observable, oldValue, newValue) ->
brushImage.set(null)
);
colorPicker.valueProperty().addListener((observable, oldValue, newValue) ->
brushImage.set(null)
);
brushHardnessSelection.selectedToggleProperty().addListener((observable, oldValue, newValue) ->
brushImage.set(null)
);
Button clear = new Button("Clear");
clear.setOnAction(e ->
canvas.getGraphicsContext2D().clearRect(
0, 0, canvas.getWidth(), canvas.getHeight()
)
);
HBox controlPanel = new HBox(
10,
colorPicker,
brushSelectionPanel,
new Label("Diameter: "),
brushDiameterSlider,
diameterLabel,
clear
);
controlPanel.setMinWidth(450);
controlPanel.setMinHeight(Pane.USE_PREF_SIZE);
return controlPanel;
}
private RadialGradient createSoftBrushGradient(Color primaryColor) {
return new RadialGradient(
0, 0,
.5, .5,
.5,
true,
CycleMethod.NO_CYCLE,
new Stop(0, primaryColor),
new Stop(1, Color.TRANSPARENT)
);
}
}

Line painting in painting program JavaFX

I created a simple paint program which allows the user to choose between 4 shapes(line,circle,rectangle,ellipse) , the user can change the width height and stroke width , he can save the design he makes , undo and redo , the user can also choose the stroke type (solid or dashed) ,I'm almost done with the program but I'm facing one problem , the line is getting displayed not in the way I want it to be displayed as the photo below :
So the line here is of width 32 and height 32 , my line is getting printed in a different way.Is it correct the way I'm printing my line ?
NOTE : I'm using the line shape in the code to make the code small.
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.geometry.Insets;
import javafx.scene.Cursor;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.*;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Line;
import javafx.stage.Stage;
public class paintLine extends Application {
#Override
public void start(Stage primaryStage) {
Image image1 = new
Image("C:\\Users\\Mhamd\\Desktop\\laol\\src\\resources\\Daco_70400.png",
100, 100, false, false);
ImageView view1 = new ImageView(image1);
view1.setFitHeight(40);
view1.setPreserveRatio(true);
ToggleButton linebtn = new ToggleButton();
linebtn.setGraphic(view1);
ToggleButton[] toolsArr = {linebtn};
ToggleGroup tools = new ToggleGroup();
for (ToggleButton tool : toolsArr) {
tool.setMinWidth(50);
tool.setToggleGroup(tools);
tool.setCursor(Cursor.HAND);
}
ColorPicker cpLine = new ColorPicker(Color.BLACK);
ColorPicker cpFill = new ColorPicker(Color.TRANSPARENT);
TextField textWidth = new TextField("32");
TextField textHeight = new TextField("32");
TextField contouring = new TextField("2");
Label line_color = new Label("Line Color");
Label fill_color = new Label("Fill Color");
Label line_width = new Label("3.0");
Label imgWidth = new Label("Width");
Label imgHeight = new Label("Height");
String week_days[] =
{"Solid", "Dotted"};
ChoiceBox choiceBox = new ChoiceBox(FXCollections
.observableArrayList(week_days));
VBox btns = new VBox(10);
btns.getChildren().addAll(linebtn, imgWidth, textWidth, imgHeight,
textHeight, line_color, cpLine,
fill_color, cpFill, line_width, contouring, choiceBox);
btns.setPadding(new Insets(5));
btns.setStyle("-fx-background-color: #999");
btns.setPrefWidth(100);
Canvas canvas = new Canvas(1080, 790);
GraphicsContext gc;
gc = canvas.getGraphicsContext2D();
gc.setLineWidth(1);
Line line = new Line();
canvas.setOnMouseClicked(e -> {
if (linebtn.isSelected()) {
// double widthSize =
Double.parseDouble(textWidth.getText());
// double heightSize =
Double.parseDouble(textHeight.getText());
double strokeWidth =
Double.parseDouble(contouring.getText());
// gc.setLineWidth(strokeWidth);
gc.setStroke(cpLine.getValue());
if (choiceBox.getSelectionModel().isSelected(0)) {
gc.setLineWidth(strokeWidth);
} else if (choiceBox.getSelectionModel().isSelected(1)) {
gc.setLineWidth(strokeWidth);
gc.setLineDashes(10);
}
gc.setFill(cpFill.getValue());
line.setStartX(e.getX());
line.setStartY(e.getY());
line.setEndX(e.getX() / 2);
line.setEndY(e.getY() / 2);
gc.strokeLine(line.getStartX(), line.getStartY(),
line.getEndX(), line.getEndY());
}
});
BorderPane pane = new BorderPane();
pane.setRight(btns);
pane.setCenter(canvas);
Scene scene = new Scene(pane, 1200, 800);
primaryStage.setTitle("Paint");
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Focusing on just drawing a line interactively, you need to at least do the following:
Record the initial point at which the mouse was pressed and then
Render the line when you know where the mouse was released:
private double startX;
private double startY;
…
canvas.setOnMousePressed(e -> {
startX = e.getX();
startY = e.getY();
});
canvas.setOnMouseReleased(e -> {
gc.strokeLine(startX, startY, e.getX(), e.getY());
});
This works as shown above, but the problem then becomes how to draw the line while dragging—without damaging previous work. One solution is to add each new line to a List<Line> and render the accumulated lines with each update:
private final List<Line> lines = new ArrayList<>();
…
canvas.setOnMouseReleased(e -> {
lines.add(new Line(startX, startY, e.getX(), e.getY()));
});
canvas.setOnMouseDragged(e -> {
if (lineButton.isSelected()) {
…
gc.clearRect(0, 0, canvas.getWidth(), canvas.getHeight());
lines.forEach(l -> {
gc.strokeLine(l.getStartX(), l.getStartY(), l.getEndX(), l.getEndY());
});
gc.strokeLine(startX, startY, e.getX(), e.getY());
}
});
A more general solution, which illustrates undo() and redo(), is offered here.

How to write custom text on JavaFX 2.2 cursor?

I want a cursor that displays the coordinates of the place the mouse is pointing to, besides the pointer itself (it should look like a crosshair with little numbers is bottom right corner, that change when the mouse is moving). How to achive such effect efficiently? I tried to use the tooltip mechanism, but then the text lags behind the cursor a lot...
You could attach a label, and change the text inside the mouse moved event everytime that the mouse coordinates changes.
You can create a Text object with the current coordinates, snapshot it to an Image, and then create a WritableImage from that and any other cursor decoration you need. Then wrap the WritableImage in an ImageCursor:
import javafx.application.Application;
import javafx.event.EventHandler;
import javafx.scene.Cursor;
import javafx.scene.ImageCursor;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.scene.image.WritableImage;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.text.Text;
import javafx.stage.Stage;
public class MouseCoordinateCursor extends Application {
#Override
public void start(Stage primaryStage) {
Pane root = new Pane();
root.setOnMouseExited(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
root.setCursor(Cursor.DEFAULT);
}
});
root.setOnMouseMoved(new EventHandler<MouseEvent>() {
final int padding = 9 ;
final int offset = 6 ;
#Override
public void handle(MouseEvent e) {
Text text = new Text(String.format("[%.1f,%.1f]", e.getX(), e.getY()));
Image textImage = text.snapshot(null, null);
int width = (int)textImage.getWidth();
int height = (int)textImage.getHeight();
WritableImage cursorImage = new WritableImage(width + offset, height + offset);
cursorImage.getPixelWriter().setPixels(offset, offset, width, height, textImage.getPixelReader(), 0, 0);
for (int i = 0; i < padding; i++) {
cursorImage.getPixelWriter().setColor(i, padding/2, Color.BLACK);
cursorImage.getPixelWriter().setColor(padding/2, i, Color.BLACK);
}
root.setCursor(new ImageCursor(cursorImage));
}
});
primaryStage.setScene(new Scene(root, 600, 600));
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}

Implementing drawing areas in tabs in JavaFX

I would want to add a drawing area to my tabs in JavaFX. I am new to the field, and tried adding a Canvas to each tab I create. However, this does not seem to be working. Relevant code included.
#FXML
void fileNewTabHandler(ActionEvent event) {
++indexTab;
Tab tab = new Tab("Untitled " + indexTab);
Canvas canvas = new Canvas(500, 285);
GraphicsContext gc = canvas.getGraphicsContext2D();
gc.setFill(Color.BLUE);
gc.fillRect(250, 856, 50, 60);
tab.setContent(canvas);
tabPane.getTabs().add(tab);
}
Here tabPane and indexTab are defined and work as long as I do not use the canvas. Where am I going wrong. Should I use a different method to implement what I want to?
Quick Sample
There is no real trick to this, you just set the canvas as the content of the tab.
import javafx.application.Application;
import javafx.scene.*;
import javafx.scene.canvas.*;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import java.util.Random;
public class TabbedCanvas extends Application {
private int tabId = 0;
private double W = 200, H = 150;
private Random random = new Random(42);
#Override
public void start(Stage stage) {
TabPane tabPane = new TabPane();
Button newTabButton = new Button("New Tab");
newTabButton.setOnAction(
event -> addTab(tabPane)
);
newTabButton.fire();
ToolBar toolBar = new ToolBar(newTabButton);
toolBar.setMinHeight(ToolBar.USE_PREF_SIZE);
VBox layout = new VBox(toolBar, tabPane);
VBox.setVgrow(tabPane, Priority.ALWAYS);
stage.setScene(new Scene(layout));
stage.show();
}
private void addTab(TabPane tabPane) {
Tab tab = new Tab("Tab: " + tabId++);
tab.setContent(createTabContent());
tabPane.getTabs().add(tab);
tabPane.getSelectionModel().select(tab);
}
private Node createTabContent() {
Canvas canvas = new Canvas(W, H);
GraphicsContext gc = canvas.getGraphicsContext2D();
gc.setFill(randomColor());
gc.fillRect(0, 0, W, H);
return canvas;
}
private Color randomColor() {
return Color.rgb(
random.nextInt(256),
random.nextInt(256),
random.nextInt(256)
);
}
public static void main(String[] args) {
launch(args);
}
}

JavaFX - Clickable line on canvas

I have this JavaFX application that lets you plot locations on a map and connect them.
I do this by drawing a map as a background image on a canvas and then drawing circles and lines on it. I have made the circles clickable by using the contains() method in the Circle class, but how can I make the lines clickable?
edit: Look at this example where I just draw a line and set an event handler:
Canvas canvas = new Canvas();
GraphicsContext gc = canvas.getGraphicsContext2D();
gc.setLineWidth(5);
gc.strokeLine(100, 100, 200, 200);
canvas.setOnMouseClicked(event -> {
double x = event.getX(), y = event.getY();
});
My question is simply this: how do I finish the event handler so that it detects if the click is inside the line I just drew?
You should create a canvas and add the nodes (Circle, Line, etc) to it. Then you add mouse listeners to the nodes.
Example:
import java.util.ArrayList;
import java.util.List;
import javafx.application.Application;
import javafx.event.EventHandler;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Line;
import javafx.stage.Stage;
public class DragNodes extends Application {
public static List<Circle> circles = new ArrayList<Circle>();
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) {
Group root = new Group();
Canvas canvas = new Canvas(300, 300);
GraphicsContext gc = canvas.getGraphicsContext2D();
drawShapes(gc);
Circle circle1 = new Circle(50);
circle1.setStroke(Color.GREEN);
circle1.setFill(Color.GREEN.deriveColor(1, 1, 1, 0.7));
circle1.relocate(100, 100);
Circle circle2 = new Circle(50);
circle2.setStroke(Color.BLUE);
circle2.setFill(Color.BLUE.deriveColor(1, 1, 1, 0.7));
circle2.relocate(200, 200);
Line line = new Line(circle1.getLayoutX(), circle1.getLayoutY(), circle2.getLayoutX(), circle2.getLayoutY());
line.setStrokeWidth(20);
Pane overlay = new Pane();
overlay.getChildren().addAll(circle1, circle2, line);
MouseGestures mg = new MouseGestures();
mg.makeDraggable(circle1);
mg.makeDraggable(circle2);
mg.makeDraggable(line);
root.getChildren().addAll(canvas, overlay);
primaryStage.setScene(new Scene(root, 800, 600));
primaryStage.show();
}
private void drawShapes(GraphicsContext gc) {
gc.setStroke(Color.RED);
gc.strokeRoundRect(10, 10, 230, 230, 10, 10);
}
public static class MouseGestures {
double orgSceneX, orgSceneY;
double orgTranslateX, orgTranslateY;
public void makeDraggable(Node node) {
node.setOnMousePressed(circleOnMousePressedEventHandler);
node.setOnMouseDragged(circleOnMouseDraggedEventHandler);
}
EventHandler<MouseEvent> circleOnMousePressedEventHandler = new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent t) {
orgSceneX = t.getSceneX();
orgSceneY = t.getSceneY();
if (t.getSource() instanceof Circle) {
Circle p = ((Circle) (t.getSource()));
orgTranslateX = p.getCenterX();
orgTranslateY = p.getCenterY();
} else {
Node p = ((Node) (t.getSource()));
orgTranslateX = p.getTranslateX();
orgTranslateY = p.getTranslateY();
}
}
};
EventHandler<MouseEvent> circleOnMouseDraggedEventHandler = new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent t) {
double offsetX = t.getSceneX() - orgSceneX;
double offsetY = t.getSceneY() - orgSceneY;
double newTranslateX = orgTranslateX + offsetX;
double newTranslateY = orgTranslateY + offsetY;
if (t.getSource() instanceof Circle) {
Circle p = ((Circle) (t.getSource()));
p.setCenterX(newTranslateX);
p.setCenterY(newTranslateY);
} else {
Node p = ((Node) (t.getSource()));
p.setTranslateX(newTranslateX);
p.setTranslateY(newTranslateY);
}
}
};
}
}

Categories

Resources