package holeg.ui.view.canvas; import holeg.model.*; import holeg.ui.controller.Control; import holeg.ui.model.GuiSettings; import holeg.ui.view.dialog.CreateTemplatePopUp; import holeg.utility.math.vector.Geometry; import holeg.utility.math.vector.Vec2f; import holeg.utility.math.vector.Vec2i; import javax.swing.*; import java.awt.*; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.awt.event.MouseMotionListener; import java.util.HashSet; import java.util.Optional; import java.util.Set; import java.util.logging.Logger; public class Canvas extends JPanel { private static final Logger log = Logger.getLogger(Canvas.class.getName()); private final Control control; private final CanvasMouseListener canvasMouseListener = new CanvasMouseListener(); private GroupNode groupNode; private boolean enabled = true; private final JPopupMenu componentPopupMenu = new JPopupMenu(); private final JMenuItem groupMenu = new JMenuItem("Group"); private final JMenuItem ungroupMenu = new JMenuItem("Ungroup"); private final JMenuItem deleteMenu = new JMenuItem("Delete"); private final JMenuItem templateMenu = new JMenuItem("Template"); public Canvas(Control control, GroupNode groupNode) { this.control = control; this.groupNode = groupNode; control.OnGuiSetEnabled.addListener(this::setCanvasEnabled); control.OnSelectionChanged.addListener(this::repaint); control.OnCanvasUpdate.addListener(this::repaint); this.setBackground(Color.WHITE); this.setPreferredSize(new Dimension(GuiSettings.canvasSize.getX(), GuiSettings.canvasSize.getY())); this.addMouseListener(canvasMouseListener); this.addMouseMotionListener(canvasMouseListener); initPopupMenu(); } private void initPopupMenu() { componentPopupMenu.add(deleteMenu); componentPopupMenu.addSeparator(); componentPopupMenu.add(groupMenu); componentPopupMenu.add(ungroupMenu); componentPopupMenu.addSeparator(); componentPopupMenu.add(templateMenu); deleteMenu.addActionListener(clicked -> { control.deleteCanvasObjects(GuiSettings.getSelectedObjects()); control.clearSelection(); }); groupMenu.addActionListener(clicked -> control.group()); ungroupMenu.addActionListener(clicked -> control.ungroup()); templateMenu.addActionListener(clicked -> GuiSettings.getSelectedObjects().stream().findAny().ifPresent(obj -> { HolonObject hObject = (HolonObject)obj; new CreateTemplatePopUp(hObject, (JFrame) SwingUtilities.getWindowAncestor(this), control); })); } public static Rectangle getBoundsOfObject(AbstractCanvasObject obj) { int pictureScale = GuiSettings.getPictureScale(); int pictureScaleDiv2 = GuiSettings.getPictureScaleDiv2(); Vec2i pos = obj.getPosition(); return new Rectangle(pos.getX() - pictureScaleDiv2, pos.getY() - pictureScaleDiv2, pictureScale, pictureScale); } private void setCanvasEnabled(boolean state) { enabled = state; } public GroupNode getGroupNode() { return this.groupNode; } public void setGroupNode(GroupNode groupNode) { this.groupNode = groupNode; } @Override public void paintComponent(java.awt.Graphics g) { super.paintComponent(g); Graphics2D g2d = Rendering.initGraphics2D(g); g2d.setColor(Color.gray); g2d.fillRect(0,0,getWidth(), getHeight()); g2d.setColor(Color.white); g2d.fillRect(0,0,GuiSettings.canvasSize.getX(), GuiSettings.canvasSize.getY()); Rendering.drawSelection(g2d); paintEdges(g2d); groupNode.getHolonObjects().forEach(hO -> Rendering.drawHolonObject(g2d, hO)); groupNode.getSwitches().forEach(hS -> Rendering.drawSwitchObject(g2d, hS)); groupNode.getGroupNodes().forEach(groupNode -> Rendering.drawGroupNode(g2d, groupNode)); groupNode.getNodes().forEach(node -> Rendering.drawNode(g2d, node)); switch (canvasMouseListener.state) { case BoxSelection -> Rendering.drawSelectionBox(g2d, canvasMouseListener.getRectangleOfSelectionBox()); case EdgeCreation -> Rendering.drawNewEdgeLine(g2d, canvasMouseListener.selectedOnPressed.getPosition(), canvasMouseListener.lastPosition); } if(canvasMouseListener.canBeReplaced){ Rendering.drawReplacementSymbol(g2d, canvasMouseListener.selectedOnPressed); } } private void paintEdges(Graphics2D g2d) { control.getModel().getEdgesOnCanvas().forEach(edge -> { if (edge.getA().getGroupNode().isEmpty() || edge.getB().getGroupNode().isEmpty()) { return; } boolean aInside = edge.getA().getGroupNode().get() == groupNode; boolean bInside = edge.getB().getGroupNode().get() == groupNode; //both if (aInside && bInside) { Rendering.drawEdge(g2d, edge, edge.getA(), edge.getB()); } else if (aInside) { SearchObjectIfParentOfGroupNode(edge.getB()).ifPresentOrElse( alternative -> Rendering.drawEdge(g2d, edge, edge.getA(), alternative), () -> Rendering.drawExternConnection(g2d, edge.getA())); } else if (bInside) { SearchObjectIfParentOfGroupNode(edge.getA()).ifPresentOrElse( alternative -> Rendering.drawEdge(g2d, edge, alternative, edge.getB()), () -> Rendering.drawExternConnection(g2d, edge.getB())); } else { Optional alternativeA = SearchObjectIfParentOfGroupNode(edge.getA()); Optional alternativeB = SearchObjectIfParentOfGroupNode(edge.getB()); if (alternativeA.isPresent() && alternativeB.isPresent() && !alternativeA.equals(alternativeB)) { Rendering.drawEdge(g2d, edge, alternativeA.get(), alternativeB.get()); } } //none }); } private Optional SearchObjectIfParentOfGroupNode(AbstractCanvasObject current) { while (current.getGroupNode().isPresent()) { if (current.getGroupNode().get() == this.groupNode) { return Optional.of(current); } current = current.getGroupNode().get(); } return Optional.empty(); } private Optional getObjectAtPosition(Vec2i pos) { return groupNode.getObjectsInThisLayer().filter(obj -> getBoundsOfObject(obj).contains(pos.getX(), pos.getY()) ).findAny(); } public static Vec2i boundsToCanvas(Vec2i pos){ Vec2i position = new Vec2i(pos); position.clampX(GuiSettings.getPictureScaleDiv2(), GuiSettings.canvasSize.getX() - GuiSettings.getPictureScaleDiv2()); position.clampY(GuiSettings.getPictureScaleDiv2(), GuiSettings.canvasSize.getY() - GuiSettings.getPictureScaleDiv2()); return position; } /** * Microsoft Windows10 selection & dragging behavior */ private class CanvasMouseListener implements MouseListener, MouseMotionListener { private Vec2i lastPosition = new Vec2i(); private Vec2i pressedPosition = new Vec2i(); private Set selectionBeforeBoxSelection = new HashSet<>(); private State state = State.None; private AbstractCanvasObject selectedOnPressed = null; private boolean canBeReplaced = false; @Override public void mousePressed(MouseEvent e) { if (!enabled) { return; } log.finest(state.toString()); Vec2i pos = new Vec2i(e.getPoint()); getObjectAtPosition(pos).ifPresentOrElse(obj -> { if (!e.isControlDown() && !GuiSettings.getSelectedObjects().contains(obj)) { GuiSettings.getSelectedObjects().clear(); } state = State.Selection; GuiSettings.getSelectedObjects().add(obj); selectedOnPressed = obj; }, () -> { if (!e.isControlDown()) { GuiSettings.getSelectedObjects().clear(); } state = State.BoxSelection; selectionBeforeBoxSelection = Set.copyOf(GuiSettings.getSelectedObjects()); }); control.OnSelectionChanged.broadcast(); lastPosition = pressedPosition = pos; } @Override public void mouseDragged(MouseEvent e) { if (!enabled) { return; } log.finest(state.toString()); Vec2i actualPos = new Vec2i(e.getPoint()); switch (state) { case Selection -> { // Not handle to small mouse dragging if (!(pressedPosition.getSquaredDistance(actualPos) > GuiSettings.dragThresholdDistance)) { return; } if (SwingUtilities.isLeftMouseButton(e)) { state = State.ObjectDragging; } else if (SwingUtilities.isRightMouseButton(e) && !(selectedOnPressed instanceof GroupNode)) { state = State.EdgeCreation; } } case BoxSelection -> { Rectangle selectionBox = getRectangleOfSelectionBox(); groupNode.getObjectsInThisLayer().forEach(obj -> { Rectangle bounds = getBoundsOfObject(obj); if (selectionBox.intersects(bounds) ^ selectionBeforeBoxSelection.contains(obj)) { GuiSettings.getSelectedObjects().add(obj); } else { GuiSettings.getSelectedObjects().remove(obj); } }); repaint(); } case ObjectDragging -> { Vec2i delta = actualPos.subtract(lastPosition); GuiSettings.getSelectedObjects().forEach(obj -> obj.setPosition(boundsToCanvas(obj.getPosition().add(delta)))); canBeReplaced = checkForReplacement(actualPos).isPresent(); repaint(); } case EdgeCreation -> repaint(); } lastPosition = actualPos; } private Optional checkForReplacement(Vec2i pos){ return groupNode.getObjectsInThisLayer().filter(obj -> obj != selectedOnPressed && getBoundsOfObject(obj).contains(pos.getX(), pos.getY()) ).findAny(); } @Override public void mouseReleased(MouseEvent e) { if (!enabled) { return; } log.info(state.toString()); switch (state) { case None -> { if(SwingUtilities.isRightMouseButton(e)){ preparePopupMenu(); componentPopupMenu.show(Canvas.this, e.getX(), e.getY()); }else { if (GuiSettings.getSelectedObjects().contains(selectedOnPressed)) { control.removeObjectFromSelection(selectedOnPressed); } else { control.addObjectToSelection(selectedOnPressed); } } } case Selection, BoxSelection -> { control.OnSelectionChanged.broadcast(); if(SwingUtilities.isRightMouseButton(e)){ preparePopupMenu(); componentPopupMenu.show(Canvas.this, e.getX(), e.getY()); } } case EdgeCreation -> getObjectAtPosition(lastPosition).ifPresentOrElse(obj -> { boolean isGroupNode = obj instanceof GroupNode; if (!isGroupNode) { control.addEdgeOnCanvasOrRemoveExisting(new Edge(selectedOnPressed, obj, GuiSettings.maxCapacityForNewCreatedEdges)); } }, () -> { Node node = new Node("Node"); groupNode.add(node); final float splitDetectionDistance = 15f; Geometry.Circle detectionCircle = new Geometry.Circle(new Vec2f(lastPosition), splitDetectionDistance); node.setPosition(new Vec2i(boundsToCanvas(lastPosition))); for(Edge edge : control.getModel().getEdgesOnCanvas()) { if(edge.getA().getGroupNode().isEmpty() || edge.getB().getGroupNode().isEmpty() || edge.getA().getGroupNode().get() != groupNode || edge.getB().getGroupNode().get() != groupNode){ continue; } Optional pos = Geometry.getProjectionOnSegmentIfInRange(new Geometry.Line(new Vec2f(edge.getA().getPosition()) , new Vec2f(edge.getB().getPosition())), detectionCircle); if(pos.isPresent()){ Vec2f position = pos.get(); node.setPosition(new Vec2i((int)position.getX(), (int)position.getY())); splitEdge(edge, node); break; } } control.addEdgeOnCanvas(new Edge(selectedOnPressed, node, GuiSettings.maxCapacityForNewCreatedEdges)); control.calculateStateForCurrentIteration(); }); case ObjectDragging -> checkForReplacement(new Vec2i(e.getPoint())).ifPresent(obj -> control.replaceCanvasObject(obj, selectedOnPressed)); } canBeReplaced = false; state = State.None; repaint(); } private void preparePopupMenu(){ int count = GuiSettings.getSelectedObjects().size(); boolean isAGroupNodeSelected = GuiSettings.getSelectedObjects().stream().anyMatch(obj -> obj instanceof GroupNode); switch (count){ case 0 -> { groupMenu.setEnabled(false); ungroupMenu.setEnabled(false); deleteMenu.setEnabled(false); templateMenu.setEnabled(false); } case 1 -> { deleteMenu.setEnabled(true); boolean isSelectedObjectAHolonObject = GuiSettings.getSelectedObjects().stream().anyMatch(obj -> obj instanceof HolonObject); templateMenu.setEnabled(isSelectedObjectAHolonObject); groupMenu.setEnabled(true); ungroupMenu.setEnabled(isAGroupNodeSelected); } default -> { deleteMenu.setEnabled(true); templateMenu.setEnabled(false); groupMenu.setEnabled(true); ungroupMenu.setEnabled(isAGroupNodeSelected); } } } @Override public void mouseClicked(MouseEvent e) { boolean doubleLeftClick = e.getClickCount() % 2 == 0 && SwingUtilities.isLeftMouseButton(e); if (doubleLeftClick) { log.finest(state.toString()); getObjectAtPosition(new Vec2i(e.getPoint())).ifPresent(obj -> { if (obj instanceof HolonSwitch sw) { sw.setMode(HolonSwitch.SwitchMode.Manual); sw.flipManualState(); control.calculateStateForCurrentIteration(); } else if (obj instanceof GroupNode gNode) { control.showGroupNode(gNode); } }); } } Rectangle getRectangleOfSelectionBox() { return Geometry.createRectangleFromCorners(lastPosition, pressedPosition); } public void splitEdge(Edge edge, Node node){ AbstractCanvasObject end = edge.getB(); edge.setB(node); Edge additional = new Edge(node, end, edge.maxCapacity); control.getModel().addEdgeOnCanvas(additional); } @Override public void mouseEntered(MouseEvent e) { } @Override public void mouseExited(MouseEvent e) { } @Override public void mouseMoved(MouseEvent e) { } private enum State { None, BoxSelection, EdgeCreation, ObjectDragging, Selection } } }