package ru.khasang.cachoeira.view.mainwindow.diagram.ganttplan.objectslayer.taskbar; import javafx.animation.Interpolator; import javafx.animation.KeyFrame; import javafx.animation.KeyValue; import javafx.animation.Timeline; import javafx.beans.property.DoubleProperty; import javafx.geometry.Insets; import javafx.scene.Cursor; import javafx.scene.control.Label; import javafx.scene.control.OverrunStyle; import javafx.scene.control.Tooltip; import javafx.scene.layout.Pane; import javafx.scene.paint.Color; import javafx.scene.shape.Rectangle; import javafx.util.Duration; import ru.khasang.cachoeira.commands.task.SetTaskFinishDateCommand; import ru.khasang.cachoeira.commands.task.SetTaskStartAndFinishDateCommand; import ru.khasang.cachoeira.commands.task.SetTaskStartDateCommand; import ru.khasang.cachoeira.model.IResource; import ru.khasang.cachoeira.model.ITask; import ru.khasang.cachoeira.viewcontroller.MainWindowController; import ru.khasang.cachoeira.viewcontroller.contextmenus.TaskContextMenu; import java.time.LocalDate; import java.time.temporal.ChronoUnit; public abstract class TaskBar extends Pane { private static final double TASK_HEIGHT = 18; //высота прямоугольника задачи protected static final double ROW_HEIGHT = 31; protected MainWindowController controller; protected ITask task; private IResource resource; private TaskContextMenu taskContextMenu; protected boolean wasMovedByMouse = false; protected int rowIndex; //координата Y (строка задачи) protected Rectangle backgroundRectangle; protected Rectangle donePercentRectangle; public TaskBar() { this.setPadding(new Insets(0, 0, 5, 0)); } public void initTaskRectangle(ITask task, IResource resource) { backgroundRectangle = new Rectangle(); backgroundRectangle.setFill(Color.valueOf("#03A9F4")); //цвет прямоугольника backgroundRectangle.setStroke(Color.valueOf("#03bdf4")); //цвет окантовки backgroundRectangle.setArcHeight(5); //скругление углов backgroundRectangle.setArcWidth(5); backgroundRectangle.setHeight(TASK_HEIGHT); this.setParameters(task, resource, backgroundRectangle); donePercentRectangle = new Rectangle(); donePercentRectangle.setFill(Color.valueOf("#0381f4")); donePercentRectangle.arcHeightProperty().bind(backgroundRectangle.arcHeightProperty()); donePercentRectangle.arcWidthProperty().bind(backgroundRectangle.arcWidthProperty()); donePercentRectangle.layoutYProperty().bind(backgroundRectangle.layoutYProperty().add(1.25)); donePercentRectangle.heightProperty().bind(backgroundRectangle.heightProperty().subtract(2.5)); //ширина зависит от ширины backgroundRectangle и task.donePercent donePercentRectangle.widthProperty().bind( backgroundRectangle.widthProperty().divide(100).multiply(task.donePercentProperty())); //также привязываем все ивенты от backgroundRectangle donePercentRectangle.onMousePressedProperty().bind(backgroundRectangle.onMousePressedProperty()); donePercentRectangle.onMouseDraggedProperty().bind(backgroundRectangle.onMouseDraggedProperty()); this.getChildren().add(backgroundRectangle); this.getChildren().add(donePercentRectangle); this.enableDrag(task, backgroundRectangle); this.enableResize(task, backgroundRectangle); this.setListeners(task, resource, backgroundRectangle, donePercentRectangle); } protected void setLabel(ITask task, Rectangle backgroundRectangle) { // Вешаем лэйбл с наименованием задачи Label taskLabel = new Label(task.getName()); taskLabel.setTextOverrun(OverrunStyle.CLIP); taskLabel.setTextFill(Color.WHITE); taskLabel.setLayoutY(6); taskLabel.prefWidthProperty().bind(backgroundRectangle.widthProperty()); taskLabel.onMousePressedProperty().bind(backgroundRectangle.onMousePressedProperty()); taskLabel.onMouseDraggedProperty().bind(backgroundRectangle.onMouseDraggedProperty()); this.getChildren().add(taskLabel); } protected abstract void setParameters(ITask task, IResource resource, Rectangle backgroundRectangle); protected double taskWidth(LocalDate taskStartDate, LocalDate taskFinishDate, int columnWidth) { return (ChronoUnit.DAYS.between(taskStartDate, taskFinishDate) * columnWidth); } protected abstract double taskY(int rowIndex); protected double taskX(LocalDate taskStartDate, LocalDate projectStartDate, int columnWidth) { return ((ChronoUnit.DAYS.between(projectStartDate, taskStartDate)) * columnWidth) - 1.5; } /** * Метод для анимации движения объекта * * @param target Параметр который двигаем * @param endValue Параметр который задает конечную точку движения * @param duration Время движения в миллисекундах * @return Возвращает Timeline */ protected Timeline createTimelineAnimation(DoubleProperty target, double endValue, double duration) { KeyValue endKeyValue = new KeyValue(target, endValue, Interpolator.SPLINE(0.4, 0, 0.2, 1)); KeyFrame endKeyFrame = new KeyFrame(Duration.millis(duration), endKeyValue); return new Timeline(endKeyFrame); } protected abstract void setListeners(ITask task, IResource resource, Rectangle backgroundRectangle, Rectangle donePercentRectangle); public void setContextMenu(ITask task) { taskContextMenu = new TaskContextMenu(controller.getProject(), task, controller); taskContextMenu.initMenus(); } public void setTooltip(Tooltip tooltip) { Tooltip.install(this, tooltip); } /** * Метод который включает возможность перемещения метки с помощью мышки по оси Х * * @param task Задача этой метки * @param backgroundRectangle Прямоугольник */ private void enableDrag(ITask task, Rectangle backgroundRectangle) { final Delta dragDelta = new Delta(); final OldRound oldRound = new OldRound(); backgroundRectangle.setOnMousePressed(event -> { // Выделяем нужный элемент в таблице int i = controller.getProject().getTaskList().indexOf(task); controller.getTaskTableView().getSelectionModel().select(i); if (event.isPrimaryButtonDown()) { // record a delta distance for the drag and drop operation. dragDelta.x = this.getLayoutX() - event.getSceneX(); getScene().setCursor(Cursor.MOVE); } // Условие для контекстного меню if (event.isSecondaryButtonDown()) { taskContextMenu.show(TaskBar.this, event.getScreenX(), event.getScreenY()); } }); backgroundRectangle.setOnMouseDragged(event -> { if (event.isPrimaryButtonDown()) { double newX = event.getSceneX() + dragDelta.x; if (newX > 0 && newX + backgroundRectangle.getWidth() <= this.getParent().getBoundsInParent().getWidth()) { // Хреначим привязку к сетке if (Math.round(newX / controller.getZoomMultiplier()) != oldRound.old) { oldRound.old = Math.round(newX / controller.getZoomMultiplier()); this.setLayoutX(Math.round(newX / controller.getZoomMultiplier()) * controller.getZoomMultiplier() - 1.5); wasMovedByMouse = true; // Когда начитаем двигать, то тру, чтобы не началась рекурсия controller.getCommandExecutor().execute(new SetTaskStartAndFinishDateCommand( task, controller.getProject().getStartDate().plusDays( Math.round(newX / controller.getZoomMultiplier())), Math.round(backgroundRectangle.getWidth() / controller.getZoomMultiplier()) )); wasMovedByMouse = false; // Когда окончили движение фолз } } } }); backgroundRectangle.setOnMouseReleased(event -> { if (!event.isPrimaryButtonDown()) { getScene().setCursor(Cursor.DEFAULT); } }); } /** * Метод который включает возможность изменения размера метки с помощью мышки. * <p> * Для этого создаем еще два мелких прямоугольника, привязываем их к правой и левой сторонам * того прямоугольника размер которого хотим менять. Далее прописываем драг ивенты, как в enableDrag() * к обоим прямоугольникам. Таким образом получается, что при изменении местоположения левого прямоугольника * меняется начальная координата главного прямоугольника (this.setLayout(x)) * и увеличивается ширина на пройденное расстояние (backgroundRectangle.setWidth(x - delta)). А с помощью * правого прямоугольника увеличивается, либо уменьшается ширина главного * прямоугольника (backgroundRectangle.setWidth(x)). * <p> * Проще простого. * * @param task Задача этой метки * @param backgroundRectangle Прямоугольник */ public void enableResize(ITask task, Rectangle backgroundRectangle) { // Создаем прозрачный прямоугольник шириной 10 пикселей Rectangle leftResizeHandle = new Rectangle(); this.getChildren().add(leftResizeHandle); leftResizeHandle.setFill(Color.TRANSPARENT); // leftResizeHandle.setWidth(4); leftResizeHandle.widthProperty().bind(backgroundRectangle.widthProperty().divide(100).multiply(5)); // Привязываем этот прямоугольник к левой стороне таскбара leftResizeHandle.xProperty().bind(backgroundRectangle.xProperty()); leftResizeHandle.heightProperty().bind(backgroundRectangle.heightProperty()); leftResizeHandle.layoutYProperty().bind(backgroundRectangle.layoutYProperty()); // При наведении на левую сторону таскбара будет меняться курсор leftResizeHandle.hoverProperty().addListener(observable -> { if (leftResizeHandle.isHover()) { getScene().setCursor(Cursor.H_RESIZE); leftResizeHandle.setFill(Color.valueOf("#0381f4")); } else { getScene().setCursor(Cursor.DEFAULT); leftResizeHandle.setFill(Color.TRANSPARENT); } }); final Delta dragDeltaLeft = new Delta(); final OldRound oldRoundLeft = new OldRound(); // Ивент при нажатии на прямоугольник leftResizeHandle.setOnMousePressed(event -> { // Выделяем нужный элемент в таблице int i = controller.getProject().getTaskList().indexOf(task); controller.getTaskTableView().getSelectionModel().select(i); if (event.isPrimaryButtonDown()) { // record a delta distance for the drag and drop operation. dragDeltaLeft.x = getLayoutX() - event.getSceneX(); getScene().setCursor(Cursor.H_RESIZE); } }); // Ивент при движении прямоугольника leftResizeHandle.setOnMouseDragged(event -> { if (event.isPrimaryButtonDown()) { double newX = event.getSceneX() + dragDeltaLeft.x; if (newX >= 0 && newX <= getLayoutX() + backgroundRectangle.getWidth()) { // Хреначим привязку к сетке if (Math.round(newX / controller.getZoomMultiplier()) != oldRoundLeft.old) { if (!(Math.round(newX / controller.getZoomMultiplier()) * controller.getZoomMultiplier() - 1.5 == getLayoutX() + backgroundRectangle.getWidth())) { // Условие против нулевой длины тасбара oldRoundLeft.old = Math.round(newX / controller.getZoomMultiplier()); double oldX = getLayoutX(); this.setLayoutX(Math.round(newX / controller.getZoomMultiplier()) * controller.getZoomMultiplier() - 1.5); backgroundRectangle.setWidth(backgroundRectangle.getWidth() - (this.getLayoutX() - oldX)); wasMovedByMouse = true; // Когда начитаем двигать, то тру, чтобы не началась рекурсия controller.getCommandExecutor().execute(new SetTaskStartDateCommand( task, controller.getProject().getStartDate().plusDays((Math.round(newX / controller.getZoomMultiplier()))))); wasMovedByMouse = false; // Когда окончили движение фолз } } } } }); leftResizeHandle.setOnMouseReleased(event -> { if (!event.isPrimaryButtonDown()) { getScene().setCursor(Cursor.DEFAULT); } }); // Создаем прозрачный прямоугольник шириной 10 пикселей Rectangle rightResizeHandle = new Rectangle(); this.getChildren().add(rightResizeHandle); rightResizeHandle.setFill(Color.TRANSPARENT); // rightResizeHandle.setWidth(4); rightResizeHandle.widthProperty().bind(backgroundRectangle.widthProperty().divide(100).multiply(5)); // Привязываем этот прямоугольник к правой стороне таскбара rightResizeHandle.xProperty().bind( backgroundRectangle.xProperty() .add(backgroundRectangle.widthProperty()) .subtract(rightResizeHandle.widthProperty()) ); rightResizeHandle.heightProperty().bind(backgroundRectangle.heightProperty()); rightResizeHandle.layoutYProperty().bind(backgroundRectangle.layoutYProperty()); // При наведении на левую сторону таскбара будет меняться курсор rightResizeHandle.hoverProperty().addListener(observable -> { if (rightResizeHandle.isHover()) { getScene().setCursor(Cursor.H_RESIZE); rightResizeHandle.setFill(Color.valueOf("#0381f4")); } else { getScene().setCursor(Cursor.DEFAULT); rightResizeHandle.setFill(Color.TRANSPARENT); } }); final Delta dragDeltaRight = new Delta(); final OldRound oldRoundRight = new OldRound(); // Ивент при нажатии на прямоугольник rightResizeHandle.setOnMousePressed(event -> { // Выделяем нужный элемент в таблице controller.getTaskTableView().getSelectionModel().select(rowIndex); if (event.isPrimaryButtonDown()) { // record a delta distance for the drag and drop operation. dragDeltaRight.x = backgroundRectangle.getWidth() - event.getSceneX(); getScene().setCursor(Cursor.H_RESIZE); } }); // Ивент при движении прямоугольника rightResizeHandle.setOnMouseDragged(event -> { if (event.isPrimaryButtonDown()) { double newWidth = event.getSceneX() + dragDeltaRight.x; if (newWidth >= controller.getZoomMultiplier() && this.getLayoutX() + newWidth <= this.getParent().getBoundsInLocal().getWidth()) { // Хреначим привязку к сетке if (Math.round(newWidth / controller.getZoomMultiplier()) != oldRoundRight.old) { oldRoundRight.old = Math.round(newWidth / controller.getZoomMultiplier()); backgroundRectangle.setWidth(Math.round(newWidth / controller.getZoomMultiplier()) * controller.getZoomMultiplier()); wasMovedByMouse = true; // Когда начитаем двигать, то тру, чтобы не началась рекурсия controller.getCommandExecutor().execute(new SetTaskFinishDateCommand( task, task.getStartDate().plusDays(Math.round(backgroundRectangle.getWidth() / controller.getZoomMultiplier())))); wasMovedByMouse = false; // Когда окончили движение фолз } } } }); rightResizeHandle.setOnMouseReleased(event -> { if (!event.isPrimaryButtonDown()) { getScene().setCursor(Cursor.DEFAULT); } }); } public ITask getTask() { return task; } public void setTask(ITask task) { this.task = task; } public IResource getResource() { return resource; } public void setResource(IResource resource) { this.resource = resource; } // records relative x co-ordinate. private class Delta { double x; } private class OldRound { long old; } }