// Copyright (c) 2023 Tobias Briones. All rights reserved.
// SPDX-License-Identifier: BSD-3-Clause
// This file is part of https://github.com/tobiasbriones/blog
package engineer.mathsoftware.blog.slides.ui;
import engineer.mathsoftware.blog.slides.Enums;
import engineer.mathsoftware.blog.slides.Palette;
import engineer.mathsoftware.blog.slides.ShapeItem;
import engineer.mathsoftware.blog.slides.data.ImageItem;
import engineer.mathsoftware.blog.slides.drawing.ShapeRenderer;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.Group;
import javafx.scene.control.Button;
import javafx.scene.control.ComboBox;
import javafx.scene.control.ScrollPane;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseButton;
import javafx.scene.input.ScrollEvent;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Line;
import javafx.scene.shape.Rectangle;
import java.util.*;
class SlideDrawingController {
private final Deque<ShapeRenderer> shapes;
private final ChangeState changes;
private final ObjectProperty<ShapeItem> shapeProperty;
private final ObjectProperty<Palette> paletteProperty;
private final AIController aiController;
private final AIInvalidation aiInvalidation;
private final AutoSave autoSave;
private final BooleanProperty autoSaveProperty;
private Group group;
private ScrollPane scrollPane;
private boolean shiftPressed;
SlideDrawingController() {
shapes = new LinkedList<>();
changes = new ChangeState();
paletteProperty = new SimpleObjectProperty<>();
shapeProperty = new SimpleObjectProperty<>();
aiController = new AIController();
aiInvalidation = new AIInvalidation(this::loadAI);
autoSave = new AutoSave();
autoSaveProperty = new SimpleBooleanProperty();
group = null;
scrollPane = null;
shiftPressed = false;
}
void setStatus(BackgroundStatus newStatus) {
aiController.setStatus(newStatus);
autoSave.setStatus(newStatus);
}
void setDrawing(Group newDrawing) {
if (group != null) {
unbindEvents();
}
group = newDrawing;
autoSave.setDrawing(group);
aiInvalidation.slideChanged();
clearState();
bindEvents();
}
BooleanProperty autoSaveProperty() {
return autoSaveProperty;
}
void onSlideChanged(ImageItem newItem) {
autoSave.onSlideChanged(newItem);
}
void onDrawingChanged(ImageItem item) {
changes.setCurrentItem(item);
loadState();
autoSave.onDrawingChanged(item);
}
void setScrollPane(ScrollPane newScrollPane) {
scrollPane = newScrollPane;
}
void setShapeComboBox(ComboBox<ShapeItem> shapeComboBox) {
shapeComboBox.getItems().addAll(ShapeItem.values());
shapeComboBox.setConverter(new Enums.EnglishConverter<>(ShapeItem.class));
shapeComboBox.getSelectionModel().select(0);
shapeProperty.bind(shapeComboBox.valueProperty());
}
void setShapePaletteComboBox(ComboBox<Palette> shapePaletteComboBox) {
shapePaletteComboBox.getItems().addAll(Palette.values());
shapePaletteComboBox.setConverter(new Enums.EnglishConverter<>(Palette.class));
shapePaletteComboBox.getSelectionModel().select(0);
paletteProperty.bind(shapePaletteComboBox.valueProperty());
}
void setShapeBackButton(Button shapeBackButton) {
var icBack = new Image(
Objects.requireNonNull(
getClass().getResourceAsStream("/ic_back.png")
)
);
var iv = new ImageView(icBack);
iv.setFitWidth(18.0);
iv.setFitHeight(18.0);
shapeBackButton.setGraphic(iv);
shapeBackButton.setOnAction(event -> popShape());
}
void saveCurrentSlide() {
autoSave.saveSlide();
}
private void bindEvents() {
group.setOnMouseMoved(
event -> aiController.onMouseMoved(event.getX(), event.getY())
);
group.setOnMouseExited(event -> aiController.onMouseExited());
group.setOnMousePressed(event -> {
if (event.getButton() == MouseButton.SECONDARY) {
var startX = event.getX();
var startY = event.getY();
var shape = pushShape();
shape.start(startX, startY);
scrollPane.setPannable(false);
}
else {
aiController
.onMouseClicked()
.ifPresent(line -> {
var shape = pushShape();
clearAiLineRow();
shape.start(line.getStartX(), line.getStartY());
shape.end(line.getEndX(), line.getEndY());
shape.setStrokeWidth(2.0);
shape.render();
saveState();
});
}
});
group.setOnMouseDragged(event -> {
if (event.getButton() != MouseButton.SECONDARY) {
return;
}
if (shapes.peek() == null) {
return;
}
var shape = shapes.peek();
var currentX = event.getX();
var currentY = event.getY();
var normX = normalizeX(currentX);
var normY = normalizeY(currentY);
shape.keepProportions(shiftPressed);
shape.end(normX, normY);
shape.render();
});
group.setOnMouseReleased(event -> {
saveState();
scrollPane.setPannable(true);
});
group.setOnScroll((ScrollEvent event) -> {
var zoomFactor = 1.05;
var deltaY = event.getDeltaY();
if (deltaY < 0.0) {
zoomFactor = 2.0 - zoomFactor;
}
group.setScaleX(group.getScaleX() * zoomFactor);
group.setScaleY(group.getScaleY() * zoomFactor);
});
group.sceneProperty().addListener((observable, oldValue, newValue) -> {
if (newValue == null) {
return;
}
newValue.setOnKeyPressed(event -> {
switch (event.getCode()) {
case SHIFT -> shiftPressed = true;
case F1 -> onF1Pressed();
}
});
newValue.setOnKeyReleased(event -> {
switch (event.getCode()) {
case SHIFT -> shiftPressed = false;
case F1 -> aiController.onHideTextBoxes();
}
});
});
autoSaveProperty.addListener((observable, oldValue, newValue) ->
autoSave.enable(newValue)
);
}
private void clearAiLineRow() {
aiController
.getFocusLinesInRow()
.forEach(line -> shapes
.stream()
.filter(shape ->
shape.getStartX() == line.getStartX()
&& shape.getEndX() == line.getEndX()
&& shape.getStartY() == line.getStartY()
&& shape.getEndY() == line.getEndY()
)
.findFirst()
.ifPresent(shape -> {
shape.remove();
shapes.remove(shape);
}));
}
private void unbindEvents() {
group.setOnMousePressed(null);
}
private void saveState() {
changes.saveCopy(shapes);
}
private void loadState() {
clearState();
changes
.get()
.ifPresent(state ->
state
.shapes()
.forEach(this::restoreShape)
);
}
private void restoreShape(ShapeRenderer shape) {
shape.setGroup(group);
shape.render();
shapes.addLast(shape);
}
private void clearState() {
shapes.clear();
}
private ShapeRenderer pushShape() {
var shape = switch (shapeProperty.get()) {
case Line -> new Line();
case Rectangle -> new Rectangle();
case Circle -> new Circle();
};
var renderer = new ShapeRenderer(shape, paletteProperty.get());
renderer.setGroup(group);
shapes.push(renderer);
return renderer;
}
private void popShape() {
if (shapes.isEmpty()) {
return;
}
var shape = shapes.pop();
shape.remove();
saveState();
}
private void onF1Pressed() {
aiInvalidation.validate();
aiController.onShowTextBoxes();
}
private void loadAI() {
aiController.init(group);
}
private double normalizeX(double x) {
var bounds = group.getBoundsInLocal();
return Math.max(0.0, Math.min(bounds.getWidth() - 1.0, x));
}
private double normalizeY(double y) {
var bounds = group.getBoundsInLocal();
return Math.max(0.0, Math.min(bounds.getHeight() - 1.0, y));
}
private static class ChangeState {
record SlideDrawingState(
Collection<ShapeRenderer> shapes
) {}
final Map<ImageItem, SlideDrawingState> changes;
ImageItem currentItem;
ChangeState() {
changes = new HashMap<>();
currentItem = null;
}
void setCurrentItem(ImageItem newCurrentItem) {
currentItem = newCurrentItem;
}
Optional<SlideDrawingState> get() {
return Optional.ofNullable(changes.get(currentItem));
}
void saveCopy(
Deque<ShapeRenderer> shapes
) {
if (currentItem == null) {
return;
}
var shapesCopy = new ArrayList<>(shapes);
var state = new SlideDrawingState(shapesCopy);
changes.put(currentItem, state);
}
}
private static class AIInvalidation {
final Runnable validator;
boolean isInvalid;
AIInvalidation(Runnable validator) {
this.validator = validator;
this.isInvalid = true;
}
void validate() {
if (isInvalid) {
validator.run();
}
isInvalid = false;
}
void slideChanged() {
isInvalid = true;
}
}
}