All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.jhotdraw8.draw.SimpleDrawingView Maven / Gradle / Ivy

/*
 * @(#)SimpleDrawingView.java
 * Copyright © 2023 The authors and contributors of JHotDraw. MIT License.
 */

package org.jhotdraw8.draw;

import javafx.application.Platform;
import javafx.beans.Observable;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.ReadOnlyBooleanWrapper;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.beans.property.ReadOnlySetProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.ObservableSet;
import javafx.geometry.BoundingBox;
import javafx.geometry.Bounds;
import javafx.scene.Node;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.transform.Transform;
import org.jspecify.annotations.Nullable;
import org.jhotdraw8.application.EditableComponent;
import org.jhotdraw8.base.event.Listener;
import org.jhotdraw8.draw.constrain.Constrainer;
import org.jhotdraw8.draw.constrain.NullConstrainer;
import org.jhotdraw8.draw.figure.Drawing;
import org.jhotdraw8.draw.figure.Figure;
import org.jhotdraw8.draw.figure.SimpleDrawing;
import org.jhotdraw8.draw.gui.ZoomableScrollPane;
import org.jhotdraw8.draw.handle.Handle;
import org.jhotdraw8.draw.model.DrawingModel;
import org.jhotdraw8.draw.model.SimpleDrawingModel;
import org.jhotdraw8.draw.render.InteractiveDrawingRenderer;
import org.jhotdraw8.draw.render.InteractiveHandleRenderer;
import org.jhotdraw8.draw.tool.Tool;
import org.jhotdraw8.fxbase.beans.NonNullObjectProperty;
import org.jhotdraw8.fxbase.binding.CustomBinding;
import org.jhotdraw8.fxbase.tree.TreeBreadthFirstSpliterator;
import org.jhotdraw8.fxbase.tree.TreeModelEvent;

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Deque;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.StreamSupport;

/**
 * A simple implementation of {@link DrawingView}.
 * 

* The SimpleDrawingView has the following scene structure: *

    *
  • {@value #DRAWING_VIEW_STYLE_CLASS} – {@link BorderPane}
      *
    • {@value ZoomableScrollPane#ZOOMABLE_SCROLL_PANE_STYLE_CLASS} – see {@link ZoomableScrollPane}
        *
      • {@value ZoomableScrollPane#ZOOMABLE_SCROLL_PANE_VIEWPORT_STYLE_CLASS}
          *
        • {@value ZoomableScrollPane#ZOOMABLE_SCROLL_PANE_BACKGROUND_STYLE_CLASS}
            *
          • {@value #CANVAS_REGION_STYLE_CLASS} – {@link Region}
          • *
        • *
        • {@value ZoomableScrollPane#ZOOMABLE_SCROLL_PANE_SUBSCENE_STYLE_CLASS}
            *
          • content
          • *
        • *
        • {@value ZoomableScrollPane#ZOOMABLE_SCROLL_PANE_FOREGROUND_STYLE_CLASS}
        • *
      • *
    • *
  • *
* The scene node of the SimpleDrawingView has the following structure and * CSS style classes: */ public class SimpleDrawingView extends AbstractDrawingView { /** * The style class of the canvas pane is {@value #CANVAS_REGION_STYLE_CLASS}. */ public static final String CANVAS_REGION_STYLE_CLASS = "jhotdraw8-drawing-view-canvas-region"; /** * The style class of the drawing view is {@value #DRAWING_VIEW_STYLE_CLASS}. */ public static final String DRAWING_VIEW_STYLE_CLASS = "jhotdraw8-drawing-view"; private final ZoomableScrollPane zoomableScrollPane = ZoomableScrollPane.create(); private final SimpleDrawingViewNode node = new SimpleDrawingViewNode(); private final NonNullObjectProperty model // = new NonNullObjectProperty<>(this, MODEL_PROPERTY, new SimpleDrawingModel()); private final ReadOnlyObjectWrapper drawing = new ReadOnlyObjectWrapper<>(this, DRAWING_PROPERTY); private final ObjectProperty
activeParent = new SimpleObjectProperty<>(this, ACTIVE_PARENT_PROPERTY); private final NonNullObjectProperty constrainer = new NonNullObjectProperty<>(this, CONSTRAINER_PROPERTY, new NullConstrainer()); private final ReadOnlyBooleanWrapper focused = new ReadOnlyBooleanWrapper(this, FOCUSED_PROPERTY); private final Region background = new Region(); private final StackPane foreground = new StackPane(); private final InteractiveDrawingRenderer drawingRenderer = new InteractiveDrawingRenderer(); private final InteractiveHandleRenderer handleRenderer = new InteractiveHandleRenderer(); private boolean constrainerNodeValid; private boolean isLayoutValid = true; private @Nullable Runnable repainter = null; private final Listener> treeModelListener = this::onTreeModelEvent; public SimpleDrawingView() { initStyle(); initLayout(); initBindings(); initBehavior(); } @Override public ObjectProperty
activeParentProperty() { return activeParent; } public void clearSelection() { getSelectedFigures().clear(); } @Override public NonNullObjectProperty constrainerProperty() { return constrainer; } public void deleteSelection() { ArrayList
figures = new ArrayList<>(getSelectedFigures()); DrawingModel model = getModel(); // Also delete dependent figures. Deque
cascade = new ArrayDeque<>(figures); for (Figure f : figures) { for (Figure ff : f.preorderIterable()) { StreamSupport.stream(new TreeBreadthFirstSpliterator<>( figure -> () -> figure.getReadOnlyLayoutObservers().stream() .filter(x -> x.getLayoutSubjects().size() == 1).iterator(), ff ), false) .forEach(cascade::addFirst); } } for (Figure f : cascade) { if (f.isDeletable()) { for (Figure d : f.preorderIterable()) { model.disconnect(d); } model.removeFromParent(f); } } } @Override public ReadOnlyObjectProperty drawingProperty() { return drawing.getReadOnlyProperty(); } public void duplicateSelection() { throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. } @Override public @Nullable Node findFigureNode(Figure figure, double vx, double vy) { return drawingRenderer.findFigureNode(figure, vx, vy); } @Override public List> findFigures(double vx, double vy, boolean decompose, Predicate
predicate) { return drawingRenderer.findFigures(vx, vy, decompose, predicate); } @Override public List> findFiguresInside(double vx, double vy, double vwidth, double vheight, boolean decompose) { return drawingRenderer.findFiguresInside(vx, vy, vwidth, vheight, decompose, Figure::isSelectable); } @Override public List> findFiguresIntersecting(double vx, double vy, double vwidth, double vheight, boolean decompose, Predicate
predicate) { return drawingRenderer.findFiguresIntersecting(vx, vy, vwidth, vheight, decompose, predicate); } @Override public @Nullable Handle findHandle(double vx, double vy) { return handleRenderer.findHandle(vx, vy); } @Override public ReadOnlyBooleanProperty focusedProperty() { return focused.getReadOnlyProperty(); } @Override public Set
getFiguresWithCompatibleHandle(Collection
figures, Handle handle) { return handleRenderer.getFiguresWithCompatibleHandle(figures, handle); } @Override public Node getNode() { return node; } @Override public @Nullable Node getNode(Figure f) { return drawingRenderer.getNode(f); } @Override public Transform getViewToWorld() { return zoomableScrollPane.getViewToContent(); } @Override public Bounds getVisibleRect() { return worldToView(zoomableScrollPane.getVisibleContentRect()); } @Override public Transform getWorldToView() { return zoomableScrollPane.getContentToView(); } @Override public ReadOnlySetProperty handlesProperty() { return handleRenderer.handlesProperty(); } protected void initBehavior() { drawingRenderer.setRenderContext(this); } private void initBindings() { CustomBinding.bind(drawing, model, DrawingModel::drawingProperty); model.addListener(this::onDrawingModelChanged); model.get().setRoot(new SimpleDrawing()); onDrawingModelChanged(model, null, model.getValue()); drawingRenderer.modelProperty().bind(this.modelProperty()); drawingRenderer.clipBoundsProperty().bind(zoomableScrollPane.visibleContentRectProperty()); drawingRenderer.editorProperty().bind(this.editorProperty()); drawingRenderer.setDrawingView(this); handleRenderer.modelProperty().bind(this.modelProperty()); handleRenderer.setSelectedFigures(getSelectedFigures()); handleRenderer.editorProperty().bind(this.editorProperty()); handleRenderer.setDrawingView(this); zoomFactorProperty().addListener(this::onZoomFactorChanged); constrainer.addListener(this::onConstrainerChanged); zoomableScrollPane.visibleContentRectProperty().addListener(this::onViewRectChanged); zoomableScrollPane.contentToViewProperty().addListener(this::onContentToViewChanged); CustomBinding.bind(drawing, model, DrawingModel::drawingProperty); CustomBinding.bind(focused, toolProperty(), Tool::focusedProperty); } private void initLayout() { node.setCenter(zoomableScrollPane.getNode()); background.setManaged(false); zoomableScrollPane.getContentChildren().add(drawingRenderer.getNode()); zoomableScrollPane.getBackgroundChildren().add(background); zoomableScrollPane.getForegroundChildren().addAll( handleRenderer.getNode(), foreground); foreground.setManaged(false); } protected void initStyle() { background.getStyleClass().add(CANVAS_REGION_STYLE_CLASS); node.getStyleClass().add(DRAWING_VIEW_STYLE_CLASS); } private void invalidateConstrainer() { constrainerNodeValid = false; } @Override protected void invalidateHandles() { } @Override public void jiggleHandles() { handleRenderer.jiggleHandles(); } @Override public NonNullObjectProperty modelProperty() { return model; } private void onConstrainerChanged(Observable o, @Nullable Constrainer oldValue, @Nullable Constrainer newValue) { if (oldValue != null) { foreground.getChildren().remove(oldValue.getNode()); oldValue.removeListener(this::onConstrainerInvalidated); } if (newValue != null) { Node node = newValue.getNode(); node.setManaged(false); foreground.getChildren().addFirst(node); node.applyCss(); newValue.updateNode(this); newValue.addListener(this::onConstrainerInvalidated); invalidateConstrainer(); repaint(); } } private void onConstrainerInvalidated(Observable o) { invalidateConstrainer(); repaint(); } private void onContentToViewChanged(Observable observable) { updateBackgroundNode(); } private void onDrawingChanged() { } private void onDrawingModelChanged(Observable o, @Nullable DrawingModel oldValue, @Nullable DrawingModel newValue) { if (oldValue != null) { oldValue.removeTreeModelListener(treeModelListener); } if (newValue != null) { newValue.addTreeModelListener(treeModelListener); revalidateLayout(); } } private void onNodeChanged(Figure f) { if (f == getDrawing()) { revalidateLayout(); } } private void onNodeRemoved(Figure f) { ObservableSet
selectedFigures = getSelectedFigures(); for (Figure d : f.preorderIterable()) { selectedFigures.remove(d); } repaint(); } private void onRootChanged() { onDrawingChanged(); clearSelection(); revalidateLayout(); repaint(); } private void onSubtreeNodesChanged(Figure f) { } @Override protected void onToolChanged(Observable observable, @Nullable Tool oldValue, @Nullable Tool newValue) { if (oldValue != null) { foreground.getChildren().remove(oldValue.getNode()); oldValue.setDrawingView(null); } if (newValue != null) { Node node = newValue.getNode(); node.setManaged(true);// we want the tool to fill the view foreground.getChildren().add(node); newValue.setDrawingView(this); } } private void onTreeModelEvent(TreeModelEvent
event) { Figure f = event.getNode(); switch (event.getEventType()) { case NODE_ADDED_TO_PARENT: case NODE_REMOVED_FROM_PARENT: case NODE_ADDED_TO_TREE: break; case NODE_REMOVED_FROM_TREE: onNodeRemoved(f); break; case NODE_CHANGED: onNodeChanged(f); break; case ROOT_CHANGED: onRootChanged(); break; case SUBTREE_NODES_CHANGED: onSubtreeNodesChanged(f); break; default: throw new UnsupportedOperationException(event.getEventType() + " not supported"); } } private void onViewRectChanged(Observable observable, @Nullable Bounds oldValue, @Nullable Bounds newValue) { revalidateLayout(); } private void onZoomFactorChanged(Observable observable) { revalidateLayout(); } private void paint() { repainter = null; if (!constrainerNodeValid) { updateConstrainerNode(); constrainerNodeValid = true; } } /** * For testing: paints the drawing immediately. */ public void paintImmediately() { drawingRenderer.paintImmediately(); paint(); } @Override public void recreateHandles() { handleRenderer.recreateHandles(); } @Override protected void repaint() { if (repainter == null) { repainter = this::paint; Platform.runLater(repainter); } } private void revalidateLayout() { if (isLayoutValid) { isLayoutValid = false; validateLayout(); //Platform.runLater(this::validateLayout); } } @Override public void scrollRectToVisible(Bounds boundsInView) { zoomableScrollPane.scrollViewRectToVisible(boundsInView); } /** * Selects all enabled and selectable figures in all enabled layers. */ public void selectAll() { ArrayList
figures = new ArrayList<>(); Drawing d = getDrawing(); if (d != null) { for (Figure layer : d.getChildren()) { if (layer.isEditable() && layer.isVisible()) { for (Figure f : layer.getChildren()) { if (f.isSelectable()) { figures.add(f); } } } } } getSelectedFigures().clear(); getSelectedFigures().addAll(figures); } private void updateBackgroundNode() { Drawing drawing = getDrawing(); Bounds bounds = drawing == null ? new BoundingBox(0, 0, 10, 10) : drawing.getLayoutBounds(); Bounds bounds1 = worldToView(bounds); double x = bounds1.getMinX(); double y = bounds1.getMinY(); double w = bounds1.getWidth(); double h = bounds1.getHeight(); double p = 0; background.resizeRelocate(x - p, y - p, w + 2 * p, h + 2 * p); } private void updateConstrainerNode() { Constrainer c = getConstrainer(); if (c != null) { c.updateNode(this); } } private void updateLayout() { Drawing drawing = getDrawing(); Bounds bounds = drawing == null ? new BoundingBox(0, 0, 10, 10) : drawing.getLayoutBounds(); double f = getZoomFactor(); double w = bounds.getWidth(); double h = bounds.getHeight(); zoomableScrollPane.setContentSize(w, h); Bounds vp = zoomableScrollPane.getViewportRect(); foreground.resize(vp.getWidth(), vp.getHeight()); handleRenderer.invalidateHandleNodes(); handleRenderer.repaint(); updateConstrainerNode(); updateBackgroundNode(); } private void validateLayout() { if (!isLayoutValid) { updateLayout(); isLayoutValid = true; } } @Override public DoubleProperty zoomFactorProperty() { return zoomableScrollPane.zoomFactorProperty(); } private class SimpleDrawingViewNode extends BorderPane implements EditableComponent { public SimpleDrawingViewNode() { setFocusTraversable(true); } @Override public void clearSelection() { SimpleDrawingView.this.clearSelection(); } @Override public void copy() { SimpleDrawingView.this.copy(); } @Override public void cut() { SimpleDrawingView.this.cut(); } @Override public void deleteSelection() { SimpleDrawingView.this.deleteSelection(); } @Override public void duplicateSelection() { SimpleDrawingView.this.duplicateSelection(); } @Override public void paste() { SimpleDrawingView.this.paste(); } @Override public void selectAll() { SimpleDrawingView.this.selectAll(); } @Override public ReadOnlyBooleanProperty selectionEmptyProperty() { return SimpleDrawingView.this.selectedFiguresProperty().emptyProperty(); } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy