diff --git a/src/main/java/edu/rpi/legup/controller/TreeController.java b/src/main/java/edu/rpi/legup/controller/TreeController.java index 36c1a9b68..53ab32b68 100644 --- a/src/main/java/edu/rpi/legup/controller/TreeController.java +++ b/src/main/java/edu/rpi/legup/controller/TreeController.java @@ -1,186 +1,99 @@ package edu.rpi.legup.controller; -import static edu.rpi.legup.app.GameBoardFacade.getInstance; - import edu.rpi.legup.model.Puzzle; -import edu.rpi.legup.model.tree.Tree; -import edu.rpi.legup.ui.boardview.BoardView; -import edu.rpi.legup.ui.proofeditorui.treeview.*; -import java.awt.*; -import java.awt.event.MouseEvent; -import java.awt.event.MouseWheelEvent; -import javax.swing.*; +import edu.rpi.legup.ui.proofeditorui.treeview.TreeElementView; +import edu.rpi.legup.ui.proofeditorui.treeview.TreePanel; +import edu.rpi.legup.ui.proofeditorui.treeview.TreeView; +import edu.rpi.legup.ui.proofeditorui.treeview.TreeViewSelection; +import org.jetbrains.annotations.NotNull; + +import static edu.rpi.legup.app.GameBoardFacade.getInstance; /** - * The TreeController class handles UI events from a TreePanel. It extends the Controller class to - * provide specific behavior for tree interactions + * The {@code TreeController} class handles UI events from a {@code TreePanel}. It extends the + * {@code Controller} class to provide specific behavior for tree interactions. */ -public class TreeController extends Controller { - /** - * TreeController Constructor creates a controller object to listen to ui events from a {@link - * TreePanel} - */ - public TreeController() {} +public class TreeController extends ZoomViewController { - /** - * Mouse Clicked event no default action - * - * @param e MouseEvent object - */ - @Override - public void mouseClicked(MouseEvent e) {} - - /** - * Mouse Pressed event sets the cursor to the move cursor and stores info for possible panning - * - * @param e MouseEvent object - */ - @Override - public void mousePressed(MouseEvent e) { - super.mousePressed(e); - } + /** {@code TreeController} constructor creates a controller object to listen to ui events from a {@link TreePanel}. */ + public TreeController() {} - /** - * Mouse Released event sets the cursor back to the default cursor and reset info for panning - * Set board modifiability - * - * @param e MouseEvent object - */ @Override - public void mouseReleased(MouseEvent e) { + public void mouseReleased(@NotNull ControllerMouseEvent e) { super.mouseReleased(e); - TreeView treeView = (TreeView) viewer; - Point point = treeView.getActualPoint(e.getPoint()); - TreeElementView treeElementView = treeView.getTreeElementView(point); + TreeView treeView = (TreeView) getView(); + TreeElementView treeElementView = treeView.getTreeElementView(e.getPrecisePoint()); Puzzle puzzle = getInstance().getPuzzleModule(); TreeViewSelection selection = treeView.getSelection(); + if (treeElementView != null) { if (e.isShiftDown()) { selection.addToSelection(treeElementView); + } else if (e.isControlDown()) { + selection.toggleSelection(treeElementView); } else { - if (e.isControlDown()) { - if (!(selection.getSelectedViews().size() == 1 - && treeElementView == selection.getFirstSelection())) { - selection.toggleSelection(treeElementView); - } - } else { - selection.newSelection(treeElementView); - } + selection.newSelection(treeElementView); } - puzzle.notifyTreeListeners(listener -> listener.onTreeSelectionChanged(selection)); - puzzle.notifyBoardListeners( - listener -> listener.onTreeElementChanged(treeElementView.getTreeElement())); - } - } - /** - * Mouse Entered event no default action - * - * @param e MouseEvent object - */ - @Override - public void mouseEntered(MouseEvent e) { - TreeView treeView = (TreeView) viewer; - Point point = treeView.getActualPoint(e.getPoint()); - Tree tree = getInstance().getTree(); - BoardView boardView = getInstance().getLegupUI().getBoardView(); - TreeElementView treeElementView = treeView.getTreeElementView(point); - Puzzle puzzle = getInstance().getPuzzleModule(); - if (treeElementView != null) { + puzzle.notifyTreeListeners(listener -> listener.onTreeSelectionChanged(selection)); puzzle.notifyBoardListeners( listener -> listener.onTreeElementChanged(treeElementView.getTreeElement())); } } - /** - * Mouse Exited event no default action - * - * @param e MouseEvent object - */ @Override - public void mouseExited(MouseEvent e) { - TreeView treeView = (TreeView) viewer; - Point point = treeView.getActualPoint(e.getPoint()); - TreeElementView elementView = treeView.getTreeElementView(point); - Puzzle puzzle = getInstance().getPuzzleModule(); + public void mouseExited(@NotNull ControllerMouseEvent e) { + TreeView treeView = (TreeView) getView(); TreeViewSelection selection = treeView.getSelection(); - selection.setMousePoint(null); - if (elementView != null) { - TreeElementView selectedView = selection.getFirstSelection(); - puzzle.notifyBoardListeners( - listener -> listener.onTreeElementChanged(selectedView.getTreeElement())); - } + selection.clearHover(); } - /** - * Mouse Dragged event adjusts the viewport - * - * @param e MouseEvent object - */ @Override - public void mouseDragged(MouseEvent e) { - TreeView treeView = (TreeView) viewer; - Point point = treeView.getActualPoint(e.getPoint()); - TreeElementView treeElementView = treeView.getTreeElementView(point); + public void mouseDragged(@NotNull ControllerMouseEvent e) { + TreeView treeView = (TreeView) getView(); + TreeElementView treeElementView = treeView.getTreeElementView(e.getPrecisePoint()); Puzzle puzzle = getInstance().getPuzzleModule(); - TreeViewSelection selection = treeView.getSelection(); + if (puzzle != null) { - selection.setMousePoint(treeView.getActualPoint(e.getPoint())); + TreeViewSelection selection = treeView.getSelection(); + selection.setMousePoint(e.getPrecisePoint()); + if (treeElementView != null && treeElementView != selection.getHover()) { puzzle.notifyBoardListeners( - listener -> - listener.onTreeElementChanged(treeElementView.getTreeElement())); + listener -> listener.onTreeElementChanged(treeElementView.getTreeElement())); selection.newSelection(treeElementView); selection.newHover(treeElementView); puzzle.notifyTreeListeners(listener -> listener.onTreeSelectionChanged(selection)); - } else { - if (treeElementView == null && selection.getHover() != null) { + } + else if (treeElementView == null && selection.getHover() != null) { puzzle.notifyBoardListeners( listener -> - listener.onTreeElementChanged( - selection.getFirstSelection().getTreeElement())); + listener.onTreeElementChanged(selection.getFirstSelection().getTreeElement())); selection.clearHover(); - } } } } - /** - * Mouse Moved event no default action - * - * @param e MouseEvent object - */ @Override - public void mouseMoved(MouseEvent e) { - TreeView treeView = (TreeView) viewer; - Point point = treeView.getActualPoint(e.getPoint()); - TreeElementView treeElementView = treeView.getTreeElementView(point); + public void mouseMoved(@NotNull ControllerMouseEvent e) { + TreeView treeView = (TreeView) getView(); + TreeElementView treeElementView = treeView.getTreeElementView(e.getPrecisePoint()); Puzzle puzzle = getInstance().getPuzzleModule(); + if (puzzle != null) { TreeViewSelection selection = treeView.getSelection(); - selection.setMousePoint(treeView.getActualPoint(e.getPoint())); + selection.setMousePoint(e.getPrecisePoint()); + if (treeElementView != null && treeElementView != selection.getHover()) { selection.newHover(treeElementView); puzzle.notifyTreeListeners(listener -> listener.onTreeSelectionChanged(selection)); - } else { - if (treeElementView == null && selection.getHover() != null) { - selection.clearHover(); - puzzle.notifyTreeListeners( - listener -> listener.onTreeSelectionChanged(selection)); - } + } + else if (treeElementView == null && selection.getHover() != null) { + selection.clearHover(); + puzzle.notifyTreeListeners(listener -> listener.onTreeSelectionChanged(selection)); } } } - - /** - * Mouse Wheel Moved event zooms in on the viewport - * - * @param e MouseEvent object - */ - @Override - public void mouseWheelMoved(MouseWheelEvent e) { - super.viewer.scroll(e.getWheelRotation()); - } -} +} \ No newline at end of file diff --git a/src/main/java/edu/rpi/legup/controller/ZoomViewController.java b/src/main/java/edu/rpi/legup/controller/ZoomViewController.java new file mode 100644 index 000000000..e3647ccab --- /dev/null +++ b/src/main/java/edu/rpi/legup/controller/ZoomViewController.java @@ -0,0 +1,173 @@ +package edu.rpi.legup.controller; + +import edu.rpi.legup.ui.zoompane.ZoomView; +import org.intellij.lang.annotations.MagicConstant; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.awt.*; +import java.awt.event.*; +import java.awt.geom.Point2D; + +/** + * {@code ZoomViewController} is an abstract class designed to handle various mouse and key events to provide + * interactability for a {@code ZoomView}. + */ +public abstract class ZoomViewController implements KeyListener, ActionListener { + + /** The view that this controller is installed on. */ + private ZoomView view; + + /** Set the {@code ZoomView} that this controller is installed in. */ + public void setView(@Nullable ZoomView newView) { view = newView; } + + /** @return the {@code ZoomView} that this controller is installed in. */ + public ZoomView getView() { return view; } + + @Override + public void actionPerformed(@NotNull ActionEvent e) {} + + @Override + public void keyTyped(@NotNull KeyEvent e) {} + + @Override + public void keyPressed(@NotNull KeyEvent e) {} + + @Override + public void keyReleased(@NotNull KeyEvent e) {} + + /** + * Invoked when the mouse button has been clicked (pressed and released) on a component. + * @param e the event to be processed + */ + public void mouseClicked(@NotNull ControllerMouseEvent e) {} + + /** + * Invoked when a mouse button has been pressed on a component. + * @param e the event to be processed + */ + public void mousePressed(@NotNull ControllerMouseEvent e) {} + + /** + * Invoked when a mouse button has been released on a component. + * @param e the event to be processed + */ + public void mouseReleased(@NotNull ControllerMouseEvent e) {} + + /** + * Invoked when the mouse enters a component. + * @param e the event to be processed + */ + public void mouseEntered(@NotNull ControllerMouseEvent e) {} + + /** + * Invoked when the mouse exits a component. + * @param e the event to be processed + */ + public void mouseExited(@NotNull ControllerMouseEvent e) {} + + /** + * Invoked when a mouse button is pressed on a component and then dragged. {@code MOUSE_DRAGGED} events + * will continue to be delivered to the component where the drag originated until the mouse button is + * released (regardless of whether the mouse position is within the bounds of the component). + *

+ * Due to platform-dependent Drag&Drop implementations, {@code MOUSE_DRAGGED} events may not be + * delivered during a native Drag&Drop operation. + * + * @param e the event to be processed + */ + public void mouseDragged(@NotNull ControllerMouseEvent e) {} + + /** + * Invoked when the mouse cursor has been moved onto a component but no buttons have been pushed. + * @param e the event to be processed + */ + public void mouseMoved(@NotNull ControllerMouseEvent e) {} + + /** A {@code MouseEvent} that maintains a {@code Point2D} location to support higher precision. */ + public static class ControllerMouseEvent extends MouseEvent { + + /** High precision location of the event. */ + private final Point2D precisePoint; + + /** + * Constructs a {@code ControllerMouseEvent} object with the specified source component, type, time, + * modifiers, coordinates, click count, popupTrigger flag, and button number. + *

+ * Creating an invalid event (such as by using more than one of the old _MASKs, or modifier/button + * values which don't match) results in unspecified behavior. + * + * @param source The {@code Component} that originated the event + * @param id An integer indicating the type of event. + * For information on allowable values, see the class description for {@link MouseEvent} + * @param when A long integer that gives the time the event occurred. + * Passing negative or zero value is not recommended + * @param modifiers a modifier mask describing the modifier keys and mouse buttons (for example, shift, + * ctrl, alt, and meta) that are down during the event. Only extended modifiers are + * allowed to be used as a value for this parameter (see the + * {@link InputEvent#getModifiersEx} class for the description of extended modifiers). + * Passing negative parameter is not recommended. + * Zero value means that no modifiers were passed + * @param where The variable precision point for the mouse location. + * @param clickCount The number of mouse clicks associated with event. + * Passing negative value is not recommended + * @param popupTrigger A boolean that equals {@code true} if this event is a trigger for a popup menu + * @param button An integer that indicates, which of the mouse buttons has changed its state. + * The following rules are applied to this parameter: + *

+ * @throws IllegalArgumentException if {@code button} is less than zero + * @throws IllegalArgumentException if {@code button} is greater than BUTTON3 + * and the support for extended mouse buttons is + * {@link Toolkit#areExtraMouseButtonsEnabled() disabled} by Java + * @throws IllegalArgumentException if {@code button} is greater than the + * {@link java.awt.MouseInfo#getNumberOfButtons() current number of buttons} + * and the support for extended mouse buttons is + * {@link Toolkit#areExtraMouseButtonsEnabled() enabled} by Java + * @throws IllegalArgumentException if an invalid {@code button} value is passed in + * @see #getSource() + * @see #getID() + * @see #getWhen() + * @see #getModifiersEx() + * @see #getPrecisePoint() + * @see #getClickCount() + * @see #isPopupTrigger() + * @see #getButton() + */ + public ControllerMouseEvent(@NotNull Component source, + int id, + long when, + @MagicConstant(intValues = {InputEvent.SHIFT_DOWN_MASK, InputEvent.CTRL_DOWN_MASK, + InputEvent.META_DOWN_MASK, InputEvent.ALT_DOWN_MASK, + InputEvent.BUTTON1_DOWN_MASK, InputEvent.BUTTON2_DOWN_MASK, + InputEvent.BUTTON3_DOWN_MASK, InputEvent.ALT_GRAPH_DOWN_MASK}) + int modifiers, + @NotNull Point2D where, + int clickCount, + boolean popupTrigger, + int button) + { + super(source, id, when, modifiers, (int) Math.round(where.getX()), + (int) Math.round(where.getY()), clickCount, popupTrigger, button); + + precisePoint = where; + } + + /** + * Returns the {@code x,y} location of the event in view coordinates. + * + * @return a {@code Point2D} object containing the x and y view coordinates + */ + public Point2D getPrecisePoint() { return precisePoint; } + } +} diff --git a/src/main/java/edu/rpi/legup/history/AutoCaseRuleCommand.java b/src/main/java/edu/rpi/legup/history/AutoCaseRuleCommand.java index a8baa946a..bcb5b5ef4 100644 --- a/src/main/java/edu/rpi/legup/history/AutoCaseRuleCommand.java +++ b/src/main/java/edu/rpi/legup/history/AutoCaseRuleCommand.java @@ -118,7 +118,7 @@ public String getErrorString() { } TreeNodeView nodeView = (TreeNodeView) treeElementView; - if (!nodeView.getChildrenViews().isEmpty()) { + if (!nodeView.getChildViews().isEmpty()) { return CommandError.NO_CHILDREN.toString(); } diff --git a/src/main/java/edu/rpi/legup/history/DeleteTreeElementCommand.java b/src/main/java/edu/rpi/legup/history/DeleteTreeElementCommand.java index bfd9cd7f5..7a89adbc1 100644 --- a/src/main/java/edu/rpi/legup/history/DeleteTreeElementCommand.java +++ b/src/main/java/edu/rpi/legup/history/DeleteTreeElementCommand.java @@ -51,7 +51,6 @@ public void executeCommand() { } for (TreeElementView selectedView : selectedViews) { - System.out.println("DELETED"); TreeElement element = selectedView.getTreeElement(); tree.removeTreeElement(element); puzzle.notifyTreeListeners(listener -> listener.onTreeElementRemoved(element)); diff --git a/src/main/java/edu/rpi/legup/history/EditDataCommand.java b/src/main/java/edu/rpi/legup/history/EditDataCommand.java index 6f8173975..c219ddc10 100644 --- a/src/main/java/edu/rpi/legup/history/EditDataCommand.java +++ b/src/main/java/edu/rpi/legup/history/EditDataCommand.java @@ -10,10 +10,9 @@ import edu.rpi.legup.ui.boardview.BoardView; import edu.rpi.legup.ui.boardview.ElementView; import edu.rpi.legup.ui.proofeditorui.treeview.*; -import java.awt.*; + import java.awt.event.MouseEvent; import java.util.List; -import javax.swing.*; /** * The EditDataCommand class represents a command to edit the data of a puzzle element within a tree @@ -112,21 +111,18 @@ public String getErrorString() { PuzzleElement selectedPuzzleElement = elementView.getPuzzleElement(); if (selectedView.getType() == TreeElementType.NODE) { TreeNodeView nodeView = (TreeNodeView) selectedView; - if (!nodeView.getChildrenViews().isEmpty()) { + if (!nodeView.getChildViews().isEmpty()) { return CommandError.UNMODIFIABLE_BOARD.toString(); } else if (!board.getPuzzleElement(selectedPuzzleElement).isModifiable()) { return CommandError.UNMODIFIABLE_DATA.toString(); } } else { - TreeTransitionView transitionView = (TreeTransitionView) selectedView; - if (!transitionView.getTreeElement().getBoard().isModifiable()) { + if (!board.isModifiable()) { return CommandError.UNMODIFIABLE_BOARD.toString(); - } else { - if (!board.getPuzzleElement(selectedPuzzleElement).isModifiable()) { - return CommandError.UNMODIFIABLE_DATA.toString(); - } else if (!board.getPuzzleElement(selectedPuzzleElement).isModifiableCaseRule()) { - return CommandError.UNMODIFIABLE_DATA_CASE_RULE.toString(); - } + } else if (!board.isModifiableCaseRule()) { + return CommandError.UNMODIFIABLE_DATA_CASE_RULE.toString(); + } else if (!board.getPuzzleElement(selectedPuzzleElement).isModifiable()) { + return CommandError.UNMODIFIABLE_DATA.toString(); } } return null; diff --git a/src/main/java/edu/rpi/legup/history/MergeCommand.java b/src/main/java/edu/rpi/legup/history/MergeCommand.java index 2d180465b..68701e96e 100644 --- a/src/main/java/edu/rpi/legup/history/MergeCommand.java +++ b/src/main/java/edu/rpi/legup/history/MergeCommand.java @@ -107,7 +107,7 @@ public String getErrorString() { for (TreeElementView view : selection.getSelectedViews()) { if (view.getType() == TreeElementType.NODE) { TreeNodeView nodeView = (TreeNodeView) view; - if (!nodeView.getChildrenViews().isEmpty()) { + if (!nodeView.getChildViews().isEmpty()) { return CommandError.NO_CHILDREN.toString(); } nodeList.add(nodeView.getTreeElement()); diff --git a/src/main/java/edu/rpi/legup/history/ValidateCaseRuleCommand.java b/src/main/java/edu/rpi/legup/history/ValidateCaseRuleCommand.java index 3f534bcda..bcfc15f87 100644 --- a/src/main/java/edu/rpi/legup/history/ValidateCaseRuleCommand.java +++ b/src/main/java/edu/rpi/legup/history/ValidateCaseRuleCommand.java @@ -74,7 +74,7 @@ public void executeCommand() { final TreeElement finalTreeElement; if (firstSelectedView.getType() == TreeElementType.NODE) { TreeNodeView nodeView = (TreeNodeView) firstSelectedView; - finalTreeElement = nodeView.getChildrenViews().get(0).getTreeElement(); + finalTreeElement = nodeView.getChildViews().get(0).getTreeElement(); } else { TreeTransitionView transitionView = (TreeTransitionView) firstSelectedView; finalTreeElement = transitionView.getChildView().getTreeElement(); diff --git a/src/main/java/edu/rpi/legup/history/ValidateContradictionRuleCommand.java b/src/main/java/edu/rpi/legup/history/ValidateContradictionRuleCommand.java index b3e44dfbe..2cb8dbcaf 100644 --- a/src/main/java/edu/rpi/legup/history/ValidateContradictionRuleCommand.java +++ b/src/main/java/edu/rpi/legup/history/ValidateContradictionRuleCommand.java @@ -90,8 +90,8 @@ public void executeCommand() { final TreeElement finalTreeElement; if (firstSelectedView.getType() == TreeElementType.NODE) { TreeNodeView nodeView = (TreeNodeView) firstSelectedView; - if (!nodeView.getChildrenViews().isEmpty()) { - finalTreeElement = nodeView.getChildrenViews().get(0).getTreeElement(); + if (!nodeView.getChildViews().isEmpty()) { + finalTreeElement = nodeView.getChildViews().get(0).getTreeElement(); } else { finalTreeElement = null; } diff --git a/src/main/java/edu/rpi/legup/history/ValidateDirectRuleCommand.java b/src/main/java/edu/rpi/legup/history/ValidateDirectRuleCommand.java index 49bf378ee..bfdb75bd2 100644 --- a/src/main/java/edu/rpi/legup/history/ValidateDirectRuleCommand.java +++ b/src/main/java/edu/rpi/legup/history/ValidateDirectRuleCommand.java @@ -53,7 +53,7 @@ public void executeCommand() { if (element.getType() == TreeElementType.NODE) { TreeNodeView nodeView = (TreeNodeView) selectedView; - transitionView = nodeView.getChildrenViews().get(0); + transitionView = nodeView.getChildViews().get(0); } else { transitionView = (TreeTransitionView) selectedView; } @@ -89,11 +89,11 @@ public void executeCommand() { final TreeElement finalTreeElement; if (firstSelectedView.getType() == TreeElementType.NODE) { TreeNodeView nodeView = (TreeNodeView) firstSelectedView; - if (nodeView.getChildrenViews().isEmpty()) { + if (nodeView.getChildViews().isEmpty()) { LOGGER.error("NodeView has no children views"); return; } - finalTreeElement = nodeView.getChildrenViews().get(0).getTreeElement(); + finalTreeElement = nodeView.getChildViews().get(0).getTreeElement(); } else { TreeTransitionView transitionView = (TreeTransitionView) firstSelectedView; TreeNodeView childView = transitionView.getChildView(); @@ -130,7 +130,7 @@ public String getErrorString() { for (TreeElementView view : selectedViews) { if (view.getType() == TreeElementType.NODE) { TreeNodeView nodeView = (TreeNodeView) view; - if (nodeView.getChildrenViews().size() != 1) { + if (nodeView.getChildViews().size() != 1) { return CommandError.ONE_CHILD.toString(); } } else { @@ -154,7 +154,7 @@ public void undoCommand() { TreeTransitionView transitionView; if (element.getType() == TreeElementType.NODE) { TreeNodeView nodeView = (TreeNodeView) selectedView; - transitionView = nodeView.getChildrenViews().get(0); + transitionView = nodeView.getChildViews().get(0); } else { transitionView = (TreeTransitionView) selectedView; } diff --git a/src/main/java/edu/rpi/legup/model/Puzzle.java b/src/main/java/edu/rpi/legup/model/Puzzle.java index 3a9ff6612..32cc708a4 100644 --- a/src/main/java/edu/rpi/legup/model/Puzzle.java +++ b/src/main/java/edu/rpi/legup/model/Puzzle.java @@ -152,7 +152,7 @@ private void registerRules() { Rule rule = (Rule) cons.newInstance(); switch (rule.getRuleType()) { - case BASIC: + case DIRECT: this.addDirectRule((DirectRule) rule); break; case CASE: diff --git a/src/main/java/edu/rpi/legup/model/gameboard/Board.java b/src/main/java/edu/rpi/legup/model/gameboard/Board.java index 210e920ce..efacdf3e6 100644 --- a/src/main/java/edu/rpi/legup/model/gameboard/Board.java +++ b/src/main/java/edu/rpi/legup/model/gameboard/Board.java @@ -1,6 +1,9 @@ package edu.rpi.legup.model.gameboard; import edu.rpi.legup.model.Goal; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + import java.util.ArrayList; import java.util.HashSet; import java.util.List; @@ -16,17 +19,19 @@ public abstract class Board { protected Set modifiedData; protected Goal goal; protected boolean isModifiable; + protected boolean isModifiableCaseRule; - /** Board Constructor creates an empty board. */ + /** {@code Board} constructor creates an empty board. */ public Board() { this.puzzleElements = new ArrayList<>(); this.modifiedData = new HashSet<>(); this.isModifiable = true; + this.isModifiableCaseRule = true; this.goal = null; } /** - * Board Constructor creates a board with null elements. + * {@code Board} constructor creates a board with null elements. * * @param size number of elements for the board */ @@ -38,12 +43,12 @@ public Board(int size) { } /** - * Gets a specific {@link PuzzleElement} from the board. + * Gets a specific {@code PuzzleElement} from the board. * * @param puzzleElement the puzzle element to retrieve - * @return the puzzle element at the corresponding index, or null if not found + * @return the puzzle element at the corresponding index, or {@code null} if not found */ - public PuzzleElement getPuzzleElement(PuzzleElement puzzleElement) { + public PuzzleElement getPuzzleElement(@Nullable PuzzleElement puzzleElement) { if (puzzleElement == null) { return null; } @@ -52,142 +57,152 @@ public PuzzleElement getPuzzleElement(PuzzleElement puzzleElement) { } /** - * Sets a specific {@link PuzzleElement} on the board + * Sets a specific {@code PuzzleElement} on the board. * - * @param index index of the puzzleElement - * @param puzzleElement the puzzleElement to set at the index + * @param index index of the puzzle element + * @param puzzleElement the puzzle element to set at the index */ - public void setPuzzleElement(int index, PuzzleElement puzzleElement) { + public void setPuzzleElement(int index, @Nullable PuzzleElement puzzleElement) { if (index < puzzleElements.size()) { puzzleElements.set(index, puzzleElement); } } /** - * Gets the number of elements on the board + * Gets the number of elements on the board. * * @return number of elements on the board */ - public int getElementCount() { - return puzzleElements.size(); - } + public int getElementCount() { return puzzleElements.size(); } /** - * Gets the {@link PuzzleElement} on the board. + * Gets the {@code PuzzleElement}s on the board. * * @return puzzle elements on the board */ - public List getPuzzleElements() { - return puzzleElements; - } + public List getPuzzleElements() { return puzzleElements; } /** - * Sets the {@link PuzzleElement} on the board. + * Sets the {@code PuzzleElement}s on the board. * * @param puzzleElements elements on the board */ - public void setPuzzleElements(List puzzleElements) { + public void setPuzzleElements(@NotNull List puzzleElements) { this.puzzleElements = puzzleElements; } /** * Gets the modifiable attribute for the board. * - * @return true if the board is modifiable, false otherwise + * @return {@code true} if the board is modifiable; {@code false} otherwise */ - public boolean isModifiable() { - return isModifiable; - } + public boolean isModifiable() { return isModifiable; } /** * Sets the modifiable attribute for the board. * - * @param isModifiable true if the board is modifiable, false otherwise + * @param isModifiable {@code true} if the board is modifiable; {@code false} otherwise */ - public void setModifiable(boolean isModifiable) { - this.isModifiable = isModifiable; - } + public void setModifiable(boolean isModifiable) { this.isModifiable = isModifiable; } /** - * Gets whether any of {@link PuzzleElement} of this board has been modified by the user. + * Gets whether this board is modifiable as a result of a case rule. * - * @return true if the board has been modified, false otherwise + * @return {@code true} if this board is modifiable; false otherwise */ - public boolean isModified() { - return !modifiedData.isEmpty(); - } + public boolean isModifiableCaseRule() { return isModifiableCaseRule; } /** - * Gets the set of modified {@link PuzzleElement} of the board. + * Sets whether this board is modifiable as a result of a case rule. * - * @return set of modified puzzle element of the board + * @param isModifiableCaseRule {@code true} if this board is modifiable; {@code false} otherwise */ - public Set getModifiedData() { - return modifiedData; + public void setModifiableCaseRule(boolean isModifiableCaseRule) { + this.isModifiableCaseRule = isModifiableCaseRule; } /** - * Adds a {@link PuzzleElement} that has been modified to the list. + * Gets whether any of {@code PuzzleElement}s of this board has been modified by the user. + * + * @return {@code true} if the board has been modified; {@code false} otherwise + */ + public boolean isModified() { return !modifiedData.isEmpty(); } + + /** + * Gets the set of modified {@code PuzzleElement}s from the board. * - * @param puzzleElement puzzleElement that has been modified + * @return set of modified puzzle element from the board */ - public void addModifiedData(PuzzleElement puzzleElement) { + public Set getModifiedData() { return modifiedData; } + + /** + * Adds a {@code PuzzleElement} that has been modified to the list. + * + * @param puzzleElement puzzle element that has been modified + */ + public void addModifiedData(@NotNull PuzzleElement puzzleElement) { modifiedData.add(puzzleElement); puzzleElement.setModified(true); } /** - * Removes a {@link PuzzleElement} that is no longer modified. + * Removes a {@code PuzzleElement} that has no longer been modified from the list. * - * @param data puzzleElement that is no longer modified + * @param data puzzle element that has no longer been modified */ - public void removeModifiedData(PuzzleElement data) { + public void removeModifiedData(@NotNull PuzzleElement data) { modifiedData.remove(data); data.setModified(false); } /** - * Called when a {@link PuzzleElement} data on this has changed and passes in the equivalent + * Called when a {@code PuzzleElement} on this board's data has changed and passes in the equivalent * puzzle element with the new data. * - * @param puzzleElement equivalent puzzle element with the new data. + * @param puzzleElement equivalent puzzle element with the new data */ - @SuppressWarnings("unchecked") - public void notifyChange(PuzzleElement puzzleElement) { + public void notifyChange(@NotNull PuzzleElement puzzleElement) { puzzleElements.set(puzzleElement.getIndex(), puzzleElement); } /** - * Called when a {@link PuzzleElement} has been added and passes in the equivalent puzzle + * Called when a {@code PuzzleElement} has been added and passes in the equivalent puzzle * element with the data. * - * @param puzzleElement equivalent puzzle element with the data. + * @param puzzleElement equivalent puzzle element */ - public void notifyAddition(PuzzleElement puzzleElement) {} + public void notifyAddition(@NotNull PuzzleElement puzzleElement) {} /** - * Called when a {@link PuzzleElement} has been deleted and passes in the equivalent puzzle - * element with the data. + * Called when a {@code PuzzleElement} has been deleted and passes in the equivalent puzzle element. * - * @param puzzleElement equivalent puzzle element with the data. + * @param puzzleElement equivalent puzzle element */ - public void notifyDeletion(PuzzleElement puzzleElement) {} + public void notifyDeletion(@NotNull PuzzleElement puzzleElement) {} - @SuppressWarnings("unchecked") - public Board mergedBoard(Board lca, List boards) { - if (lca == null || boards.isEmpty()) { - return null; - } + /** + * Creates a {@code Board} that is the result of merging all the boards in {@code boards}. For each puzzle + * element, if the value is shared by all of the {@code boards}, that value is used; otherwise the value from + * {@code lca} is used. + * + * @param lca the lowest common ancestor of all of the {@code boards} + * @param boards the boards to merge + * @return the result of merging all of the {@code boards} + */ + public Board mergedBoard(@NotNull Board lca, @NotNull List boards) { Board mergedBoard = lca.copy(); - Board firstBoard = boards.get(0); + Board firstBoard = boards.getFirst(); for (PuzzleElement lcaData : lca.getPuzzleElements()) { PuzzleElement mData = firstBoard.getPuzzleElement(lcaData); boolean isSame = true; for (Board board : boards) { - isSame &= mData.equalsData(board.getPuzzleElement(lcaData)); + if (!mData.equalsData(board.getPuzzleElement(lcaData))) { + isSame = false; + break; + } } if (isSame && !lcaData.equalsData(mData)) { @@ -201,13 +216,12 @@ public Board mergedBoard(Board lca, List boards) { } /** - * Determines if this board contains the equivalent puzzle elements as the one specified + * Determines if this board contains the equivalent puzzle elements as the one specified. * * @param board board to check equivalence - * @return true if the boards are equivalent, false otherwise + * @return {@code true} if the boards are equivalent; {@code false} otherwise */ - @SuppressWarnings("unchecked") - public boolean equalsBoard(Board board) { + public boolean equalsBoard(@NotNull Board board) { for (PuzzleElement element : puzzleElements) { if (!element.equalsData(board.getPuzzleElement(element))) { return false; diff --git a/src/main/java/edu/rpi/legup/model/gameboard/GridBoard.java b/src/main/java/edu/rpi/legup/model/gameboard/GridBoard.java index b4a976ae2..fcb2beb7e 100644 --- a/src/main/java/edu/rpi/legup/model/gameboard/GridBoard.java +++ b/src/main/java/edu/rpi/legup/model/gameboard/GridBoard.java @@ -1,13 +1,12 @@ package edu.rpi.legup.model.gameboard; import edu.rpi.legup.model.elements.Element; -import edu.rpi.legup.puzzle.treetent.TreeTentBoard; -import edu.rpi.legup.puzzle.treetent.TreeTentClue; -import java.awt.*; -import java.awt.event.MouseEvent; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.awt.*; +import java.awt.event.MouseEvent; + /** * GridBoard represents a grid-based board where each cell can be manipulated based on its * coordinates. The board supports operations such as getting and setting cells, and provides @@ -18,7 +17,7 @@ public class GridBoard extends Board { protected Dimension dimension; /** - * GridBoard Constructor creates a board for grid using puzzles from a width and height. + * {@code GridBoard} constructor creates a board for grid using puzzles from a width and height. * * @param width width of the board * @param height height of the board @@ -32,22 +31,20 @@ public GridBoard(int width, int height) { } /** - * GridBoard Constructor creates a board for grid using puzzles from a size. + * {@code GridBoard} constructor creates a board for grid using puzzles from a size. * - * @param size width and height of the GridBoard + * @param size width and height of the {@code GridBoard} */ - public GridBoard(int size) { - this(size, size); - } + public GridBoard(int size) { this(size, size); } /** - * Gets a {@link GridCell} from the board. + * Gets a {@code GridCell} from the board. * * @param x x location of the cell * @param y y location of the cell - * @return grid cell at location (x, y) + * @return grid cell at location {@code (x, y)} */ - @Nullable public GridCell getCell(int x, int y) { + public GridCell getCell(int x, int y) { if (y * dimension.width + x >= puzzleElements.size() || x >= dimension.width || y >= dimension.height @@ -58,17 +55,21 @@ public GridBoard(int size) { return (GridCell) puzzleElements.get(y * dimension.width + x); } - public GridCell getCell(Point point) { - return getCell(point.x, point.y); - } + /** + * Gets a {@code GridCell} from the board. + * + * @param point x and y location of the cell + * @return grid cell at {@code point} + */ + public GridCell getCell(@NotNull Point point) { return getCell(point.x, point.y); } /** - * Sets the {@link GridCell} at the location (x,y). This method does not set the cell if the + * Sets the {@code GridCell} at the location {@code (x,y)}. This method does not set the cell if the * location specified is out of bounds. * * @param x x location of the cell * @param y y location of the cell - * @param cell grid cell to set at location (x,y) + * @param cell grid cell to set at location {@code (x,y)} */ public void setCell(int x, int y, @NotNull GridCell cell) { if (y * dimension.width + x >= puzzleElements.size() @@ -82,65 +83,19 @@ public void setCell(int x, int y, @NotNull GridCell cell) { } /** - * Sets the {@link GridCell} at the location (x,y). This method does not set the cell if the + * Sets the {@code GridCell} at the location {@code (x,y)}. This method does not set the cell if the * location specified is out of bounds. * * @param x x location of the cell * @param y y location of the cell - * @param e Puzzle Element to set cell to + * @param e puzzle element to set cell to * @param m mouse click event, used to differentiate left/right click */ public void setCell(int x, int y, @Nullable Element e, @NotNull MouseEvent m) { - if (this instanceof TreeTentBoard - && ((y == dimension.height && 0 <= x && x < dimension.width) - || (x == dimension.width && 0 <= y && y < dimension.height))) { - TreeTentBoard treeTentBoard = ((TreeTentBoard) this); - TreeTentClue clue = treeTentBoard.getClue(x, y); - if (y == dimension.height) { - if (m.getButton() == MouseEvent.BUTTON1) { - if (clue.getData() < dimension.height) { - clue.setData(clue.getData() + 1); - } else { - clue.setData(0); - } - } else { - if (clue.getData() > 0) { - clue.setData(clue.getData() - 1); - } else { - clue.setData(dimension.height); - } - } - } else { // x == dimension.width - if (m.getButton() == MouseEvent.BUTTON1) { - if (clue.getData() < dimension.width) { - clue.setData(clue.getData() + 1); - } else { - clue.setData(0); - } - } else { - if (clue.getData() > 0) { - clue.setData(clue.getData() - 1); - } else { - clue.setData(dimension.width); - } - } - } - } else { - if (e != null && y * dimension.width + x >= puzzleElements.size() - || x >= dimension.width - || y >= dimension.height - || x < 0 - || y < 0) { - return; - } else { - if (e != null) { - puzzleElements.get(y * dimension.width + x).setType(e, m); - } - } + + if (e != null && x < dimension.width && y < dimension.height && x >= 0 && y >= 0) { + puzzleElements.get(y * dimension.width + x).setType(e, m); } - // puzzleElements.set(y * dimension.width + x, puzzleElements.get(y * dimension.width - // + - // x)); } /** @@ -148,33 +103,23 @@ public void setCell(int x, int y, @Nullable Element e, @NotNull MouseEvent m) { * * @return width of the board */ - public int getWidth() { - return dimension.width; - } + public int getWidth() { return dimension.width; } /** * Gets the height of the board. * * @return height of the board */ - public int getHeight() { - return dimension.height; - } + public int getHeight() { return dimension.height; } /** - * Gets the dimension of the grid board + * Gets the dimension of the grid board. * * @return the dimension of the grid board */ - @NotNull public Dimension getDimension() { - return dimension; - } + @NotNull public Dimension getDimension() { return dimension; } - /** - * Performs a deep copy of the Board - * - * @return a new copy of the board that is independent of this one - */ + @Override @NotNull public GridBoard copy() { GridBoard newGridBoard = new GridBoard(this.dimension.width, this.dimension.height); for (int x = 0; x < this.dimension.width; x++) { diff --git a/src/main/java/edu/rpi/legup/model/gameboard/PuzzleElement.java b/src/main/java/edu/rpi/legup/model/gameboard/PuzzleElement.java index f4fab6abc..1310305bb 100644 --- a/src/main/java/edu/rpi/legup/model/gameboard/PuzzleElement.java +++ b/src/main/java/edu/rpi/legup/model/gameboard/PuzzleElement.java @@ -16,7 +16,6 @@ public abstract class PuzzleElement { protected T goalData; protected boolean isModifiable; protected boolean isModified; - protected boolean isModifiableCaseRule; protected boolean isGiven; protected boolean isGoal; protected boolean isValid; @@ -28,7 +27,6 @@ public PuzzleElement() { this.data = null; this.goalData = null; this.isModifiable = true; - this.isModifiableCaseRule = true; this.isModified = false; this.isGiven = false; this.isGoal = false; @@ -104,24 +102,6 @@ public void setModifiable(boolean isModifiable) { this.isModifiable = isModifiable; } - /** - * Gets whether this puzzle element is modifiable as a result of a case rule. - * - * @return true if this puzzle element is modifiable, false otherwise - */ - public boolean isModifiableCaseRule() { - return isModifiableCaseRule; - } - - /** - * Sets whether this puzzle element is modifiable as a result of a case rule. - * - * @param isModifiableCaseRule true if this puzzle element is modifiable, false otherwise - */ - public void setModifiableCaseRule(boolean isModifiableCaseRule) { - this.isModifiableCaseRule = isModifiableCaseRule; - } - /** * Gets whether the puzzle element has been modified. * diff --git a/src/main/java/edu/rpi/legup/model/observer/ITreeListener.java b/src/main/java/edu/rpi/legup/model/observer/ITreeListener.java index 713b464e2..fb3e3bbbd 100644 --- a/src/main/java/edu/rpi/legup/model/observer/ITreeListener.java +++ b/src/main/java/edu/rpi/legup/model/observer/ITreeListener.java @@ -28,7 +28,4 @@ public interface ITreeListener { * @param selection tree view selection that was changed */ void onTreeSelectionChanged(TreeViewSelection selection); - - /** Called when the model has finished updating the tree. */ - void onUpdateTree(); } diff --git a/src/main/java/edu/rpi/legup/model/rules/DirectRule.java b/src/main/java/edu/rpi/legup/model/rules/DirectRule.java index 76d1b968f..3a1902b46 100644 --- a/src/main/java/edu/rpi/legup/model/rules/DirectRule.java +++ b/src/main/java/edu/rpi/legup/model/rules/DirectRule.java @@ -1,6 +1,6 @@ package edu.rpi.legup.model.rules; -import static edu.rpi.legup.model.rules.RuleType.BASIC; +import static edu.rpi.legup.model.rules.RuleType.DIRECT; import edu.rpi.legup.model.gameboard.Board; import edu.rpi.legup.model.gameboard.PuzzleElement; @@ -26,7 +26,7 @@ public abstract class DirectRule extends Rule { */ public DirectRule(String ruleID, String ruleName, String description, String imageName) { super(ruleID, ruleName, description, imageName); - this.ruleType = BASIC; + this.ruleType = DIRECT; } /** diff --git a/src/main/java/edu/rpi/legup/model/rules/RuleType.java b/src/main/java/edu/rpi/legup/model/rules/RuleType.java index 7eee80ea3..fc7fa7d63 100644 --- a/src/main/java/edu/rpi/legup/model/rules/RuleType.java +++ b/src/main/java/edu/rpi/legup/model/rules/RuleType.java @@ -5,7 +5,7 @@ * rules into various types based on their functionality and application. */ public enum RuleType { - BASIC, + DIRECT, CASE, CONTRADICTION, MERGE diff --git a/src/main/java/edu/rpi/legup/model/tree/Tree.java b/src/main/java/edu/rpi/legup/model/tree/Tree.java index cfd453515..8d0e479b3 100644 --- a/src/main/java/edu/rpi/legup/model/tree/Tree.java +++ b/src/main/java/edu/rpi/legup/model/tree/Tree.java @@ -1,8 +1,9 @@ package edu.rpi.legup.model.tree; -import edu.rpi.legup.controller.TreeController; import edu.rpi.legup.model.gameboard.Board; -import edu.rpi.legup.ui.proofeditorui.treeview.TreeView; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + import java.util.ArrayList; import java.util.HashSet; import java.util.List; @@ -16,19 +17,17 @@ public class Tree { private TreeNode rootNode; /** - * Tree Constructor creates the tree structure from the initial {@link Board} + * {@code Tree} constructor creates the tree structure from the initial {@code Board}. * * @param initBoard initial board */ - public Tree(Board initBoard) { + public Tree(@NotNull Board initBoard) { this.rootNode = new TreeNode(initBoard); this.rootNode.setRoot(true); } - /** Tree Constructor creates the tree structure with null root node */ - public Tree() { - this.rootNode = null; - } + /** {@code Tree} constructor creates the tree structure with {@code null} root node. */ + public Tree() { this.rootNode = null; } /** * Adds a new transition to the specified node. @@ -36,7 +35,7 @@ public Tree() { * @param treeNode the node to add a transition to * @return the created transition */ - public TreeTransition addNewTransition(TreeNode treeNode) { + public TreeTransition addNewTransition(@NotNull TreeNode treeNode) { TreeTransition transition = new TreeTransition(treeNode, treeNode.getBoard().copy()); treeNode.addChild(transition); treeNode.getChildren().forEach(TreeTransition::reverify); @@ -49,7 +48,7 @@ public TreeTransition addNewTransition(TreeNode treeNode) { * @param element the tree element to add * @return the added tree element */ - public TreeElement addTreeElement(TreeElement element) { + public TreeElement addTreeElement(@NotNull TreeElement element) { if (element.getType() == TreeElementType.NODE) { TreeNode treeNode = (TreeNode) element; return addTreeElement( @@ -69,7 +68,7 @@ public TreeElement addTreeElement(TreeElement element) { * @param transition the transition to associate with the node * @return the added transition */ - public TreeElement addTreeElement(TreeNode treeNode, TreeTransition transition) { + public TreeElement addTreeElement(@NotNull TreeNode treeNode, @NotNull TreeTransition transition) { treeNode.addChild(transition); treeNode.getChildren().forEach(TreeTransition::reverify); return transition; @@ -82,7 +81,7 @@ public TreeElement addTreeElement(TreeNode treeNode, TreeTransition transition) * @param treeNode the tree node to associate with the transition * @return the added tree node */ - public TreeElement addTreeElement(TreeTransition transition, TreeNode treeNode) { + public TreeElement addTreeElement(@NotNull TreeTransition transition, @NotNull TreeNode treeNode) { transition.setChildNode(treeNode); treeNode.setParent(transition); return treeNode; @@ -93,37 +92,50 @@ public TreeElement addTreeElement(TreeTransition transition, TreeNode treeNode) * * @param element the tree element to remove */ - public void removeTreeElement(TreeElement element) { - if (element.getType() == TreeElementType.NODE) { - TreeNode node = (TreeNode) element; + public void removeTreeElement(@NotNull TreeElement element) { + if (element.getType() == TreeElementType.NODE) { removeTreeNode((TreeNode) element); } + else { removeTreeTransition((TreeTransition) element); } + } - node.getParent().removeChild(node); + /** + * Recursively removes a {@code TreeNode} from the tree. + * + * @param node {@code TreeNode} to remove subtree of + */ + private void removeTreeNode(@NotNull TreeNode node) { + while (!node.getChildren().isEmpty()) { removeTreeTransition(node.getChildren().getFirst()); } + if (node.getParent() != null) { node.getParent().setChildNode(null); - } else { - TreeTransition transition = (TreeTransition) element; - - transition.getParents().forEach(n -> n.removeChild(transition)); - TreeController treeController = new TreeController(); - TreeView treeView = new TreeView(treeController); - treeView.removeTreeTransition(transition); - transition.getParents().get(0).getChildren().forEach(TreeTransition::reverify); + node.setParent(null); } } /** - * Determines if the tree is valid by checking whether this tree puzzleElement and all - * descendants of this tree puzzleElement is justified and justified correctly + * Recursively removes a {@code TreeTransition} from the tree. * - * @return true if tree is valid, false otherwise + * @param transition {@code TreeTransition} to remove subtree of */ - public boolean isValid() { - return rootNode.isValidBranch(); + private void removeTreeTransition(@NotNull TreeTransition transition) { + if (transition.getChildNode() != null) { removeTreeNode(transition.getChildNode()); } + for (TreeNode parent : transition.getParents()) { + parent.removeChild(transition); + parent.getChildren().forEach(TreeTransition::reverify); + } + transition.setParents(new ArrayList<>()); } /** - * Gets a Set of TreeNodes that are leaf nodes + * Determines if the tree is valid by checking whether this tree element and all + * descendants of this tree element is justified and justified correctly + * + * @return {@code true} if tree is valid; {@code false} otherwise + */ + public boolean isValid() { return rootNode.isValidBranch(); } + + /** + * Gets a {@code Set} of {@code TreeNodes} that are leaf nodes of the tree. * - * @return Set of TreeNodes that are leaf nodes + * @return {@code Set} of {@code TreeNodes} that are leaf nodes */ public Set getLeafTreeElements() { Set leafs = new HashSet<>(); @@ -132,24 +144,24 @@ public Set getLeafTreeElements() { } /** - * Gets a Set of TreeNodes that are leaf nodes from the subtree rooted at the specified node + * Gets a {@code Set} of {@code TreeNodes} that are leaf nodes of the subtree rooted at the specified node. * - * @param node node that is input - * @return Set of TreeNodes that are leaf nodes from the subtree + * @param node root of the subtree + * @return {@code Set} of {@code TreeNodes} that are leaf nodes of the subtree */ - public Set getLeafTreeElements(TreeNode node) { + public Set getLeafTreeElements(@NotNull TreeNode node) { Set leafs = new HashSet<>(); getLeafTreeElements(leafs, node); return leafs; } /** - * Recursively gets a Set of TreeNodes that are leaf nodes + * Recursively gets a {@code Set} of {@code TreeNodes} that are leaf nodes. * - * @param leafs Set of TreeNodes that are leaf nodes - * @param element current TreeNode being evaluated + * @param leafs {@code Set} of {@code TreeNodes} that are leaf nodes + * @param element current {@code TreeNode} being evaluated */ - private void getLeafTreeElements(Set leafs, TreeElement element) { + private void getLeafTreeElements(@NotNull Set leafs, @NotNull TreeElement element) { if (element.getType() == TreeElementType.NODE) { TreeNode node = (TreeNode) element; List childTrans = node.getChildren(); @@ -170,48 +182,46 @@ private void getLeafTreeElements(Set leafs, TreeElement element) { } /** - * Gets the lowest common ancestor (LCA) among the list of {@link TreeNode} passed into the + * Gets the lowest common ancestor (LCA) among the list of {@link TreeNode}s passed into the * function. This lowest common ancestor is the most immediate ancestor node such that the list - * of tree nodes specified are descendants of the node. This will return null if no such - * ancestor exists + * of tree nodes specified are descendants of the node. * - * @param nodes list of tree nodes to find the LCA - * @return the first ancestor node that all tree nodes have in common, otherwise null if none - * exists + * @param nodes list of tree nodes to find the LCA of + * @return the first ancestor node that all tree nodes have in common + * @throws IllegalArgumentException if {@code nodes} is empty or tree nodes do not all belong to the same tree */ - public static TreeNode getLowestCommonAncestor(List nodes) { - if (nodes.isEmpty()) { - return null; - } else { - if (nodes.size() == 1) { - return nodes.get(0); - } else { + public static TreeNode getLowestCommonAncestor(@NotNull List nodes) { + if (!nodes.isEmpty()) { + + if (nodes.size() == 1) { return nodes.getFirst(); } + else { List> ancestors = new ArrayList<>(); - for (TreeNode node : nodes) { - ancestors.add(node.getAncestors()); - } + for (TreeNode node : nodes) { ancestors.add(node.getAncestors()); } - List first = ancestors.get(0); + List first = ancestors.getFirst(); for (TreeNode node : first) { + boolean isCommon = true; for (List nList : ancestors) { - isCommon &= nList.contains(node); + if (!nList.contains(node)) { + isCommon = false; + break; + } } - if (isCommon) { - return node; - } + if (isCommon) { return node; } } } } - return null; + + throw new IllegalArgumentException("List of nodes is empty or the nodes do not all belong to the same tree."); } /** - * Determines if the tree contains all contradictory branches (puzzle has no solution) + * Determines if the tree contains all contradictory branches (puzzle has no solution). * - * @return true if the whole tree is contradictory, false otherwise + * @return {@code true} if the whole tree is contradictory; {@code false} otherwise */ public boolean isContradictory() { for (TreeElement leaf : getLeafTreeElements()) { @@ -227,23 +237,24 @@ public boolean isContradictory() { } /** - * Gets the root node of this tree + * Gets the root node of this tree. * * @return the root node of the tree */ - public TreeNode getRootNode() { - return rootNode; - } + public TreeNode getRootNode() { return rootNode; } /** - * Sets the root node of this tree + * Sets the root node of this tree. * * @param rootNode the root node of the tree */ - public void setRootNode(TreeNode rootNode) { - this.rootNode = rootNode; - } + public void setRootNode(@Nullable TreeNode rootNode) { this.rootNode = rootNode; } + /** + * Checks if every leaf of the tree is a {@code TreeNode}. + * + * @return {@code true} if every leaf of the tree is a {@code TreeNode}; {@code false} otherwise + */ public boolean isClosed() { for (TreeElement leaf : getLeafTreeElements()) { if (leaf.getType() != TreeElementType.NODE) { diff --git a/src/main/java/edu/rpi/legup/model/tree/TreeNode.java b/src/main/java/edu/rpi/legup/model/tree/TreeNode.java index f8ea768f9..84d4de6a3 100644 --- a/src/main/java/edu/rpi/legup/model/tree/TreeNode.java +++ b/src/main/java/edu/rpi/legup/model/tree/TreeNode.java @@ -1,8 +1,11 @@ package edu.rpi.legup.model.tree; import edu.rpi.legup.model.gameboard.Board; +import edu.rpi.legup.model.rules.CaseRule; import edu.rpi.legup.model.rules.RuleType; import edu.rpi.legup.utility.DisjointSets; +import org.jetbrains.annotations.NotNull; + import java.util.*; /** @@ -11,16 +14,17 @@ * the tree. */ public class TreeNode extends TreeElement { + private TreeTransition parent; private List children; private boolean isRoot; /** - * TreeNode Constructor creates a tree node whenever a rule has been made + * {@code TreeNode} constructor creates a tree node whenever a rule has been made. * * @param board board associated with this tree node */ - public TreeNode(Board board) { + public TreeNode(@NotNull Board board) { super(TreeElementType.NODE); this.board = board; this.parent = null; @@ -30,9 +34,9 @@ public TreeNode(Board board) { /** * Determines if this tree node leads to a contradiction. Every path from this tree node must - * lead to a contradiction including all of its children + * lead to a contradiction including all of its children. * - * @return true if this tree node leads to a contradiction, false otherwise + * @return {@code true} if this tree node leads to a contradiction; {@code false} otherwise */ @Override public boolean isContradictoryBranch() { @@ -50,12 +54,12 @@ public boolean isContradictoryBranch() { } /** - * Recursively determines if the sub tree rooted at this tree puzzleElement is valid by checking - * whether this tree puzzleElement and all descendants of this tree puzzleElement is justified - * and justified correctly + * Recursively determines if the subtree rooted at this tree element is valid by checking + * whether this tree element and all descendants of this tree element is justified + * and justified correctly. * - * @return true if this tree puzzleElement and all descendants of this tree puzzleElement is - * valid, false otherwise + * @return {@code true} if this tree element and all descendants of this tree element is + * valid; {@code false} otherwise */ @Override public boolean isValidBranch() { @@ -68,7 +72,7 @@ public boolean isValidBranch() { } /** - * Gets a list of the ancestors of this node + * Gets a list of the ancestors of this node. * * @return list of all the ancestors for this node */ @@ -95,7 +99,7 @@ public List getAncestors() { } /** - * Gets a list of the descendants of this node + * Gets a list of the descendants of this node. * * @return list of all the descendants for this node */ @@ -128,11 +132,11 @@ public List getDescendants() { } /** - * Gets a DisjointSets containing the children of this node such that the sets contained within - * the DisjointSets are such that elements in the same set are branches of this tree node that + * Gets a {@code DisjointSets} containing the children of this node such that the sets contained within + * the {@code DisjointSets} are such that elements in the same set are branches of this tree node that * will eventually merge. This could mean that multiple merges take place before this happens. * - * @return DisjointSets of tree transitions containing unique non-merging branches + * @return {@code DisjointSets} of tree transitions containing unique non-merging branches */ public DisjointSets findMergingBranches() { DisjointSets branches = new DisjointSets<>(); @@ -179,13 +183,13 @@ public DisjointSets findMergingBranches() { /** * Finds the point at which the set of tree elements passed in will merge. This must be a set - * gotten from findMergingBranches method DisjointSets + * gotten from {@link #findMergingBranches()} method {@code DisjointSets}. * - * @param branches tree elements to find the merging point - * @return tree transition of the merging point or null if no such point exists + * @param branches tree elements to find the merging point of + * @return tree transition of the merging point or {@code null} if no such point exists */ @SuppressWarnings("unchecked") - public static TreeTransition findMergingPoint(Set branches) { + public static TreeTransition findMergingPoint(@NotNull Set branches) { DisjointSets mergeSet = new DisjointSets<>(); Set branchesCopy = new HashSet<>(branches); TreeElement headBranch = branchesCopy.iterator().next(); @@ -247,102 +251,110 @@ public static TreeTransition findMergingPoint(Set branche } /** - * Determines if the specified tree transition is a parent of this node + * Determines if the specified tree transition is a parent of this node. * * @param parent tree transition that could be a parent - * @return true if the specified tree transition is a parent of this node, false otherwise + * @return {@code true} if the specified tree transition is a parent of this node; {@code false} otherwise */ - public boolean isParent(TreeTransition parent) { + public boolean isParent(@NotNull TreeTransition parent) { return this.parent == parent; } /** - * Adds a child to this tree node + * Adds a child to this tree node. * * @param child child to add */ - public void addChild(TreeTransition child) { + public void addChild(@NotNull TreeTransition child) { children.add(child); + if (parent != null && ((child.getRule() instanceof CaseRule) || !child.getBoard().isModifiableCaseRule())) { + parent.propagateModifiableCaseRule(false); + } } /** - * Removes a child to this tree node + * Removes a child to this tree node. * * @param child child to remove */ - public void removeChild(TreeTransition child) { + public void removeChild(@NotNull TreeTransition child) { children.remove(child); + if (parent != null && ((child.getRule() instanceof CaseRule) || !child.getBoard().isModifiableCaseRule())) { + parent.propagateModifiableCaseRule(true); + } } /** - * Determines if the specified tree node is a child of this node + * Determines if the specified tree node is a child of this node. * * @param child tree node that could be a child - * @return true if the specified tree node is a child of this node, false otherwise + * @return {@code true} if the specified tree node is a child of this node; {@code false} otherwise */ - public boolean isChild(TreeNode child) { - return children.contains(child); - } + public boolean isChild(@NotNull TreeNode child) { return children.contains(child); } /** - * Gets the TreeNode's parent + * Gets the {@code TreeNode}'s parent. * - * @return the TreeNode's parent + * @return the {@code TreeNode}'s parent */ - public TreeTransition getParent() { - return parent; - } + public TreeTransition getParent() { return parent; } /** - * Sets the TreeNode's parent + * Sets the {@code TreeNode}'s parent. * - * @param parent the TreeNode's parent + * @param parent the {@code TreeNode}'s parent */ - public void setParent(TreeTransition parent) { - this.parent = parent; - } + public void setParent(TreeTransition parent) { this.parent = parent; } /** - * Gets the TreeNode's children + * Gets the {@code TreeNode}'s children. * - * @return the TreeNode's children + * @return the {@code TreeNode}'s children */ - public List getChildren() { - return children; - } + public List getChildren() { return children; } /** - * Sets the TreeNode's children + * Sets the {@code TreeNode}'s children. * - * @param children the TreeNode's children + * @param children the {@code TreeNode}'s children */ public void setChildren(List children) { + boolean hadCaseRule = false; + boolean hasCaseRule = false; + if (parent != null) { + + for (TreeTransition child : this.children) { + if ((child.getRule() instanceof CaseRule) || !child.getBoard().isModifiableCaseRule()) { + hadCaseRule = true; + break; + } + } + for (TreeTransition child : children) { + if ((child.getRule() instanceof CaseRule) || !child.getBoard().isModifiableCaseRule()) { + hasCaseRule = true; + break; + } + } + } + this.children = children; + if (hasCaseRule != hadCaseRule) { parent.propagateModifiableCaseRule(!hasCaseRule); } } /** - * Is this node the root of the tree + * Determines if this node the root of the tree. * - * @return true if this node is the root of the tree, false otherwise + * @return {@code true} if this node is the root of the tree; {@code false} otherwise */ - public boolean isRoot() { - return isRoot; - } + public boolean isRoot() { return isRoot; } /** - * Sets the root of the tree + * Sets whether this node is the root of the tree. * - * @param isRoot true if this node is the root of the tree, false otherwise + * @param isRoot {@code true} if this node is the root of the tree; {@code false} otherwise */ - public void setRoot(boolean isRoot) { - this.isRoot = isRoot; - } + public void setRoot(boolean isRoot) { this.isRoot = isRoot; } - /** - * Clears all children transitions from this tree node. After calling this method, the node will - * have no child transitions. - */ - public void clearChildren() { - this.children.clear(); - } + /** Clears all children transitions from this tree node. */ + public void clearChildren() { this.children.clear(); } } diff --git a/src/main/java/edu/rpi/legup/model/tree/TreeTransition.java b/src/main/java/edu/rpi/legup/model/tree/TreeTransition.java index 10c8fc7a0..43d9dd600 100644 --- a/src/main/java/edu/rpi/legup/model/tree/TreeTransition.java +++ b/src/main/java/edu/rpi/legup/model/tree/TreeTransition.java @@ -5,11 +5,14 @@ import edu.rpi.legup.model.rules.CaseRule; import edu.rpi.legup.model.rules.Rule; import edu.rpi.legup.model.rules.RuleType; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + import java.util.ArrayList; import java.util.List; /** - * Represents a transition between two nodes in a tree structure within a game. A transition is + * Represents a transition between two nodes in a tree structure within a proof. A transition is * responsible for propagating changes through the tree and managing and verifying the associated * rules and puzzle elements. */ @@ -23,11 +26,11 @@ public class TreeTransition extends TreeElement { private boolean isVerified; /** - * TreeTransition Constructor create a transition from one node to another + * {@code TreeTransition} constructor creates a transition from one node to another. * * @param board board state of the transition */ - public TreeTransition(Board board) { + public TreeTransition(@NotNull Board board) { super(TreeElementType.TRANSITION); this.parents = new ArrayList<>(); this.childNode = null; @@ -39,23 +42,23 @@ public TreeTransition(Board board) { } /** - * TreeTransition Constructor - create a transition from one node to another + * {@code TreeTransition} constructor creates a transition from one node to another. * * @param parent parent tree node associated with the transition * @param board board state of the transition */ - public TreeTransition(TreeNode parent, Board board) { + public TreeTransition(@NotNull TreeNode parent, @NotNull Board board) { this(board); this.parents.add(parent); } /** - * Recursively propagates the change of puzzleElement down the tree + * Recursively propagates the change of a puzzle element down the tree. * - * @param element puzzleElement of the change made + * @param element puzzle element of the change made */ @SuppressWarnings("unchecked") - public void propagateChange(PuzzleElement element) { + public void propagateChange(@NotNull PuzzleElement element) { if (isJustified() && rule.getRuleType() == RuleType.MERGE) { TreeNode lca = Tree.getLowestCommonAncestor(parents); Board lcaBoard = lca.getBoard(); @@ -96,12 +99,12 @@ public void propagateChange(PuzzleElement element) { // Overwrite previous modifications to this element board.removeModifiedData(board.getPuzzleElement(element)); - // apply changes to tranistion + // apply changes to transition board.notifyChange(element); // mark first transition as modified if (!board.getPuzzleElement(element) - .equalsData(parents.get(0).getBoard().getPuzzleElement(element))) { + .equalsData(parents.getFirst().getBoard().getPuzzleElement(element))) { board.addModifiedData(element); } @@ -111,7 +114,7 @@ public void propagateChange(PuzzleElement element) { // find starting board TreeNode head = childNode; while (head.getParent() != null) { - head = head.getParent().getParents().get(0); + head = head.getParent().getParents().getFirst(); } Board headBoard = head.getBoard(); @@ -136,12 +139,12 @@ public void propagateChange(PuzzleElement element) { } /** - * Recursively propagates the addition of puzzleElement down the tree + * Recursively propagates the addition of a puzzle element down the tree. * - * @param element puzzleElement of the addition made + * @param element puzzle element of the addition made */ @SuppressWarnings("unchecked") - public void propagateAddition(PuzzleElement element) { + public void propagateAddition(@NotNull PuzzleElement element) { if (isJustified() && rule.getRuleType() == RuleType.MERGE) { TreeNode lca = Tree.getLowestCommonAncestor(parents); Board lcaBoard = lca.getBoard(); @@ -189,12 +192,12 @@ public void propagateAddition(PuzzleElement element) { } /** - * Recursively propagates the change of puzzleElement down the tree + * Recursively propagates the change of puzzle element down the tree. * - * @param element puzzleElement of the change made + * @param element puzzle element of the change made */ @SuppressWarnings("unchecked") - public void propagateDeletion(PuzzleElement element) { + public void propagateDeletion(@NotNull PuzzleElement element) { if (isJustified() && rule.getRuleType() == RuleType.MERGE) { TreeNode lca = Tree.getLowestCommonAncestor(parents); Board lcaBoard = lca.getBoard(); @@ -243,9 +246,9 @@ public void propagateDeletion(PuzzleElement element) { /** * Determines if this tree node leads to a contradiction. Every path from this tree node must - * lead to a contradiction including all of its children + * lead to a contradiction including all of its children. * - * @return true if this tree node leads to a contradiction, false otherwise + * @return {@code true} if this tree node leads to a contradiction; {@code false} otherwise */ @Override public boolean isContradictoryBranch() { @@ -261,12 +264,12 @@ public boolean isContradictoryBranch() { } /** - * Recursively determines if the sub tree rooted at this tree puzzleElement is valid by checking - * whether this tree puzzleElement and all descendants of this tree puzzleElement is justified - * and justified correctly + * Recursively determines if the subtree rooted at this tree puzzle element is valid by checking + * whether this tree puzzle element and all descendants of this tree puzzle element are justified + * and justified correctly. * - * @return true if this tree puzzleElement and all descendants of this tree puzzleElement is - * valid, false otherwise + * @return {@code true} if this tree puzzle element and all descendants of this tree puzzle element + * are valid; {@code false} otherwise */ @Override public boolean isValidBranch() { @@ -274,110 +277,111 @@ public boolean isValidBranch() { } /** - * Gets the parent tree nodes of this transition + * Gets the parent tree nodes of this transition. * * @return parent tree nodes of this tree transition */ - public ArrayList getParents() { - return parents; - } + public ArrayList getParents() { return parents; } /** - * Sets the parent tree nodes of this transition + * Sets the parent tree nodes of this transition. * * @param parents parents tree nodes of this tree transition */ - public void setParents(ArrayList parents) { - this.parents = parents; - } + public void setParents(@NotNull ArrayList parents) { this.parents = parents; } /** - * Adds a parent tree node to this tree transition + * Adds a parent tree node to this tree transition. * * @param parent parent tree node to add */ - public void addParent(TreeNode parent) { - parents.add(parent); - } + public void addParent(@NotNull TreeNode parent) { parents.add(parent); } /** - * Removes a parent tree node to this tree transition + * Removes a parent tree node from this tree transition. * * @param parent parent tree node to remove */ - public void removeParent(TreeNode parent) { - parents.remove(parent); - } + public void removeParent(@NotNull TreeNode parent) { parents.remove(parent); } /** - * Determines if the specified tree node is a parent of this transition + * Determines if the specified tree node is a parent of this transition. * * @param parent tree node that could be a parent - * @return true if the specified tree node is a parent of this transition, false otherwise + * @return {@code true} if the specified tree node is a parent of this transition; {@code false} otherwise */ - public boolean isParent(TreeNode parent) { - return parents.contains(parent); - } + public boolean isParent(@NotNull TreeNode parent) { return parents.contains(parent); } /** - * Gets the childNode tree node of this transition + * Gets the child tree node of this transition. * - * @return childNode tree node + * @return child tree node */ - public TreeNode getChildNode() { - return childNode; - } + public TreeNode getChildNode() { return childNode; } /** - * Sets the childNode tree node of this transition + * Sets the child tree node of this transition. * - * @param childNode childNode tree node + * @param childNode new child tree node */ - public void setChildNode(TreeNode childNode) { - this.childNode = childNode; - } + public void setChildNode(@Nullable TreeNode childNode) { this.childNode = childNode; } /** - * Removes the child to this tree transition + * Gets the rule associated with this transition. * - * @param child child to remove + * @return rule of this transition */ - public void removeChild(TreeNode child) { - parents.remove(child); - } + public Rule getRule() { return rule; } /** - * Add the child to this tree transition + * Sets the rule associated with this transition. * - * @param child child to add + * @param rule rule for this transition */ - public void addChild(TreeNode child) { - parents.add(child); - } + public void setRule(@Nullable Rule rule) { + boolean wasCaseRule = this.rule instanceof CaseRule; + boolean isCaseRule = rule instanceof CaseRule; + if (wasCaseRule != isCaseRule) { + for (TreeNode parentNode : parents) { parentNode.getParent().propagateModifiableCaseRule(wasCaseRule); } + } - /** - * Gets the rule associated with this transition - * - * @return rule of this transition - */ - public Rule getRule() { - return rule; + this.rule = rule; + isVerified = false; + for (TreeNode parentNode : parents) { parentNode.getChildren().forEach(TreeTransition::reverify); } } /** - * Sets the rule associated with this transition + * Recursively propagates the transition's modifiability due to case rules up the tree. If + * {@code isModifiableCaseRule} is {@code true}, this operation will propagate until a transition with + * a case rule or a parent whose {@code isModifiableCaseRule} is {@code false} is encountered. * - * @param rule rule of this transition + * @param isModifiableCaseRule modifiability to propagate */ - public void setRule(Rule rule) { - this.rule = rule; - isVerified = false; + public void propagateModifiableCaseRule(boolean isModifiableCaseRule) { + + boolean valid = !isModifiableCaseRule || !(rule instanceof CaseRule); + if (!isModifiableCaseRule || valid && childNode != null) { + for (TreeTransition childTransition : childNode.getChildren()) { + if (!childTransition.getBoard().isModifiableCaseRule()) { + valid = false; + break; + } + } + } + if (valid) { + board.setModifiableCaseRule(isModifiableCaseRule); + for (TreeNode parentNode : parents) { + if (parentNode != null && parentNode.getParent() != null) { + parentNode.getParent().propagateModifiableCaseRule(isModifiableCaseRule); + } + } + } } /** - * Gets he selected element associated with this transition + * Gets the selected element associated with this transition. * - * @return If this is a case rule, the selected element for that rule, null otherwise + * @return If this is a case rule, the selected element for that rule; {@code null} otherwise */ public PuzzleElement getSelection() { if (this.rule instanceof CaseRule) { @@ -387,31 +391,31 @@ public PuzzleElement getSelection() { } /** - * Sets the selected element associated with this transition + * Sets the selected element associated with this transition. * * @param selection selected element for this transition */ - public void setSelection(PuzzleElement selection) { - this.selection = selection; - } + public void setSelection(@Nullable PuzzleElement selection) { this.selection = selection; } /** - * Gets whether this transition is correctly justified + * Gets whether this transition is correctly justified. * - * @return true if this transition is correctly justified, false otherwise + * @return {@code true} if this transition is correctly justified; {@code false} otherwise */ public boolean isCorrect() { - if (isJustified() && !isVerified) { + if (!isJustified()) { return false; } + + if (!isVerified) { isCorrect = rule.checkRule(this) == null; isVerified = true; } - return isJustified() && isCorrect; + return isCorrect; } /** - * Sets whether this transition is correctly justified + * Sets whether this transition is correctly justified. * - * @param isCorrect true if this transition is correctly justified, false otherwise + * @param isCorrect {@code true} if this transition is correctly justified; {@code false} otherwise */ public void setCorrect(boolean isCorrect) { this.isCorrect = isCorrect; @@ -419,9 +423,9 @@ public void setCorrect(boolean isCorrect) { } /** - * Forces check of rule on this transition regardless if it has been cached already + * Forces check of rule on this transition regardless if it has been cached already. * - * @return true if this transition is correctly justified, false otherwise + * @return {@code true} if this transition is correctly justified; {@code false} otherwise */ public boolean reverify() { isVerified = false; @@ -429,15 +433,9 @@ public boolean reverify() { } /** - * Gets whether this transition is justified + * Gets whether this transition is justified. * - * @return true if this transition is justified, false otherwise + * @return {@code true} if this transition is justified; {@code false} otherwise */ - public boolean isJustified() { - return rule != null; - } - - /*public int modifiedData_size(){ - return board.getModifiedData().size(); - }*/ + public boolean isJustified() { return rule != null; } } diff --git a/src/main/java/edu/rpi/legup/puzzle/battleship/BattleshipBoard.java b/src/main/java/edu/rpi/legup/puzzle/battleship/BattleshipBoard.java index 555c8471f..e334ba1c7 100644 --- a/src/main/java/edu/rpi/legup/puzzle/battleship/BattleshipBoard.java +++ b/src/main/java/edu/rpi/legup/puzzle/battleship/BattleshipBoard.java @@ -2,6 +2,8 @@ import edu.rpi.legup.model.gameboard.GridBoard; import edu.rpi.legup.model.gameboard.PuzzleElement; +import org.jetbrains.annotations.NotNull; + import java.awt.*; import java.util.ArrayList; import java.util.List; @@ -69,7 +71,7 @@ public BattleshipCell getCell(int x, int y) { * * @return the copy of the board */ - public BattleshipBoard copy() { + public @NotNull BattleshipBoard copy() { BattleshipBoard copy = new BattleshipBoard(dimension.width, dimension.height); for (int x = 0; x < this.dimension.width; x++) { for (int y = 0; y < this.dimension.height; y++) { diff --git a/src/main/java/edu/rpi/legup/puzzle/binary/BinaryBoard.java b/src/main/java/edu/rpi/legup/puzzle/binary/BinaryBoard.java index d5d65ed26..554dfcd53 100644 --- a/src/main/java/edu/rpi/legup/puzzle/binary/BinaryBoard.java +++ b/src/main/java/edu/rpi/legup/puzzle/binary/BinaryBoard.java @@ -2,6 +2,8 @@ import edu.rpi.legup.model.gameboard.GridBoard; import edu.rpi.legup.model.gameboard.PuzzleElement; +import org.jetbrains.annotations.NotNull; + import java.util.ArrayList; import java.util.HashSet; import java.util.Set; @@ -103,7 +105,7 @@ public ArrayList getColTypes(int colNum) { * @return copy of current BinaryBoard */ @Override - public BinaryBoard copy() { + public @NotNull BinaryBoard copy() { BinaryBoard copy = new BinaryBoard(dimension.width, dimension.height); for (int x = 0; x < this.dimension.width; x++) { for (int y = 0; y < this.dimension.height; y++) { diff --git a/src/main/java/edu/rpi/legup/puzzle/fillapix/FillapixBoard.java b/src/main/java/edu/rpi/legup/puzzle/fillapix/FillapixBoard.java index a6672bd4f..a00d99b84 100644 --- a/src/main/java/edu/rpi/legup/puzzle/fillapix/FillapixBoard.java +++ b/src/main/java/edu/rpi/legup/puzzle/fillapix/FillapixBoard.java @@ -2,6 +2,8 @@ import edu.rpi.legup.model.gameboard.GridBoard; import edu.rpi.legup.model.gameboard.PuzzleElement; +import org.jetbrains.annotations.NotNull; + import java.awt.*; import java.util.logging.Logger; @@ -26,7 +28,7 @@ public FillapixCell getCell(int x, int y) { * @return a new copy of the board that is independent of this one */ @Override - public FillapixBoard copy() { + public @NotNull FillapixBoard copy() { FillapixBoard copy = new FillapixBoard(dimension.width, dimension.height); for (int x = 0; x < this.dimension.width; x++) { for (int y = 0; y < this.dimension.height; y++) { diff --git a/src/main/java/edu/rpi/legup/puzzle/heyawake/HeyawakeBoard.java b/src/main/java/edu/rpi/legup/puzzle/heyawake/HeyawakeBoard.java index fe9020236..d6324b0d6 100644 --- a/src/main/java/edu/rpi/legup/puzzle/heyawake/HeyawakeBoard.java +++ b/src/main/java/edu/rpi/legup/puzzle/heyawake/HeyawakeBoard.java @@ -2,6 +2,8 @@ import edu.rpi.legup.model.gameboard.GridBoard; import edu.rpi.legup.model.gameboard.PuzzleElement; +import org.jetbrains.annotations.NotNull; + import java.awt.*; import java.util.ArrayList; import java.util.HashMap; @@ -69,7 +71,7 @@ public Map> getRegions() { * @return a new HeyawakeBoard that is a copy of this board */ @Override - public HeyawakeBoard copy() { + public @NotNull HeyawakeBoard copy() { HeyawakeBoard copy = new HeyawakeBoard(dimension.width, dimension.height); for (List region : regions.values()) { List newCpy = new ArrayList<>(); diff --git a/src/main/java/edu/rpi/legup/puzzle/masyu/EditLineCommand.java b/src/main/java/edu/rpi/legup/puzzle/masyu/EditLineCommand.java deleted file mode 100644 index 7ca9a69ed..000000000 --- a/src/main/java/edu/rpi/legup/puzzle/masyu/EditLineCommand.java +++ /dev/null @@ -1,185 +0,0 @@ -package edu.rpi.legup.puzzle.masyu; - -import static edu.rpi.legup.app.GameBoardFacade.getInstance; - -import edu.rpi.legup.history.PuzzleCommand; -import edu.rpi.legup.model.Puzzle; -import edu.rpi.legup.model.gameboard.Board; -import edu.rpi.legup.model.gameboard.PuzzleElement; -import edu.rpi.legup.model.observer.ITreeListener; -import edu.rpi.legup.model.tree.Tree; -import edu.rpi.legup.model.tree.TreeElementType; -import edu.rpi.legup.model.tree.TreeNode; -import edu.rpi.legup.model.tree.TreeTransition; -import edu.rpi.legup.ui.boardview.BoardView; -import edu.rpi.legup.ui.boardview.ElementView; -import edu.rpi.legup.ui.proofeditorui.treeview.*; -import java.awt.event.MouseEvent; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -public class EditLineCommand extends PuzzleCommand { - private static final Logger LOGGER = LogManager.getLogger(EditLineCommand.class.getName()); - - private TreeTransition transition; - private PuzzleElement oldData; - private PuzzleElement newData; - - private ElementView elementView; - private TreeElementView selectedView; - private TreeElementView newSelectedView; - private MouseEvent event; - - private TreeTransitionView transitionView; - - public EditLineCommand( - ElementView elementView, - TreeElementView selectedView, - MouseEvent event, - MasyuLine line) { - this.elementView = elementView; - this.selectedView = selectedView; - this.event = event; - this.newData = line; - this.oldData = newData.copy(); - this.transition = null; - } - - /** Executes a command */ - @Override - public void executeCommand() { - Tree tree = getInstance().getTree(); - TreeView treeView = getInstance().getLegupUI().getTreePanel().getTreeView(); - TreeViewSelection selection = treeView.getSelection(); - BoardView boardView = getInstance().getLegupUI().getBoardView(); - Puzzle puzzle = getInstance().getPuzzleModule(); - - MasyuBoard board = (MasyuBoard) selectedView.getTreeElement().getBoard(); - int index = elementView.getIndex(); - - if (selectedView.getType() == TreeElementType.NODE) { - TreeNodeView nodeView = (TreeNodeView) selectedView; - TreeNode treeNode = (TreeNode) selectedView.getTreeElement(); - - if (transition == null) { - transition = new TreeTransition(treeNode, treeNode.getBoard().copy()); - } - - treeNode.getChildren().add(transition); - puzzle.notifyTreeListeners( - (ITreeListener listener) -> listener.onTreeElementAdded(transition)); - transitionView = (TreeTransitionView) treeView.getElementView(transition); - - selection.newSelection(transitionView); - puzzle.notifyTreeListeners( - (ITreeListener listener) -> listener.onTreeSelectionChanged(selection)); - - getInstance().getLegupUI().repaintTree(); - board = (MasyuBoard) transition.getBoard(); - getInstance().getPuzzleModule().setCurrentBoard(board); - oldData = newData.copy(); - } else { - transitionView = (TreeTransitionView) selectedView; - transition = transitionView.getTreeElement(); - } - newSelectedView = transitionView; - PuzzleElement dup_line = null; - boolean mod_contains = false; - boolean contains = false; - final MasyuBoard editBoard = board; - if (LOGGER.isTraceEnabled()) { - LOGGER.trace("Size: {}", board.getModifiedData().size()); - } - for (PuzzleElement puzzleElement : board.getModifiedData()) { - if (puzzleElement instanceof MasyuLine) { - if (((MasyuLine) newData).compare((MasyuLine) puzzleElement)) { - if (LOGGER.isTraceEnabled()) { - LOGGER.trace("contains"); - } - dup_line = puzzleElement; - mod_contains = true; - } - } - } - for (int i = 0; i < board.getLines().size(); i++) { - if (board.getLines().get(i).compare((MasyuLine) newData)) { - contains = true; - } - } - if (contains || mod_contains) { - if (LOGGER.isTraceEnabled()) { - LOGGER.trace("delete"); - } - board.getModifiedData().remove(dup_line); - board.getLines().remove(dup_line); - // puzzle.notifyBoardListeners((IBoardListener listener) -> - // listener.onTreeElementChanged(editBoard)); - } else { - if (LOGGER.isTraceEnabled()) { - LOGGER.trace("adding"); - } - board.getModifiedData().add(newData); - board.getLines().add((MasyuLine) newData); - // puzzle.notifyBoardListeners((IBoardListener listener) -> - // listener.onTreeElementChanged(editBoard)); - } - - transition.propagateChange(newData); - } - - /** - * Gets the reason why the command cannot be executed - * - * @return if command cannot be executed, returns reason for why the command cannot be executed, - * otherwise null if command can be executed - */ - @Override - public String getErrorString() { - Board board = selectedView.getTreeElement().getBoard(); - if (!board.isModifiable()) { - return "Board is not modifiable"; - } else { - if (!board.getPuzzleElement(elementView.getPuzzleElement()).isModifiable()) { - return "Data is not modifiable"; - } - } - return null; - } - - /** Undoes an command */ - @Override - public void undoCommand() { - Tree tree = getInstance().getTree(); - TreeView treeView = getInstance().getLegupUI().getTreePanel().getTreeView(); - TreeViewSelection selection = treeView.getSelection(); - BoardView boardView = getInstance().getLegupUI().getBoardView(); - - Board board = transition.getBoard(); - - if (selectedView.getType() == TreeElementType.NODE) { - TreeNode treeNode = (TreeNode) selectedView.getTreeElement(); - - tree.removeTreeElement(transition); - treeView.removeTreeElement(newSelectedView); - - selection.newSelection(selectedView); - - getInstance().getLegupUI().repaintTree(); - getInstance().getPuzzleModule().setCurrentBoard(treeNode.getBoard()); - } - - Board prevBoard = null; // transition.getParentNode().getBoard(); - - newData.setData(oldData.getData()); - board.notifyChange(newData); - - // System.err.println(newData.getData() + " : " + oldData.getData()); - - if (prevBoard.getPuzzleElement(elementView.getPuzzleElement()).equalsData(newData)) { - board.removeModifiedData(newData); - } else { - board.addModifiedData(newData); - } - transition.propagateChange(newData); - } -} diff --git a/src/main/java/edu/rpi/legup/puzzle/masyu/MasyuBoard.java b/src/main/java/edu/rpi/legup/puzzle/masyu/MasyuBoard.java index f347ff4c4..3251e0d24 100644 --- a/src/main/java/edu/rpi/legup/puzzle/masyu/MasyuBoard.java +++ b/src/main/java/edu/rpi/legup/puzzle/masyu/MasyuBoard.java @@ -2,6 +2,8 @@ import edu.rpi.legup.model.gameboard.GridBoard; import edu.rpi.legup.model.gameboard.PuzzleElement; +import org.jetbrains.annotations.NotNull; + import java.util.ArrayList; import java.util.List; @@ -32,7 +34,7 @@ public void setLines(List lines) { } @Override - public void notifyChange(PuzzleElement puzzleElement) { + public void notifyChange(@NotNull PuzzleElement puzzleElement) { if (puzzleElement instanceof MasyuLine) { lines.add((MasyuLine) puzzleElement); } else { @@ -41,7 +43,7 @@ public void notifyChange(PuzzleElement puzzleElement) { } @Override - public MasyuBoard copy() { + public @NotNull MasyuBoard copy() { MasyuBoard copy = new MasyuBoard(dimension.width, dimension.height); for (int x = 0; x < this.dimension.width; x++) { for (int y = 0; y < this.dimension.height; y++) { diff --git a/src/main/java/edu/rpi/legup/puzzle/minesweeper/MinesweeperBoard.java b/src/main/java/edu/rpi/legup/puzzle/minesweeper/MinesweeperBoard.java index bc9f4a652..06f71879e 100644 --- a/src/main/java/edu/rpi/legup/puzzle/minesweeper/MinesweeperBoard.java +++ b/src/main/java/edu/rpi/legup/puzzle/minesweeper/MinesweeperBoard.java @@ -2,6 +2,7 @@ import edu.rpi.legup.model.gameboard.GridBoard; import edu.rpi.legup.model.gameboard.PuzzleElement; +import org.jetbrains.annotations.NotNull; public class MinesweeperBoard extends GridBoard { @@ -24,7 +25,7 @@ public MinesweeperCell getCell(int x, int y) { * @return a new copy of the board that is independent of this one */ @Override - public MinesweeperBoard copy() { + public @NotNull MinesweeperBoard copy() { MinesweeperBoard newMinesweeperBoard = new MinesweeperBoard(this.dimension.width, this.dimension.height); for (int x = 0; x < this.dimension.width; x++) { diff --git a/src/main/java/edu/rpi/legup/puzzle/nurikabe/NurikabeBoard.java b/src/main/java/edu/rpi/legup/puzzle/nurikabe/NurikabeBoard.java index 4662ff7bb..9c1abeb9f 100644 --- a/src/main/java/edu/rpi/legup/puzzle/nurikabe/NurikabeBoard.java +++ b/src/main/java/edu/rpi/legup/puzzle/nurikabe/NurikabeBoard.java @@ -4,6 +4,7 @@ import edu.rpi.legup.model.gameboard.PuzzleElement; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.jetbrains.annotations.NotNull; public class NurikabeBoard extends GridBoard { private static final Logger LOGGER = LogManager.getLogger(NurikabeBoard.class.getName()); @@ -44,7 +45,7 @@ public int[][] getIntArray() { } @Override - public NurikabeBoard copy() { + public @NotNull NurikabeBoard copy() { if (LOGGER.isTraceEnabled()) { LOGGER.trace("NurikabeBoard copy()"); } diff --git a/src/main/java/edu/rpi/legup/puzzle/shorttruthtable/ShortTruthTableBoard.java b/src/main/java/edu/rpi/legup/puzzle/shorttruthtable/ShortTruthTableBoard.java index 519a61974..b3fe34a3b 100644 --- a/src/main/java/edu/rpi/legup/puzzle/shorttruthtable/ShortTruthTableBoard.java +++ b/src/main/java/edu/rpi/legup/puzzle/shorttruthtable/ShortTruthTableBoard.java @@ -2,7 +2,8 @@ import edu.rpi.legup.model.gameboard.GridBoard; import edu.rpi.legup.model.gameboard.PuzzleElement; -import java.awt.*; +import org.jetbrains.annotations.NotNull; + import java.util.ArrayList; import java.util.HashSet; import java.util.List; @@ -42,7 +43,7 @@ public ShortTruthTableCell getCell(int x, int y) { } @Override - public ShortTruthTableBoard copy() { + public @NotNull ShortTruthTableBoard copy() { // Copy the statements ShortTruthTableStatement[] statementsCopy = @@ -92,7 +93,7 @@ public static List copyStatementList( // } @Override - public void notifyChange(PuzzleElement puzzleElement) { + public void notifyChange(@NotNull PuzzleElement puzzleElement) { ShortTruthTableCell cell = (ShortTruthTableCell) puzzleElement; int r = cell.getY(); int c = cell.getX(); diff --git a/src/main/java/edu/rpi/legup/puzzle/skyscrapers/SkyscrapersBoard.java b/src/main/java/edu/rpi/legup/puzzle/skyscrapers/SkyscrapersBoard.java index 1afc29766..7590eef59 100644 --- a/src/main/java/edu/rpi/legup/puzzle/skyscrapers/SkyscrapersBoard.java +++ b/src/main/java/edu/rpi/legup/puzzle/skyscrapers/SkyscrapersBoard.java @@ -9,6 +9,7 @@ import java.util.List; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.jetbrains.annotations.NotNull; public class SkyscrapersBoard extends GridBoard { private static final Logger LOGGER = LogManager.getLogger(SkyscrapersBoard.class.getName()); @@ -210,7 +211,7 @@ public void printBoard() { * tile values, though they will not be saved. */ @Override - public void setCell(int x, int y, Element e, MouseEvent m) { + public void setCell(int x, int y, Element e, @NotNull MouseEvent m) { SkyscrapersClue clue = this.getClue(x, y); if (e == null) return; if (clue != null) { @@ -256,7 +257,7 @@ public SkyscrapersClue getClue(int x, int y) { } @Override - public SkyscrapersBoard copy() { + public @NotNull SkyscrapersBoard copy() { SkyscrapersBoard copy = new SkyscrapersBoard(dimension.width); for (int x = 0; x < this.dimension.width; x++) { for (int y = 0; y < this.dimension.height; y++) { diff --git a/src/main/java/edu/rpi/legup/puzzle/starbattle/StarBattleBoard.java b/src/main/java/edu/rpi/legup/puzzle/starbattle/StarBattleBoard.java index 79cce6f05..f98f0e640 100644 --- a/src/main/java/edu/rpi/legup/puzzle/starbattle/StarBattleBoard.java +++ b/src/main/java/edu/rpi/legup/puzzle/starbattle/StarBattleBoard.java @@ -2,6 +2,8 @@ import edu.rpi.legup.model.gameboard.GridBoard; import edu.rpi.legup.model.gameboard.PuzzleElement; +import org.jetbrains.annotations.NotNull; + import java.util.*; public class StarBattleBoard extends GridBoard { @@ -165,7 +167,7 @@ public int rowStars(int rowIndex) { * * @return a new independent copy of the board */ - public StarBattleBoard copy() { + public @NotNull StarBattleBoard copy() { StarBattleBoard copy = new StarBattleBoard(size, puzzleNum); for (int r = 0; r < this.regions.size(); ++r) { StarBattleRegion regionCopy = this.regions.get(r).copy(); diff --git a/src/main/java/edu/rpi/legup/puzzle/sudoku/SudokuBoard.java b/src/main/java/edu/rpi/legup/puzzle/sudoku/SudokuBoard.java index ff1d60eeb..d73f97356 100644 --- a/src/main/java/edu/rpi/legup/puzzle/sudoku/SudokuBoard.java +++ b/src/main/java/edu/rpi/legup/puzzle/sudoku/SudokuBoard.java @@ -2,6 +2,8 @@ import edu.rpi.legup.model.gameboard.GridBoard; import edu.rpi.legup.model.gameboard.PuzzleElement; +import org.jetbrains.annotations.NotNull; + import java.awt.*; import java.util.HashSet; import java.util.Set; @@ -155,7 +157,7 @@ public Set getPossibleValues(SudokuCell cell) { * @param puzzleElement equivalent puzzle element with the new data. */ @Override - public void notifyChange(PuzzleElement puzzleElement) { + public void notifyChange(@NotNull PuzzleElement puzzleElement) { super.notifyChange(puzzleElement); Set affected = getAffected((SudokuCell) puzzleElement); for (SudokuCell c : affected) { @@ -169,7 +171,7 @@ public void notifyChange(PuzzleElement puzzleElement) { * @return a new copy of the board that is independent of this one */ @Override - public SudokuBoard copy() { + public @NotNull SudokuBoard copy() { SudokuBoard copy = new SudokuBoard(size); for (int x = 0; x < this.dimension.width; x++) { for (int y = 0; y < this.dimension.height; y++) { diff --git a/src/main/java/edu/rpi/legup/puzzle/treetent/TreeTentBoard.java b/src/main/java/edu/rpi/legup/puzzle/treetent/TreeTentBoard.java index 299881f01..944457e81 100644 --- a/src/main/java/edu/rpi/legup/puzzle/treetent/TreeTentBoard.java +++ b/src/main/java/edu/rpi/legup/puzzle/treetent/TreeTentBoard.java @@ -1,9 +1,14 @@ package edu.rpi.legup.puzzle.treetent; +import edu.rpi.legup.model.elements.Element; import edu.rpi.legup.model.gameboard.Board; import edu.rpi.legup.model.gameboard.GridBoard; import edu.rpi.legup.model.gameboard.PuzzleElement; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + import java.awt.*; +import java.awt.event.MouseEvent; import java.util.ArrayList; import java.util.List; @@ -84,6 +89,46 @@ public TreeTentCell getCell(int x, int y) { return (TreeTentCell) super.getCell(x, y); } + /** + * Sets the {@code TreeTentCell} at the location {@code (x,y)}. This method does not set the cell if the + * location specified is out of bounds. + * + * @param x {@inheritDoc} + * @param y {@inheritDoc} + * @param e {@inheritDoc} + * @param m {@inheritDoc} + */ + public void setCell(int x, int y, @Nullable Element e, @NotNull MouseEvent m) { + + if (((y == dimension.height && x >= 0 && x < dimension.width) + || (x == dimension.width && y >= 0 && y < dimension.height))) { + + TreeTentClue clue = getClue(x, y); + if (y == dimension.height) { + + if (m.getButton() == MouseEvent.BUTTON1) { + if (clue.getData() < dimension.height) { clue.setData(clue.getData() + 1); } + else { clue.setData(0); } + } + else { + if (clue.getData() > 0) { clue.setData(clue.getData() - 1); } + else { clue.setData(dimension.height); } + } + } + else { // x == dimension.width + if (m.getButton() == MouseEvent.BUTTON1) { + if (clue.getData() < dimension.width) { clue.setData(clue.getData() + 1); } + else { clue.setData(0); } + } + else { + if (clue.getData() > 0) { clue.setData(clue.getData() - 1); } + else { clue.setData(dimension.width); } + } + } + } + else { super.setCell(x, y, e, m); } + } + /** * Returns the puzzle element corresponding to the given element. For elements with index -2 or * -1, the element itself is returned directly. @@ -92,10 +137,11 @@ public TreeTentCell getCell(int x, int y) { * @return the corresponding puzzle element */ @Override - public PuzzleElement getPuzzleElement(PuzzleElement element) { + public PuzzleElement getPuzzleElement(@Nullable PuzzleElement element) { + if (element == null) { return null; } + return switch (element.getIndex()) { - case -2 -> element; - case -1 -> element; + case -2, -1 -> element; default -> super.getPuzzleElement(element); }; } @@ -123,7 +169,7 @@ public void setPuzzleElement(int index, PuzzleElement puzzleElement) { * @param puzzleElement the puzzle element that has changed */ @Override - public void notifyChange(PuzzleElement puzzleElement) { + public void notifyChange(@NotNull PuzzleElement puzzleElement) { int index = puzzleElement.getIndex(); if (index == -1) { lines.add((TreeTentLine) puzzleElement); @@ -158,7 +204,7 @@ public TreeTentClue getClue(int x, int y) { * @param puzzleElement equivalent puzzle element with the data. */ @Override - public void notifyAddition(PuzzleElement puzzleElement) { + public void notifyAddition(@NotNull PuzzleElement puzzleElement) { if (puzzleElement instanceof TreeTentLine) { lines.add((TreeTentLine) puzzleElement); } @@ -171,7 +217,7 @@ public void notifyAddition(PuzzleElement puzzleElement) { * @param puzzleElement equivalent puzzle element with the data. */ @Override - public void notifyDeletion(PuzzleElement puzzleElement) { + public void notifyDeletion(@NotNull PuzzleElement puzzleElement) { if (puzzleElement instanceof TreeTentLine) { for (TreeTentLine line : lines) { if (line.compare((TreeTentLine) puzzleElement)) { @@ -267,7 +313,7 @@ public List getRowCol(int index, TreeTentType type, boolean isRow) * @return true if the boards are equivalent, false otherwise */ @Override - public boolean equalsBoard(Board board) { + public boolean equalsBoard(@NotNull Board board) { TreeTentBoard treeTentBoard = (TreeTentBoard) board; for (TreeTentLine l1 : lines) { boolean hasLine = false; @@ -289,7 +335,7 @@ public boolean equalsBoard(Board board) { * @return a TreeTentBoard object that is a deep copy of the current TreeTentBoard */ @Override - public TreeTentBoard copy() { + public @NotNull TreeTentBoard copy() { TreeTentBoard copy = new TreeTentBoard(dimension.width, dimension.height); for (int x = 0; x < this.dimension.width; x++) { for (int y = 0; y < this.dimension.height; y++) { @@ -301,8 +347,8 @@ public TreeTentBoard copy() { lineCpy.setModifiable(false); copy.getLines().add(lineCpy); } - for (PuzzleElement e : modifiedData) { - copy.getPuzzleElement(e).setModifiable(false); + for (Object e : modifiedData) { + copy.getPuzzleElement((PuzzleElement) e).setModifiable(false); } copy.rowClues = rowClues; copy.colClues = colClues; diff --git a/src/main/java/edu/rpi/legup/ui/DynamicView.java b/src/main/java/edu/rpi/legup/ui/DynamicView.java index 421481d75..7672d20e2 100644 --- a/src/main/java/edu/rpi/legup/ui/DynamicView.java +++ b/src/main/java/edu/rpi/legup/ui/DynamicView.java @@ -54,10 +54,6 @@ public DynamicView(@NotNull ScrollView scrollView, @NotNull DynamicViewType type @Nullable private JPanel setUpZoomer(@NotNull DynamicViewType type) { if (type == DynamicViewType.BOARD) { return setUpBoardZoomer(); - } else { - if (type == DynamicViewType.PROOF_TREE) { - return setUpProofTreeZoomer(); - } } // Should never reach here; if you reach here, that's a problem! @@ -75,22 +71,6 @@ public DynamicView(@NotNull ScrollView scrollView, @NotNull DynamicViewType type return this.setUpZoomerHelper(label, listener); } - /** - * Sets up the zoomer for the proof tree view - * - * @return A JPanel containing the zoomer - */ - @NotNull private JPanel setUpProofTreeZoomer() { - final String label = "Resize Proof"; - ActionListener listener = - (ActionListener) -> - GameBoardFacade.getInstance() - .getLegupUI() - .getProofEditor() - .fitTreeViewToScreen(); - return this.setUpZoomerHelper(label, listener); - } - /** * Creates the zoomer * diff --git a/src/main/java/edu/rpi/legup/ui/LegupUI.java b/src/main/java/edu/rpi/legup/ui/LegupUI.java index aa00afcf1..a8e54f8a9 100644 --- a/src/main/java/edu/rpi/legup/ui/LegupUI.java +++ b/src/main/java/edu/rpi/legup/ui/LegupUI.java @@ -286,11 +286,6 @@ protected void displayPanel(int option) { return (PuzzleEditorPanel) panels[2]; } - /** Repaints the tree view in the proof editor. */ - public void repaintTree() { - getProofEditor().repaintTree(); - } - public void showStatus(@NotNull String status, boolean error) { showStatus(status, error, 1); } diff --git a/src/main/java/edu/rpi/legup/ui/ProofEditorPanel.java b/src/main/java/edu/rpi/legup/ui/ProofEditorPanel.java index 1237e74cc..47a8040f5 100644 --- a/src/main/java/edu/rpi/legup/ui/ProofEditorPanel.java +++ b/src/main/java/edu/rpi/legup/ui/ProofEditorPanel.java @@ -20,6 +20,7 @@ import edu.rpi.legup.ui.proofeditorui.rulesview.RuleFrame; import edu.rpi.legup.ui.proofeditorui.treeview.TreePanel; import edu.rpi.legup.ui.proofeditorui.treeview.TreeViewSelection; +import edu.rpi.legup.ui.zoompane.ZoomPaneUI; import edu.rpi.legup.user.Submission; import java.awt.*; import java.awt.event.ActionEvent; @@ -72,7 +73,7 @@ public class ProofEditorPanel extends LegupPanel implements IHistoryListener { preferences, exit; private JMenu edit; - private JMenuItem undo, redo, fitBoardToScreen, fitTreeToScreen; + private JMenuItem undo, redo, fitBoardToScreen; private JMenu view; @@ -195,7 +196,6 @@ public void makeVisible() { redo = new JMenuItem("Redo"); fitBoardToScreen = new JMenuItem("Fit Board to Screen"); - fitTreeToScreen = new JMenuItem("Fit Tree to Screen"); view = new JMenu("View"); @@ -468,9 +468,6 @@ public void actionPerformed(ActionEvent e) { fitBoardToScreen.addActionListener( (ActionEvent) -> dynamicBoardView.fitBoardViewToScreen()); - edit.add(fitTreeToScreen); - fitTreeToScreen.addActionListener((ActionEvent) -> this.fitTreeViewToScreen()); - mBar.add(proof); about.add(aboutLegup); @@ -1098,19 +1095,6 @@ public void setPuzzleView(@NotNull Puzzle puzzle) { toolBar1.setVisible(false); setupToolBar2(); - reloadGui(); - } - - /** Calls {@code repaintTree()} to refresh the tree view. */ - public void reloadGui() { - repaintTree(); - } - - /** - * Updates the tree view displayed in the tree panel to reflect the current state of the tree. - */ - public void repaintTree() { - treePanel.repaintTreeView(GameBoardFacade.getInstance().getTree()); } /** Checks the proof for all files */ @@ -1361,9 +1345,4 @@ private void submit() { public void showStatus(@NotNull String status, boolean error, int timer) { // TODO: implement } - - /** Zooms the tree view to fit within the available screen space */ - protected void fitTreeViewToScreen() { - this.treePanel.getTreeView().zoomFit(); - } } diff --git a/src/main/java/edu/rpi/legup/ui/SmoothImageIcon.java b/src/main/java/edu/rpi/legup/ui/SmoothImageIcon.java index dd8ffbab4..7496e0f34 100644 --- a/src/main/java/edu/rpi/legup/ui/SmoothImageIcon.java +++ b/src/main/java/edu/rpi/legup/ui/SmoothImageIcon.java @@ -1,11 +1,15 @@ package edu.rpi.legup.ui; -import javax.swing.ImageIcon; +import com.formdev.flatlaf.FlatLaf; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; import java.awt.Component; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.RenderingHints; -import java.awt.image.BufferedImage; +import java.awt.Image; import java.net.URL; /** @@ -13,17 +17,25 @@ * {@link RenderingHints#VALUE_INTERPOLATION_BILINEAR} rendering hint on, counteracting * the pixel snapping caused by scaling or HiDPI displays. */ -public class SmoothImageIcon extends ImageIcon { +public class SmoothImageIcon extends ImageIcon implements FlatLaf.DisabledIconProvider { - public SmoothImageIcon(URL url) { super(url); } + public SmoothImageIcon(@NotNull URL url) { super(url); } - public SmoothImageIcon(BufferedImage image) { super(image); } + public SmoothImageIcon(@NotNull Image image) { super(image); } - public synchronized void paintIcon(Component c, Graphics graphics, int x, int y) { + @Override + public synchronized void paintIcon(@Nullable Component c, @NotNull Graphics graphics, int x, int y) { Graphics2D g = (Graphics2D) graphics.create(); g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); super.paintIcon(c, g, x, y); g.dispose(); } + + @Override + public Icon getDisabledIcon() { + ImageIcon disabledIcon = (ImageIcon) UIManager.getLookAndFeel().getDisabledIcon( + null, new ImageIcon(getImage())); + return new SmoothImageIcon(disabledIcon.getImage()); + } } diff --git a/src/main/java/edu/rpi/legup/ui/proofeditorui/treeview/TreeElementView.java b/src/main/java/edu/rpi/legup/ui/proofeditorui/treeview/TreeElementView.java index e5e523e11..e833f65e5 100644 --- a/src/main/java/edu/rpi/legup/ui/proofeditorui/treeview/TreeElementView.java +++ b/src/main/java/edu/rpi/legup/ui/proofeditorui/treeview/TreeElementView.java @@ -2,6 +2,8 @@ import edu.rpi.legup.model.tree.TreeElement; import edu.rpi.legup.model.tree.TreeElementType; +import org.jetbrains.annotations.NotNull; + import java.awt.*; /** @@ -11,7 +13,7 @@ */ public abstract class TreeElementView implements Shape { protected TreeElement treeElement; - protected double span; + protected int span; protected int depth; protected boolean isSelected; protected boolean isHover; @@ -20,12 +22,12 @@ public abstract class TreeElementView implements Shape { protected boolean isCollapsed; /** - * TreeElementView Constructor creates a tree puzzleElement view + * {@code TreeElementView} constructor creates a tree element view. * - * @param type tree puzzleElement type - * @param treeElement tree puzzleElement puzzleElement associated with this view + * @param type tree element type + * @param treeElement tree element associated with this view */ - protected TreeElementView(TreeElementType type, TreeElement treeElement) { + protected TreeElementView(@NotNull TreeElementType type, @NotNull TreeElement treeElement) { this.type = type; this.treeElement = treeElement; this.isSelected = false; @@ -34,145 +36,114 @@ protected TreeElementView(TreeElementType type, TreeElement treeElement) { } /** - * Draws the tree puzzleElement view + * Draws the tree element view. * - * @param graphics2D graphics2D object used to draw the tree puzzleElement view + * @param graphics2D {@code Graphics2D} object used to draw the tree element view */ - public abstract void draw(Graphics2D graphics2D); + public abstract void draw(@NotNull Graphics2D graphics2D); /** - * Gets the span for the subtree rooted at this view + * Gets the span for the subtree rooted at this view. * - * @return span bounded y span + * @return minor axis span of this element view */ - public double getSpan() { - return span; - } + public int getSpan() { return span; } /** * Sets the span for the subtree rooted at this view. * - * @param span bounded y span + * @param span minor axis span of this element view */ - public void setSpan(double span) { - this.span = span; - } + public void setSpan(int span) { this.span = span; } /** - * Gets the depth of this tree puzzleElement in the tree + * Gets the depth of this tree element in the tree. * - * @return depth of this tree puzzleElement + * @return depth of this tree element */ - public int getDepth() { - return depth; - } + public int getDepth() { return depth; } /** - * Sets the depth of this tree puzzleElement in the tree + * Sets the depth of this tree element in the tree. * - * @param depth depth of this tree puzzleElement + * @param depth depth of this tree element */ - public void setDepth(int depth) { - this.depth = depth; - } + public void setDepth(int depth) { this.depth = depth; } /** - * Gets the tree puzzleElement type for this view + * Gets the tree element type for this view. * - * @return tree puzzleElement type + * @return tree element type */ - public TreeElementType getType() { - return type; - } + public TreeElementType getType() { return type; } /** - * Gets the tree puzzleElement associated with this view + * Gets the tree element associated with this view. * - * @return tree puzzleElement associated with this view + * @return tree element associated with this view */ - public TreeElement getTreeElement() { - return treeElement; - } + public TreeElement getTreeElement() { return treeElement; } /** - * Sets the tree puzzleElement associated with this view + * Sets the tree element associated with this view. * - * @param treeElement tree puzzleElement associated with this view + * @param treeElement tree element associated with this view */ - public void setTreeElement(TreeElement treeElement) { - this.treeElement = treeElement; - } + public void setTreeElement(@NotNull TreeElement treeElement) { this.treeElement = treeElement; } /** - * Gets the mouse selection + * Gets the mouse selection status. * - * @return mouse selection + * @return selection status */ - public boolean isSelected() { - return isSelected; - } + public boolean isSelected() { return isSelected; } /** - * Sets the mouse selection + * Sets the mouse selection status. * - * @param isSelected mouse selection + * @param isSelected selection status */ - public void setSelected(boolean isSelected) { - this.isSelected = isSelected; - } + public void setSelected(boolean isSelected) { this.isSelected = isSelected; } /** - * Gets the mouse hover + * Gets the mouse hover status. * - * @return mouse hover + * @return hover status */ - public boolean isHover() { - return isHover; - } + public boolean isHover() { return isHover; } /** - * Sets the mouse hover + * Sets the mouse hover status. * - * @param isHovered mouse hover + * @param isHovered hover status */ - public void setHover(boolean isHovered) { - this.isHover = isHovered; - } + public void setHover(boolean isHovered) { this.isHover = isHovered; } /** - * Gets the visibility of the tree puzzleElement. Tells the TreeView whether or not to draw the - * tree puzzleElement + * Gets the visibility of the tree element. Tells the TreeView whether to draw the tree element. * - * @return visibility of the tree puzzleElement + * @return visibility of the tree element */ - public boolean isVisible() { - return isVisible; - } + public boolean isVisible() { return isVisible; } /** - * Sets the visibility of the tree puzzleElement + * Sets the visibility of the tree element. * - * @param isVisible visibility of the tree puzzleElement + * @param isVisible visibility of the tree element */ - public void setVisible(boolean isVisible) { - this.isVisible = isVisible; - } + public void setVisible(boolean isVisible) { this.isVisible = isVisible; } /** - * Is this tree node view collapsed in the view + * Is this tree node view collapsed in the view. * - * @return true if the node is collapsed, false otherwise + * @return {@code true} if the node is collapsed; {@code false} otherwise */ - public boolean isCollapsed() { - return isCollapsed; - } + public boolean isCollapsed() { return isCollapsed; } /** - * Sets the tree node view collapsed field + * Sets the tree node view collapsed field. * - * @param isCollapsed true if the node is collapsed, false otherwise + * @param isCollapsed {@code true} if the node is collapsed; {@code false} otherwise */ - public void setCollapsed(boolean isCollapsed) { - this.isCollapsed = isCollapsed; - } + public void setCollapsed(boolean isCollapsed) { this.isCollapsed = isCollapsed; } } diff --git a/src/main/java/edu/rpi/legup/ui/proofeditorui/treeview/TreeNodeView.java b/src/main/java/edu/rpi/legup/ui/proofeditorui/treeview/TreeNodeView.java index d8ffdde15..f758333fd 100644 --- a/src/main/java/edu/rpi/legup/ui/proofeditorui/treeview/TreeNodeView.java +++ b/src/main/java/edu/rpi/legup/ui/proofeditorui/treeview/TreeNodeView.java @@ -3,10 +3,13 @@ import edu.rpi.legup.model.rules.RuleType; import edu.rpi.legup.model.tree.TreeElementType; import edu.rpi.legup.model.tree.TreeNode; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; import java.awt.*; import java.awt.geom.*; import java.util.ArrayList; -import javax.swing.UIManager; /** * Represents a view of a tree node in the tree structure. This class extends {@link @@ -15,360 +18,306 @@ * relationships with other nodes. */ public class TreeNodeView extends TreeElementView { - static final int RADIUS = 25; - static final int DIAMETER = 2 * RADIUS; private Point location; private TreeTransitionView parentView; - private ArrayList childrenViews; + private ArrayList childViews; - private boolean isCollapsed; private boolean isContradictoryState; + /** @return the radius of node views in the tree view */ + public static int getRadius() { return UIManager.getInt("Tree.nodeRadius"); } + + /** @return the diameter of node views in the tree view */ + public static int getDiameter() { return getRadius() * 2; } + /** - * TreeNodeView Constructor creates a node for display + * {@code TreeNodeView} constructor creates a node for display. * - * @param treeNode treeElement associated with this transition + * @param treeNode tree node associated with this transition */ public TreeNodeView(TreeNode treeNode) { super(TreeElementType.NODE, treeNode); this.treeElement = treeNode; this.location = new Point(); this.parentView = null; - this.childrenViews = new ArrayList<>(); + this.childViews = new ArrayList<>(); this.isCollapsed = false; this.isContradictoryState = false; this.isVisible = true; } /** - * Draws the TreeNodeView + * Draws the {@code TreeNodeView}. * - * @param graphics2D graphics2D used for drawing + * @param graphics2D {@code Graphics2D} used for drawing */ - public void draw(Graphics2D graphics2D) { - if (isVisible() && treeElement != null) { + public void draw(@NotNull Graphics2D graphics2D) { + if (isVisible()) { Graphics2D g = (Graphics2D) graphics2D.create(); + int radius = getRadius(); + int diameter = getDiameter(); + if (getTreeElement().getParent() != null && getTreeElement().getParent().isJustified() && getTreeElement().getParent().getRule().getRuleType() == RuleType.CONTRADICTION) { + isContradictoryState = true; + double r = (radius - UIManager.getInt("Tree.contradictionWidth") / 2.0) / Math.sqrt(2); + Line2D l1 = new Line2D.Double(location.x - r, location.y - r, + location.x + r, location.y + r); + Line2D l2 = new Line2D.Double(location.x + - r, location.y + r, + location.x + r, location.y - r); + + g.setColor(UIManager.getColor("Tree.outline")); + g.setStroke(new BasicStroke(UIManager.getInt("Tree.contradictionWidth") + + UIManager.getInt("Tree.outlineWidth") * 2, + BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); + g.draw(l1); + g.draw(l2); + g.setColor(UIManager.getColor("Tree.contradiction")); - g.drawLine( - location.x - RADIUS, - location.y - RADIUS, - location.x + RADIUS, - location.y + RADIUS); - g.drawLine( - location.x + RADIUS, - location.y - RADIUS, - location.x - RADIUS, - location.y + RADIUS); + g.setStroke(new BasicStroke(UIManager.getInt("Tree.contradictionWidth"), + BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); + g.draw(l1); + g.draw(l2); + } else { - isContradictoryState = false; - g.setStroke(new BasicStroke(UIManager.getInt("Tree.outlineWidth"))); - boolean isContraBranch = getTreeElement().isContradictoryBranch(); - - if (isSelected) { - g.setColor(UIManager.getColor("Tree.selected")); - g.fillOval(location.x - RADIUS, location.y - RADIUS, DIAMETER, DIAMETER); - - g.setColor(UIManager.getColor("Tree.outline")); - g.drawOval(location.x - RADIUS, location.y - RADIUS, DIAMETER, DIAMETER); - - g.setStroke(new BasicStroke(UIManager.getInt("Tree.selectedWidth"))); - g.setColor(UIManager.getColor("Tree.selectedOutline")); - g.drawOval( - location.x - RADIUS - 4, - location.y - RADIUS - 4, - DIAMETER + 8, - DIAMETER + 8); - } else if (isHover) { - g.setColor(UIManager.getColor("Tree.hover")); - g.fillOval(location.x - RADIUS, location.y - RADIUS, DIAMETER, DIAMETER); - - g.setColor(UIManager.getColor("Tree.outline")); - g.drawOval(location.x - RADIUS, location.y - RADIUS, DIAMETER, DIAMETER); - - g.setStroke(new BasicStroke(UIManager.getInt("Tree.selectedWidth"))); - g.setColor(UIManager.getColor("Tree.hoverOutline")); - g.drawOval( - location.x - RADIUS - 4, - location.y - RADIUS - 4, - DIAMETER + 8, - DIAMETER + 8); - } else { - g.setColor( - UIManager.getColor( - isContraBranch ? "Tree.contradiction" : "Tree.default")); - g.fillOval(location.x - RADIUS, location.y - RADIUS, DIAMETER, DIAMETER); - - g.setColor(UIManager.getColor("Tree.outline")); - g.drawOval(location.x - RADIUS, location.y - RADIUS, DIAMETER, DIAMETER); + + if (isSelected || isHover) { + g.setStroke(new BasicStroke(UIManager.getInt("Tree.selectedWidth") * 2 + + UIManager.getInt("Tree.outlineWidth") * 2)); + g.setColor(UIManager.getColor(isSelected ? "Tree.selectedOutline" : "Tree.hoverOutline")); + + g.drawOval(location.x - radius, location.y - radius, diameter, diameter); } + + isContradictoryState = false; + g.setStroke(new BasicStroke(UIManager.getInt("Tree.outlineWidth") * 2)); + + g.setColor(UIManager.getColor("Tree.outline")); + g.drawOval(location.x - radius, location.y - radius, diameter, diameter); + + g.setColor(UIManager.getColor( + isSelected ? "Tree.selected" : ( + isHover ? "Tree.hover" : ( + getTreeElement().isContradictoryBranch() ? "Tree.contradiction" : + "Tree.default" + )))); + g.fillOval(location.x - radius, location.y - radius, diameter, diameter); } g.dispose(); } } - public boolean isContradictoryState() { - return isContradictoryState; - } + /** @return {@code true} if view is in a contradictory state; {@code false} otherwise */ + public boolean isContradictoryState() { return isContradictoryState; } /** - * Gets the list of children views associated with this tree node + * Gets the list of child views associated with this tree node. * - * @return list of children views for this tree node + * @return list of child views for this tree node */ - public ArrayList getChildrenViews() { - return childrenViews; - } + public ArrayList getChildViews() { return childViews; } /** - * Sets the list of children views associated with this tree node + * Sets the list of child views associated with this tree node. * - * @param childrenViews list of children views for this tree node + * @param childViews list of child views for this tree node */ - public void setChildrenViews(ArrayList childrenViews) { - this.childrenViews = childrenViews; - } + public void setChildViews(@NotNull ArrayList childViews) { this.childViews = childViews; } /** - * Adds a TreeTransitionView to the list of children views + * Adds a {@code TreeTransitionView} to the list of child views. * - * @param nodeView TreeTransitionView to add to the list of children views + * @param nodeView {@code TreeTransitionView} to add to the list of child views */ - public void addChildrenView(TreeTransitionView nodeView) { - childrenViews.add(nodeView); - } + public void addChildView(@NotNull TreeTransitionView nodeView) { childViews.add(nodeView); } /** - * Removes a TreeTransitionView from the list of children views + * Inserts a {@code TreeTransitionView} to the list of child views at a specified index. * - * @param nodeView TreeTransitionView to remove from the list of children views + * @param index index at which to add {@code nodeView} + * @param nodeView {@code TreeTransitionView} to add to the list of child views */ - public void removeChildrenView(TreeTransitionView nodeView) { - childrenViews.remove(nodeView); - } + public void insertChildView(int index, @NotNull TreeTransitionView nodeView) { childViews.add(index, nodeView); } /** - * Sets the parent tree transition view + * Removes a {@code TreeTransitionView} from the list of child views. + * + * @param nodeView {@code TreeTransitionView} to remove from the list of child views + */ + public void removeChildView(@NotNull TreeTransitionView nodeView) { childViews.remove(nodeView); } + + /** + * Sets the parent tree transition view. * * @param parentView parent tree transition view */ - public void setParentView(TreeTransitionView parentView) { - this.parentView = parentView; - } + public void setParentView(@Nullable TreeTransitionView parentView) { this.parentView = parentView; } /** - * Gets the parent tree transition view + * Gets the parent tree transition view. * * @return parent tree transition view */ - public TreeTransitionView getParentView() { - return parentView; - } + public TreeTransitionView getParentView() { return parentView; } /** - * Gets the tree node associated with this view + * Gets the tree node associated with this view. * * @return tree node */ - public TreeNode getTreeElement() { - return (TreeNode) treeElement; - } + public TreeNode getTreeElement() { return (TreeNode) treeElement; } /** - * Gets the location of the tree node + * Gets the location of the tree node. * * @return location of the tree node */ - public Point getLocation() { - return location; - } + public Point getLocation() { return location; } /** - * Sets the location of the tree node + * Sets the location of the tree node. * * @param location location of the tree node */ - public void setLocation(Point location) { - this.location = location; - } + public void setLocation(@NotNull Point location) { this.location = location; } /** - * Gets the x location of the tree node + * Gets the x location of the tree node. * * @return x location */ - public int getX() { - return location.x; - } + public int getX() { return location.x; } /** - * Sets the x location of the tree node + * Sets the x location of the tree node. * * @param x x location */ - public void setX(int x) { - location.x = x; - } + public void setX(int x) { location.x = x; } /** - * Gets the y location of the tree node + * Gets the y location of the tree node. * * @return y location */ - public int getY() { - return location.y; - } + public int getY() { return location.y; } /** - * Sets the y location of the tree node + * Sets the y location of the tree node. * * @param y y location */ - public void setY(int y) { - location.y = y; - } - - /** - * Gets the radius of the tree node - * - * @return radius - */ - public int getRadius() { - return RADIUS; - } + public void setY(int y) { location.y = y; } /** - * Returns the bounding rectangle of this TreeNodeView + * Returns the bounding rectangle of this {@code TreeNodeView}. * - * @return a Rectangle representing the bounding box of this TreeNodeView + * @return a {@code Rectangle} representing the bounding box of this {@code TreeNodeView} */ @Override - public Rectangle getBounds() { - return new Rectangle(location.x, location.y, DIAMETER, DIAMETER); - } + public Rectangle getBounds() { return new Rectangle(location.x, location.y, getDiameter(), getDiameter()); } /** - * Returns the bounding rectangle of this TreeNodeView as a Rectangle2D + * Returns the bounding rectangle of this {@code TreeNodeView} as a {@code Rectangle2D}. * - * @return a Rectangle2D representing the bounding box of this TreeNodeView + * @return a {@code Rectangle2D} representing the bounding box of this {@code TreeNodeView} */ @Override - public Rectangle2D getBounds2D() { - return new Rectangle(location.x, location.y, DIAMETER, DIAMETER); - } + public Rectangle2D getBounds2D() { return new Rectangle(location.x, location.y, getDiameter(), getDiameter()); } /** - * Determines if the specified point (x, y) is within the bounds of this TreeNodeView + * Determines if the specified point (x, y) is within the bounds of this {@code TreeNodeView}. * * @param x the x-coordinate of the point to check * @param y the y-coordinate of the point to check - * @return {@code true} if the point is within the bounds of this TreeNodeView; {@code false} - * otherwise + * @return {@code true} if the point is within the bounds of this {@code TreeNodeView}; {@code false} otherwise */ @Override - public boolean contains(double x, double y) { - return Math.sqrt(Math.pow(x - location.x, 2) + Math.pow(y - location.y, 2)) <= RADIUS; - } + public boolean contains(double x, double y) { return Math.hypot(x - location.x, y - location.y) <= getRadius(); } /** - * Determines if the specified Point2D object is within the bounds of this TreeNodeView + * Determines if the specified {@code Point2D} object is within the bounds of this {@code TreeNodeView}. * - * @param p the Point2D object representing the point to check - * @return {@code true} if the point is within the bounds of this TreeNodeView; {@code false} - * otherwise + * @param p the {@code Point2D} object representing the point to check + * @return {@code true} if the point is within the bounds of this {@code TreeNodeView}; {@code false} otherwise */ @Override - public boolean contains(Point2D p) { - return contains(p.getX(), p.getY()); - } + public boolean contains(@NotNull Point2D p) { return contains(p.getX(), p.getY()); } /** * Determines if the specified rectangle defined by (x, y, width, height) intersects with the - * bounds of this TreeNodeView. + * bounds of this {@code TreeNodeView}. * * @param x The x-coordinate of the rectangle to check * @param y The y-coordinate of the rectangle to check * @param w The width of the rectangle to check * @param h The height of the rectangle to check - * @return {@code true} if the rectangle intersects with the bounds of this TreeNodeView; {@code - * false} otherwise + * @return {@code true} if the rectangle intersects with the bounds of this {@code TreeNodeView}; + * {@code false} otherwise */ @Override - public boolean intersects(double x, double y, double w, double h) { - return false; - } + public boolean intersects(double x, double y, double w, double h) { return false; } /** - * Determines if the specified Rectangle2D object intersects with the bounds of this - * TreeNodeView. + * Determines if the specified {@code Rectangle2D} object intersects with the bounds of this + * {@code TreeNodeView}. * - * @param r the Rectangle2D object representing the rectangle to check - * @return {@code true} if the rectangle intersects with the bounds of this TreeNodeView; {@code - * false} otherwise + * @param r the {@code Rectangle2D} object representing the rectangle to check + * @return {@code true} if the rectangle intersects with the bounds of this {@code TreeNodeView}; + * {@code false} otherwise */ @Override - public boolean intersects(Rectangle2D r) { - return intersects(r.getX(), r.getY(), r.getWidth(), r.getHeight()); - } + public boolean intersects(Rectangle2D r) { return intersects(r.getX(), r.getY(), r.getWidth(), r.getHeight()); } /** * Determines if the specified rectangle defined by (x, y, width, height) is entirely contained - * within the bounds of this TreeNodeView + * within the bounds of this {@code TreeNodeView}. * * @param x the x-coordinate of the rectangle to check * @param y the y-coordinate of the rectangle to check * @param w the width of the rectangle to check * @param h the height of the rectangle to check - * @return {@code true} if the rectangle is entirely contained within the bounds of this - * TreeNodeView; {@code false} otherwise + * @return {@code true} if the rectangle is entirely contained within the bounds of this {@code TreeNodeView}; + * {@code false} otherwise */ @Override - public boolean contains(double x, double y, double w, double h) { - return false; - } + public boolean contains(double x, double y, double w, double h) { return false; } /** - * Determines if the specified Rectangle2D object is entirely contained within the bounds of - * this TreeNodeView. + * Determines if the specified {@code Rectangle2D} object is entirely contained within the bounds of + * this {@code TreeNodeView}. * - * @param r the Rectangle2D object representing the rectangle to check - * @return {@code true} if the rectangle is entirely contained within the bounds of this - * TreeNodeView; {@code false} otherwise + * @param r the {@code Rectangle2D} object representing the rectangle to check + * @return {@code true} if the rectangle is entirely contained within the bounds of this {@code TreeNodeView}; + * {@code false} otherwise */ @Override - public boolean contains(Rectangle2D r) { - return false; - } + public boolean contains(@NotNull Rectangle2D r) { return false; } /** - * Returns an iterator over the path geometry of this TreeNodeView. The iterator provides access + * Returns an iterator over the path geometry of this {@code TreeNodeView}. The iterator provides access * to the path's segments and their coordinates, which can be used for rendering or hit testing. * - * @param at the AffineTransform to apply to the path geometry - * @return a PathIterator that iterates over the path geometry of this TreeNodeView + * @param at the {@code AffineTransform} to apply to the path geometry + * @return a {@code PathIterator} that iterates over the path geometry of this {@code TreeNodeView} */ @Override - public PathIterator getPathIterator(AffineTransform at) { - return null; - } + public PathIterator getPathIterator(@NotNull AffineTransform at) { return null; } /** - * Returns an iterator over the path geometry of this TreeNodeView with the specified flatness. + * Returns an iterator over the path geometry of this {@code TreeNodeView} with the specified flatness. * The iterator provides access to the path's segments and their coordinates, which can be used * for rendering or hit testing. * - * @param at the AffineTransform to apply to the path geometry + * @param at the {@code AffineTransform} to apply to the path geometry * @param flatness the maximum distance that the line segments can deviate from the true path - * @return a PathIterator that iterates over the path geometry of this TreeNodeView + * @return a {@code PathIterator} that iterates over the path geometry of this {@code TreeNodeView} */ @Override - public PathIterator getPathIterator(AffineTransform at, double flatness) { - return null; - } + public PathIterator getPathIterator(@NotNull AffineTransform at, double flatness) { return null; } } diff --git a/src/main/java/edu/rpi/legup/ui/proofeditorui/treeview/TreePanel.java b/src/main/java/edu/rpi/legup/ui/proofeditorui/treeview/TreePanel.java index 3717aa5c2..62bb8041d 100644 --- a/src/main/java/edu/rpi/legup/ui/proofeditorui/treeview/TreePanel.java +++ b/src/main/java/edu/rpi/legup/ui/proofeditorui/treeview/TreePanel.java @@ -1,5 +1,7 @@ package edu.rpi.legup.ui.proofeditorui.treeview; +import static edu.rpi.legup.ui.zoompane.ZoomViewport.ZoomPeer.NOTIFICATION; + import com.formdev.flatlaf.FlatClientProperties; import edu.rpi.legup.app.GameBoardFacade; import edu.rpi.legup.controller.TreeController; @@ -7,14 +9,11 @@ import edu.rpi.legup.history.DeleteTreeElementCommand; import edu.rpi.legup.history.ICommand; import edu.rpi.legup.history.MergeCommand; -import edu.rpi.legup.model.gameboard.Board; -import edu.rpi.legup.model.tree.Tree; -import edu.rpi.legup.ui.DynamicView; -import edu.rpi.legup.ui.DynamicViewType; +import edu.rpi.legup.ui.zoompane.ZoomPane; +import org.jetbrains.annotations.NotNull; + import java.awt.*; -import javax.swing.BorderFactory; -import javax.swing.JLabel; -import javax.swing.JPanel; +import javax.swing.*; import javax.swing.border.TitledBorder; /** @@ -23,158 +22,144 @@ * and merging tree elements, and updating the status based on actions performed. */ public class TreePanel extends JPanel { - public boolean modifiedSinceSave = false; - public boolean modifiedSinceUndoPush = false; - public int updateStatusTimer = 0; - private JPanel main; - private TreeView treeView; - private TreeToolbarPanel toolbar; - // private LegupUI legupUI; + private final ZoomPane treeViewPane; + private final TreeView treeView; + private final TreeToolbarPanel toolbar; - private JLabel status; + private final JLabel infoLabel; + private final JLabel errorLabel; /** Constructs a {@code TreePanel} and initializes the UI components. */ - public TreePanel(/*LegupUI legupUI*/ ) { - // this.legupUI = legupUI; + public TreePanel() { - main = new JPanel(); - - main.setLayout(new BorderLayout()); + setLayout(new BorderLayout()); + TitledBorder title = BorderFactory.createTitledBorder("Proof Tree"); + title.setTitleJustification(TitledBorder.CENTER); + setBorder(title); - TreeController treeController = new TreeController(); - treeView = new TreeView(treeController); - treeController.setViewer(treeView); + treeView = new TreeView(new TreeController()); toolbar = new TreeToolbarPanel(this); + treeViewPane = new ZoomPane(treeView); - DynamicView dynamicTreeView = new DynamicView(treeView, DynamicViewType.PROOF_TREE); - main.add(dynamicTreeView, BorderLayout.CENTER); - dynamicTreeView.add(toolbar, BorderLayout.WEST); + add(treeViewPane, BorderLayout.CENTER); + add(toolbar, BorderLayout.WEST); - status = new JLabel(); - status.setPreferredSize(new Dimension(150, 15)); - dynamicTreeView.getZoomWrapper().add(status, BorderLayout.CENTER); + errorLabel = new JLabel(); + errorLabel.putClientProperty(FlatClientProperties.STYLE_CLASS, "error"); + errorLabel.setVisible(false); + treeViewPane.getZoomViewport().getPeer().add(errorLabel, NOTIFICATION); - TitledBorder title = BorderFactory.createTitledBorder("Proof Tree"); - title.setTitleJustification(TitledBorder.CENTER); - main.setBorder(title); + infoLabel = new JLabel(); + infoLabel.putClientProperty(FlatClientProperties.STYLE_CLASS, "info"); + infoLabel.setVisible(false); + treeViewPane.getZoomViewport().getPeer().add(infoLabel, NOTIFICATION); - setLayout(new BorderLayout()); - add(main); - - updateStatusTimer = 0; - } - - /** - * Repaints the tree view with the provided {@link Tree} object - * - * @param tree the {@link Tree} object to update the view with - */ - public void repaintTreeView(Tree tree) { - treeView.updateTreeView(tree); + updateUI(); } - /** - * Updates the status of the panel based on changes to the {@link Board} - * - * @param board the {@link Board} object representing the current board state - */ - public void boardDataChanged(Board board) { - modifiedSinceSave = true; - modifiedSinceUndoPush = true; - updateStatus(); - // colorTransitions(); - } + @Override + public void updateUI() { + super.updateUI(); - /** - * Updates the status display based on the status timer. If the timer is greater than 0, the - * status will not be updated. Otherwise, it clears the status text. - */ - public void updateStatus() { - updateStatusTimer = ((updateStatusTimer - 1) > 0) ? (updateStatusTimer - 1) : 0; - if (updateStatusTimer > 0) { - return; + if (treeViewPane != null) { + treeViewPane.putClientProperty("ZoomPane.viewPadding", null); + Insets treeViewPadding = UIManager.getInsets("Tree.viewPadding"); + if (treeViewPadding != null) { + treeViewPane.putClientProperty("ZoomPane.viewPadding", treeViewPadding); + } } - status.putClientProperty(FlatClientProperties.STYLE_CLASS, null); - status.setText(""); } /** - * Updates the status display with the given status string + * Updates the info display with a status message. * - * @param statusString the status string to display + * @param status the status message to display */ - public void updateStatus(String statusString) { - status.putClientProperty(FlatClientProperties.STYLE_CLASS, "info"); - status.setText(statusString); + public void updateStatus(String status) { + if (status.isEmpty()) { infoLabel.setVisible(false); } + else { + infoLabel.setText(status); + infoLabel.setVisible(true); + } } /** - * Updates the status display as an error with an error message + * Updates the error display with an error message. * * @param error the error message to display */ - public void updateError(String error) { - status.putClientProperty(FlatClientProperties.STYLE_CLASS, "error"); - status.setText(error); + public void updateError(@NotNull String error) { + if (error.isEmpty()) { errorLabel.setVisible(false); } + else { + errorLabel.setText(error); + errorLabel.setVisible(true); + } } /** - * Gets the {@link TreeView} instance associated with this panel + * Gets the {@code TreeView} instance associated with this panel. * - * @return the {@link TreeView} instance + * @return the {@code TreeView} instance */ - public TreeView getTreeView() { - return treeView; - } + public TreeView getTreeView() { return treeView; } /** * Adds a new tree element by executing an {@link AddTreeElementCommand}. If the command cannot - * be executed, it updates the status display with an error and error message. + * be executed, it updates the error message. */ public void add() { TreeViewSelection selection = treeView.getSelection(); + String error = ""; AddTreeElementCommand add = new AddTreeElementCommand(selection); + if (add.canExecute()) { add.execute(); GameBoardFacade.getInstance().getHistory().pushChange(add); - } else { - updateError(add.getError()); } + else { error = add.getError(); } + + updateError(error); } /** * Deletes the selected tree element by executing a {@link DeleteTreeElementCommand}. If the - * command cannot be executed, it updates the status display with an error and an error message. + * command cannot be executed, it updates the error message. */ public void delete() { TreeViewSelection selection = treeView.getSelection(); + String error = ""; DeleteTreeElementCommand del = new DeleteTreeElementCommand(selection); + if (del.canExecute()) { del.execute(); GameBoardFacade.getInstance().getHistory().pushChange(del); - } else { - updateError(del.getError()); } + else { error = del.getError(); } + + updateError(error); } /** * Merges selected tree elements by executing a {@link MergeCommand}. If the command cannot be - * executed, it updates the status display with an error and an error message. + * executed, it updates the error message. */ public void merge() { TreeViewSelection selection = treeView.getSelection(); + String error = ""; ICommand merge = new MergeCommand(selection); + if (merge.canExecute()) { merge.execute(); GameBoardFacade.getInstance().getHistory().pushChange(merge); - } else { - updateError(merge.getError()); } + else { error = merge.getError(); } + + updateError(error); } /** diff --git a/src/main/java/edu/rpi/legup/ui/proofeditorui/treeview/TreeToolBarButton.java b/src/main/java/edu/rpi/legup/ui/proofeditorui/treeview/TreeToolBarButton.java index 232498723..a1cd56746 100644 --- a/src/main/java/edu/rpi/legup/ui/proofeditorui/treeview/TreeToolBarButton.java +++ b/src/main/java/edu/rpi/legup/ui/proofeditorui/treeview/TreeToolBarButton.java @@ -3,7 +3,7 @@ import java.awt.Dimension; import javax.swing.*; -/** {@code TreeToolBarButton} is a JButton that represents a button in the tree toolbar. */ +/** {@code TreeToolBarButton} is a {@code JButton} that represents a button in the tree toolbar. */ public class TreeToolBarButton extends JButton { private TreeToolBarName name; @@ -12,8 +12,8 @@ public class TreeToolBarButton extends JButton { /** * Constructs a {@code TreeToolBarButton} with the specified icon and name. * - * @param imageIcon the {@link ImageIcon} to be displayed on the button - * @param name the {@link TreeToolBarName} associated with this button + * @param imageIcon the {@code ImageIcon} to be displayed on the button + * @param name the {@code TreeToolBarName} associated with this button */ public TreeToolBarButton(ImageIcon imageIcon, TreeToolBarName name) { super(imageIcon); @@ -24,11 +24,9 @@ public TreeToolBarButton(ImageIcon imageIcon, TreeToolBarName name) { } /** - * Gets the {@link TreeToolBarName} associated with this button + * Gets the {@code TreeToolBarName} associated with this button. * - * @return the {@link TreeToolBarName} associated with this button + * @return the {@code TreeToolBarName} associated with this button */ - public TreeToolBarName getToolBarName() { - return name; - } + public TreeToolBarName getToolBarName() { return name; } } diff --git a/src/main/java/edu/rpi/legup/ui/proofeditorui/treeview/TreeToolbarPanel.java b/src/main/java/edu/rpi/legup/ui/proofeditorui/treeview/TreeToolbarPanel.java index 06af84c57..f599bb85e 100644 --- a/src/main/java/edu/rpi/legup/ui/proofeditorui/treeview/TreeToolbarPanel.java +++ b/src/main/java/edu/rpi/legup/ui/proofeditorui/treeview/TreeToolbarPanel.java @@ -1,5 +1,8 @@ package edu.rpi.legup.ui.proofeditorui.treeview; +import edu.rpi.legup.ui.SmoothImageIcon; +import org.jetbrains.annotations.NotNull; + import java.awt.*; import javax.swing.*; @@ -12,35 +15,35 @@ public class TreeToolbarPanel extends JPanel { private TreeToolBarButton addChild, delChild, merge, collapse; /** - * TreeToolbarPanel Constructor - creates the tree tool mBar panel + * {@code TreeToolbarPanel} constructor - creates the tree toolbar panel * - * @param treePanel treePanel input + * @param treePanel {@code TreePanel} parent */ - public TreeToolbarPanel(TreePanel treePanel) { + public TreeToolbarPanel(@NotNull TreePanel treePanel) { this.treePanel = treePanel; this.setLayout(new GridLayout(4, 1, 0, 2)); addChild = new TreeToolBarButton( - new ImageIcon( + new SmoothImageIcon( ClassLoader.getSystemResource( "edu/rpi/legup/images/Legup/AddChild.png")), TreeToolBarName.ADD_CHILD); delChild = new TreeToolBarButton( - new ImageIcon( + new SmoothImageIcon( ClassLoader.getSystemResource( "edu/rpi/legup/images/Legup/DelChild.png")), TreeToolBarName.DEL_CHILD); merge = new TreeToolBarButton( - new ImageIcon( + new SmoothImageIcon( ClassLoader.getSystemResource( "edu/rpi/legup/images/Legup/Merge.png")), TreeToolBarName.MERGE); collapse = new TreeToolBarButton( - new ImageIcon( + new SmoothImageIcon( ClassLoader.getSystemResource( "edu/rpi/legup/images/Legup/Collapse.png")), TreeToolBarName.COLLAPSE); diff --git a/src/main/java/edu/rpi/legup/ui/proofeditorui/treeview/TreeTransitionView.java b/src/main/java/edu/rpi/legup/ui/proofeditorui/treeview/TreeTransitionView.java index 028e44d30..d1a871dc9 100644 --- a/src/main/java/edu/rpi/legup/ui/proofeditorui/treeview/TreeTransitionView.java +++ b/src/main/java/edu/rpi/legup/ui/proofeditorui/treeview/TreeTransitionView.java @@ -1,14 +1,15 @@ package edu.rpi.legup.ui.proofeditorui.treeview; -import static java.lang.Math.*; - import edu.rpi.legup.model.tree.TreeElementType; import edu.rpi.legup.model.tree.TreeTransition; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; import java.awt.*; import java.awt.geom.*; import java.util.ArrayList; import java.util.List; -import javax.swing.UIManager; /** * {@code TreeTransitionView} is a visual representation of a tree transition in the tree view. It @@ -16,396 +17,478 @@ * visual states such as selection, hover, and correctness. */ public class TreeTransitionView extends TreeElementView { - static final int RADIUS = 25; - static final int DIAMETER = 2 * RADIUS; - static final int GAP = 5; + /** The child node view of this transition. */ private TreeNodeView childView; + + /** The list of parent node views of this transition. */ private ArrayList parentViews; + + /** + * The node view responsible for reserving space for a transition from multiple parent node views. + * This should remain {@code null} for a transition from one parent node view. + * This node view should not have an arrow drawn from it. + */ + private TreeNodeView layoutParentView; + + /** Arrowhead shape to draw. */ private Polygon arrowhead; - private List lineStartPoints; - private Point lineEndPoint; + /** List of start points to draw arrow tails from. */ + private final List tailStartPoints; + + /** List of paths to draw tails along. */ + private final List tailPaths; - private Point endPoint; + /** End point to draw the tail of the arrows to. */ + private final Point tailEndPoint; + + /** End point to draw arrows to. */ + private final Point endPoint; /** - * TreeTransitionView Constructor creates a transition arrow for display + * {@code TreeTransitionView} constructor creates a transition arrow for display. * * @param transition tree transition associated with this view */ - public TreeTransitionView(TreeTransition transition) { + public TreeTransitionView(@NotNull TreeTransition transition) { super(TreeElementType.TRANSITION, transition); this.parentViews = new ArrayList<>(); - this.isCollapsed = false; this.endPoint = new Point(); - this.lineStartPoints = new ArrayList<>(); - this.lineEndPoint = new Point(); + this.tailStartPoints = new ArrayList<>(); + this.tailPaths = new ArrayList<>(); + this.tailEndPoint = new Point(); + updateArrowHead(); } /** - * TreeTransitionView Constructor creates a transition arrow for display + * {@code TreeTransitionView} constructor creates a transition arrow for display with a single parent + * node view. * * @param transition tree transition associated with this view - * @param parentView TreeNodeView of the parent associated with this transition + * @param parentView {@code TreeNodeView} of the single parent view associated with this transition */ - public TreeTransitionView(TreeTransition transition, TreeNodeView parentView) { + public TreeTransitionView(@NotNull TreeTransition transition, @NotNull TreeNodeView parentView) { this(transition); this.parentViews.add(parentView); - this.lineStartPoints.add(new Point()); + this.tailStartPoints.add(new Point()); + this.tailPaths.add(null); + } + + /** + * {@code TreeTransitionView} constructor creates a transition arrow for display with multiple parent + * node views and an additional parent for reserving space for the transition's subtree. It is recommended + * to use the deepest common ancestor of the elements of {@code parentViews} for the {@code layoutParentView}. + * + * @param transition tree transition associated with this view + * @param parentViews {@code TreeNodeView}s of the multiple parent views associated with this transition + * @param layoutParentView {@code TreeNodeView} responsible for reserving space for the transition's subtree + */ + public TreeTransitionView(@NotNull TreeTransition transition, @NotNull List parentViews, + @NotNull TreeNodeView layoutParentView) { + this(transition); + this.parentViews.addAll(parentViews); + for (TreeNodeView ignored : parentViews) { + tailStartPoints.add(new Point()); + tailPaths.add(null); + } + this.layoutParentView = layoutParentView; } /** - * Draws the TreeTransitionView + * Draws the {@code TreeTransitionView}. * - * @param graphics2D graphics2D used for drawing + * @param graphics2D {@code Graphics2D} used for drawing */ - public void draw(Graphics2D graphics2D) { + public void draw(@NotNull Graphics2D graphics2D) { + updateArrowHead(); + for (int i = 0; i < tailPaths.size(); ++i) { updateTailPath(i); } + Graphics2D g = (Graphics2D) graphics2D.create(); - arrowhead = createTransitionTriangle(RADIUS); - g.setColor(UIManager.getColor("Tree.outline")); - g.setStroke(new BasicStroke(UIManager.getInt("Tree.outlineWidth"))); - - for (Point lineStartPoint : lineStartPoints) { - CubicCurve2D c = new CubicCurve2D.Double(); - double ctrlx1 = lineEndPoint.x - 25; - double ctrly1 = lineStartPoint.y; - double ctrlx2 = lineEndPoint.x - 25; - double ctrly2 = lineEndPoint.y; - - c.setCurve( - lineStartPoint.x, - lineStartPoint.y, - ctrlx1, - ctrly1, - ctrlx2, - ctrly2, - lineEndPoint.x, - lineEndPoint.y); - g.draw(c); - } + if (isSelected || isHover) { - if (isSelected) { - if (getTreeElement().isJustified()) { - g.setColor( - UIManager.getColor( - getTreeElement().isCorrect() ? "Tree.valid" : "Tree.invalid")); - } else { - g.setColor(UIManager.getColor("Tree.arrowDefault")); - } + g.setColor(UIManager.getColor(isSelected ? "Tree.selectedOutline" : "Tree.hoverOutline")); + g.setStroke(new BasicStroke(UIManager.getInt("Tree.selectedWidth") * 2 + + UIManager.getInt("Tree.outlineWidth") * 2 + + UIManager.getInt("Tree.transitionTailWeight"), + BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); + for (Path2D tailPath : tailPaths) { g.draw(tailPath); } - g.fillPolygon(arrowhead); - - g.setColor(UIManager.getColor("Tree.outline")); - g.drawPolygon(arrowhead); - - Polygon selection_triangle = createTransitionTriangle(RADIUS + 10); - selection_triangle.translate(7, 0); - - g.setStroke(new BasicStroke(UIManager.getInt("Tree.selectedWidth"))); - g.setColor(UIManager.getColor("Tree.selectedOutline")); - g.drawPolygon(selection_triangle); - } else { - if (isHover) { - g.setColor(UIManager.getColor("Tree.hover")); - g.fillPolygon(arrowhead); - - g.setColor(UIManager.getColor("Tree.outline")); - g.drawPolygon(arrowhead); - - Polygon selection_triangle = createTransitionTriangle(RADIUS + 10); - selection_triangle.translate(7, 0); - - g.setStroke(new BasicStroke(UIManager.getInt("Tree.selectedWidth"))); - g.setColor(UIManager.getColor("Tree.hoverOutline")); - g.drawPolygon(selection_triangle); - } else { - if (getTreeElement().isJustified()) { - g.setColor( - UIManager.getColor( - getTreeElement().isCorrect() ? "Tree.valid" : "Tree.invalid")); - } else { - g.setColor(UIManager.getColor("Tree.arrowDefault")); - } - g.fillPolygon(arrowhead); - - g.setColor(UIManager.getColor("Tree.outline")); - g.drawPolygon(arrowhead); - } + g.setStroke(new BasicStroke(UIManager.getInt("Tree.selectedWidth") * 2 + + UIManager.getInt("Tree.outlineWidth") * 2, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); + g.draw(arrowhead); } - g.dispose(); - } - /** Constructs the arrowhead shape from the start and end points */ - private Polygon createTransitionTriangle(int radius) { - double thetaArrow = Math.toRadians(30); + g.setColor(UIManager.getColor("Tree.outline")); + g.setStroke(new BasicStroke(UIManager.getInt("Tree.outlineWidth") * 2 + + UIManager.getInt("Tree.transitionTailWeight"), BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); + for (Path2D tailPath : tailPaths) { g.draw(tailPath); } + + g.setStroke(new BasicStroke(UIManager.getInt("Tree.outlineWidth") * 2, + BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); + g.draw(arrowhead); + + g.setColor(UIManager.getColor( + isHover && !isSelected ? "Tree.hover" : ( + !getTreeElement().isJustified() ? "Tree.arrowDefault" : ( + getTreeElement().isCorrect() ? "Tree.valid" : + "Tree.invalid" + )))); + g.setStroke(new BasicStroke(UIManager.getInt("Tree.transitionTailWeight"), + BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); + + for (Path2D tailPath : tailPaths) { g.draw(tailPath); } + g.fill(arrowhead); - int point1X = endPoint.x; - int point1Y = endPoint.y; + g.dispose(); + } - int point2X = point1X - radius; - int point2Y = point1Y + (int) Math.round(radius / (2 * cos(thetaArrow))); + /** Update arrow head if {@code null} or desired dimensions have changed. */ + private void updateArrowHead() { + int base = UIManager.getInt("Tree.transitionHeadBase"); + int height = UIManager.getInt("Tree.transitionHeadHeight"); - int point3X = point1X - radius; - int point3Y = point1Y - (int) Math.round(radius / (2 * cos(thetaArrow))); + if (arrowhead != null) { + Rectangle board = arrowhead.getBounds(); + if (board.width == height && board.height == base) { return; } + } - lineEndPoint.x = point2X; - lineEndPoint.y = (point3Y - point2Y) / 2 + point2Y; + arrowhead = new Polygon(); + arrowhead.addPoint(endPoint.x, endPoint.y); + arrowhead.addPoint(endPoint.x - height, endPoint.y + base / 2); + arrowhead.addPoint(endPoint.x - height, endPoint.y - base / 2); + } - Polygon tri = new Polygon(); - tri.addPoint(point1X, point1Y); - tri.addPoint(point2X, point2Y); - tri.addPoint(point3X, point3Y); + /** + * Update arrow tail at {@code index} if desired end points have changed. + * + * @param index index of tail path to update + */ + private void updateTailPath(int index) { + if (tailPaths.get(index) != null) { return; } + + Path2D.Double tailPath = new Path2D.Double(); + Point tailStartPoint = tailStartPoints.get(index); + tailPath.moveTo(tailStartPoint.x, tailStartPoint.y); + + if (tailStartPoint.y != tailEndPoint.y) { + // If there is enough space, use two arcs of radius 'arc' to get from starting y to end y + int dir = (int) Math.signum(tailEndPoint.y - tailStartPoint.y); + + int halfCurveSpace = UIManager.getInt("Tree.transitionTailGap") / 2; + int arc = Math.min(UIManager.getInt("Tree.transitionArc"), halfCurveSpace); + + if (dir * (tailEndPoint.y - tailStartPoint.y) >= arc * 2) { + + tailPath.append(new Arc2D.Double( + tailEndPoint.x - halfCurveSpace - arc * 2, + tailStartPoint.y - (dir < 0 ? arc * 2 : 0), + arc * 2, + arc * 2, + dir * 90, + dir * -90, + Arc2D.OPEN + ), true); + tailPath.append(new Arc2D.Double( + tailEndPoint.x - halfCurveSpace, + tailEndPoint.y - (dir > 0 ? arc * 2 : 0), + arc * 2, + arc * 2, + 180, + dir * 90, + Arc2D.OPEN + ), true); + } + else { + // If there is not enough space, use two arcs of whatever radius smoothly connects points + // Visual aid at https://www.desmos.com/geometry/lvpyacqoeq + int x1 = tailEndPoint.x - halfCurveSpace - arc; + int y1 = tailStartPoint.y; + int x2 = tailEndPoint.x - halfCurveSpace + arc; + int y2 = tailEndPoint.y; + double x3 = (x1 + x2) / 2.0; + double y3 = (y1 + y2) / 2.0; + double radius = Math.abs(((2 * x1 * x3) + (y1 * y1) - (x1 * x1) - (x3 * x3) - (y3 * y3)) + / (2 * (y1 - y3)) - y1); + double extent = 90 - Math.toDegrees(Math.asin((radius - Math.abs(y1 - y3)) / radius)); + + tailPath.append(new Arc2D.Double( + x1 - radius, + y1 - (dir < 0 ? radius * 2 : 0), + radius * 2, + radius * 2, + dir * 90, + dir * -extent, + Arc2D.OPEN + ), true); + tailPath.append(new Arc2D.Double( + x3 + arc - radius, + y2 - (dir > 0 ? radius * 2 : 0) , + radius * 2, + radius * 2, + dir * (-90 - extent), + dir * extent, + Arc2D.OPEN + ), false); + } + } - return tri; + tailPath.lineTo(tailEndPoint.x, tailEndPoint.y); + tailPaths.set(index, tailPath); } /** - * Gets the TreeElement associated with this view + * Gets the {@code TreeElement} associated with this view. * - * @return the TreeElement associated with this view + * @return the {@code TreeElement} associated with this view */ - public TreeTransition getTreeElement() { - return (TreeTransition) treeElement; - } + public TreeTransition getTreeElement() { return (TreeTransition) treeElement; } /** - * Gets the TreeNodeView child view + * Gets the {@code TreeNodeView} child view. * - * @return TreeNodeView child view + * @return {@code TreeNodeView} child view */ - public TreeNodeView getChildView() { - return childView; - } + public TreeNodeView getChildView() { return childView; } /** - * Sets the TreeNodeView child view + * Sets the {@code TreeNodeView} child view. * - * @param childView TreeNodeView child view + * @param childView {@code TreeNodeView} child view */ - public void setChildView(TreeNodeView childView) { - this.childView = childView; - } + public void setChildView(@Nullable TreeNodeView childView) { this.childView = childView; } /** - * Gets the list of parent views associated with this tree transition view + * Gets the list of parent views associated with this tree transition view. * * @return list of parent views for this tree transition view */ - public ArrayList getParentViews() { - return parentViews; - } + public ArrayList getParentViews() { return parentViews; } /** - * Sets the list of parent views associated with this tree transition view + * Sets the list of parent views associated with this tree transition view. * * @param parentViews list of parent views for this tree transition view */ - public void setParentViews(ArrayList parentViews) { + public void setParentViews(@NotNull ArrayList parentViews) { this.parentViews = parentViews; - this.lineStartPoints.clear(); - for (TreeNodeView parentView : this.parentViews) { - this.lineStartPoints.add(new Point()); + tailStartPoints.clear(); + tailPaths.clear(); + for (TreeNodeView ignored : parentViews) { + tailStartPoints.add(new Point()); + tailPaths.add(null); } } /** - * Adds a TreeNodeView to the list of parent views + * Adds a {@code TreeNodeView} to the list of parent views. * - * @param nodeView TreeNodeView to add to the list of parent views + * @param nodeView {@code TreeNodeView} to add to the list of parent views */ - public void addParentView(TreeNodeView nodeView) { + public void addParentView(@NotNull TreeNodeView nodeView) { parentViews.add(nodeView); - lineStartPoints.add(new Point()); + tailStartPoints.add(new Point()); + tailPaths.add(null); } /** - * Removes a TreeNodeView from the list of parent views + * Removes a {@code TreeNodeView} from the list of parent views. * - * @param nodeView TreeNodeView to remove from the list of parent views + * @param nodeView {@code TreeNodeView} to remove from the list of parent views */ - public void removeParentView(TreeNodeView nodeView) { + public void removeParentView(@NotNull TreeNodeView nodeView) { int index = parentViews.indexOf(nodeView); parentViews.remove(nodeView); if (index != -1) { - lineStartPoints.remove(index); + tailStartPoints.remove(index); + tailPaths.remove(index); } } /** - * Gets the x-coordinate of the end point of the transition arrow + * Gets the {@code layoutParentView} that reserves space for this transition's subtree. + * + * @return the current {@code layoutParentView} + */ + public TreeNodeView getLayoutParentView() { return layoutParentView; } + + /** + * Sets the {@code layoutParentView} that reserves space for this transition's subtree. + * + * @param nodeView the new {@code layoutParentView} + */ + public void setLayoutParentView(@Nullable TreeNodeView nodeView) { layoutParentView = nodeView; } + + /** + * Gets the x-coordinate of the end point of the transition arrow. * * @return the x-coordinate of the end point */ - public int getEndX() { - return endPoint.x; - } + public int getEndX() { return endPoint.x; } /** - * Sets the x-coordinate of the end point of the transition arrow + * Sets the x-coordinate of the end point of the transition arrow. * * @param x the new x-coordinate of the end point */ public void setEndX(int x) { - this.endPoint.x = x; + arrowhead.translate(x - endPoint.x, 0); + endPoint.x = x; + tailEndPoint.x = x - UIManager.getInt("Tree.transitionHeadHeight") + - UIManager.getInt("Tree.outlineWidth") / 2; + tailPaths.replaceAll(ignored -> null); } /** - * Gets the y-coordinate of the end point of the transition arrow + * Gets the y-coordinate of the end point of the transition arrow. * * @return the y-coordinate of the end point */ - public int getEndY() { - return endPoint.y; - } + public int getEndY() { return endPoint.y; } /** - * Sets the y-coordinate of the end point of the transition arrow + * Sets the y-coordinate of the end point of the transition arrow. * * @param y the new y-coordinate of the end point */ public void setEndY(int y) { - this.endPoint.y = y; + arrowhead.translate(0, y - endPoint.y); + endPoint.y = y; + tailEndPoint.y = y; + tailPaths.replaceAll(ignored -> null); } /** - * Gets the start point at the specified index from the list of start points + * Gets the start point at the specified index from the list of start points. * * @param index the index of the start point to retrieve - * @return the start point at the specified index, or null if the index is out of range + * @return the start point at the specified index, or {@code null} if the index is out of range */ - public Point getLineStartPoint(int index) { - return index < lineStartPoints.size() ? lineStartPoints.get(index) : null; + public Point getTailStartPoint(int index) { + return 0 <= index && index < tailStartPoints.size() ? tailStartPoints.get(index) : null; } /** - * Returns the bounding rectangle of this TreeTransitionView + * Sets the start point at the specified index from the list of start points. * - * @return a Rectangle representing the bounding box of this TreeTransitionView + * @param index the index of the start point to set + * @param startPoint the new start point */ - @Override - public Rectangle getBounds() { - return arrowhead.getBounds(); + public void setTailStartPoint(int index, @NotNull Point startPoint) { + tailStartPoints.set(index, startPoint); + tailPaths.set(index, null); } /** - * Returns the bounding rectangle of this TreeTransitionView as a Rectangle2D + * Returns the bounding rectangle of this {@code TreeTransitionView}. * - * @return a Rectangle2D representing the bounding box of this TreeTransitionView + * @return a {@code Rectangle} representing the bounding box of this {@code TreeTransitionView} */ @Override - public Rectangle2D getBounds2D() { - return arrowhead.getBounds2D(); - } + public Rectangle getBounds() { return arrowhead.getBounds(); } + + /** + * Returns the bounding rectangle of this {@code TreeTransitionView} as a {@code Rectangle2D}. + * + * @return a {@code Rectangle2D} representing the bounding box of this {@code TreeTransitionView} + */ + @Override + public Rectangle2D getBounds2D() { return arrowhead.getBounds2D(); } /** - * Determines if the specified point (x, y) is within the bounds of this TreeTransitionView + * Determines if the specified point (x, y) is within the bounds of this {@code TreeTransitionView}. * * @param x the x-coordinate of the point to check * @param y the y-coordinate of the point to check - * @return {@code true} if the point is within the bounds of this TreeTransitionView; {@code + * @return {@code true} if the point is within the bounds of this {@code TreeTransitionView}; {@code * false} otherwise */ @Override - public boolean contains(double x, double y) { - return arrowhead.contains(x, y); - } + public boolean contains(double x, double y) { return arrowhead.contains(x, y); } /** - * Determines if the specified Point2D object is within the bounds of this TreeTransitionView + * Determines if the specified {@code Point2D} object is within the bounds of this {@code TreeTransitionView}. * - * @param p the Point2D object representing the point to check - * @return {@code true} if the point is within the bounds of this TreeTransitionView; {@code + * @param p the {@code Point2D} object representing the point to check + * @return {@code true} if the point is within the bounds of this {@code TreeTransitionView}; {@code * false} otherwise */ @Override - public boolean contains(Point2D p) { - return arrowhead != null && arrowhead.contains(p); - } + public boolean contains(@NotNull Point2D p) { return arrowhead != null && arrowhead.contains(p); } /** * Determines if the specified rectangle defined by (x, y, width, height) intersects with the - * bounds of this TreeTransitionView. + * bounds of this {@code TreeTransitionView}. * * @param x The x-coordinate of the rectangle to check * @param y The y-coordinate of the rectangle to check * @param w The width of the rectangle to check * @param h The height of the rectangle to check - * @return {@code true} if the rectangle intersects with the bounds of this TreeTransitionView; + * @return {@code true} if the rectangle intersects with the bounds of this {@code TreeTransitionView}; * {@code false} otherwise */ @Override - public boolean intersects(double x, double y, double w, double h) { - return arrowhead.intersects(x, y, w, h); - } + public boolean intersects(double x, double y, double w, double h) { return arrowhead.intersects(x, y, w, h); } /** - * Determines if the specified Rectangle2D object intersects with the bounds of this - * TreeTransitionView. + * Determines if the specified {@code Rectangle2D} object intersects with the bounds of this + * {@code TreeTransitionView}. * - * @param r the Rectangle2D object representing the rectangle to check - * @return {@code true} if the rectangle intersects with the bounds of this TreeTransitionView; + * @param r the {@code Rectangle2D} object representing the rectangle to check + * @return {@code true} if the rectangle intersects with the bounds of this {@code TreeTransitionView}; * {@code false} otherwise */ @Override - public boolean intersects(Rectangle2D r) { - return arrowhead.intersects(r); - } + public boolean intersects(@NotNull Rectangle2D r) { return arrowhead.intersects(r); } /** * Determines if the specified rectangle defined by (x, y, width, height) is entirely contained - * within the bounds of this TreeTransitionView + * within the bounds of this {@code TreeTransitionView}. * * @param x the x-coordinate of the rectangle to check * @param y the y-coordinate of the rectangle to check * @param w the width of the rectangle to check * @param h the height of the rectangle to check * @return {@code true} if the rectangle is entirely contained within the bounds of this - * TreeTransitionView; {@code false} otherwise + * {@code TreeTransitionView}; {@code false} otherwise */ @Override - public boolean contains(double x, double y, double w, double h) { - return arrowhead.contains(x, y, w, h); - } + public boolean contains(double x, double y, double w, double h) { return arrowhead.contains(x, y, w, h); } /** - * Determines if the specified Rectangle2D object is entirely contained within the bounds of - * this TreeTransitionView. + * Determines if the specified {@code Rectangle2D} object is entirely contained within the bounds of + * this {@code TreeTransitionView}. * - * @param r the Rectangle2D object representing the rectangle to check + * @param r the {@code Rectangle2D} object representing the rectangle to check * @return {@code true} if the rectangle is entirely contained within the bounds of this - * TreeTransitionView; {@code false} otherwise + * {@code TreeTransitionView}; {@code false} otherwise */ @Override - public boolean contains(Rectangle2D r) { - return arrowhead.contains(r); - } + public boolean contains(@NotNull Rectangle2D r) { return arrowhead.contains(r); } /** - * Returns an iterator over the path geometry of this TreeTransitionView. The iterator provides + * Returns an iterator over the path geometry of this {@code TreeTransitionView}. The iterator provides * access to the path's segments and their coordinates, which can be used for rendering or hit * testing. * - * @param at the AffineTransform to apply to the path geometry - * @return a PathIterator that iterates over the path geometry of this TreeTransitionView + * @param at the {@code AffineTransform} to apply to the path geometry + * @return a {@code PathIterator} that iterates over the path geometry of this {@code TreeTransitionView} */ @Override - public PathIterator getPathIterator(AffineTransform at) { - return arrowhead.getPathIterator(at); - } + public PathIterator getPathIterator(@NotNull AffineTransform at) { return arrowhead.getPathIterator(at); } /** - * Returns an iterator over the path geometry of this TreeTransitionView with the specified + * Returns an iterator over the path geometry of this {@code TreeTransitionView} with the specified * flatness. The iterator provides access to the path's segments and their coordinates, which * can be used for rendering or hit testing. * - * @param at the AffineTransform to apply to the path geometry + * @param at the {@code AffineTransform} to apply to the path geometry * @param flatness the maximum distance that the line segments can deviate from the true path - * @return a PathIterator that iterates over the path geometry of this TreeTransitionView + * @return a {@code PathIterator} that iterates over the path geometry of this {@code TreeTransitionView} */ @Override - public PathIterator getPathIterator(AffineTransform at, double flatness) { + public PathIterator getPathIterator(@NotNull AffineTransform at, double flatness) { return arrowhead.getPathIterator(at, flatness); } } diff --git a/src/main/java/edu/rpi/legup/ui/proofeditorui/treeview/TreeView.java b/src/main/java/edu/rpi/legup/ui/proofeditorui/treeview/TreeView.java index 562fad1a8..77293b727 100644 --- a/src/main/java/edu/rpi/legup/ui/proofeditorui/treeview/TreeView.java +++ b/src/main/java/edu/rpi/legup/ui/proofeditorui/treeview/TreeView.java @@ -1,30 +1,27 @@ package edu.rpi.legup.ui.proofeditorui.treeview; -import static edu.rpi.legup.model.tree.TreeElementType.NODE; -import static edu.rpi.legup.model.tree.TreeElementType.TRANSITION; -import static edu.rpi.legup.ui.proofeditorui.treeview.TreeNodeView.DIAMETER; -import static edu.rpi.legup.ui.proofeditorui.treeview.TreeNodeView.RADIUS; - -import edu.rpi.legup.app.GameBoardFacade; import edu.rpi.legup.controller.TreeController; -import edu.rpi.legup.model.gameboard.Board; -import edu.rpi.legup.model.gameboard.PuzzleElement; import edu.rpi.legup.model.observer.ITreeListener; -import edu.rpi.legup.model.rules.CaseRule; -import edu.rpi.legup.model.rules.Rule; import edu.rpi.legup.model.tree.Tree; import edu.rpi.legup.model.tree.TreeElement; import edu.rpi.legup.model.tree.TreeNode; import edu.rpi.legup.model.tree.TreeTransition; -import edu.rpi.legup.ui.ScrollView; -import edu.rpi.legup.utility.DisjointSets; -import java.awt.*; -import java.awt.image.BufferedImage; -import java.util.*; -import java.util.List; -import javax.swing.*; +import edu.rpi.legup.ui.zoompane.ZoomView; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; +import java.awt.*; +import java.awt.geom.Point2D; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static edu.rpi.legup.model.tree.TreeElementType.NODE; +import static edu.rpi.legup.model.tree.TreeElementType.TRANSITION; /** * The {@code TreeView} class provides a graphical representation of a {@code Tree} structure, @@ -32,28 +29,12 @@ * {@code ScrollView} and implements {@code ITreeListener} to respond to updates in the tree * structure. */ -public class TreeView extends ScrollView implements ITreeListener { - private static final Logger LOGGER = LogManager.getLogger(TreeView.class.getName()); - - private static final int TRANS_GAP = 5; - - private static final int NODE_GAP_WIDTH = 70; - private static final int NODE_GAP_HEIGHT = 15; - - private static final int BORDER_GAP_HEIGHT = 20; - private static final int BORDER_GAP_WIDTH = 20; - - private static final int BORDER_SPACING = 100; - - private TreeNodeView nodeHover; - - private ArrayList currentStateBoxes; - private Rectangle bounds = new Rectangle(0, 0, 0, 0); +public class TreeView extends ZoomView implements ITreeListener { private Tree tree; private TreeNodeView rootNodeView; - private Map viewMap; - private Dimension dimension; + private final Map viewMap; + private int maxDepth; private TreeViewSelection selection; @@ -62,276 +43,134 @@ public class TreeView extends ScrollView implements ITreeListener { * * @param treeController the {@code TreeController} used to manage tree operations */ - public TreeView(TreeController treeController) { + public TreeView(@Nullable TreeController treeController) { super(treeController); - currentStateBoxes = new ArrayList<>(); - setSize(dimension = new Dimension(100, 200)); - setPreferredSize(new Dimension(640, 160)); viewMap = new HashMap<>(); - selection = new TreeViewSelection(); } /** - * Gets the current tree view selection + * Gets the current tree view selection. * * @return the {@code TreeViewSelection} object representing the current selection */ - public TreeViewSelection getSelection() { - return selection; - } - - /** - * Gets the tree node puzzleElement that the mouse is hovering over - * - * @return tree node puzzleElement that the mouse is hovering over - */ - public TreeNodeView getNodeHover() { - return nodeHover; - } - - /** - * Sets the tree node puzzleElement that the mouse is hovering over - * - * @param nodeHover tree node puzzleElement the mouse is hovering over - */ - public void setNodeHover(TreeNodeView nodeHover) { - this.nodeHover = nodeHover; - } + public TreeViewSelection getSelection() { return selection; } /** - * Gets the TreeElementView by the specified point or null if no view exists at the specified - * point + * Gets the {@code TreeElementView} by the specified point or {@code null} if no view exists at the + * specified point. * * @param point location to query for a view - * @return TreeElementView at the point specified, otherwise null + * @return {@code TreeElementView} at the point specified, otherwise {@code null} */ - public TreeElementView getTreeElementView(Point point) { + public TreeElementView getTreeElementView(@NotNull Point2D point) { return getTreeElementView(point, rootNodeView); } /** - * Recursively gets the TreeElementView by the specified point or null if no view exists at the - * specified point or the view specified is null + * Recursively gets the {@code TreeElementView} by the specified point or {@code null} if no view exists + * at the specified point or the view specified is null. * * @param point location to query for a view * @param elementView view to determine if the point is contained within it - * @return TreeElementView at the point specified, otherwise null + * @return {@code TreeElementView} at the point specified, otherwise {@code null} */ - private TreeElementView getTreeElementView(Point point, TreeElementView elementView) { - if (elementView == null) { - return null; - } else { - if (elementView.contains(point) && elementView.isVisible()) { - if (elementView.getType() == NODE - && ((TreeNodeView) elementView).isContradictoryState()) { - return null; - } - return elementView; - } else { - if (elementView.getType() == NODE) { - TreeNodeView nodeView = (TreeNodeView) elementView; - for (TreeTransitionView transitionView : nodeView.getChildrenViews()) { - TreeElementView view = getTreeElementView(point, transitionView); - if (view != null) { - return view; - } - } - } else { - TreeTransitionView transitionView = (TreeTransitionView) elementView; - return getTreeElementView(point, transitionView.getChildView()); - } + private TreeElementView getTreeElementView(@NotNull Point2D point, @Nullable TreeElementView elementView) { + if (elementView == null) { return null; } + + if (elementView.contains(point) && elementView.isVisible()) { + if (elementView.getType() == NODE && ((TreeNodeView) elementView).isContradictoryState()) { + return null; } + return elementView; } - return null; - } + else if (elementView.getType() == NODE) { + for (TreeTransitionView transitionView : ((TreeNodeView) elementView).getChildViews()) { - /** - * Updates the tree view with the specified {@code Tree} - * - * @param tree the {@code Tree} to display in the view - */ - public void updateTreeView(Tree tree) { - this.tree = tree; - if (selection.getSelectedViews().size() == 0) { - selection.newSelection(new TreeNodeView(tree.getRootNode())); + TreeElementView view = getTreeElementView(point, transitionView); + if (view != null) { return view; } + } } - repaint(); + else { + TreeTransitionView transitionView = (TreeTransitionView) elementView; + return getTreeElementView(point, transitionView.getChildView()); + } + + return null; } /** - * Sets the tree associated with this TreeView + * Updates the tree view with the specified {@code Tree}. * - * @param tree tree + * @param tree the {@code Tree} to display in the view */ - public void setTree(Tree tree) { - this.tree = tree; - } + public void setTree(@NotNull Tree tree) { + if (tree != this.tree) { + this.tree = tree; - /** Updates the size of the tree view based on the bounds of its tree */ - public void updateTreeSize() { - if (GameBoardFacade.getInstance().getTree() == null) { - return; - } - setSize(bounds.getSize()); - } + if (tree.getRootNode() != null) { + addTreeNode(tree.getRootNode()); + rootNodeView = (TreeNodeView) viewMap.get(tree.getRootNode()); + selection.newSelection(rootNodeView); + calculateSpans(rootNodeView); + calculatePositions(rootNodeView); - /** Resets the view if the tree bounds have been modified */ - public void reset() { - if (bounds.x != 0 || bounds.y != 0) { - updateTreeSize(); + updateSize(); + } + else { setSize(new Dimension()); } } } /** - * Adjusts the zoom level to fit the entire tree within the viewport when the Resize Proof - * button is selected - */ - public void zoomFit() { - final int MIN_HEIGHT = 200; - double fitWidth = (viewport.getWidth() - 7.0) / (getSize().width - 75); - double fitHeight = (viewport.getHeight()) / Math.max((getSize().height - 115), MIN_HEIGHT); - zoomTo(Math.min(fitWidth, fitHeight)); - viewport.setViewPosition(new Point(0, viewport.getHeight() / 2)); - } - - /** - * Creates a customized viewport for the scroll pane + * Draws the tree view on the provided {@code Graphics} context. * - * @return viewport for the scroll pane + * @param graphics the {@code Graphics} context to draw on */ @Override - protected JViewport createViewport() { - return new JViewport() { - @Override - protected LayoutManager createLayoutManager() { - return new ViewportLayout() { - @Override - public void layoutContainer(Container parent) { - Point point = viewport.getViewPosition(); - // determine the maximum x and y view positions - int mx = getCanvas().getWidth() - viewport.getWidth(); - int my = getCanvas().getHeight() - viewport.getHeight(); - // obey edge boundaries - if (point.x < 0) { - point.x = 0; - } - if (point.x > mx) { - point.x = mx; - } - if (point.y < 0) { - point.y = 0; - } - if (point.y > my) { - point.y = my; - } - // center margins - if (mx < 0) { - point.x = 0; - } - if (my < 0) { - point.y = my / 2; - } - viewport.setViewPosition(point); - } - }; - } - }; - } + public void draw(@NotNull Graphics graphics) { + if (rootNodeView != null) { + Graphics2D g = (Graphics2D) graphics.create(); + + drawSubtree(g, rootNodeView); + if (selection.getHover() != null) { drawRuleHover(g); } - /** - * Draws the tree view on the provided {@code Graphics2D} context - * - * @param graphics2D the {@code Graphics2D} context to draw on - */ - public void draw(Graphics2D graphics2D) { - currentStateBoxes.clear(); - Tree tree = GameBoardFacade.getInstance().getTree(); - if (tree != null) { - Graphics2D g = (Graphics2D) graphics2D.create(); - // setSize(bounds.getDimension()); - g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - g.setRenderingHint( - RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); - - drawTree(g); - - dimension.width += BORDER_SPACING; - setSize(dimension); - // g.drawRect(0,0, dimension.width, dimension.height); - - if (selection.getHover() != null) { - drawMouseOver(g); - } g.dispose(); } } /** - * Resets the zoom level to its default state and positions the viewport from the top-left - * corner - */ - public void zoomReset() { - zoomTo(1.0); - viewport.setViewPosition(new Point(0, 0)); - } - - /** - * Recursively redraws the tree starting from the specified node view + * Recursively redraws the tree starting from the specified node view. * * @param graphics2D the {@code Graphics2D} context to draw on * @param nodeView the {@code TreeNodeView} to start drawing from */ - private void redrawTree(Graphics2D graphics2D, TreeNodeView nodeView) { - if (nodeView != null) { - nodeView.draw(graphics2D); - for (TreeTransitionView transitionView : nodeView.getChildrenViews()) { - transitionView.draw(graphics2D); - redrawTree(graphics2D, transitionView.getChildView()); - } - } - } - - /** - * Removes the specified {@code TreeElementView} from the tree view - * - * @param view the {@code TreeElementView} to remove - */ - public void removeTreeElement(TreeElementView view) { - if (view.getType() == NODE) { - TreeNodeView nodeView = (TreeNodeView) view; - nodeView.getParentView().setChildView(null); - } else { - TreeTransitionView transitionView = (TreeTransitionView) view; - transitionView - .getParentViews() - .forEach((TreeNodeView n) -> n.removeChildrenView(transitionView)); + private void drawSubtree(@NotNull Graphics2D graphics2D, @NotNull TreeNodeView nodeView) { + for (TreeTransitionView transitionView : nodeView.getChildViews()) { + if (transitionView.getParentViews().getFirst() == nodeView) { transitionView.draw(graphics2D); } + if (transitionView.getChildView() != null) { drawSubtree(graphics2D, transitionView.getChildView()); } } + nodeView.draw(graphics2D); } /** - * When the edu.rpi.legup.user hovers over the transition, draws the corresponding rules image + * When the user hovers over the transition, draws the corresponding rules image. * - * @param g the graphics to use to draw + * @param graphics2D the graphics context to draw on */ - public void drawMouseOver(Graphics2D g) { + public void drawRuleHover(@NotNull Graphics2D graphics2D) { if (selection.getHover().getType() == TRANSITION && ((TreeTransitionView) selection.getHover()).getTreeElement().isJustified()) { - TreeTransition transition = (TreeTransition) selection.getHover().treeElement; - int imgWidth = 100; - int imgHeight = 100; - - BufferedImage image = - new BufferedImage(imgWidth, imgHeight, BufferedImage.TYPE_INT_ARGB); - image.createGraphics() - .drawImage(transition.getRule().getImageIcon().getImage(), 0, 0, null); - Point mousePoint = selection.getMousePoint(); - g.drawImage(image, mousePoint.x, mousePoint.y - 50, imgWidth, imgHeight, null); + + TreeTransition transition = (TreeTransition) selection.getHover().getTreeElement(); + Point2D mousePoint = selection.getMousePoint(); + + transition.getRule().getImageIcon().paintIcon(null, graphics2D, + (int) mousePoint.getX() + 25, (int) mousePoint.getY() - 50); } } - /** Resets the view by clearing the current tree, root node view, and selection */ + /** Resets the view by clearing the current tree, root node view, and selection. */ public void resetView() { this.tree = null; this.rootNodeView = null; @@ -339,702 +178,382 @@ public void resetView() { this.selection.clearHover(); } - /** - * Called when a tree puzzleElement is added to the tree - * - * @param treeElement TreeElement that was added to the tree - */ @Override - public void onTreeElementAdded(TreeElement treeElement) { - if (treeElement.getType() == NODE) { - addTreeNode((TreeNode) treeElement); - } else { - addTreeTransition((TreeTransition) treeElement); - } + public void onTreeElementAdded(@NotNull TreeElement treeElement) { + if (treeElement.getType() == NODE) { addTreeNode((TreeNode) treeElement); } + else { addTreeTransition((TreeTransition) treeElement); } + TreeElementView elementView = viewMap.get(treeElement); + + calculateSpans(elementView); + propagateSpan(elementView); + calculatePositions(rootNodeView); + updateSize(); + repaint(); } - /** - * Called when a tree puzzleElement is removed from the tree - * - * @param element TreeElement that was removed to the tree - */ @Override - public void onTreeElementRemoved(TreeElement element) { - if (element.getType() == NODE) { - TreeNode node = (TreeNode) element; - TreeNodeView nodeView = (TreeNodeView) viewMap.get(node); + public void onTreeElementRemoved(@NotNull TreeElement element) { + TreeElementView parentView, elementView = viewMap.get(element); - nodeView.getParentView().setChildView(null); - removeTreeNode(node); - } else { - TreeTransition trans = (TreeTransition) element; - TreeTransitionView transView = (TreeTransitionView) viewMap.get(trans); - - // unlock ancestor elements if case rule deleted - Rule rule = trans.getRule(); - for (TreeNode node : trans.getParents()) { - - // only if the last case of a case rule will be deleted - if (!(rule instanceof CaseRule && node.getChildren().isEmpty())) { - continue; - } - - CaseRule caseRule = (CaseRule) rule; - // set dependent elements to be modifiable by ancestors (if not dependent on others) - List ancestors = node.getAncestors(); - for (TreeNode ancestor : ancestors) { - // for all ancestors but root - if (ancestor.getParent() == null) { - continue; - } + if (element.getType() == NODE) { + TreeNodeView nodeView = (TreeNodeView) elementView; + parentView = ((TreeNodeView) elementView).getParentView(); + removeTreeNode(nodeView); + if (((TreeNode) element).isRoot()) { rootNodeView = null; } + } + else { + TreeTransitionView transitionView = (TreeTransitionView) elementView; + if (transitionView.getLayoutParentView() != null) { parentView = transitionView.getLayoutParentView(); } + else { parentView = transitionView.getParentViews().getFirst(); } + removeTreeTransition(transitionView); + } - for (PuzzleElement pelement : - caseRule.dependentElements(node.getBoard(), trans.getSelection())) { - // decrement, unlock if 0 cases depended - PuzzleElement oldElement = - ancestor.getParent().getBoard().getPuzzleElement(pelement); - oldElement.setCasesDepended(oldElement.getCasesDepended() - 1); - if (oldElement.getCasesDepended() != 0) { - continue; - } + propagateSpan(parentView); + calculatePositions(rootNodeView); + updateSize(); - // set modifiable if started modifiable - boolean modifiable = - tree.getRootNode() - .getBoard() - .getPuzzleElement(oldElement) - .isModifiable(); - - // unmodifiable if already modified - TreeNode modNode = ancestor.getParent().getParents().get(0); - while (modNode.getParent() != null) { - Board modBoard = modNode.getParent().getBoard(); - if (modBoard.getModifiedData() - .contains(modBoard.getPuzzleElement(oldElement))) { - modifiable = false; - break; - } - modNode = modNode.getParent().getParents().get(0); - } - oldElement.setModifiable(modifiable); - } - } - } - - transView.getParentViews().forEach(n -> n.removeChildrenView(transView)); - removeTreeTransition(trans); - } repaint(); } - /** - * Called when the tree selection was changed - * - * @param selection tree selection that was changed - */ @Override - public void onTreeSelectionChanged(TreeViewSelection selection) { + public void onTreeSelectionChanged(@NotNull TreeViewSelection selection) { this.selection.getSelectedViews().forEach(v -> v.setSelected(false)); selection.getSelectedViews().forEach(v -> v.setSelected(true)); this.selection = selection; repaint(); } - /** Called when the model has finished updating the tree. */ - @Override - public void onUpdateTree() { - repaint(); - } - /** - * Gets the TreeElementView by the corresponding TreeElement associated with it + * Gets the {@code TreeElementView} by the corresponding {@code TreeElement} associated with it. * - * @param element TreeElement of the view - * @return TreeElementView of the TreeElement associated with it + * @param element {@code TreeElement} of the view + * @return {@code TreeElementView} of the {@code TreeElement} associated with it */ - public TreeElementView getElementView(TreeElement element) { - return viewMap.get(element); - } + public TreeElementView getElementView(@NotNull TreeElement element) { return viewMap.get(element); } /** - * Removes the specified {@link TreeNode} and its associated views + * Recursively removes the specified {@code TreeNodeView}. * - * @param node the {@link TreeNode} to be removed + * @param nodeView the {@code TreeNodeView} to be removed */ - public void removeTreeNode(TreeNode node) { - viewMap.remove(node); - if (node.getChildren() != null) { - node.getChildren().forEach(t -> removeTreeTransition(t)); - } - - List children = node.getChildren(); - - // if child is a case rule, unlock ancestor elements - if (!children.isEmpty()) { - Rule rule = children.get(0).getRule(); - if (rule instanceof CaseRule) { - CaseRule caseRule = (CaseRule) rule; - // set dependent elements to be modifiable by ancestors (if not dependent on others) - List ancestors = node.getAncestors(); - for (TreeNode ancestor : ancestors) { - // for all ancestors but root - if (ancestor.getParent() == null) { - continue; - } - for (PuzzleElement pelement : - caseRule.dependentElements( - node.getBoard(), children.get(0).getSelection())) { - // decrement, unlock if 0 cases depended - PuzzleElement oldElement = - ancestor.getParent().getBoard().getPuzzleElement(pelement); - oldElement.setCasesDepended(oldElement.getCasesDepended() - 1); - if (oldElement.getCasesDepended() == 0) { - continue; - } + private void removeTreeNode(@NotNull TreeNodeView nodeView) { + while (!nodeView.getChildViews().isEmpty()) { removeTreeTransition(nodeView.getChildViews().getFirst()); } + viewMap.remove(nodeView.getTreeElement()); - // set modifiable if started modifiable - boolean modifiable = false; - if (tree != null) { - tree.getRootNode() - .getBoard() - .getPuzzleElement(oldElement) - .isModifiable(); - } - - // unmodifiable if already modified - TreeNode modNode = ancestor.getParent().getParents().get(0); - while (modNode.getParent() != null) { - Board modBoard = modNode.getParent().getBoard(); - if (modBoard.getModifiedData() - .contains(modBoard.getPuzzleElement(oldElement))) { - modifiable = false; - break; - } - modNode = modNode.getParent().getParents().get(0); - } - oldElement.setModifiable(modifiable); - } - } - } + if (nodeView.getParentView() != null) { + nodeView.getParentView().setChildView(null); + nodeView.setParentView(null); } + if (nodeView.getDepth() == maxDepth) { updateMaxDepth(); } } /** - * Removes the specified {@link TreeTransition} and its associated views + * Removes the specified {@code TreeTransitionView}. * - * @param trans the {@link TreeTransition} to be removed + * @param transitionView the {@code TreeTransitionView} to be removed */ - public void removeTreeTransition(TreeTransition trans) { - viewMap.remove(trans); - if (trans.getChildNode() != null) { - removeTreeNode(trans.getChildNode()); + private void removeTreeTransition(@NotNull TreeTransitionView transitionView) { + if (transitionView.getChildView() != null) { removeTreeNode(transitionView.getChildView()); } + viewMap.remove(transitionView.getTreeElement()); + + for (TreeNodeView parentView : transitionView.getParentViews()) { parentView.removeChildView(transitionView); } + transitionView.setParentViews(new ArrayList<>()); + if (transitionView.getLayoutParentView() != null) { + transitionView.getLayoutParentView().removeChildView(transitionView); } + transitionView.setLayoutParentView(null); - // Update transition modifiability if removing a case rule - List parents = trans.getParents(); - for (TreeNode parent : parents) { - // if transition is a case rule, unlock ancestor elements up until latest case rule or - // root node - boolean nextAncestorIsCaseRule = false; - Rule rule = trans.getRule(); - if (rule instanceof CaseRule) { - List ancestors = parent.getAncestors(); - for (int i = 0; i < ancestors.size(); i++) { - if (ancestors.get(i).getParent() == null) { - continue; - } - if (nextAncestorIsCaseRule) { - break; - } - for (PuzzleElement element : parent.getBoard().getPuzzleElements()) { - PuzzleElement curElement = - ancestors.get(i).getParent().getBoard().getPuzzleElement(element); - if (!curElement.isModifiableCaseRule()) { - curElement.setModifiableCaseRule(true); - } - } - if (ancestors.get(i).getParent().getRule() instanceof CaseRule) { - nextAncestorIsCaseRule = true; - } - } - } - } + if (transitionView.getDepth() == maxDepth) { updateMaxDepth(); } } /** - * Adds the specified {@link TreeNode} and its associated views + * Adds the specified {@code TreeNode} and its associated views. * - * @param node the {@link TreeNode} to be added + * @param node the {@code TreeNode} to be added */ - private void addTreeNode(TreeNode node) { + private void addTreeNode(@NotNull TreeNode node) { TreeTransition parent = node.getParent(); TreeNodeView nodeView = new TreeNodeView(node); - TreeTransitionView parentView = (TreeTransitionView) viewMap.get(parent); - nodeView.setParentView(parentView); - parentView.setChildView(nodeView); + if (parent != null) { + TreeTransitionView parentView = (TreeTransitionView) viewMap.get(parent); + parentView.setChildView(nodeView); + nodeView.setParentView(parentView); + + int newDepth = parentView.getDepth() + 1; + nodeView.setDepth(newDepth); + maxDepth = Math.max(maxDepth, newDepth); + } viewMap.put(node, nodeView); - if (!node.getChildren().isEmpty()) { + // Add transition children only once all their parent views have been created + for (TreeTransition transition : node.getChildren()) { - // if adding a case rule, lock dependent ancestor elements - Rule rule = node.getChildren().get(0).getRule(); - if (rule instanceof CaseRule) { - CaseRule caseRule = (CaseRule) rule; + boolean allParentViewsExist = true; + for (TreeNode parentNode : transition.getParents()) { - List ancestors = node.getAncestors(); - for (TreeNode ancestor : ancestors) { - // for all ancestors but root - if (ancestor.getParent() == null) { - continue; - } - for (PuzzleElement element : - caseRule.dependentElements( - node.getBoard(), node.getChildren().get(0).getSelection())) { - // increment and lock - PuzzleElement oldElement = - ancestor.getParent().getBoard().getPuzzleElement(element); - oldElement.setCasesDepended(oldElement.getCasesDepended() + 1); - oldElement.setModifiable(false); - } + if (parentNode != node && viewMap.get(parentNode) == null) { + allParentViewsExist = false; + break; } } - node.getChildren().forEach(t -> addTreeTransition(t)); + if (allParentViewsExist) { addTreeTransition(transition); } } } /** - * Adds the specified {@link TreeTransition} and its associated views + * Adds the specified {@code TreeTransition} and its associated views. + *

+ * If this transition is a merge, this operation will do nothing if any of {@code TreeNode} parents' views + * have not yet been created. It will also take the additional step of assigning the new + * {@code TreeTransitionView} to be the layout child of the deepest common ancestor of all of its parent + * node views. * - * @param trans The {@link TreeTransition} to be added + * @param transition The {@code TreeTransition} to be added */ - private void addTreeTransition(TreeTransition trans) { - List parents = trans.getParents(); - TreeTransitionView transView = new TreeTransitionView(trans); - - for (TreeNode parent : parents) { - TreeNodeView parentNodeView = (TreeNodeView) viewMap.get(parent); - transView.addParentView(parentNodeView); - parentNodeView.addChildrenView(transView); - - viewMap.put(trans, transView); - - // if transition is a new case rule, lock dependent ancestor elements - Rule rule = trans.getRule(); - if (rule instanceof CaseRule && parent.getChildren().size() == 1) { - List ancestors = parent.getAncestors(); - for (TreeNode ancestor : ancestors) { - // for all ancestors but root - if (ancestor.getParent() == null) { - continue; - } + private void addTreeTransition(@NotNull TreeTransition transition) { - for (PuzzleElement element : parent.getBoard().getPuzzleElements()) { - PuzzleElement curElement = - ancestor.getParent().getBoard().getPuzzleElement(element); - curElement.setModifiableCaseRule(false); - } - } - } - } + TreeTransitionView transitionView; + if (transition.getParents().size() > 1) { - if (trans.getChildNode() != null) { - addTreeNode(trans.getChildNode()); - } - } + List parentViews = new ArrayList<>(); - /** - * Draws the tree using the provided {@link Graphics2D} object - * - * @param graphics2D the {@link Graphics2D} object used for drawing the tree - */ - public void drawTree(Graphics2D graphics2D) { - if (tree == null) { - LOGGER.error("Unable to draw tree."); - } else { - if (rootNodeView == null) { - rootNodeView = new TreeNodeView(tree.getRootNode()); - - if (LOGGER.isDebugEnabled()) { - LOGGER.debug("Creating new views for tree view."); - } - createViews(rootNodeView); + for (TreeNode parentNode : transition.getParents()) { + TreeNodeView parentView = (TreeNodeView) viewMap.get(parentNode); - selection.newSelection(rootNodeView); + if (parentView == null) { return; } + else { parentViews.add(parentView); } } - dimension = new Dimension(0, 0); - calcSpan(rootNodeView); - rootNodeView.setSpan(rootNodeView.getSpan() + DIAMETER + BORDER_SPACING); + ArrayList indexTotal = new ArrayList<>(); + ArrayList ancestors = new ArrayList<>(); - calculateViewLocations(rootNodeView, 0); - dimension.height = (int) rootNodeView.getSpan(); + // Find the deepest common ancestor of all elements of parentViews + TreeNodeView currentNodeView = parentViews.getFirst(); + TreeTransitionView currentTransitionView; + while ((currentTransitionView = currentNodeView.getParentView()) != null) { + + currentNodeView = currentTransitionView.getParentViews().getFirst(); + if (currentTransitionView.getParentViews().size() > 1) { + currentNodeView = currentTransitionView.getLayoutParentView(); + } - redrawTree(graphics2D, rootNodeView); - if (LOGGER.isDebugEnabled()) { - LOGGER.debug("DrawTree: dimensions - {}x{}", dimension.width, dimension.height); + indexTotal.addFirst(currentNodeView.getChildViews().indexOf(currentTransitionView)); + ancestors.addFirst(currentNodeView); } - } - } - /** - * Creates views for the given {@link TreeNodeView} and its children - * - * @param nodeView the {@link TreeNodeView} for which to create views - */ - public void createViews(TreeNodeView nodeView) { - if (nodeView != null) { - viewMap.put(nodeView.getTreeElement(), nodeView); - - TreeNode node = nodeView.getTreeElement(); - for (TreeTransition trans : node.getChildren()) { - TreeTransitionView transView = (TreeTransitionView) viewMap.get(trans); - if (transView != null) { - nodeView.addChildrenView(transView); - transView.addParentView(nodeView); - break; - } - transView = new TreeTransitionView(trans); + // Remove non-shared ancestors and add up indices to average for placement later + for (int i = 1; i < parentViews.size(); ++i) { - viewMap.put(transView.getTreeElement(), transView); + currentNodeView = parentViews.get(i); + int j = 0; - transView.addParentView(nodeView); - nodeView.addChildrenView(transView); + while ((currentTransitionView = currentNodeView.getParentView()) != null) { - TreeNode childNode = trans.getChildNode(); - if (childNode != null) { - TreeNodeView childNodeView = new TreeNodeView(childNode); - viewMap.put(childNodeView.getTreeElement(), childNodeView); + currentNodeView = currentTransitionView.getParentViews().getFirst(); + if (currentTransitionView.getParentViews().size() > 1) { + currentNodeView = currentTransitionView.getLayoutParentView(); + } - childNodeView.setParentView(transView); - transView.setChildView(childNodeView); + if ((j = ancestors.indexOf(currentNodeView)) != -1) { + while (ancestors.size() > j + 1) { + ancestors.removeLast(); + indexTotal.removeLast(); + } - createViews(childNodeView); + indexTotal.set(j, indexTotal.get(j) + + currentNodeView.getChildViews().indexOf(currentTransitionView)); + break; + } } + + // All parent views to this point must have had same ancestor chain so no need to manually check + while (--j > 0) { indexTotal.set(j, indexTotal.get(j) * (i + 1) / i); } } + + transitionView = new TreeTransitionView(transition, parentViews, ancestors.getLast()); + int depth = 0; + for (TreeNodeView parentView : parentViews) { + parentView.addChildView(transitionView); + depth = Math.max(depth, parentView.getDepth()); + } + transitionView.setDepth(depth + 1); + ancestors.getLast().insertChildView((int) Math.round((double) indexTotal.getLast() / + parentViews.size()), transitionView); + } + else { + TreeNodeView nodeView = (TreeNodeView) viewMap.get(transition.getParents().getFirst()); + transitionView = new TreeTransitionView(transition, nodeView); + nodeView.addChildView(transitionView); + transitionView.setDepth(nodeView.getDepth() + 1); } + + maxDepth = Math.max(maxDepth, transitionView.getDepth()); + viewMap.put(transition, transitionView); + if (transition.getChildNode() != null) { addTreeNode(transition.getChildNode()); } } /** - * Calculates the layout locations (x and y coordinates) of the nodes in the tree. This method - * recursively traverses the tree and updates the positions of nodes and transitions based on - * their depth and parent relationships. + * Recursively calculates the spans of element views starting from the specified element view. * - * @param nodeView the node view to calculate the positions for - * @param depth the depth of the node in the tree, used to calculate its x-coordinate + * @param elementView the element view to start calculating spans from */ - public void calculateViewLocations(TreeNodeView nodeView, int depth) { - nodeView.setDepth(depth); - int xLoc = (NODE_GAP_WIDTH + DIAMETER) * depth + DIAMETER; - nodeView.setX(xLoc); - dimension.width = Math.max(dimension.width, xLoc); - - TreeTransitionView parentTransView = nodeView.getParentView(); - int yLoc = - parentTransView == null ? (int) nodeView.getSpan() / 2 : parentTransView.getEndY(); - nodeView.setY(yLoc); - - ArrayList children = nodeView.getChildrenViews(); - switch (children.size()) { - case 0: - break; - case 1: - { - TreeTransitionView childView = children.get(0); - - List parentsViews = childView.getParentViews(); - if (parentsViews.size() == 1) { - childView.setEndY(yLoc); - - childView.setDepth(depth); - - Point lineStartPoint = childView.getLineStartPoint(0); - lineStartPoint.x = xLoc + RADIUS + TRANS_GAP / 2; - lineStartPoint.y = yLoc; - childView.setEndX( - (NODE_GAP_WIDTH + DIAMETER) * (depth + 1) + RADIUS - TRANS_GAP / 2); - - dimension.width = Math.max(dimension.width, childView.getEndX()); - - TreeNodeView childNodeView = childView.getChildView(); - if (childNodeView != null) { - calculateViewLocations(childNodeView, depth + 1); - } - } else { - if (parentsViews.size() > 1 - && parentsViews.get(parentsViews.size() - 1) == nodeView) { - int yAvg = 0; - for (int i = 0; i < parentsViews.size(); i++) { - TreeNodeView parentNodeView = parentsViews.get(i); - depth = Math.max(depth, parentNodeView.getDepth()); - yAvg += parentNodeView.getY(); - - Point lineStartPoint = childView.getLineStartPoint(i); - lineStartPoint.x = parentNodeView.getX() + RADIUS + TRANS_GAP / 2; - lineStartPoint.y = parentNodeView.getY(); - } - yAvg /= parentsViews.size(); - childView.setEndY(yAvg); - - childView.setDepth(depth); - - childView.setEndX( - (NODE_GAP_WIDTH + DIAMETER) * (depth + 1) - + RADIUS - - TRANS_GAP / 2); - - dimension.width = Math.max(dimension.width, childView.getEndX()); - - TreeNodeView childNodeView = childView.getChildView(); - if (childNodeView != null) { - calculateViewLocations(childNodeView, depth + 1); - } - } - } - break; - } - default: - { - int span = 0; - for (TreeTransitionView childView : children) { - span += childView.getSpan(); - } - - span = (int) ((nodeView.getSpan() - span) / 2); - for (int i = 0; i < children.size(); i++) { - TreeTransitionView childView = children.get(i); - - childView.setDepth(depth); - - Point lineStartPoint = childView.getLineStartPoint(0); - lineStartPoint.x = xLoc + RADIUS + TRANS_GAP / 2; - lineStartPoint.y = yLoc; - childView.setEndX( - (NODE_GAP_WIDTH + DIAMETER) * (depth + 1) + RADIUS - TRANS_GAP / 2); - childView.setEndY( - yLoc - - (int) (nodeView.getSpan() / 2) - + span - + (int) (childView.getSpan() / 2)); - - span += childView.getSpan(); - TreeNodeView childNodeView = childView.getChildView(); - if (childNodeView != null) { - calculateViewLocations(childNodeView, depth + 1); - } + private void calculateSpans(@NotNull TreeElementView elementView) { + + if (elementView.getType() == NODE) { + TreeNodeView nodeView = (TreeNodeView) elementView; + if (nodeView.getChildViews().isEmpty()) { elementView.setSpan(getMinSpan()); } + else { + int span = 0; + for (TreeTransitionView transitionView : nodeView.getChildViews()) { + + if (transitionView.getParentViews().size() == 1 || + transitionView.getLayoutParentView() == nodeView) { + calculateSpans(transitionView); + span += transitionView.getSpan(); } - break; + else { span += getMinSpan(); } } + nodeView.setSpan(span); + } + } + else { + TreeTransitionView transitionView = (TreeTransitionView) elementView; + if (transitionView.getChildView() != null) { + calculateSpans(transitionView.getChildView()); + transitionView.setSpan(Math.max(transitionView.getChildView().getSpan(), getMinSpan())); + } + else { transitionView.setSpan(getMinSpan()); } } } /** - * Calculates the span (height) required for the given view, including its children. This method - * recursively determines the span for nodes and transitions based on their children and the - * merging branches they belong to. + * Recursively propagates a span change in {@code elementView}'s children up the tree. * - * @param view the view whose span is to be calculated + * @param elementView the element view to propagate a span change from */ - public void calcSpan(TreeElementView view) { - if (view.getType() == NODE) { - TreeNodeView nodeView = (TreeNodeView) view; - TreeNode node = nodeView.getTreeElement(); - if (nodeView.getChildrenViews().size() == 0) { - nodeView.setSpan(DIAMETER + NODE_GAP_HEIGHT); - } else { - if (nodeView.getChildrenViews().size() == 1) { - TreeTransitionView childView = nodeView.getChildrenViews().get(0); - calcSpan(childView); - if (childView.getParentViews().size() > 1) { - nodeView.setSpan(DIAMETER + NODE_GAP_HEIGHT); - } else { - nodeView.setSpan(childView.getSpan()); - } - } else { - DisjointSets branches = node.findMergingBranches(); - List children = node.getChildren(); - - if (node == children.get(0).getParents().get(0)) { - reorderBranches(node, branches); - ArrayList newChildrenViews = new ArrayList<>(); - for (TreeTransition trans : node.getChildren()) { - newChildrenViews.add((TreeTransitionView) viewMap.get(trans)); - } - nodeView.setChildrenViews(newChildrenViews); - } + private void propagateSpan(@NotNull TreeElementView elementView) { - List> mergingSets = branches.getAllSets(); - - double span = 0.0; - for (Set mergeSet : mergingSets) { - if (mergeSet.size() > 1) { - TreeTransition mergePoint = TreeNode.findMergingPoint(mergeSet); - TreeTransitionView mergePointView = - (TreeTransitionView) viewMap.get(mergePoint); - double subSpan = 0.0; - for (TreeTransition branch : mergeSet) { - TreeTransitionView branchView = - (TreeTransitionView) viewMap.get(branch); - subCalcSpan(branchView, mergePointView); - subSpan += branchView.getSpan(); - } - calcSpan(mergePointView); - span += Math.max(mergePointView.getSpan(), subSpan); - } else { - TreeTransition trans = mergeSet.iterator().next(); - TreeTransitionView transView = (TreeTransitionView) viewMap.get(trans); - calcSpan(transView); - span += transView.getSpan(); - } - } - nodeView.setSpan(span); + if (elementView.getType() == NODE) { + TreeNodeView nodeView = (TreeNodeView) elementView; + int span = 0; + + for (TreeTransitionView transitionView : nodeView.getChildViews()) { + if (transitionView.getParentViews().size() == 1 || transitionView.getLayoutParentView() == nodeView) { + span += transitionView.getSpan(); } } - } else { - TreeTransitionView transView = (TreeTransitionView) view; - TreeNodeView nodeView = transView.getChildView(); - if (nodeView == null) { - transView.setSpan(DIAMETER + NODE_GAP_HEIGHT); - } else { - calcSpan(nodeView); - transView.setSpan(nodeView.getSpan()); + nodeView.setSpan(Math.max(span, getMinSpan())); + if (nodeView.getParentView() != null) { propagateSpan(nodeView.getParentView()); } + } + else { + TreeTransitionView transitionView = (TreeTransitionView) elementView; + if (transitionView.getChildView() != null) { + transitionView.setSpan(transitionView.getChildView().getSpan()); + } + else { transitionView.setSpan(getMinSpan()); } + + if (transitionView.getParentViews().size() == 1) { + propagateSpan(transitionView.getParentViews().getFirst()); } + else { propagateSpan(transitionView.getLayoutParentView()); } } } /** - * Calculates the span of a subtree rooted at the specified view, stopping at the given stop - * view. The stop view is not included in the span calculation. + * Recursively lays out the positions to draw the nodes and transitions starting from the specified node view. * - * @param view the root view of the subtree to calculate the span for - * @param stop the view at which to stop the span calculation. The stop view itself is not - * included in the span calculation + * @param nodeView the {@code TreeNodeView} to start laying out from */ - private void subCalcSpan(TreeElementView view, TreeElementView stop) { - // safe-guard for infinite loop - if (view == stop) { - return; + private void calculatePositions(@NotNull TreeNodeView nodeView) { + int xPos = getNodeX(nodeView.getDepth()); + nodeView.setX(xPos); + + int yPos = (nodeView.getParentView() == null) ? nodeView.getSpan() / 2 : nodeView.getParentView().getEndY(); + nodeView.setY(yPos); + + List childViews = nodeView.getChildViews(); + int spanPos = yPos - nodeView.getSpan() / 2; + for (TreeTransitionView transitionView : childViews) { + + if (transitionView.getLayoutParentView() != nodeView) { + transitionView.setTailStartPoint( + transitionView.getParentViews().indexOf(nodeView), + new Point( + xPos + getNodeWidth() / 2 + getGapWidth() + + (UIManager.getInt("Tree.outlineWidth") + + UIManager.getInt("Tree.transitionTailWeight")) / 2, + yPos + ) + ); + } + if (transitionView.getParentViews().size() == 1 || transitionView.getLayoutParentView() == nodeView) { + transitionView.setEndX( + getNodeX(transitionView.getDepth() + 1) - getNodeWidth() / 2 - getGapWidth() + - UIManager.getInt("Tree.outlineWidth") / 2 + ); + transitionView.setEndY(spanPos + transitionView.getSpan() / 2); + spanPos += transitionView.getSpan(); + + if (transitionView.getChildView() != null) { calculatePositions(transitionView.getChildView()); } + } } + } - if (view.getType() == NODE) { - TreeNodeView nodeView = (TreeNodeView) view; - TreeNode node = nodeView.getTreeElement(); - if (nodeView.getChildrenViews().size() == 0) { - nodeView.setSpan(DIAMETER + NODE_GAP_HEIGHT); - } else { - if (nodeView.getChildrenViews().size() == 1) { - TreeTransitionView childView = nodeView.getChildrenViews().get(0); - if (childView == stop) { - nodeView.setSpan(DIAMETER + NODE_GAP_HEIGHT); - } else { - subCalcSpan(childView, stop); - if (childView.getParentViews().size() > 1) { - nodeView.setSpan(DIAMETER + NODE_GAP_HEIGHT); - } else { - nodeView.setSpan(childView.getSpan()); - } - } - } else { - DisjointSets branches = node.findMergingBranches(); - List children = node.getChildren(); - - if (node == children.get(0).getParents().get(0)) { - reorderBranches(node, branches); - } - - List> mergingSets = branches.getAllSets(); - - double span = 0.0; - for (Set mergeSet : mergingSets) { - if (mergeSet.size() > 1) { - TreeTransition mergePoint = TreeNode.findMergingPoint(mergeSet); - TreeTransitionView mergePointView = - (TreeTransitionView) viewMap.get(mergePoint); - double subSpan = 0.0; - for (TreeTransition branch : mergeSet) { - TreeTransitionView branchView = - (TreeTransitionView) viewMap.get(branch); - subCalcSpan(branchView, mergePointView); - subSpan += branchView.getSpan(); - } - subCalcSpan(mergePointView, stop); - span += Math.max(mergePointView.getSpan(), subSpan); - } else { - TreeTransition trans = mergeSet.iterator().next(); - TreeTransitionView transView = (TreeTransitionView) viewMap.get(trans); - subCalcSpan(transView, stop); - span += transView.getSpan(); - } - } + /** Updates size of the view based on the max depth of the tree and the span of the root node. */ + private void updateSize() { + setSize(new Dimension( + getNodeWidth() * (maxDepth / 2 + 1) + getNodeGapWidth() * ((maxDepth + 1) / 2), + rootNodeView.getSpan() + )); + } - nodeView.setSpan(span); - } - } - } else { - TreeTransitionView transView = (TreeTransitionView) view; - TreeNodeView nodeView = transView.getChildView(); - if (nodeView == null || nodeView == stop) { - transView.setSpan(DIAMETER + NODE_GAP_HEIGHT); - } else { - calcSpan(nodeView); - transView.setSpan(nodeView.getSpan()); - } + /** Updates the max depth of the tree after an element has been removed. */ + private void updateMaxDepth() { + int newMaxDepth = 0; + for (TreeElementView elementView : viewMap.values()) { + newMaxDepth = Math.max(elementView.getDepth(), newMaxDepth); + if (newMaxDepth == maxDepth) { return; } } + maxDepth = newMaxDepth; } /** - * Reorders the branches of a given node such that branches that merge are grouped together - * sequentially. Transitions are kept in their relative order based on their original positions - * in the list of child transitions of the specified node. This ensures that the visual - * representation of the branches and transitions maintains a logical and readable structure. + * Gets the x coordinate of a node view based on its depth. * - * @param node the root node whose branches are to be reordered - * @param branches a DisjointSets structure representing the merging relationships of the child - * branches of the specified node. This determines which branches should be grouped together + * @param depth the depth of the node view + * @return the x coordinate to place the node view */ - private void reorderBranches(TreeNode node, DisjointSets branches) { - List children = node.getChildren(); - List> mergingSets = branches.getAllSets(); - - List> newOrder = new ArrayList<>(); - for (Set set : mergingSets) { - List mergeBranch = new ArrayList<>(); - newOrder.add(mergeBranch); - children.forEach( - t -> { - if (set.contains(t)) { - mergeBranch.add(t); - } - }); - mergeBranch.sort( - (TreeTransition t1, TreeTransition t2) -> - children.indexOf(t1) <= children.indexOf(t2) ? -1 : 1); - } + private int getNodeX(int depth) { + int nodeWidth = getNodeWidth(); + return (nodeWidth + getNodeGapWidth()) * (depth / 2) + (nodeWidth / 2); + } - newOrder.sort( - (List b1, List b2) -> { - int low1 = -1; - int low2 = -1; - for (TreeTransition t1 : b1) { - int curIndex = children.indexOf(t1); - if (low1 == -1 || curIndex < low1) { - low1 = curIndex; - } - } - for (TreeTransition t1 : b2) { - int curIndex = children.indexOf(t1); - if (low1 == -1 || curIndex < low1) { - low1 = curIndex; - } - } - return low1 < low2 ? -1 : 1; - }); + /** @return the width in view coordinates of each node */ + private int getNodeWidth() { + return UIManager.getInt("Tree.nodeRadius") * 2 + + UIManager.getInt("Tree.outlineWidth"); + } - List newChildren = new ArrayList<>(); - newOrder.forEach(l -> newChildren.addAll(l)); - node.setChildren(newChildren); + /** @return the width of the gap between nodes horizontally */ + private int getNodeGapWidth() { + return UIManager.getInt("Tree.transitionTailGap") + + UIManager.getInt("Tree.transitionHeadHeight") + + (UIManager.getInt("Tree.transitionTailWeight") / 2) + + UIManager.getInt("Tree.outlineWidth") + getGapWidth() * 2; } + + /** @return the width of the gap between connected nodes and transitions */ + private int getGapWidth() { return UIManager.getInt("Tree.horizontalGap"); } + + /** + * Gets the minimum span of any {@code TreeElementView}. + * + * @return the minimum span + */ + private int getMinSpan() { return UIManager.getInt("Tree.nodeRadius") * 2 + + UIManager.getInt("Tree.outlineWidth") + UIManager.getInt("Tree.verticalGap"); } } diff --git a/src/main/java/edu/rpi/legup/ui/proofeditorui/treeview/TreeViewSelection.java b/src/main/java/edu/rpi/legup/ui/proofeditorui/treeview/TreeViewSelection.java index c509b6cb9..2629f4282 100644 --- a/src/main/java/edu/rpi/legup/ui/proofeditorui/treeview/TreeViewSelection.java +++ b/src/main/java/edu/rpi/legup/ui/proofeditorui/treeview/TreeViewSelection.java @@ -1,6 +1,10 @@ package edu.rpi.legup.ui.proofeditorui.treeview; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + import java.awt.*; +import java.awt.geom.Point2D; import java.util.ArrayList; import java.util.List; @@ -12,9 +16,9 @@ public class TreeViewSelection { private ArrayList selectedViews; private TreeElementView hover; - private Point mousePoint; + private Point2D mousePoint; - /** TreeViewSelection Constructor creates a tree view selection */ + /** {@code TreeViewSelection} constructor creates a tree view selection. */ public TreeViewSelection() { this.selectedViews = new ArrayList<>(); this.hover = null; @@ -22,49 +26,45 @@ public TreeViewSelection() { } /** - * TreeViewSelection Constructor creates a tree view selection with a selected view + * {@code TreeViewSelection} constructor creates a tree view selection with a selected view. * * @param view selected view */ - public TreeViewSelection(TreeElementView view) { + public TreeViewSelection(@NotNull TreeElementView view) { this(); this.selectedViews.add(view); } /** - * TreeViewSelection Constructor creates a tree view selection with a list of selected views + * {@code TreeViewSelection} constructor creates a tree view selection with a list of selected views. * * @param views list of selected views */ - public TreeViewSelection(List views) { + public TreeViewSelection(@NotNull List views) { this(); this.selectedViews.addAll(views); } /** - * Gets the list of selected tree puzzleElement views + * Gets the list of selected tree element views. * - * @return list of selected tree puzzleElement views + * @return list of selected tree element views */ - public List getSelectedViews() { - return selectedViews; - } + public List getSelectedViews() { return selectedViews; } /** - * Gets the first selectedViews in the list of views + * Gets the first element view in the list of views. * - * @return first selectedViews in the list of views + * @return first element view in the list of views */ - public TreeElementView getFirstSelection() { - return selectedViews.size() == 0 ? null : selectedViews.get(0); - } + public TreeElementView getFirstSelection() { return selectedViews.isEmpty() ? null : selectedViews.getFirst(); } /** - * Toggles a tree puzzleElement view selectedViews + * Toggles the selected state of an element view. * - * @param treeElementView a tree puzzleElement view to toggle + * @param treeElementView a tree element view to toggle */ - public void toggleSelection(TreeElementView treeElementView) { + public void toggleSelection(@NotNull TreeElementView treeElementView) { if (selectedViews.contains(treeElementView)) { selectedViews.remove(treeElementView); treeElementView.setSelected(false); @@ -75,11 +75,11 @@ public void toggleSelection(TreeElementView treeElementView) { } /** - * Adds a tree puzzleElement view selectedViews + * Selects an element view. * - * @param treeElementView a tree puzzleElement view to add + * @param treeElementView a tree element view to select */ - public void addToSelection(TreeElementView treeElementView) { + public void addToSelection(@NotNull TreeElementView treeElementView) { if (!selectedViews.contains(treeElementView)) { selectedViews.add(treeElementView); treeElementView.setSelected(true); @@ -87,17 +87,17 @@ public void addToSelection(TreeElementView treeElementView) { } /** - * Creates a new selectedViews and add the specified tree puzzleElement view + * Creates a new selection containing only the specified tree element view. * - * @param treeElementView tree puzzleElement view + * @param treeElementView a tree element view to select */ - public void newSelection(TreeElementView treeElementView) { + public void newSelection(@NotNull TreeElementView treeElementView) { clearSelection(); selectedViews.add(treeElementView); treeElementView.setSelected(true); } - /** Clears all selected views */ + /** Deselects all selected views. */ public void clearSelection() { for (TreeElementView treeElementView : selectedViews) { treeElementView.setSelected(false); @@ -106,29 +106,24 @@ public void clearSelection() { } /** - * Gets tree puzzleElement view that the mouse is hovering over or null is no such view exists + * Gets tree element view that the mouse is hovering over or {@code null} is no such view exists. * - * @return tree puzzleElement view that the mouse is hovering over or null is no such view - * exists + * @return tree element view that the mouse is hovering over or {@code null} is no such view exists */ - public TreeElementView getHover() { - return hover; - } + public TreeElementView getHover() { return hover; } /** - * Clears the previous hover and sets the specified tree puzzleElement view to the new hover + * Clears the previous hover and sets the specified tree puzzleElement view to the new hover. * * @param newHovered tree puzzleElement view for the new hover */ - public void newHover(TreeElementView newHovered) { + public void newHover(@NotNull TreeElementView newHovered) { newHovered.setHover(true); - if (hover != null) { - hover.setHover(false); - } + if (hover != null) { hover.setHover(false); } hover = newHovered; } - /** Clears the current hover tree puzzleElement view */ + /** Clears the current hover tree element view. */ public void clearHover() { if (hover != null) { hover.setHover(false); @@ -137,34 +132,29 @@ public void clearHover() { } /** - * Gets the current mouse location relative to the tree view + * Gets the current mouse location relative to the tree view. * * @return the current mouse location relative to the tree view */ - public Point getMousePoint() { - return mousePoint; - } + public Point2D getMousePoint() { return mousePoint; } /** - * Sets the current mouse location relative to the tree view + * Sets the current mouse location relative to the tree view. * * @param point the current mouse location relative to the tree view */ - public void setMousePoint(Point point) { - this.mousePoint = point; - } + public void setMousePoint(@Nullable Point2D point) { this.mousePoint = point; } /** - * Copies the TreeViewSelection + * Copies the {@code TreeViewSelection}. * - * @return a copy of this TreeViewSelection + * @return a copy of this {@code TreeViewSelection} */ - @SuppressWarnings("unchecked") public TreeViewSelection copy() { - TreeViewSelection cpy = new TreeViewSelection(); - cpy.selectedViews = (ArrayList) selectedViews.clone(); - cpy.hover = hover; - cpy.mousePoint = mousePoint; - return cpy; + TreeViewSelection copy = new TreeViewSelection(); + copy.selectedViews = new ArrayList<>(selectedViews); + copy.hover = hover; + copy.mousePoint = mousePoint; + return copy; } } diff --git a/src/main/java/edu/rpi/legup/ui/zoompane/ZoomBar.java b/src/main/java/edu/rpi/legup/ui/zoompane/ZoomBar.java new file mode 100644 index 000000000..fe7ff3a72 --- /dev/null +++ b/src/main/java/edu/rpi/legup/ui/zoompane/ZoomBar.java @@ -0,0 +1,87 @@ +package edu.rpi.legup.ui.zoompane; + +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; +import javax.swing.plaf.ScrollBarUI; +import java.beans.BeanProperty; + +/** An implementation of a zoom bar with a fit button. */ +public class ZoomBar extends JScrollBar { + + private static final String uiClassID = "ZoomBarUI"; + + /** The button that users can press to zoom to fit. */ + protected JButton fitButton; + + /** Creates a horizontal {@code ZoomBar}. */ + public ZoomBar() { + super(HORIZONTAL); + unitIncrement = blockIncrement = 25; + } + + /** + * Sets the {@code ZoomBarUI} object that provides the look and feel (L&F) for this component. + * + * @param ui the {@code ZoomBarUI} L&F object + * @see #getUI + */ + public void setUI(@Nullable ZoomBarUI ui) { super.setUI(ui); } + + /** + * {@inheritDoc} + * @deprecated use {@link #setUI(ZoomBarUI)} instead + * + * @param ui {@inheritDoc} + * @throws IllegalArgumentException if ui is not an instance of {@code ZoomBarUI} + */ + @Deprecated + @Override + public void setUI(@Nullable ScrollBarUI ui) { + if (ui instanceof ZoomBarUI || ui == null) { setUI((ZoomBarUI) ui); } + else { throw new IllegalArgumentException("ZoomBar's UI delegate must be an instance of ZoomBarUI"); } + } + + /** + * Replaces the current {@code ZoomBarUI} object with a version from the current default look and feel. + * To be called when the default look and feel changes. + * + * @see JComponent#updateUI() + * @see UIManager#getUI(javax.swing.JComponent) + */ + @Override + public void updateUI() { setUI((ZoomBarUI) UIManager.getUI(this)); } + + /** + * {@inheritDoc} + * @return the string "ZoomPaneUI" + * @see JScrollBar#getUIClassID() + */ + @Override + public String getUIClassID() { return uiClassID; } + + /** + * Returns the button that users can press to zoom to fit the view that this {@code ZoomBar} controls. + * + * @return the fit button + * @see #setFitButton(JButton) + */ + public JButton getFitButton() { return fitButton; } + + /** + * Sets the button that users can press to zoom to fit the view that this {@code ZoomBar} controls. + * + * @param newFitButton a new fit button + * @see #getFitButton() + */ + @BeanProperty(expert = true, description = "The zoom bar's fit button.") + public void setFitButton(@Nullable JButton newFitButton) { + + if (fitButton != null) { remove(fitButton); } + JButton oldFitButton = fitButton; + fitButton = newFitButton; + if (fitButton != null) { add(fitButton); } + + firePropertyChange("fitButton", oldFitButton, fitButton); + } +} diff --git a/src/main/java/edu/rpi/legup/ui/zoompane/ZoomBarUI.java b/src/main/java/edu/rpi/legup/ui/zoompane/ZoomBarUI.java new file mode 100644 index 000000000..f579aecbe --- /dev/null +++ b/src/main/java/edu/rpi/legup/ui/zoompane/ZoomBarUI.java @@ -0,0 +1,426 @@ +package edu.rpi.legup.ui.zoompane; + +import com.formdev.flatlaf.ui.FlatScrollBarUI; +import com.formdev.flatlaf.ui.FlatStylingSupport; +import com.formdev.flatlaf.util.LoggingFacade; +import com.formdev.flatlaf.util.UIScale; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; +import javax.swing.plaf.ComponentUI; +import javax.swing.plaf.UIResource; +import java.awt.*; +import java.awt.geom.AffineTransform; +import java.awt.geom.Arc2D; +import java.awt.geom.Path2D; +import java.beans.PropertyChangeListener; +import java.util.Objects; + +/** Provides the Flat Laf UI Delegate for {@link ZoomBar}. */ +public class ZoomBarUI extends FlatScrollBarUI { + + /** Fit button. */ + protected JButton fitButton; + + /** + * Distance between the fit button and the adjacent button. This may be a negative number. + * If negative, then an overlap between the buttons will occur. + */ + protected int fitGap; + + /** + * Creates the UI. + * + * @param c the component + * @return the UI + */ + public static ComponentUI createUI(@NotNull JComponent c) { return new ZoomBarUI(); } + + @Override + @SuppressWarnings("RedundantCollectionOperation") + protected void configureScrollBarColors() { + super.configureScrollBarColors(); + + if (UIManager.getDefaults().keySet().contains("ZoomBar.background")) { + LookAndFeel.installColors(scrollbar, "ZoomBar.background", "ScrollBar.foreground"); + } + thumbColor = getDefaultIfDefined("ZoomBar.thumb", thumbColor, Color.class); + trackColor = getDefaultIfDefined("ZoomBar.track", trackColor, Color.class); + } + + @Override + protected void installDefaults() { + super.installDefaults(); + + // All defaults should try to use ZoomBar values and fallback to ScrollBar values + int width = getDefaultIfDefined("ZoomBar.width", scrollBarWidth, Integer.class); + scrollBarWidth = width >= 0 ? width : scrollBarWidth; + + minimumThumbSize = getDefaultIfDefined("ZoomBar.minimumThumbSize", minimumThumbSize, Dimension.class); + maximumThumbSize = getDefaultIfDefined("ZoomBar.maximumThumbSize", maximumThumbSize, Dimension.class); + allowsAbsolutePositioning = getDefaultIfDefined("ZoomBar.allowsAbsolutePositioning", allowsAbsolutePositioning, Boolean.class); + if (UIManager.getDefaults().containsKey("ZoomBar.border")) { + LookAndFeel.installBorder(scrollbar, "ZoomBar.border"); + } + incrGap = getDefaultIfDefined("ZoomBar.incrementButtonGap", incrGap, Integer.class); + decrGap = getDefaultIfDefined("ZoomBar.decrementButtonGap", decrGap, Integer.class); + fitGap = UIManager.getInt("ZoomBar.fitButtonGap"); + + minimumButtonSize = getDefaultIfDefined("ZoomBar.minimumButtonSize", minimumButtonSize, Dimension.class); + trackInsets = getDefaultIfDefined("ZoomBar.trackInsets", trackInsets, Insets.class); + thumbInsets = getDefaultIfDefined("ZoomBar.thumbInsets", thumbInsets, Insets.class); + trackArc = getDefaultIfDefined("ZoomBar.trackArc", trackArc, Integer.class); + thumbArc = getDefaultIfDefined("ZoomBar.thumbArc", thumbArc, Integer.class); + hoverTrackColor = getDefaultIfDefined("ZoomBar.hoverTrackColor", hoverTrackColor, Color.class); + hoverThumbColor = getDefaultIfDefined("ZoomBar.hoverThumbColor", hoverThumbColor, Color.class); + hoverThumbWithTrack = getDefaultIfDefined("ZoomBar.hoverThumbWithTrack", hoverThumbWithTrack, Boolean.class); + pressedTrackColor = getDefaultIfDefined("ZoomBar.pressedTrackColor", pressedTrackColor, Color.class); + pressedThumbColor = getDefaultIfDefined("ZoomBar.pressedThumbColor", pressedThumbColor, Color.class); + pressedThumbWithTrack = getDefaultIfDefined("ZoomBar.pressedThumbWithTrack", pressedThumbWithTrack, Boolean.class); + + showButtons = getDefaultIfDefined("ZoomBar.showButtons", showButtons, Boolean.class); + buttonArrowColor = getDefaultIfDefined("ZoomBar.buttonArrowColor", buttonArrowColor, Color.class); + buttonDisabledArrowColor = getDefaultIfDefined("ZoomBar.buttonDisabledArrowColor", buttonDisabledArrowColor, Color.class); + hoverButtonBackground = getDefaultIfDefined("ZoomBar.hoverButtonBackground", hoverButtonBackground, Color.class); + pressedButtonBackground = getDefaultIfDefined("ZoomBar.pressedButtonBackground", pressedButtonBackground, Color.class); + } + + /** + * Returns the UI Default specified by {@code key} if it has been defined, otherwise {@code defaultValue}. + * + * @param key the key to look for + * @param defaultValue the default value to fall back to + * @param type the expected type of the value + * @return UI Default {@code key} if defined, otherwise {@code defaultValue} + * @param the expected type of the value + */ + @SuppressWarnings("RedundantCollectionOperation") + private static T getDefaultIfDefined(@NotNull String key, @Nullable T defaultValue, @NotNull Class type) { + if (!UIManager.getDefaults().keySet().contains(key)) { return defaultValue; } + Object value = UIManager.get(key); + return type.isInstance(value) ? type.cast(value) : defaultValue; + } + + @Override + protected void installStyle() { + try { + applyStyle(FlatStylingSupport.getResolvedStyle(scrollbar, "ZoomBar")); + } catch (RuntimeException ex) { + LoggingFacade.INSTANCE.logSevere(null, ex); + } + } + + @Override + protected void installComponents() { + + fitButton = ((ZoomBar) scrollbar).getFitButton(); + if (fitButton == null || fitButton instanceof UIResource) { + fitButton = createFitButton(); + ((ZoomBar) scrollbar).setFitButton(fitButton); + } + + super.installComponents(); + } + + @Override + protected void uninstallComponents() { + if (fitButton instanceof UIResource) { ((ZoomBar) scrollbar).setFitButton(null); } + super.uninstallComponents(); + } + + @Override + protected PropertyChangeListener createPropertyChangeListener() { + PropertyChangeListener superListener = super.createPropertyChangeListener(); + + return e -> { + superListener.propertyChange(e); + if (e.getPropertyName().equals("fitButton")) { fitButton = (JButton) e.getNewValue(); } + }; + } + + @Override + protected boolean isShowButtons() { + Object showButtons = scrollbar.getClientProperty("ZoomBar.showButtons"); + if (showButtons == null && scrollbar.getParent() instanceof ZoomPane) { + showButtons = ((ZoomPane) scrollbar.getParent()).getClientProperty("ZoomPane.showButtons"); + } + return (showButtons != null) ? Objects.equals(showButtons, true) : this.showButtons; + } + + /** @return {@code true} if the {@code ZoomBar} should display the fit button, {@code false} otherwise */ + protected boolean isShowFitButton() { + Object showFitButton = scrollbar.getClientProperty("ZoomBar.showFitButton"); + if (showFitButton == null && scrollbar.getParent() instanceof ZoomPane) { + showFitButton = ((ZoomPane) scrollbar.getParent()).getClientProperty("ZoomPane.showFitButton"); + } + return (showFitButton != null) ? Objects.equals(showFitButton, true) : + getDefaultIfDefined("ZoomBar.showFitButton", isShowButtons(), Boolean.class); + } + + @Override + protected void layoutVScrollbar(@NotNull JScrollBar sb) { + Dimension sbSize = sb.getSize(); + Insets sbInsets = sb.getInsets(); + + int itemW = sbSize.width - (sbInsets.left + sbInsets.right); + int itemX = sbInsets.left; + + boolean squareButtons = getDefaultIfDefined("ZoomBar.squareButtons", + UIManager.getBoolean("ScrollPane.squareButtons"), Boolean.class); + int decrButtonH = squareButtons ? itemW : decrButton.getPreferredSize().height; + int decrButtonY = sbInsets.top; + int fitButtonH = squareButtons ? itemW : fitButton.getPreferredSize().height; + int fitButtonY = sbSize.height - (sbInsets.bottom + fitButtonH); + int incrButtonH = squareButtons ? itemW : incrButton.getPreferredSize().height; + int incrButtonY = fitButtonY - incrButtonH - fitGap; + + int sbInsetsH = sbInsets.top + sbInsets.bottom; + int sbButtonsH = decrButtonH + incrButtonH + fitButtonH; + int gaps = decrGap + incrGap + fitGap; + float trackH = sbSize.height - (sbInsetsH + sbButtonsH) - gaps; + + float min = sb.getMinimum(); + float extent = sb.getVisibleAmount(); + float range = sb.getMaximum() - min; + float value = sb.getValue(); + + int thumbH = (range <= 0) ? getMaximumThumbSize().height : (int)(trackH * (extent / range)); + thumbH = Math.min(Math.max(thumbH, getMinimumThumbSize().height), getMaximumThumbSize().height); + + int thumbY = incrButtonY - incrGap - thumbH; + if (value < (sb.getMaximum() - sb.getVisibleAmount())) { + float thumbRange = trackH - thumbH; + thumbY = (int) (0.5f + (thumbRange * ((value - min) / (range - extent)))); + thumbY += decrButtonY + decrButtonH + decrGap; + } + + int sbAvailButtonH = (sbSize.height - sbInsetsH); + if (sbAvailButtonH < sbButtonsH) { + fitButtonH = incrButtonH = decrButtonH = sbAvailButtonH / 3; + fitButtonY = sbSize.height - (sbInsets.bottom + fitButtonH); + incrButtonY = fitButtonY - incrButtonH - fitGap; + } + decrButton.setBounds(itemX, decrButtonY, itemW, decrButtonH); + incrButton.setBounds(itemX, incrButtonY, itemW, incrButtonH); + fitButton.setBounds(itemX, fitButtonY, itemW, fitButtonH); + + int itrackY = decrButtonY + decrButtonH + decrGap; + int itrackH = incrButtonY - incrGap - itrackY; + trackRect.setBounds(itemX, itrackY, itemW, itrackH); + + if (thumbH >= (int) trackH) { + if (getDefaultIfDefined("ZoomBar.alwaysShowThumb", + UIManager.getBoolean("ScrollBar.alwaysShowThumb"), Boolean.class)) { + setThumbBounds(itemX, itrackY, itemW, itrackH); + } + else { setThumbBounds(0, 0, 0, 0); } + } + else { + if ((thumbY + thumbH) > incrButtonY - incrGap) { thumbY = incrButtonY - incrGap - thumbH; } + if (thumbY < (decrButtonY + decrButtonH + decrGap)) { thumbY = decrButtonY + decrButtonH + decrGap + 1; } + setThumbBounds(itemX, thumbY, itemW, thumbH); + } + } + + @Override + protected void layoutHScrollbar(@NotNull JScrollBar sb) { + Dimension sbSize = sb.getSize(); + Insets sbInsets = sb.getInsets(); + + int itemH = sbSize.height - (sbInsets.top + sbInsets.bottom); + int itemY = sbInsets.top; + boolean ltr = sb.getComponentOrientation().isLeftToRight(); + + boolean squareButtons = getDefaultIfDefined("ZoomBar.squareButtons", + UIManager.getBoolean("ScrollPane.squareButtons"), Boolean.class); + int leftButtonW = squareButtons ? itemH : decrButton.getPreferredSize().width; + int rightButtonW = squareButtons ? itemH : incrButton.getPreferredSize().width; + int fitButtonW = squareButtons ? itemH : fitButton.getPreferredSize().width; + if (!ltr) { + int temp = leftButtonW; + leftButtonW = rightButtonW; + rightButtonW = temp; + } + int leftButtonX = sbInsets.left; + int fitButtonX = sbSize.width - (sbInsets.right + fitButtonW); + int rightButtonX = fitButtonX - rightButtonW - fitGap; + int leftGap = ltr ? decrGap : incrGap; + int rightGap = ltr ? incrGap : decrGap; + + int sbInsetsW = sbInsets.left + sbInsets.right; + int sbButtonsW = leftButtonW + rightButtonW + fitButtonW; + float trackW = sbSize.width - (sbInsetsW + sbButtonsW) - (leftGap + rightGap); + + float min = sb.getMinimum(); + float max = sb.getMaximum(); + float extent = sb.getVisibleAmount(); + float range = max - min; + float value = sb.getValue(); + + int thumbW = (range <= 0) ? getMaximumThumbSize().width : (int) (trackW * (extent / range)); + thumbW = Math.min(Math.max(thumbW, getMinimumThumbSize().width), getMaximumThumbSize().width); + + int thumbX = ltr ? rightButtonX - rightGap - thumbW : leftButtonX + leftButtonW + leftGap; + if (value < (max - sb.getVisibleAmount())) { + float thumbRange = trackW - thumbW; + if (ltr) { thumbX = (int)(0.5f + (thumbRange * ((value - min) / (range - extent)))); } + else { thumbX = (int)(0.5f + (thumbRange * ((max - extent - value) / (range - extent)))); } + thumbX += leftButtonX + leftButtonW + leftGap; + } + + int sbAvailButtonW = (sbSize.width - sbInsetsW); + if (sbAvailButtonW < sbButtonsW) { + fitButtonW = rightButtonW = leftButtonW = sbAvailButtonW / 3; + fitButtonX = sbSize.width - (sbInsets.right + fitButtonW); + rightButtonX = fitButtonX - rightButtonW - rightGap; + } + + (ltr ? decrButton : incrButton).setBounds(leftButtonX, itemY, leftButtonW, itemH); + (ltr ? incrButton : decrButton).setBounds(rightButtonX, itemY, rightButtonW, itemH); + fitButton.setBounds(fitButtonX, itemY, fitButtonW, itemH); + + int itrackX = leftButtonX + leftButtonW + leftGap; + int itrackW = rightButtonX - rightGap - itrackX; + trackRect.setBounds(itrackX, itemY, itrackW, itemH); + + if (thumbW >= (int) trackW) { + if (getDefaultIfDefined("ZoomBar.alwaysShowThumb", + UIManager.getBoolean("ScrollBar.alwaysShowThumb"), Boolean.class)) { + setThumbBounds(itrackX, itemY, itrackW, itemH); + } + else { setThumbBounds(0, 0, 0, 0); } + } + else { + if (thumbX + thumbW > rightButtonX - rightGap) { thumbX = rightButtonX - rightGap - thumbW; } + if (thumbX < leftButtonX + leftButtonW + leftGap) { thumbX = leftButtonX + leftButtonW + leftGap + 1; } + setThumbBounds(thumbX, itemY, thumbW, itemH); + } + } + + @Override + protected JButton createIncreaseButton(int orientation) { return new ZoomBarButton("increase"); } + + @Override + protected JButton createDecreaseButton(int orientation) { return new ZoomBarButton("decrease"); } + + /** + * Creates a fit button. + * + * @return a fit button + */ + protected JButton createFitButton() { return new ZoomBarButton("fit"); } + + /** + * A button displaying plus, minus, and fit icons based on its type. + */ + protected class ZoomBarButton extends FlatScrollBarButton { + + /** Type of the button. */ + protected String type; + + /** Path of the icon to display based on type. */ + protected Path2D iconPath; + + protected ZoomBarButton(@NotNull String type) { + super(0); + this.type = type; + } + + @Override + protected void paintArrow(@NotNull Graphics2D graphics) { + Graphics2D g = (Graphics2D) graphics.create(); + + float size = UIScale.scale(getArrowWidth()) - 1; + float thickness = UIScale.scale(getArrowThickness()); + int width = getWidth(); + int height = getHeight(); + float xOffset = UIScale.scale(getXOffset()); + float yOffset = UIScale.scale(getYOffset()); + + g.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE); + g.translate( + (width - size) / 2.0 + UIScale.scale(xOffset), + (height - size) / 2.0 + UIScale.scale(yOffset) + ); + g.setStroke(new BasicStroke(thickness, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); + + Path2D path = getPath(); + path.transform(AffineTransform.getScaleInstance(size, size)); + g.draw(path); + + g.dispose(); + } + + /** + * Gets a deep copy of the path of the icon for this type of button. + * + * @return the path of the icon to draw + */ + protected Path2D getPath() { + if (iconPath != null) { return (Path2D) iconPath.clone(); } + return (Path2D) (iconPath = loadPath()).clone(); + } + + /** @return a new icon path for a button of this type */ + protected Path2D loadPath() { + Path2D path = new Path2D.Double(); + + switch(type) { + case "increase" -> { + path.moveTo(0, 0.5); + path.lineTo(1, 0.5); + path.moveTo(0.5, 0); + path.lineTo(0.5, 1); + } + case "decrease" -> { + path.moveTo(0.125, 0.5); + path.lineTo(0.875, 0.5); + } + case "fit" -> { + path.moveTo(0, 0.25); + path.append(new Arc2D.Double(0, 0, 0.25, 0.25, 180, -90, Arc2D.OPEN), true); + path.lineTo(0.25, 0); + + path.moveTo(0.75, 0); + path.append(new Arc2D.Double(0.75, 0, 0.25, 0.25, 90, -90, Arc2D.OPEN), true); + path.lineTo(1, 0.25); + + path.moveTo(1, 0.75); + path.append(new Arc2D.Double(0.75, 0.75, 0.25, 0.25, 0, -90, Arc2D.OPEN), true); + path.lineTo(0.75, 1); + + path.moveTo(0.25, 1); + path.append(new Arc2D.Double(0, 0.75, 0.25, 0.25, 270, -90, Arc2D.OPEN), true); + path.lineTo(0, 0.75); + } + default -> {} + } + + return path; + } + + @Override + public Dimension getPreferredSize() { + if (type.equals("fit") ? isShowFitButton() : isShowButtons()) { + return new Dimension( + UIScale.scale(Math.max(scrollBarWidth, (minimumButtonSize != null) ? minimumButtonSize.width : 0)), + UIScale.scale(Math.max(scrollBarWidth, (minimumButtonSize != null) ? minimumButtonSize.height : 0)) + ); + } + else { return new Dimension(); } + } + + @Override + public Dimension getMinimumSize() { + return (type.equals("fit") ? isShowFitButton() : isShowButtons()) + ? super.getMinimumSize() : new Dimension(); + } + + @Override + public Dimension getMaximumSize() { + return (type.equals("fit") ? isShowFitButton() : isShowButtons()) + ? super.getMaximumSize() : new Dimension(); + } + } +} diff --git a/src/main/java/edu/rpi/legup/ui/zoompane/ZoomPane.java b/src/main/java/edu/rpi/legup/ui/zoompane/ZoomPane.java new file mode 100644 index 000000000..768baefa5 --- /dev/null +++ b/src/main/java/edu/rpi/legup/ui/zoompane/ZoomPane.java @@ -0,0 +1,643 @@ +package edu.rpi.legup.ui.zoompane; + +import org.intellij.lang.annotations.MagicConstant; +import org.jetbrains.annotations.Nullable; + +import java.awt.*; +import java.beans.BeanProperty; +import java.beans.PropertyChangeEvent; +import java.beans.Transient; +import javax.accessibility.AccessibleContext; +import javax.accessibility.AccessibleRelation; +import javax.swing.*; +import javax.swing.plaf.ScrollPaneUI; +import javax.swing.plaf.UIResource; + +/** + * Provides an interactive window for the display of and interaction with {@link ZoomView} objects. A + * {@code ZoomPane} manages a {@link ZoomViewport}, vertical and horizontal {@link JScrollBar}s, and + * a {@link ZoomBar}. + *

+ * Due to AWT and Swing limitations, only {@link ZoomView} and its subclasses can be used as the view of + * a {@code ZoomPane}, whereas {@link JScrollPane} and {@link JViewport} typically display {@link Component}s + * and {@link JComponent}s. {@code ZoomPane} and {@link ZoomViewport} are still subclasses of + * those classes despite these fundamental differences to preserve behavioral and styling consistency. + *

+ * Note: if this component's performance impact is of concern, smooth scrolling can be disabled + * by setting the {@code ZoomPane.smoothScrolling} client property or UI default to false. + * + * @see JScrollPane + * @see ZoomViewport + * @see ZoomView + */ +public class ZoomPane extends JScrollPane implements ZoomPaneConstants { + + private static final String uiClassID = "ZoomPaneUI"; + + /** + * The display policy for the zoombar. The default is {@code ZoomPaneConstants.ZOOMBAR_ALWAYS}. + * @see #setZoomBarPolicy + */ + protected int zoomBarPolicy = ZOOMBAR_ALWAYS; + + /** The zoom pane's zoom bar child. Default is a {@code ZoomBar}. */ + protected ZoomBar zoomBar; + + // ---------- Zoom behavior settings ---------- + + /** Zooming sensitivity: {@code (1 / # steps to cover entire zoom range)}. */ + protected double sensitivity = 0.1; + + /** The minimum scale factor that can be applied to the viewport's view. */ + protected double minScaleFactor = 1.0 / 3.0; + + /** + * Flag for whether the minimum scale factor is a direct scale factor value or a ratio for how many + * multiples of the view's size should be able to fit into the viewport on its relatively longest side. + */ + protected boolean minScaleRelative = true; + + /** The maximum scale factor that can be applied to the viewport's view. */ + protected double maxScaleFactor = 5.0; + + /** + * Flag for whether the maximum scale factor is a direct scale factor value or a ratio for how many + * multiples of the view's size should be able to fit into the viewport on its relatively shortest side. + */ + protected boolean maxScaleRelative = false; + + /** + * Creates a {@code ZoomPane} that displays the zoom view in a zoom viewport whose view + * position can be controlled with a pair of scrollbars and whose zoom level can be controlled + * with a zoom bar. + * + * @param view the zoomable view to display in the zoom pane's viewport + * + * @see #setViewportView(ZoomView) + */ + public ZoomPane(@Nullable ZoomView view) { + setLayout(new ScrollPaneLayout.UIResource()); + setVerticalScrollBarPolicy(VERTICAL_SCROLLBAR_ALWAYS); + setHorizontalScrollBarPolicy(HORIZONTAL_SCROLLBAR_ALWAYS); + setZoomBarPolicy(ZOOMBAR_ALWAYS); + setViewport((ZoomViewport) createViewport()); + setVerticalScrollBar(createVerticalScrollBar()); + setHorizontalScrollBar(createHorizontalScrollBar()); + setZoomBar(createZoomBar()); + if (view != null) { setViewportView(view); } + setOpaque(true); + updateUI(); + } + + /** Creates an empty (no view) {@code ZoomPane}. */ + public ZoomPane() { this(null); } + + /** + * Sets the {@code ZoomPaneUI} object that provides the look and feel (L&F) for this component. + * + * @param ui the {@code ZoomPaneUI} L&F object + * @see #getUI + */ + public void setUI(@Nullable ZoomPaneUI ui) { super.setUI(ui); } + + /** + * {@inheritDoc} + * @deprecated use {@link #setUI(ZoomPaneUI)} instead + * + * @param ui {@inheritDoc} + * @throws IllegalArgumentException if ui is not an instance of {@code ZoomPaneUI} + */ + @Deprecated + @Override + public void setUI(@Nullable ScrollPaneUI ui) { + if (ui instanceof ZoomPaneUI) { setUI((ZoomPaneUI) ui); } + else { throw new IllegalArgumentException("ZoomPane's UI delegate must be an instance of ZoomPaneUI"); } + } + + /** + * Replaces the current {@code ZoomPaneUI} object with a version from the current default look and feel. + * To be called when the default look and feel changes. + * + * @see JComponent#updateUI() + * @see UIManager#getUI(javax.swing.JComponent) + */ + @Override + public void updateUI() { setUI((ZoomPaneUI) UIManager.getUI(this)); } + + /** + * {@inheritDoc} + * @return the string "ZoomPaneUI" + * @see JScrollPane#getUIClassID() + */ + @Override + public String getUIClassID() { return uiClassID; } + + /** + * Determines when the vertical scrollbar appears in the {@code ZoomPane}. + * Legal values are: + *

    + *
  • {@code ZoomPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED} + *
  • {@code ZoomPaneConstants.VERTICAL_SCROLLBAR_NEVER} + *
  • {@code ZoomPaneConstants.VERTICAL_SCROLLBAR_ALWAYS} + *
+ *

+ * Warning: using {@code ZoomPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED} may result in jumps in + * view size and position as a result of creating space for the scroll bars. + * + * @param policy {@inheritDoc} + * @throws IllegalArgumentException {@inheritDoc} + * @see JScrollPane#setVerticalScrollBarPolicy(int) + */ + @Override + public void setVerticalScrollBarPolicy(int policy) { super.setVerticalScrollBarPolicy(policy); } + + /** + * Determines when the horizontal scrollbar appears in the {@code ZoomPane}. + * Legal values are: + *

    + *
  • {@code ZoomPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED} + *
  • {@code ZoomPaneConstants.HORIZONTAL_SCROLLBAR_NEVER} + *
  • {@code ZoomPaneConstants.HORIZONTAL_SCROLLBAR_ALWAYS} + *
+ *

+ * Warning: using {@code ZoomPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED} may result in jumps in + * view size and position as a result of creating space for the scroll bars. + * + * @param policy {@inheritDoc} + * @throws IllegalArgumentException {@inheritDoc} + * @see JScrollPane#setHorizontalScrollBarPolicy(int) + */ + @Override + public void setHorizontalScrollBarPolicy(int policy) { super.setHorizontalScrollBarPolicy(policy); } + + @Override + public Rectangle getViewportBorderBounds() { + + Rectangle borderR = super.getViewportBorderBounds(); + ZoomBar zb = getZoomBar(); + if (zb != null && zb.isVisible()) { borderR.height -= zb.getHeight(); } + + return borderR; + } + + /** Scroll bar implementing {@code UIResource}. */ + private static class ScrollBarUIResource extends JScrollBar implements UIResource { + + public ScrollBarUIResource(@MagicConstant(intValues = + {Adjustable.HORIZONTAL, Adjustable.VERTICAL}) int policy) { + super(policy); + } + } + + /** + * Returns a new {@code JScrollBar} by default. Subclasses may override this method to force + * {@code ZoomPaneUI} implementations to use a {@code JScrollBar} subclass. Used by + * {@code ZoomPaneUI} implementations to create the vertical scrollbar. + * + * @return {@inheritDoc} + * @see JScrollBar + */ + @Override + public JScrollBar createVerticalScrollBar() { return new ScrollBarUIResource(JScrollBar.VERTICAL); } + + /** + * Returns a new {@code JScrollBar} by default. Subclasses may override this method to force + * {@code ZoomPaneUI} implementations to use a {@code JScrollBar} subclass. Used by + * {@code ZoomPaneUI} implementations to create the horizontal scrollbar. + * + * @return {@inheritDoc} + * @see JScrollBar + */ + @Override + public JScrollBar createHorizontalScrollBar() { return new ScrollBarUIResource(JScrollBar.HORIZONTAL); } + + /** + * Returns the zoom bar policy value. + * + * @return the {@code zoomBarPolicy} property + * @see #setZoomBarPolicy(int) + */ + public int getZoomBarPolicy() { return zoomBarPolicy; } + + /** + * Determines when the zoombar appears in the zoompane. The options are: + *

    ] + *
  • {@code ZoomPaneConstants.HORIZONTAL_SCROLLBAR_NEVER} + *
  • {@code ZoomPaneConstants.HORIZONTAL_SCROLLBAR_ALWAYS} + *
+ * + * @param policy one of the two values listed above + * @throws IllegalArgumentException if {@code policy} is not one of the legal values shown above + * @see #getZoomBarPolicy + */ + @BeanProperty(preferred = true, + enumerationValues = {"ZoomPaneConstants.ZOOMBAR_NEVER", "ZoomPaneConstants.ZOOMBAR_ALWAYS"}, + description = "The ZoomPane ZoomBar policy") + public void setZoomBarPolicy(int policy) { + if (policy != ZOOMBAR_NEVER && policy != ZOOMBAR_ALWAYS) { + throw new IllegalArgumentException("invalid zoomBarPolicy"); + } + int old = zoomBarPolicy; + zoomBarPolicy = policy; + firePropertyChange("zoomBarPolicy", old, policy); + revalidate(); + repaint(); + } + + private static class ZoomBarUIResource extends ZoomBar implements UIResource {} + + /** + * Returns a new {@code ZoomBar} by default. Subclasses may override this method to force + * {@code ZoomPaneUI} implementations to use a {@code ZoomBar} subclass. Used by + * {@code ZoomPaneUI} implementations to create the zoom bar. + * + * @return a {@code ZoomBar} + * @see ZoomBar + */ + public ZoomBar createZoomBar() { return new ZoomBarUIResource(); } + + /** + * Returns the zoom bar that controls the viewport's zoom level. + * + * @return the {@code zoomBar} property + * @see #setZoomBar(ZoomBar) + */ + @Transient + public ZoomBar getZoomBar() { return zoomBar; } + + /** + * Adds the zoom bar that controls the viewport's zoom to the zoom pane. This is usually unnecessary, + * as {@code ZoomPane} creates a zoom bar by default. + * + * @param zoomBar the zoom bar to be added + * @see #createZoomBar() + * @see #getZoomBar() + */ + @BeanProperty(expert = true, description = "The zoom bar.") + public void setZoomBar(@Nullable ZoomBar zoomBar) { + ZoomBar old = getZoomBar(); + this.zoomBar = zoomBar; + if (zoomBar != null) { try { add(zoomBar, ZOOMBAR); } catch (IllegalArgumentException ignored) {;} } + else if (old != null) { remove(old); } + firePropertyChange("zoomBar", old, zoomBar); + + revalidate(); + repaint(); + } + + /** + * Returns a new {@code ZoomViewport} by default. Used to create the viewport (as needed) + * in {@link #setViewportView(ZoomView)}. Subclasses may override this method to return a subclass of + * {@code JViewport}. + * + * @return a new {@code ZoomViewport} + */ + @Override + protected JViewport createViewport() { return new ZoomViewport(); } + + /** + * Removes the old viewport (if there is one); syncs the scroll bars and zoom bar with the new viewport. + *

+ * Most applications will find it more convenient to use {@link #setViewportView(ZoomView)} to add a viewport + * and a view to the zoompane. + * + * @param viewport the new viewport to be used; if viewport is {@code null}, the old viewport is still + * removed and the new viewport is set to {@code null} + * @see #createViewport() + * @see #getViewport() + * @see #setViewportView(ZoomView) + */ + public void setViewport(@Nullable ZoomViewport viewport) { super.setViewport(viewport); } + + /** + * Removes the old viewport (if there is one); syncs the scroll bars and zoom bar with the new viewport. + *

+ * Most applications will find it more convenient to use {@link #setViewportView(ZoomView)} to add a viewport + * and a view to the zoompane. + * @deprecated use {@link #setViewport(ZoomViewport)} instead + * + * @param viewport the new viewport to be used; if viewport is {@code null}, the old viewport is still + * removed and the new viewport is set to {@code null} + * @throws IllegalArgumentException if viewport is not an instance of {@code ZoomViewport} or {@code null} + * @see #createViewport() + * @see #getViewport() + * @see #setViewportView(ZoomView) + */ + @Deprecated + @Override + public void setViewport(@Nullable JViewport viewport) { + if (viewport instanceof ZoomViewport || viewport == null) { setViewport((ZoomViewport) viewport); } + else { throw new IllegalArgumentException("ZoomPane's viewport must be an instance of ZoomViewport"); } + } + + /** + * Returns the current {@code ZoomViewport}. + * + * @return the viewport property + * @see #setViewport(ZoomViewport) + */ + public ZoomViewport getZoomViewport() { return (ZoomViewport) getViewport(); } + + /** + * Creates a viewport if necessary and then sets its view. Applications that don't provide the view + * directly to the {@code ZoomPane} constructor should use this method to specify the {@code ZoomView} + * child that's going to be displayed in the {@code ZoomPane}. For example: + *

+     * ZoomPane zoompane = new ZoomPane();
+     * zoompane.setViewportView(zoomViewObject);
+     * 
+ * Applications should not add children directly to the {@code ZoomPane}. + * + * @param view the view to add to the viewport + * @see #setViewport(ZoomViewport) + * @see ZoomViewport#setZoomView(ZoomView) + */ + public void setViewportView(@Nullable ZoomView view) { + if (getViewport() == null) { setViewport((ZoomViewport) createViewport()); } + ((ZoomViewport) getViewport()).setZoomView(view); + } + + /** + * Creates a viewport if necessary and then sets its view. Applications that don't provide the view + * directly to the {@code ZoomPane} constructor should use this method to specify the {@code ZoomView} + * child that's going to be displayed in the {@code ZoomPane}. For example: + *
+     * ZoomPane zoompane = new ZoomPane();
+     * zoompane.setViewportView(zoomViewObject);
+     * 
+ * Applications should not add children directly to the {@code ZoomPane}. + * @deprecated use {@link #setViewportView(ZoomView)} instead; {@code ZoomViewport} accepts a + * {@code ZoomView} as its view rather than a {@code Component} + * + * @param view {@inheritDoc} + * @see #setViewport(ZoomViewport) + * @see ZoomViewport#setZoomView(ZoomView) + */ + @Deprecated + @Override + public void setViewportView(@Nullable Component view) { + if (view == null) { setViewportView((ZoomView) null); } + else { throw new IllegalArgumentException("ZoomPane's view cannot be set to a Component"); } + } + + /** + * Removes the old rowHeader (if it exists). + * @deprecated implementing a rowHeader for {@code ZoomPane} is difficult and so far unnecessary, so + * attempting to set the rowHeader to a non-{@code null} value will throw an exception. + * + * @param rowHeader {@inheritDoc} + * @throws IllegalArgumentException if rowHeader is non-{@code null} + * @see #getRowHeader() + * @see #setRowHeaderView(Component) + */ + @Deprecated + @Override + public void setRowHeader(JViewport rowHeader) { + if (rowHeader == null) { super.setRowHeader(null); } + else { throw new IllegalArgumentException("rowHeader for ZoomPane has not yet been implemented."); } + } + + /** + * Removes the old columnHeader (if it exists). + * @deprecated implementing a columnHeader for {@code ZoomPane} is difficult and so far unnecessary, + * so attempting to set the columnHeader to a non-{@code null} value will throw an exception. + * + * @param columnHeader {@inheritDoc} + * @throws IllegalArgumentException if columnHeader is non-{@code null} + * @see #getColumnHeader() + * @see #setColumnHeaderView(Component) + */ + @Deprecated + @Override + public void setColumnHeader(JViewport columnHeader) { + if (columnHeader == null) { super.setColumnHeader(null); } + else { throw new IllegalArgumentException("columnHeader for ZoomPane has not yet been implemented."); } + } + + /** + * Sets the orientation for the vertical and horizontal scroll bars and the zoom bar as determined + * by the {@code ComponentOrientation} argument. + * + * @param co {@inheritDoc} + * @see java.awt.ComponentOrientation + */ + @Override + public void setComponentOrientation(ComponentOrientation co) { + super.setComponentOrientation(co); + if (zoomBar != null) { zoomBar.setComponentOrientation(co); } + } + + /** + * Returns the sensitivity of zooming on this {@code ZoomPane}. + * + * @return zooming sensitivity + * @see #setSensitivity(double) + */ + @BeanProperty(description = "Sensitivity of zooming") + public double getSensitivity() { return sensitivity; } + + /** + * Sets the sensitivity of zooming on this {@code ZoomPane}. To gauge a good value use the formula + * {@code sensitivity = 1 / steps} where steps is the number of scroll wheel ticks to cover the zoom range. + * + * @param newSensitivity the new sensitivity value to set + * @see #getSensitivity() + */ + @BeanProperty(description = "Sensitivity of zooming") + public void setSensitivity(double newSensitivity) { + double old = sensitivity; + sensitivity = newSensitivity; + firePropertyChange("sensitivity", old, sensitivity); + } + + /** + * Returns the minimum scale factor to be applied to the viewport's view. If {@code minScaleRelative} + * is {@code true}, this value should instead be used as the ratio of how many of the view's size + * should be able to fit into the viewport on its relatively longest size. + * + * @return the minimum scale factor + * @see #setMinScaleFactor(double) + * @see #isMinScaleRelative() + * @see #getMaxScaleFactor() + */ + @BeanProperty(description = "Minimum scale factor") + public double getMinScaleFactor() { return minScaleFactor; } + + /** + * Sets the minimum scale factor to be applied to the viewport's view. If {@code minScaleRelative} + * is {@code true}, this value will instead be used as the ratio of how many of the view's size + * should be able to fit into the viewport on its relatively longest size. + * + * @param newMinScaleFactor the new minimum scale factor to set + * @see #getMinScaleFactor() + * @see #setMinScaleRelative(boolean) + * @see #setMaxScaleFactor(double) + */ + @BeanProperty(description = "Minimum scale factor") + public void setMinScaleFactor(double newMinScaleFactor) { + double old = minScaleFactor; + minScaleFactor = newMinScaleFactor; + firePropertyChange("minScaleFactor", old, minScaleFactor); + } + + /** + * Returns whether {@code minScaleFactor} is a direct scale factor value or a ratio for how many + * multiples of the view's size should be able to fit into the viewport on its relatively longest side. + * + * @return {@code true} if the {@code minScaleFactor} is relative, {@code false} otherwise + * @see #getMinScaleFactor() + * @see #setMinScaleRelative(boolean) + */ + @BeanProperty(description = "Flag for whether the minimum scale factor is relative to its fit") + public boolean isMinScaleRelative() { return minScaleRelative; } + + /** + * Sets whether {@code minScaleFactor} is a direct scale factor value or a ratio for how many + * multiples of the view's size should be able to fit into the viewport on its relatively longest side. + * + * @param newMinScaleRelative {@code true} if the {@code minScaleFactor} should be relative, + * {@code false} otherwise + * @see #setMinScaleFactor(double) + * @see #isMinScaleRelative() + */ + @BeanProperty(description = "Flag for whether the minimum scale factor is relative to its fit") + public void setMinScaleRelative(boolean newMinScaleRelative) { + boolean old = minScaleRelative; + minScaleRelative = newMinScaleRelative; + firePropertyChange("minScaleRelative", old, minScaleRelative); + } + + /** + * Returns the maximum scale factor to be applied to the viewport's view. If {@code maxScaleRelative} + * is {@code true}, this value should instead be used as the ratio of how many of the view's size + * should be able to fit into the viewport on its relatively shortest size. + * + * @return the maximum scale factor + * @see #setMaxScaleFactor(double) + * @see #isMaxScaleRelative() + * @see #getMinScaleFactor() + */ + @BeanProperty(description = "Maximum scale factor") + public double getMaxScaleFactor() { return maxScaleFactor; } + + /** + * Sets the maximum scale factor to be applied to the viewport's view. If {@code maxScaleRelative} + * is {@code true}, this value will instead be used as the ratio of how many of the view's size + * should be able to fit into the viewport on its relatively shortest size. + * + * @param newMaxScaleFactor the new maximum scale factor to set + * @see #getMaxScaleFactor() + * @see #setMaxScaleRelative(boolean) + * @see #setMinScaleFactor(double) + */ + @BeanProperty(description = "Maximum scale factor") + public void setMaxScaleFactor(double newMaxScaleFactor) { + double old = maxScaleFactor; + maxScaleFactor = newMaxScaleFactor; + firePropertyChange("maxScaleFactor", old, maxScaleFactor); + } + + /** + * Returns whether {@code maxScaleFactor} is a direct scale factor value or a ratio for how many + * multiples of the view's size should be able to fit into the viewport on its relatively shortest side. + * + * @return {@code true} if the {@code maxScaleFactor} is relative, {@code false} otherwise + * @see #getMaxScaleFactor() + * @see #setMaxScaleRelative(boolean) + */ + @BeanProperty(description = "Flag for whether the maximum scale factor is relative to its fit") + public boolean isMaxScaleRelative() { return maxScaleRelative; } + + /** + * Sets whether {@code maxScaleFactor} is a direct scale factor value or a ratio for how many + * multiples of the view's size should be able to fit into the viewport on its relatively shortest side. + * + * @param newMaxScaleRelative {@code true} if the {@code maxScaleFactor} should be relative, + * {@code false} otherwise + * @see #setMaxScaleFactor(double) + * @see #isMaxScaleRelative() + */ + @BeanProperty(description = "Flag for whether the maximum scale factor is relative to its fit") + public void setMaxScaleRelative(boolean newMaxScaleRelative) { + boolean old = maxScaleRelative; + maxScaleRelative = newMaxScaleRelative; + firePropertyChange("maxScaleRelative", old, maxScaleRelative); + } + + /** + * Returns a string representation of this {@code ZoomPane}. This method is intended to be used + * only for debugging purposes, and the content and format of the returned string may vary between + * implementations. The returned string may be empty but may not be {@code null}. + * + * @return a string representation of this {@code ZoomPane} + */ + @Override + public String paramString() { + return super.paramString() + + ",zoomBar=" + (zoomBar != null ? zoomBar : "") + + ",sensitivity=" + sensitivity + + ",minScaleFactor=" + minScaleFactor + + ",minScaleRelative=" + minScaleRelative + + ",maxScaleFactor=" + maxScaleFactor + + ",maxScaleRelative=" + maxScaleRelative; + } + +///////////////// +// Accessibility support +//////////////// + + /** + * Gets the {@code AccessibleContext} associated with this {@code ZoomPane}. For zoom panes, this + * takes the form of an {@code AccessibleZoomPane}, of which a new instance is created if necessary. + * + * @return an {@code AccessibleZoomPane} that serves as the {@code AccessibleContext} of this + * {@code ZoomPane} + */ + @Override + public AccessibleContext getAccessibleContext() { + if (accessibleContext == null) { accessibleContext = new AccessibleZoomPane(); } + return accessibleContext; + } + + /** + * This class implements accessibility support for the ZoomPane class. It provides + * an implementation of the Java Accessibility API appropriate to scroll pane user-interface elements. + */ + protected class AccessibleZoomPane extends AccessibleJScrollPane { + + /** + * AccessibleZoomPane constructor. + */ + public AccessibleZoomPane() { + super(); + ZoomBar zoomBar = getZoomBar(); + if (zoomBar != null) { setZoomBarRelations(zoomBar); } + } + + @Override + public void propertyChange(PropertyChangeEvent e) { + super.propertyChange(e); + if (e.getPropertyName().equals("zoomBar") && e.getNewValue() instanceof ZoomBar) { + setZoomBarRelations((ZoomBar) e.getNewValue()); + } + } + + /** + * Sets the {@code CONTROLLER_FOR} and {@code CONTROLLED_BY} AccessibleRelations for the + * {@code ZoomPane} and {@code ZoomBar}. + * + * @param zoomBar the {@code ZoomBar} to set relations with. Must not be {@code null} + */ + void setZoomBarRelations(ZoomBar zoomBar) { + // The ZoomBar is a CONTROLLER_FOR the ZoomPane. The ZoomPane is CONTROLLED_BY the ZoomBar. + AccessibleRelation controlledBy = + new AccessibleRelation(AccessibleRelation.CONTROLLED_BY, zoomBar); + AccessibleRelation controllerFor = + new AccessibleRelation(AccessibleRelation.CONTROLLER_FOR, ZoomPane.this); + + AccessibleContext ac = zoomBar.getAccessibleContext(); + ac.getAccessibleRelationSet().add(controllerFor); + getAccessibleRelationSet().add(controlledBy); + } + } +} \ No newline at end of file diff --git a/src/main/java/edu/rpi/legup/ui/zoompane/ZoomPaneConstants.java b/src/main/java/edu/rpi/legup/ui/zoompane/ZoomPaneConstants.java new file mode 100644 index 000000000..377842342 --- /dev/null +++ b/src/main/java/edu/rpi/legup/ui/zoompane/ZoomPaneConstants.java @@ -0,0 +1,19 @@ +package edu.rpi.legup.ui.zoompane; + +import javax.swing.ScrollPaneConstants; + +/** Constants used with the ZoomPane component. */ +public interface ZoomPaneConstants extends ScrollPaneConstants { + + /** Identifies a horizontal scrollbar. */ + String ZOOMBAR = "ZOOMBAR"; + + /** Identifies the zoom bar policy property. */ + String ZOOMBAR_POLICY = "ZOOMBAR_POLICY"; + + /** Used to set the zoom bar policy so that zoom bars are never displayed. */ + int ZOOMBAR_NEVER = 41; + + /** Used to set the zoom bar policy so that zoom bars are always displayed. */ + int ZOOMBAR_ALWAYS = 42; +} diff --git a/src/main/java/edu/rpi/legup/ui/zoompane/ZoomPaneUI.java b/src/main/java/edu/rpi/legup/ui/zoompane/ZoomPaneUI.java new file mode 100644 index 000000000..e7a054530 --- /dev/null +++ b/src/main/java/edu/rpi/legup/ui/zoompane/ZoomPaneUI.java @@ -0,0 +1,1069 @@ +package edu.rpi.legup.ui.zoompane; + +import com.formdev.flatlaf.ui.FlatScrollPaneUI; +import com.formdev.flatlaf.ui.FlatStylingSupport; +import com.formdev.flatlaf.util.LoggingFacade; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.awt.*; +import java.awt.event.*; +import java.awt.geom.Dimension2D; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.util.Objects; +import javax.swing.*; +import javax.swing.border.Border; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; +import javax.swing.plaf.ActionMapUIResource; +import javax.swing.plaf.ComponentUI; +import javax.swing.plaf.UIResource; +import javax.swing.plaf.basic.BasicScrollPaneUI; + +/** Provides the Flat Laf UI delegate for {@link ZoomPane}. */ +public class ZoomPaneUI extends FlatScrollPaneUI { + + /** Re-usable handler instance for event handling. */ + private Handler handler; + + /** {@code MouseWheelListener} installed on the viewport. */ + private MouseWheelListener mouseZoomListener; + + /** {@code ActionListener} installed on the zoom bar for fitting the view to the viewport. */ + protected ActionListener zbFitListener; + + /** {@code ChangeListener} installed on the zoom bar. */ + protected ChangeListener zbChangeListener; + + /** {@code PropertyChangeListener} installed on the zoom bar. */ + private PropertyChangeListener zbPropertyChangeListener; + + /** {@code PropertyChangeListener} installed on the viewport. */ + private PropertyChangeListener viewportPropertyChangeListener; + + /** {@code ComponentListener} installed on the viewport. */ + private ComponentListener viewportResizeListener; + + /** + * Flag indicating that the viewport should fit its view as soon as it is possible. + * @see Actions#readyToFit(ZoomPane) + */ + protected boolean waitingToFit; + + /** + * Flag indicating that the viewport has fit its view and should re-fit its view every time the view changes + * until an action is taken. + * + * @see Actions#readyToFit(ZoomPane) + */ + protected boolean isFit; + + /** + * Flag indicating that state change events should be ignored (usually because they were triggered by + * a call from this class). + * @see #syncScrollPaneWithViewport() + * @see Handler#actionPerformed(ActionEvent) + */ + protected boolean ignoreStateChanges; + + public static ComponentUI createUI(@NotNull JComponent c) { return new ZoomPaneUI(); } + + /** @return the {@code viewPadding} client property or UI Default associated with the given {@code ZoomPane} */ + protected static Insets getViewPadding(ZoomPane zoomPane) { + Object viewPadding = zoomPane.getClientProperty("ZoomPane.viewPadding"); + if (viewPadding instanceof Insets) { return (Insets) viewPadding; } + viewPadding = UIManager.getInsets("ZoomPane.viewPadding"); + if (viewPadding != null) { return (Insets) viewPadding; } + return new Insets(0, 0, 0, 0); + } + + /** + * {@inheritDoc} + * + * @param c {@inheritDoc} + * @throws IllegalArgumentException if c is not a {@code ZoomPane} + */ + @Override + public void installUI(@NotNull JComponent c) { + if (c instanceof ZoomPane) { super.installUI(c); } + else { throw new IllegalArgumentException("ZoomPaneUI can only be applied to a ZoomPane."); } + + int focusWidth = UIManager.getInt("Component.focusWidth"); + int arc = getArc(); + LookAndFeel.installProperty(c, "opaque", focusWidth == 0 && arc == 0); + + if (Actions.readyToFit((ZoomPane) scrollpane)) { + Actions.zoomToFit((ZoomPane) scrollpane); + waitingToFit = false; + } + else { waitingToFit = true; } + } + + @Override + protected void installDefaults(@NotNull JScrollPane c) { + super.installDefaults(scrollpane); + + UIDefaults defaults = UIManager.getDefaults(); + if (defaults.containsKey("ZoomPane.border")) { + LookAndFeel.installBorder(scrollpane, "ZoomPane.border"); + } + if (defaults.containsKey("ZoomPane.background")) { + LookAndFeel.installColorsAndFont(scrollpane, "ZoomPane.background", + "ScrollPane.foreground", "ScrollPane.font"); + } + + Border vpBorder = scrollpane.getViewportBorder(); + if (defaults.containsKey("ZoomPane.viewportBorder") && + ((vpBorder == null) ||( vpBorder instanceof UIResource))) { + scrollpane.setViewportBorder(UIManager.getBorder("ZoomPane.viewportBorder")); + } + } + + @Override + protected void installListeners(@NotNull JScrollPane c) { + super.installListeners(c); + + mouseZoomListener = createViewportMouseWheelListener(); + zbChangeListener = createZBChangeListener(); + zbFitListener = createZBFitListener(); + zbPropertyChangeListener = createZBPropertyChangeListener(); + viewportPropertyChangeListener = createViewportPropertyChangeListener(); + viewportResizeListener = createViewportResizeListener(); + + JViewport viewport = scrollpane.getViewport(); + ZoomBar zoomBar = ((ZoomPane) scrollpane).getZoomBar(); + + if (viewport != null) { + viewport.addMouseWheelListener(mouseZoomListener); + viewport.addPropertyChangeListener(viewportPropertyChangeListener); + viewport.addComponentListener(viewportResizeListener); + } + if (zoomBar != null) { + zoomBar.getModel().addChangeListener(zbChangeListener); + JButton fit = zoomBar.getFitButton(); + if (fit != null) { fit.addActionListener(zbFitListener); } + zoomBar.addPropertyChangeListener(zbPropertyChangeListener); + } + } + + @Override + protected void uninstallListeners(@NotNull JComponent c) { + super.uninstallListeners(c); + + JViewport viewport = scrollpane.getViewport(); + ZoomBar zoomBar = ((ZoomPane) scrollpane).getZoomBar(); + + if (viewport != null) { + viewport.removeMouseWheelListener(mouseZoomListener); + viewport.removePropertyChangeListener(viewportPropertyChangeListener); + viewport.removeComponentListener(viewportResizeListener); + } + if (zoomBar != null) { + zoomBar.getModel().removeChangeListener(zbChangeListener); + JButton fit = zoomBar.getFitButton(); + if (fit != null) { fit.removeActionListener(zbFitListener); } + zoomBar.removePropertyChangeListener(zbPropertyChangeListener); + } + + mouseZoomListener = null; + zbChangeListener = null; + zbFitListener = null; + zbPropertyChangeListener = null; + viewportPropertyChangeListener = null; + viewportResizeListener = null; + handler = null; + } + + @Override + protected void installKeyboardActions(@NotNull JScrollPane c) { + SwingUtilities.replaceUIInputMap(c, JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT, getInputMap()); + SwingUtilities.replaceUIActionMap(c, getActionMap()); + } + + /** Attempts to retrieve the {@code InputMap} from the {@code UIManager} and creates one if none is found. */ + private InputMap getInputMap() { + InputMap keyMap = (InputMap) UIManager.get("ZoomPane.ancestorInputMap"); + if (keyMap != null) { return keyMap; } + + keyMap = LookAndFeel.makeInputMap(new Object[] { + "ctrl HOME", Actions.SCROLL_HOME, + "UP", Actions.UNIT_SCROLL_UP, + "KP_UP", Actions.UNIT_SCROLL_UP, + "DOWN", Actions.UNIT_SCROLL_DOWN, + "KP_DOWN", Actions.UNIT_SCROLL_DOWN, + "LEFT", Actions.UNIT_SCROLL_LEFT, + "KP_LEFT", Actions.UNIT_SCROLL_LEFT, + "RIGHT", Actions.UNIT_SCROLL_RIGHT, + "KP_RIGHT", Actions.UNIT_SCROLL_RIGHT, + "PAGE_UP", Actions.SCROLL_UP, + "PAGE_DOWN", Actions.SCROLL_DOWN, + "ctrl PAGE_UP", Actions.SCROLL_LEFT, + "ctrl PAGE_DOWN", Actions.SCROLL_RIGHT, + "PLUS", Actions.UNIT_ZOOM_IN, + "ADD", Actions.UNIT_ZOOM_IN, + "ctrl PLUS", Actions.ZOOM_IN, + "ctrl ADD", Actions.ZOOM_IN, + "MINUS", Actions.UNIT_ZOOM_OUT, + "SUBTRACT", Actions.UNIT_ZOOM_OUT, + "ctrl MINUS", Actions.ZOOM_OUT, + "ctrl SUBTRACT", Actions.ZOOM_OUT + }); + UIManager.getLookAndFeelDefaults().put("ZoomPane.ancestorInputMap", keyMap); + return keyMap; + } + + /** Attempts to retrieve the {@code ActionMap} from the {@code UIManager} and creates one if none is found. */ + private ActionMap getActionMap() { + ActionMap actionMap = (ActionMap) UIManager.get("ZoomPane.actionMap"); + if (actionMap != null) { return actionMap; } + + actionMap = new ActionMapUIResource(); + actionMap.put(Actions.SCROLL_HOME, new Actions(Actions.SCROLL_HOME)); + actionMap.put(Actions.UNIT_SCROLL_UP, new Actions(Actions.UNIT_SCROLL_UP)); + actionMap.put(Actions.UNIT_SCROLL_DOWN, new Actions(Actions.UNIT_SCROLL_DOWN)); + actionMap.put(Actions.UNIT_SCROLL_LEFT, new Actions(Actions.UNIT_SCROLL_LEFT)); + actionMap.put(Actions.UNIT_SCROLL_RIGHT, new Actions(Actions.UNIT_SCROLL_RIGHT)); + actionMap.put(Actions.SCROLL_UP, new Actions(Actions.SCROLL_UP)); + actionMap.put(Actions.SCROLL_DOWN, new Actions(Actions.SCROLL_DOWN)); + actionMap.put(Actions.SCROLL_LEFT, new Actions(Actions.SCROLL_LEFT)); + actionMap.put(Actions.SCROLL_RIGHT, new Actions(Actions.SCROLL_RIGHT)); + actionMap.put(Actions.UNIT_ZOOM_IN, new Actions(Actions.UNIT_ZOOM_IN)); + actionMap.put(Actions.UNIT_ZOOM_OUT, new Actions(Actions.UNIT_ZOOM_OUT)); + actionMap.put(Actions.ZOOM_IN, new Actions(Actions.ZOOM_IN)); + actionMap.put(Actions.ZOOM_OUT, new Actions(Actions.ZOOM_OUT)); + + UIManager.getLookAndFeelDefaults().put("ZoomPane.actionMap", actionMap); + return actionMap; + } + + @Override + protected ZoomPaneLayout createScrollPaneLayout() { return new ZoomPaneLayout(); } + + @Override + protected void installStyle() { + try { + applyStyle(FlatStylingSupport.getResolvedStyle(scrollpane, "ZoomPane")); + } catch (RuntimeException e) { + LoggingFacade.INSTANCE.logSevere(null, e); + } + } + + @Override + protected Object applyStyleProperty(@NotNull String key, @Nullable Object value) { + Object toRet = super.applyStyleProperty(key, value); + + if (key.equals("focusWidth") || key.equals("arc")) { + int focusWidth = (value instanceof Integer && key.equals("focusWidth")) ? + (int) value : UIManager.getInt("Component.focusWidth"); + int arc = (value instanceof Integer && key.equals("arc")) ? (int) value : getArc(); + + LookAndFeel.installProperty(scrollpane, "opaque", + focusWidth == 0 && arc == 0); + } + + return toRet; + } + + @Override + protected void syncScrollPaneWithViewport() { + ZoomPane zoomPane = (ZoomPane) scrollpane; + ZoomViewport viewport = (ZoomViewport) zoomPane.getViewport(); + JScrollBar vsb = zoomPane.getVerticalScrollBar(); + JScrollBar hsb = zoomPane.getHorizontalScrollBar(); + ZoomBar zb = zoomPane.getZoomBar(); + ZoomViewport rowHead = (ZoomViewport) zoomPane.getRowHeader(); + ZoomViewport colHead = (ZoomViewport) zoomPane.getColumnHeader(); + Insets padding = getViewPadding(zoomPane); + + // If view just changed size from 0, may need to fit it + ignoreStateChanges = true; + if ((isFit || waitingToFit) && Actions.readyToFit(zoomPane)) { + Actions.zoomToFit(zoomPane); + waitingToFit = false; + } + else { viewport.setViewPosition(Actions.clampViewPos(zoomPane, viewport.getPreciseViewPosition())); } + + if (viewport != null) { + Dimension2D extentSize = viewport.getPreciseExtentSize(); + Dimension2D viewSize = viewport.getViewSize(); + Point2D viewPos = viewport.getPreciseViewPosition(); + double scaleFactor = viewport.getScaleFactor(); + + if (vsb != null) { + int extent = (int) Math.ceil(extentSize.getHeight() * scaleFactor); + int min = -padding.top; + int max = padding.bottom + (int) Math.ceil(viewSize.getHeight() * scaleFactor); + int value = (int) Math.max(min, Math.min(max - extent, Math.round( + ((viewPos.getY() + padding.top / scaleFactor) / (viewSize.getHeight() - extentSize.getHeight() + + (padding.bottom + padding.top) / scaleFactor)) + * (max - extent - min) + min + ))); + vsb.setValues(value, extent, min, max); + } + if (hsb != null) { + int extent = (int) Math.ceil(extentSize.getWidth() * scaleFactor); + int min = -padding.left; + int max = padding.right + (int) Math.ceil(viewSize.getWidth() * scaleFactor); + int value = (int) Math.max(min, Math.min(max - extent, Math.round( + ((viewPos.getX() + padding.left / scaleFactor) / (viewSize.getWidth() - extentSize.getWidth() + + (padding.right + padding.left) / scaleFactor)) + * (max - extent - min) + min + ))); + hsb.setValues(value, extent, min, max); + } + if (zb != null) { + int max = 1000; + double zoom = Actions.scaleFactorToZoom(zoomPane, viewport.getScaleFactor()); + int value = Math.clamp(Math.round(zoom * max), 0, max); + zb.setValues(value, 0, 0, max); + } + + if (rowHead != null) { + Point2D p = rowHead.getPreciseViewPosition(); + p.setLocation(0, viewPos.getY()); + rowHead.setViewPosition(p); + } + if (colHead != null) { + Point2D p = colHead.getPreciseViewPosition(); + p.setLocation(0, viewPos.getX()); + colHead.setViewPosition(p); + } + } + ignoreStateChanges = false; + } + + /** @return the {@code Handler} for all AWT events this UI needs to process */ + private Handler getHandler() { + if (handler == null) { handler = new Handler(); } + return handler; + } + + /** Returns a {@code PropertyChangeListener} that will be installed on the {@code ZoomViewport} */ + private PropertyChangeListener createViewportPropertyChangeListener() { return getHandler(); } + + /** Returns a {@code PropertyChangeListener} that will be installed on the {@code ZoomBar} */ + private PropertyChangeListener createZBPropertyChangeListener() { return getHandler(); } + + @Override + protected ChangeListener createViewportChangeListener() { return getHandler(); } + + @Override + protected ChangeListener createVSBChangeListener() { return getHandler(); } + + @Override + protected ChangeListener createHSBChangeListener() { return getHandler(); } + + /** + * Creates the {@code ChangeListener} for the {@code ZoomBar}'s model. + * + * @return an instance of the zoom bar {@code ChangeListener} + */ + protected ChangeListener createZBChangeListener() { return getHandler(); } + + /** + * Creates the {@code ActionListener} for the {@code ZoomBar}'s fit button. + * + * @return an instance of the zoom bar {@code ActionListener} + */ + protected ActionListener createZBFitListener() { return getHandler(); } + + /** + * {@link BasicScrollPaneUI} adds the returned value from this method to the scroll pane, but since + * zooming logic may be relative to the viewport, {@link #createViewportMouseWheelListener()} + * creates the listener with the zooming logic for the viewport while this method returns {@code null}. + * + * @return {@code null} + * @see #installUI(JComponent) + */ + @Override + protected MouseWheelListener createMouseWheelListener() { return null; } + + /** + * Creates an instance of {@code MouseWheelListener}, which is added to the {@code ZoomViewport} + * by {@code installUI}. The returned {@code MouseWheelListener} is used to handle + * mouse wheel-driven zooming. + * + * @return {@code MouseWheelListener} which implements wheel-driven zooming + * @see #installUI(JComponent) + */ + protected MouseWheelListener createViewportMouseWheelListener() { return getHandler(); } + + @Override + protected boolean isSmoothScrollingEnabled() { + Object smoothScrolling = scrollpane.getClientProperty("ZoomPane.smoothScrolling"); + if (smoothScrolling != null) { return Objects.equals(smoothScrolling, true); } + smoothScrolling = UIManager.get("ZoomPane.smoothScrolling"); + if (smoothScrolling != null) { return Objects.equals(smoothScrolling, true); } + return super.isSmoothScrollingEnabled(); + } + + /** @return UI Default {@code ZoomPane.arc} if an integer, otherwise fallback to {@code ScrollPane.arc} */ + protected int getArc() { + Object arc = UIManager.get("ZoomPane.arc"); + return (arc instanceof Integer) ? (int) arc : UIManager.getInt("ScrollPane.arc"); + } + + @Override + protected void updateViewport(@NotNull PropertyChangeEvent e) { + ZoomViewport oldViewport = (ZoomViewport) e.getOldValue(); + ZoomViewport newViewport = (ZoomViewport) e.getNewValue(); + + if (oldViewport != null) { + oldViewport.removeMouseWheelListener(mouseZoomListener); + oldViewport.removePropertyChangeListener(viewportPropertyChangeListener); + oldViewport.removeComponentListener(viewportResizeListener); + } + + super.updateViewport(e); + if (newViewport != null) { + newViewport.addMouseWheelListener(mouseZoomListener); + newViewport.addPropertyChangeListener(viewportPropertyChangeListener); + newViewport.addComponentListener(viewportResizeListener); + + if (Actions.readyToFit((ZoomPane) scrollpane)) { + Actions.zoomToFit((ZoomPane) scrollpane); + waitingToFit = false; + } + else { waitingToFit = true; } + } + } + + /** + * Updates zoom bar. + * + * @param e the property change event + */ + protected void updateZoomBar(@NotNull PropertyChangeEvent e) { + ZoomBar oldZoomBar = (ZoomBar) e.getOldValue(); + ZoomBar newZoomBar = (ZoomBar) e.getNewValue(); + JButton fit; + + if (oldZoomBar != null) { + oldZoomBar.getModel().removeChangeListener(zbChangeListener); + fit = oldZoomBar.getFitButton(); + if (fit != null) { fit.removeActionListener(zbFitListener); } + oldZoomBar.removePropertyChangeListener(zbPropertyChangeListener); + } + if (newZoomBar != null) { + newZoomBar.getModel().addChangeListener(zbChangeListener); + fit = newZoomBar.getFitButton(); + if (fit != null) { fit.addActionListener(zbFitListener); } + newZoomBar.addPropertyChangeListener(zbPropertyChangeListener); + } + } + + /** + * Creates an instance of {@code ComponentListener}, which is added to the {@code ZoomViewport} + * by {@code installUI}. The returned {@code ComponentListener} adjusts the view position when + * the viewport is resized. It also checks if the view is able and needs to be fit to the viewport. + * + * @return {@code MouseWheelListener} which handles viewport resizing + * @see #installUI(JComponent) + */ + protected ComponentListener createViewportResizeListener() { return getHandler(); } + + @Override + protected PropertyChangeListener createPropertyChangeListener() { + PropertyChangeListener oldPropertyChangeListener = super.createPropertyChangeListener(); + PropertyChangeListener newPropertyChangeListener = getHandler(); + + return e -> { + oldPropertyChangeListener.propertyChange(e); + newPropertyChangeListener.propertyChange(e); + }; + } + + /** Actions for navigation of the view. */ + protected static class Actions implements Action { + + protected static final String SCROLL_HOME = "scrollHome"; + protected static final String UNIT_SCROLL_UP = "unitScrollUp"; + protected static final String UNIT_SCROLL_DOWN = "unitScrollDown"; + protected static final String UNIT_SCROLL_LEFT = "unitScrollLeft"; + protected static final String UNIT_SCROLL_RIGHT = "unitScrollRight"; + protected static final String SCROLL_UP = "scrollUp"; + protected static final String SCROLL_DOWN = "scrollDown"; + protected static final String SCROLL_LEFT = "scrollLeft"; + protected static final String SCROLL_RIGHT = "scrollRight"; + protected static final String UNIT_ZOOM_IN = "unitZoomIn"; + protected static final String UNIT_ZOOM_OUT = "unitZoomOut"; + protected static final String ZOOM_IN = "zoomIn"; + protected static final String ZOOM_OUT = "zoomOut"; + + /** The {@code Action}'s identifier. */ + protected String name; + + public Actions(@NotNull String name) { this.name = name; } + + /** @return the {@code Action}'s identifier */ + public final String getName() { return name; } + + @Override + public Object getValue(@NotNull String key) { + if (key.equals(NAME)) { return name; } + return null; + } + + @Override + public boolean isEnabled() { return accept(null); } + + @Override + public boolean accept(@Nullable Object sender) { return true; } + + // Mutator methods; Actions is immutable and therefore these methods have no implementation + @Override public void putValue(String key, Object value) {} + @Override public void setEnabled(boolean b) {} + @Override public void addPropertyChangeListener(PropertyChangeListener listener) {} + @Override public void removePropertyChangeListener(PropertyChangeListener listener) {} + + @Override + public void actionPerformed(@NotNull ActionEvent e) { + ZoomPane zoomPane = (ZoomPane) e.getSource(); + boolean ltr = zoomPane.getComponentOrientation().isLeftToRight(); + String key = getName(); + + switch(key) { + case SCROLL_HOME -> { zoomToFit(zoomPane); } + case UNIT_SCROLL_UP -> { scroll(zoomPane, SwingConstants.VERTICAL, -1, false); } + case UNIT_SCROLL_DOWN -> { scroll(zoomPane, SwingConstants.VERTICAL, 1, false); } + case UNIT_SCROLL_LEFT -> { scroll(zoomPane, SwingConstants.HORIZONTAL, ltr ? -1 : 1, false); } + case UNIT_SCROLL_RIGHT -> { scroll(zoomPane, SwingConstants.HORIZONTAL, ltr ? 1 : -1, false); } + case SCROLL_UP -> { scroll(zoomPane, SwingConstants.VERTICAL, -1, true); } + case SCROLL_DOWN -> { scroll(zoomPane, SwingConstants.VERTICAL, 1, true); } + case SCROLL_LEFT -> { scroll(zoomPane, SwingConstants.HORIZONTAL, ltr ? -1 : 1, true); } + case SCROLL_RIGHT -> { scroll(zoomPane, SwingConstants.HORIZONTAL, ltr ? 1 : -1, true); } + case UNIT_ZOOM_IN -> { zoom(zoomPane, 1, null); } + case UNIT_ZOOM_OUT -> { zoom(zoomPane, -1, null); } + case ZOOM_IN -> { zoom(zoomPane, 2.5, null); } + case ZOOM_OUT -> { zoom(zoomPane, -2.5, null); } + } + } + + // --------------------------------------- + // Viewport Navigation + // --------------------------------------- + + /** + * Scroll the view along the horizontal or vertical orientation in some direction. + *

+ * Note: this method considers one block scroll to be the extent size of the viewport minus + * the area defined by the {@code viewPadding} property, not the entire extent size of the viewport. + * + * @param zoomPane the {@code ZoomPane} performing the scroll + * @param orientation axis along which to scroll: either {@code SwingConstants.VERTICAL} or + * {@code SwingConstants.HORIZONTAL} + * @param direction signed direction in which to scroll along the axis + * @param block flag for whether to do a block scroll or a unit scroll + */ + protected static void scroll(@NotNull ZoomPane zoomPane, int orientation, double direction, boolean block) { + if (!viewportViewReady(zoomPane)) { return; } + + ((ZoomPaneUI) zoomPane.getUI()).isFit = false; + ZoomViewport viewport = (ZoomViewport) zoomPane.getViewport(); + Rectangle2D visRect = viewport.getViewRect(); + Insets padding = getViewPadding(zoomPane); + double scaleFactor = viewport.getScaleFactor(); + boolean vertical = orientation == SwingConstants.VERTICAL; + double scrollAmount; + + scrollAmount = (vertical) ? + visRect.getHeight() - (padding.top + padding.bottom) / scaleFactor : + visRect.getWidth() - (padding.left + padding.right) / scaleFactor; + if (!block) { scrollAmount /= 4.0; } + + Point2D viewPos = new Point2D.Double( + visRect.getX() + (vertical ? 0 : scrollAmount * direction), + visRect.getY() + (vertical ? scrollAmount * direction : 0) + ); + viewport.setViewPosition(clampViewPos(zoomPane, viewPos)); + } + + /** + * Zoom the view to a specified zoom level {@code [0, 1]} about a focus point. + * + * @param zoomPane the {@code ZoomPane} performing the zoom + * @param zoom the zoom level to set + * @param focus the focus point to zoom about in view coordinates. If {@code null}, it will be set to + * the center of the current display state + */ + protected static void zoomTo(@NotNull ZoomPane zoomPane, double zoom, @Nullable Point2D focus) { + if (!viewportViewReady(zoomPane)) { return; } + + ((ZoomPaneUI) zoomPane.getUI()).isFit = false; + ZoomViewport viewport = (ZoomViewport) zoomPane.getViewport(); + double oldScaleFactor = viewport.getScaleFactor(); + double newScaleFactor = zoomToScaleFactor(zoomPane, Math.clamp(zoom, 0, 1)); + if (newScaleFactor != oldScaleFactor) { + + Rectangle2D viewRect = viewport.getPreciseViewRect(); + if (focus == null) { focus = new Point2D.Double(viewRect.getCenterX(), viewRect.getCenterY()); } + + viewport.setScaleFactor(newScaleFactor); + + double scaleRatio = newScaleFactor / oldScaleFactor; + viewport.setViewPosition(clampViewPos(zoomPane, new Point2D.Double( + focus.getX() - (focus.getX() - viewRect.getX()) / scaleRatio, + focus.getY() - (focus.getY() - viewRect.getY()) / scaleRatio + ))); + } + } + + /** + * Zoom into or out of the view by some amount. Since zoom level operates on a {@code double}-precision + * {@code [0, 1]} scale, an {@code amount >= 1} will zoom all the way in and an {@code amount <= -1} + * will zoom all the way out. + *

+ * Note: this function does not take sensitivity into account. For an equivalent zooming method with + * sensitivity, use {@link #zoom(ZoomPane, double, Point2D)}. + * + * @param zoomPane the {@code ZoomPane} performing the zoom + * @param amount the amount {@code [-1, 1]} to zoom. + * @param focus the focus point to zoom about in view coordinates. + */ + public static void zoomBy(@NotNull ZoomPane zoomPane, double amount, @Nullable Point2D focus) { + if (!viewportViewReady(zoomPane)) { return; } + + double currentZoom = scaleFactorToZoom(zoomPane, ((ZoomViewport) zoomPane.getViewport()).getScaleFactor()); + double newZoom = Math.clamp(currentZoom + amount, 0, 1); + if (newZoom != currentZoom) { zoomTo(zoomPane, newZoom, focus); } + } + + /** + * Zoom into or out of the view by some number of "steps" or wheel ticks. + * + * @param zoomPane the {@code ZoomPane} performing the zoom + * @param steps the number of steps to zoom by. Zoom in if {@code steps > 0}, + * zoom out if {@code steps < 0}, unless the sensitivity is negative. + * @param focus the focus point to zoom about in view coordinates. + */ + public static void zoom(@NotNull ZoomPane zoomPane, double steps, @Nullable Point2D focus) { + if (!viewportViewReady(zoomPane)) { return; } + if (steps != 0) { zoomBy(zoomPane, steps * zoomPane.getSensitivity(), focus); } + } + + /** + * Zoom the view to fit the viewport on its relatively longest side. + * + * @param zoomPane the {@code ZoomPane} performing the fit + */ + public static void zoomToFit(@NotNull ZoomPane zoomPane) { + if (!readyToFit(zoomPane)) { return; } + + ((ZoomPaneUI) zoomPane.getUI()).isFit = true; + ZoomViewport viewport = (ZoomViewport) zoomPane.getViewport(); + Insets padding = getViewPadding(zoomPane); + double scaleFactor = clampScaleFactor(zoomPane, Math.min( + (viewport.getWidth() - padding.left - padding.right) / viewport.getViewSize().getWidth(), + (viewport.getHeight() - padding.top - padding.bottom) / viewport.getViewSize().getHeight() + )); + + viewport.setScaleFactor(scaleFactor); + viewport.setViewPosition(clampViewPos(zoomPane, + new Point2D.Double(-padding.left / scaleFactor, -padding.top / scaleFactor))); + } + + // --------------------------- + // Helpers + // --------------------------- + + /** @return {@code true} if the viewport view is ready to be manipulated, {@code false} otherwise */ + protected static boolean viewportViewReady(@NotNull ZoomPane zoomPane) { + ZoomViewport viewport = (ZoomViewport) zoomPane.getViewport(); + if (viewport == null || viewport.getZoomView() == null) { return false; } + Dimension viewSize = viewport.getViewSize(); + return viewSize.width > 0 && viewSize.height > 0; + } + + /** @return {@code true} if viewport is ready to fit its view, {@code false} otherwise. */ + protected static boolean readyToFit(@NotNull ZoomPane zoomPane) { + JViewport viewport = zoomPane.getViewport(); + return viewportViewReady(zoomPane) + && viewport.isValid() && viewport.getWidth() > 0 && viewport.getHeight() > 0; + } + + /** + * Convert a scale factor value to a zoom level based on the {@code ZoomPane}'s zoom limits. + * + * @param zoomPane the zoom pane doing the conversion + * @param scaleFactor the scale factor to be converted + * @return the zoom level of {@code scaleFactor} in {@code zoomPane} + */ + protected static double scaleFactorToZoom(@NotNull ZoomPane zoomPane, double scaleFactor) { + double min = getMinScaleFactor(zoomPane); + double max = getMaxScaleFactor(zoomPane); + if (max == min) { return 0; } // Avoid divide by 0 + return Math.log(scaleFactor / min) / Math.log(max / min); + } + + /** + * Convert a zoom level to a scale factor value based on the {@code ZoomPane}'s zoom limits. + * + * @param zoomPane the zoom pane doing the conversion + * @param zoom the zoom level to be converted + * @return the scale factor value of {@code zoom} in {@code zoomPane} + */ + protected static double zoomToScaleFactor(@NotNull ZoomPane zoomPane, double zoom) { + double min = getMinScaleFactor(zoomPane); + double max = getMaxScaleFactor(zoomPane); + return min * Math.pow(max / min, zoom); + } + + /** @return the minimum scale factor value of some {@code ZoomPane} */ + protected static double getMinScaleFactor(@NotNull ZoomPane zoomPane) { + double minScaleFactor = zoomPane.getMinScaleFactor(); + if (!viewportViewReady(zoomPane) || !zoomPane.isMinScaleRelative()) { return minScaleFactor; } + + ZoomViewport viewport = (ZoomViewport) zoomPane.getViewport(); + return minScaleFactor * Math.min( + viewport.getWidth() / viewport.getViewSize().getWidth(), + viewport.getHeight() / viewport.getViewSize().getHeight() + ); + } + + /** @return the maximum scale factor value of some {@code ZoomPane} */ + protected static double getMaxScaleFactor(@NotNull ZoomPane zoomPane) { + double maxScaleFactor = zoomPane.getMaxScaleFactor(); + if (!viewportViewReady(zoomPane) || zoomPane.isMaxScaleRelative()) { return maxScaleFactor; } + + ZoomViewport viewport = (ZoomViewport) zoomPane.getViewport(); + return maxScaleFactor * Math.max( + viewport.getWidth() / viewport.getViewSize().getWidth(), + viewport.getHeight() / viewport.getViewSize().getHeight() + ); + } + + /** @return {@code scaleFactor} clamped to the zoom limits of some {@code ZoomPane} */ + protected static double clampScaleFactor(@NotNull ZoomPane zoomPane, double scaleFactor) { + double min = getMinScaleFactor(zoomPane); + double max = getMaxScaleFactor(zoomPane); + return (min < max) ? Math.clamp(scaleFactor, min, max) : (min + max) / 2.0; + } + + /** @return {@code viewPos} clamped to the valid view position space of some {@code ZoomPane} */ + protected static Point2D clampViewPos(@NotNull ZoomPane zoomPane, @NotNull Point2D viewPos) { + ZoomViewport viewport = (ZoomViewport) zoomPane.getViewport(); + Dimension viewSize = viewport.getViewSize(); + Dimension2D extentSize = viewport.getPreciseExtentSize(); + Insets padding = getViewPadding(zoomPane); + double scaleFactor = viewport.getScaleFactor(); + + double minX = -padding.left / scaleFactor; + double minY = -padding.top / scaleFactor; + double maxX = viewSize.width - extentSize.getWidth() + padding.right / scaleFactor; + double maxY = viewSize.height - extentSize.getHeight() + padding.bottom / scaleFactor; + + viewPos.setLocation( + (minX < maxX) ? + Math.clamp(viewPos.getX(), minX, maxX) : + (minX + maxX) / 2, + (minY < maxY) ? + Math.clamp(viewPos.getY(), minY, maxY) : + (minY + maxY) / 2 + ); + return viewPos; + } + } + + /** The handler for all AWT events this UI needs to process. */ + private class Handler implements ActionListener, ChangeListener, MouseWheelListener, + PropertyChangeListener, ComponentListener { + + // Used by componentResized to remember old viewport size + private Dimension oldSize = new Dimension(); + + // MouseWheelListener: This is installed on the ZoomViewport. + @Override + public void mouseWheelMoved(@NotNull MouseWheelEvent e) { + if (scrollpane.isWheelScrollingEnabled() && Actions.viewportViewReady((ZoomPane) scrollpane) + && ((ZoomPane) scrollpane).getSensitivity() != 0) { + ZoomViewport viewport = (ZoomViewport) scrollpane.getViewport(); + + double rotation = e.getWheelRotation(); + if (isSmoothScrollingEnabled()) { rotation = e.getPreciseWheelRotation(); } + + if (rotation == 0) { return; } + e.consume(); + + if (e.getScrollType() == MouseWheelEvent.WHEEL_BLOCK_SCROLL) { rotation *= 2.5; } + Actions.zoom((ZoomPane) scrollpane, -rotation, viewport.toViewCoordinates((Point2D) e.getPoint())); + + viewport.getPeer().dispatchEvent(new MouseEvent( + viewport.getPeer(), MouseEvent.MOUSE_MOVED, e.getWhen(), 0, + e.getX(), e.getY(), 0, false + )); + } + } + + // ChangeListener: This in installed on the ZoomBar and both ScrollBar models. + @Override + public void stateChanged(@NotNull ChangeEvent e) { + + if (scrollpane.getViewport() != null && !ignoreStateChanges) { + + if (e.getSource() == scrollpane.getViewport()) { syncScrollPaneWithViewport(); } + + else { + JScrollBar hsb = scrollpane.getHorizontalScrollBar(); + if (hsb != null && e.getSource() == hsb.getModel()) { sbStateChanged(e, false); } + + else { + JScrollBar vsb = scrollpane.getVerticalScrollBar(); + if (vsb != null && e.getSource() == vsb.getModel()) { sbStateChanged(e, true); } + + else { + ZoomBar zb = ((ZoomPane) scrollpane).getZoomBar(); + if (zb != null && e.getSource() == zb.getModel()) { zbStateChanged(e); } + } + } + } + } + } + + private void sbStateChanged(@NotNull ChangeEvent e, boolean isVertical) { + ZoomViewport viewport = (ZoomViewport) scrollpane.getViewport(); + BoundedRangeModel model = (BoundedRangeModel) e.getSource(); + Dimension2D extentSize = viewport.getPreciseExtentSize(); + Dimension viewSize = viewport.getViewSize(); + Point2D p = viewport.getPreciseViewPosition(); + Insets padding = getViewPadding((ZoomPane) scrollpane); + double scaleFactor = viewport.getScaleFactor(); + + double normValue = model.getExtent() == model.getMaximum() - model.getMinimum() ? 0 : + (double) (model.getValue() - model.getMinimum()) / + (model.getMaximum() - model.getExtent() - model.getMinimum()); + double viewMin, viewMax; + + if (isVertical) { + viewMin = -padding.top / scaleFactor; + viewMax = viewSize.height - extentSize.getHeight() + padding.bottom / scaleFactor; + } + else { + viewMin = -padding.left / scaleFactor; + viewMax = viewSize.width - extentSize.getWidth() + padding.right / scaleFactor; + } + double viewValue = viewMin + normValue * (viewMax - viewMin); + p.setLocation(isVertical ? p.getX() : viewValue, isVertical ? viewValue : p.getY()); + + ignoreStateChanges = true; + viewport.setViewPosition(Actions.clampViewPos((ZoomPane) scrollpane, p)); + ignoreStateChanges = false; + } + + private void zbStateChanged(@NotNull ChangeEvent e) { + BoundedRangeModel model = (BoundedRangeModel) e.getSource(); + double min = model.getMinimum(); + double max = model.getMaximum(); + double value = model.getValue(); + // Can't ignore state changes because scrollbars may need to be updated; + // viewport sync will ignore for us + Actions.zoomTo((ZoomPane) scrollpane, (value - min) / (max - min), null); + } + + // ActionListener: This is installed on the ZoomBar fit button. + @Override + public void actionPerformed(@NotNull ActionEvent e) { Actions.zoomToFit((ZoomPane) scrollpane); } + + // PropertyChangeListener: this is installed on the ZoomPane and ZoomBar. + @Override + public void propertyChange(@NotNull PropertyChangeEvent e) { + + if (e.getSource() == scrollpane) { + if (e.getPropertyName().equals("zoomBar")) { updateZoomBar(e); } + } + else if (e.getSource() == scrollpane.getViewport()) { vpPropertyChange(e); } + else if (e.getSource() == ((ZoomPane) scrollpane).getZoomBar()) { zbPropertyChange(e); } + } + + private void vpPropertyChange(@NotNull PropertyChangeEvent e) { + if (e.getPropertyName().equals("view")) { + if (e.getNewValue() != null) { + if (Actions.readyToFit((ZoomPane) scrollpane)) { + Actions.zoomToFit((ZoomPane) scrollpane); + waitingToFit = false; + } + else { waitingToFit = true; } + } + } + } + + private void zbPropertyChange(@NotNull PropertyChangeEvent e) { + + if (e.getPropertyName().equals("model")) { + + BoundedRangeModel oldModel = (BoundedRangeModel) e.getOldValue(); + BoundedRangeModel newModel = (BoundedRangeModel) e.getNewValue(); + + if (oldModel != null) { oldModel.removeChangeListener(zbChangeListener); } + if (newModel != null) { newModel.addChangeListener(zbChangeListener); } + } + else if (e.getPropertyName().equals("fitButton")) { + + JButton oldButton = (JButton) e.getOldValue(); + JButton newButton = (JButton) e.getNewValue(); + + if (oldButton != null) { oldButton.removeActionListener(zbFitListener); } + if (newButton != null) { newButton.addActionListener(zbFitListener); } + } + } + + // ComponentListener: this is installed on the ZoomViewport. + @Override + public void componentResized(@NotNull ComponentEvent e) { + ZoomViewport viewport = (ZoomViewport) scrollpane.getViewport(); + if (e.getComponent() == viewport) { + + // If viewport has been resized from 0, may need to fit the view + if (waitingToFit && Actions.readyToFit((ZoomPane) scrollpane)) { + Actions.zoomToFit((ZoomPane) scrollpane); + waitingToFit = false; + } + + Dimension newSize = e.getComponent().getSize(); + if (newSize.width > 0 && newSize.height > 0) { + if (oldSize.width > 0 && oldSize.height > 0) { + + Rectangle2D viewRect = viewport.getPreciseViewRect(); + double widthRatio = (double) newSize.width / oldSize.width; + double heightRatio = (double) newSize.height / oldSize.height; + + viewport.setViewPosition(new Point2D.Double( + viewRect.getCenterX() - (viewRect.getCenterX() - viewRect.getX()) * widthRatio, + viewRect.getCenterY() - (viewRect.getCenterY() - viewRect.getY()) * heightRatio + )); + } + + oldSize = newSize; + } + } + } + + @Override public void componentMoved(ComponentEvent e) {} + @Override public void componentShown(ComponentEvent e) {} + @Override public void componentHidden(ComponentEvent e) {} + } + + /** The layout manager for this UI. */ + protected static class ZoomPaneLayout extends FlatScrollPaneLayout implements ZoomPaneConstants { + + /** + * The zoompane's zoombar child. Default is a {@code ZoomBar}. + * @see ZoomPane#setZoomBar(ZoomBar) + */ + protected ZoomBar zb; + + @Override + public void syncWithScrollPane(@NotNull JScrollPane sp) { + super.syncWithScrollPane(sp); + zb = ((ZoomPane) sp).getZoomBar(); + } + + /** + * {@inheritDoc} + *

  • ZoomPaneConstants.ZOOMBAR
+ * + * @param s {@inheritDoc} + * @param c {@inheritDoc} + * @throws IllegalArgumentException {@inheritDoc} + */ + @Override + public void addLayoutComponent(@NotNull String s, @Nullable Component c) { + if (s.equals(ZOOMBAR)) { zb = (ZoomBar) addSingletonComponent(zb, c); } + else { super.addLayoutComponent(s, c); } + } + + @Override + public void removeLayoutComponent(@NotNull Component c) { + if (c == zb) { zb = null; } + else { super.removeLayoutComponent(c); } + } + + /** + * {@inheritDoc} + *

+ * Lastly, the preferred size of the zoombar is added given the current zoombar displayPolicy. + * + * @param parent {@inheritDoc} + * @return {@inheritDoc} + */ + @Override + public Dimension preferredLayoutSize(@NotNull Container parent) { + Dimension superSize = super.preferredLayoutSize(parent); + int zbPolicy = ((ZoomPane) parent).getZoomBarPolicy(); + + if (zb != null && zbPolicy == ZOOMBAR_ALWAYS) { superSize.height += zb.getPreferredSize().height; } + return superSize; + } + + /** + * {@inheritDoc} Lastly, the minimum size of the zoombar is added given its displayPolicy isn't NEVER. + * + * @param parent {@inheritDoc} + * @return {@inheritDoc} + */ + @Override + public Dimension minimumLayoutSize(@NotNull Container parent) { + Dimension superSize = super.minimumLayoutSize(parent); + int zbPolicy = ((ZoomPane) parent).getZoomBarPolicy(); + + if (zb != null && zbPolicy != ZOOMBAR_NEVER) { + Dimension size = zb.getMinimumSize(); + superSize.height += size.height; + superSize.width = Math.max(superSize.width, size.width); + } + return superSize; + } + + /** + * {@inheritDoc} + *

    + *
  • If a zoombar is needed, i.e. if the {@code displayPolicy} is ALWAYS, it gets its preferred + * height and the width of its parent. All previous components' dimensions and positions are then + * adjusted to account for these new constraints.
  • + *
+ * + * @param parent {@inheritDoc} + */ + @Override + public void layoutContainer(@NotNull Container parent) { + super.layoutContainer(parent); + int zbPolicy = ((ZoomPane) parent).getZoomBarPolicy(); + + Rectangle availR = parent.getBounds(); + Insets insets = parent.getInsets(); + availR.x = insets.left; + availR.y = insets.top; + availR.width -= insets.left + insets.right; + availR.height -= insets.top + insets.bottom; + + if (zb != null && zbPolicy != ZOOMBAR_NEVER) { + + int zbHeight = zb.getPreferredSize().height; + zb.setBounds(new Rectangle(availR.x, availR.height - zbHeight, availR.width, zbHeight)); + + if (rowHead != null) { + Rectangle rowHeadBounds = rowHead.getBounds(); + rowHeadBounds.height -= zbHeight; + rowHead.setBounds(rowHeadBounds); + } + if (viewport != null) { + Rectangle viewportBounds = viewport.getBounds(); + viewportBounds.height -= zbHeight; + viewport.setBounds(viewportBounds); + } + if (vsb != null) { + Rectangle vsbBounds = vsb.getBounds(); + vsbBounds.height -= zbHeight; + vsb.setBounds(vsbBounds); + } + if (hsb != null) { + Rectangle hsbBounds = hsb.getBounds(); + hsbBounds.y -= zbHeight; + hsb.setBounds(hsbBounds); + } + } + } + } +} diff --git a/src/main/java/edu/rpi/legup/ui/zoompane/ZoomView.java b/src/main/java/edu/rpi/legup/ui/zoompane/ZoomView.java new file mode 100644 index 000000000..f0790b0af --- /dev/null +++ b/src/main/java/edu/rpi/legup/ui/zoompane/ZoomView.java @@ -0,0 +1,204 @@ +package edu.rpi.legup.ui.zoompane; + +import com.formdev.flatlaf.util.HiDPIUtils; +import edu.rpi.legup.controller.ZoomViewController; +import edu.rpi.legup.ui.zoompane.ZoomViewport.ZoomPeer; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.awt.*; +import java.awt.font.GlyphVector; +import java.awt.geom.Dimension2D; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; +import javax.swing.event.EventListenerList; + +/** + * The canvas object that draws an image or environment in a zoomable and interactive environment like a + * {@code ZoomPane}. This class maintains its base size and fires a {@code ChangeEvent} when the base size + * changes, or it otherwise needs to be repainted. Transformations to the graphics context such as translations + * and scaling factors are left to the {@code ZoomView}'s container. + */ +public abstract class ZoomView { + + /** A list of event listeners for this {@code ZoomView}. */ + private final EventListenerList listenerList = new EventListenerList(); + + /** Re-usable {@code ChangeEvent} to be fired to {@link ChangeListener}s. */ + private ChangeEvent changeEvent; + + /** The area needed to draw this view. */ + private Dimension size = new Dimension(); + + /** {@code Component} peer that can be used for displaying popups, directing AWT events, and requesting focus. */ + private ZoomPeer peer; + + /** {@code ZoomViewController} defining interactive behavior of the view. */ + private ZoomViewController controller; + + /** + * Helper for {@code ZoomView}s that want to draw strings and cannot use + * {@link Graphics#drawString} because of its kerning issues. + * + * @param graphics the graphics context to draw to + * @param text the string to be drawn + * @param x the x location from which to draw the string + * @param y the y location from which to draw the string + */ + public static void drawStringSmooth(@NotNull Graphics2D graphics, @NotNull String text, float x, float y) { + graphics.fill(graphics.getFont().createGlyphVector(graphics.getFontRenderContext(), text).getOutline(x, y)); + } + + /** + * Helper for {@code ZoomView}s that want to draw strings and cannot use + * {@link Graphics#drawString} because of its kerning issues. + * + * @param graphics the graphics context to draw to + * @param text the string to be drawn + * @param cx the center x location from which to draw the string + * @param cy the center y location from which to draw the string + */ + public static void drawStringSmoothCentered(@NotNull Graphics2D graphics, @NotNull String text, float cx, float cy) { + GlyphVector vector = graphics.getFont().createGlyphVector(graphics.getFontRenderContext(), text); + Rectangle2D bounds = vector.getVisualBounds(); + graphics.fill(vector.getOutline((float) (cx - bounds.getWidth() / 2), (float) (cy - bounds.getHeight() / 2))); + } + + /** Creates a {@code ZoomView} with a given controller. */ + public ZoomView(@Nullable ZoomViewController controller) { setController(controller); } + + /** Gets the area needed to draw this view. */ + public Dimension getSize() { return size; } + + /** + * Updates the size of the view and triggers a repaint. + * + * @param newSize the new size of the view + * @see #fireStateChanged + */ + public void setSize(@NotNull Dimension newSize) { + if (!size.equals(newSize)) { + size = newSize; + fireStateChanged(); + } + } + + /** + * Sets the {@code ZoomView}'s {@code Component} peer. The peer is used for creating popups and + * capturing user input and focus. + * + * @param newPeer the new peer to use + */ + public void setPeer(@Nullable ZoomPeer newPeer) { + + if (peer != null && controller != null) { + peer.removeZoomViewController(controller); + peer.removeKeyListener(controller); + } + peer = newPeer; + if (peer != null && controller != null) { + peer.addZoomViewController(controller); + peer.addKeyListener(controller); + } + } + + /** @return the {@code ZoomView}'s {@code Component} peer */ + public ZoomPeer getPeer() { return peer; } + + /** + * Sets the {@code ZoomViewController} used by the {@code ZoomView} to handle user input. + * + * @param newController the new controller to use + */ + public void setController(@Nullable ZoomViewController newController) { + + if (controller != null) { + controller.setView(null); + + if (peer != null) { + peer.removeZoomViewController(controller); + peer.removeKeyListener(controller); + } + } + + controller = newController; + + if (controller != null) { + controller.setView(this); + + if (peer != null) { + peer.addZoomViewController(controller); + peer.addKeyListener(controller); + } + } + } + + /** @return the {@code ZoomView}'s controller */ + public ZoomViewController getController() { return controller; } + + /** + * Adds a {@code ChangeListener} to the list that is notified each time the view's size changes. + * If listener {@code l} is {@code null}, no exception is thrown and no action is performed. + * + * @param l the {@code ChangeListener} to add + * @see #removeChangeListener + */ + public void addChangeListener(@Nullable ChangeListener l) { + if (l != null) { listenerList.add(ChangeListener.class, l); } + } + + /** + * Removes a {@code ChangeListener} from the list that's notified each time the view's size changes. + * If listener {@code l} has not previously been added to this view or is {@code null}, no exception + * is thrown and no action is performed. + * + * @param l the {@code ChangeListener} to remove + * @see #addChangeListener + */ + public void removeChangeListener(ChangeListener l) { + if (l != null) { listenerList.remove(ChangeListener.class, l); } + } + + /** + * Returns an array of all the {@code ChangeListener}s added to this {@code ZoomView} with + * {@link #addChangeListener}. + * + * @return all of the {@code ChangeListener}s added or an empty array if no listeners have been added + */ + public ChangeListener[] getChangeListeners() { return listenerList.getListeners(ChangeListener.class); } + + /** Alerts all {@link ChangeListener}s that the view's state has changed. */ + private void fireStateChanged() { + if (changeEvent == null) { changeEvent = new ChangeEvent(this); } + for (ChangeListener listener : getChangeListeners()) { listener.stateChanged(changeEvent); } + } + + /** Trigger a repaint of the {@code ZoomView} as soon as possible. */ + public void repaint() { getPeer().repaint(); } + + /** Trigger a repaint of the {@code ZoomView} contained to the given {@code Rectangle2D} as soon as possible. */ + public void repaint(@NotNull Rectangle2D r) { repaint(r.getX(), r.getY(), r.getWidth(), r.getHeight()); } + + /** Trigger a repaint of the {@code ZoomView} contained to the given bounds as soon as possible. */ + public void repaint(double x, double y, double width, double height) { + Point2D p = getPeer().fromViewCoordinates(new Point2D.Double(x, y)); + Dimension2D d = getPeer().fromViewCoordinates(new ZoomViewport.Dimension2DDouble(width, height)); + + HiDPIUtils.repaint( + getPeer(), + (int) Math.floor(p.getX()), + (int) Math.floor(p.getY()), + (int) (Math.ceil(p.getX() + d.getWidth()) - Math.floor(p.getX())), + (int) (Math.ceil(p.getY() + d.getHeight()) - Math.floor(p.getY())) + ); + } + + /** + * Draws this view's content to the given graphics context at its base scale. + * + * @param graphics the zoomed graphics context to draw to + */ + public abstract void draw(@NotNull Graphics graphics); +} \ No newline at end of file diff --git a/src/main/java/edu/rpi/legup/ui/zoompane/ZoomViewport.java b/src/main/java/edu/rpi/legup/ui/zoompane/ZoomViewport.java new file mode 100644 index 000000000..126de6e8a --- /dev/null +++ b/src/main/java/edu/rpi/legup/ui/zoompane/ZoomViewport.java @@ -0,0 +1,815 @@ +package edu.rpi.legup.ui.zoompane; + +import edu.rpi.legup.controller.ZoomViewController; +import edu.rpi.legup.controller.ZoomViewController.ControllerMouseEvent; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.accessibility.Accessible; +import javax.swing.*; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; +import javax.swing.event.MouseInputAdapter; +import javax.swing.event.MouseInputListener; +import java.awt.*; +import java.awt.event.*; +import java.awt.geom.Dimension2D; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.beans.BeanProperty; +import java.io.Serial; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +/** + * The "viewport" through which you can see the underlying {@link ZoomView}. When you pan and zoom, what + * performs the translation and scaling is the {@code ZoomViewport}. It is like peering through a camera's + * viewfinder: the {@code ZoomViewport}'s position and size stay grounded within its parent while the + * {@link ZoomView} displayed within its bounds is shifted around and scaled. + *

+ * Note: Since the size of and detail required for drawing the {@link ZoomView} is constantly changing, + * the entire view is repainted in its entirety every time the size or position of the view within the + * viewport changes. Performance conscious applications should keep these updates to a minimum. + *

+ *     zoomPane.getZoomBar().putClientProperty("ZoomBar.fastWheelScrolling", Boolean.FALSE);
+ * 
+ * + * @see JViewport + * @see ZoomView + * @see ZoomPane + */ +public class ZoomViewport extends JViewport implements Accessible { + + /** Listener that is notified each time the view changes size. */ + private ChangeListener viewListener = null; + + /** The view element that is being displayed by the {@code ZoomViewport}. */ + protected ZoomView view; + + /** The scale factor being applied to the drawing of the view. */ + protected double scaleFactor; + + /** The current position of the view in viewport coordinates. */ + protected Point2D viewPosition; + + /** Creates a {@code ZoomViewport}. */ + public ZoomViewport() { + super(); + setScrollMode(SIMPLE_SCROLL_MODE); + setPeer(createPeer()); + } + + /** + * Scrolls the view so that the {@code Rectangle2D} in view coordinates becomes visible. If + * {@code contentRect} is bigger than the current view area, the viewport will zoom out to + * accommodate the {@code contentRect}. + * + * @param contentRect the {@code Rectangle2D} to display. + * @see #setViewRect + */ + public void scrollRectToVisible(@NotNull Rectangle2D contentRect) { + if (view == null) { return; } + + Rectangle2D viewRect = getViewRect(); + if (viewRect.contains(contentRect)) { return; } + + double widthRatio = viewRect.getWidth() / contentRect.getWidth(); + double heightRatio = viewRect.getHeight() / contentRect.getHeight(); + double minRatio = Math.min(widthRatio, heightRatio); + + // Zoom out if necessary + if (minRatio < 1.0) { scaleFactor *= minRatio; } + + // Adjust view position to fit as much of the contentRect inside as possible + Point2D viewPos = new Point2D.Double(viewRect.getX(), viewRect.getY()); + if ((viewRect.getX() < contentRect.getX()) != + (viewRect.getX() + viewRect.getWidth() > contentRect.getX() + contentRect.getWidth())) { + double leftEdgesDist = contentRect.getX() - viewRect.getX(); + double rightEdgesDist = (contentRect.getX() + contentRect.getWidth()) - + (viewRect.getX() + viewRect.getWidth()); + viewPos.setLocation( + viewPos.getX() + + Math.abs(leftEdgesDist) < Math.abs(rightEdgesDist) ? leftEdgesDist : rightEdgesDist, + viewPos.getY()); + } + if ((viewRect.getY() < contentRect.getY()) != + (viewRect.getY() + viewRect.getHeight() > contentRect.getY() + contentRect.getHeight())) { + double topEdgesDist = contentRect.getY() - viewRect.getY(); + double bottomEdgesDist = (contentRect.getY() + contentRect.getHeight()) - + (viewRect.getY() + viewRect.getHeight()); + viewPos.setLocation( + viewPos.getY() + + Math.abs(topEdgesDist) < Math.abs(bottomEdgesDist) ? topEdgesDist : bottomEdgesDist, + viewPos.getY()); + } + + setViewPosition(viewPos); + } + + /** + * Scrolls the view so that the {@code Rectangle} in view coordinates becomes visible. If + * {@code contentRect} is bigger than the current view area, the viewport will zoom + * out to accommodate the {@code contentRect}. + * + * @param contentRect {@inheritDoc} + * @see #setViewRect(Rectangle2D) + */ + @Override + public void scrollRectToVisible(@NotNull Rectangle contentRect) { scrollRectToVisible((Rectangle2D) contentRect); } + + /** + * Paints this {@code ZoomViewport}'s children. Since {@code ZoomViewport} is intended to be the + * viewport to a {@link ZoomView}, the view is drawn here, before its peer. + * + * @param graphics the {@code Graphics} context to draw in + */ + public void paintChildren(@NotNull Graphics graphics) { + + Graphics2D g = (Graphics2D) graphics.create(); + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE); + g.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON); + + g.scale(scaleFactor, scaleFactor); + g.translate(-viewPosition.getX(), -viewPosition.getY()); + + view.draw(g); + g.dispose(); + + super.paintChildren(graphics); + } + + @Override + public void validate() { + super.validate(); + fireStateChanged(); + } + + /** + * {@inheritDoc} + * + * @param mode must have value {@code ZoomViewport.SIMPLE_SCROLL_MODE} + * @throws IllegalArgumentException if mode is not {@code ZoomViewport.SIMPLE_SCROLL_MODE} + */ + @Override + @BeanProperty(bound = false, enumerationValues = {"ZoomViewport.SIMPLE_SCROLL_MODE"}, + description = "Method of moving contents for incremental scrolls.") + public void setScrollMode(int mode) { + if (mode == SIMPLE_SCROLL_MODE) { super.setScrollMode(mode); } + else { + throw new IllegalArgumentException("ZoomViewport does not support BLIT and BACKING_STORE scroll modes."); + } + } + + /** + * Returns the {@code ZoomViewport}'s component view or {@code null}. + * + * @return {@inheritDoc} + * @see #setView(Component) + * @see #getPeer() + */ + @Override + public Component getView() { return super.getView(); } + + /** + * Returns the {@code ZoomViewport}'s view or {@code null}. + * + * @return the viewport's view, or {@code null} if none exists + * @see #setZoomView(ZoomView) + */ + public ZoomView getZoomView() { return view; } + + /** + * Sets the {@code ZoomViewport}'s component view, which can be {@code null}. + * + * @param view the viewport's new component view + * @throws IllegalArgumentException if view is not a {@code ZoomPeer} + * @see ZoomPeer + * @see #setPeer(ZoomPeer) + */ + @Override + public void setView(@Nullable Component view) { + if (view instanceof ZoomPeer || view == null) { super.setView(view); } + else { throw new IllegalArgumentException("ZoomViewport only permits a ZoomPeer view."); } + } + + /** + * Set the {@code ZoomViewport}'s view, which can be {@code null}. + * + * @param newView the {@code ZoomView} to be displayed, or {@code null} + * @see #getView() + */ + @BeanProperty(preferred = true, description = "The viewport view.") + public void setZoomView(@Nullable ZoomView newView) { + if (view == newView) { return; } + + if (view != null) { + view.removeChangeListener(viewListener); + view.setPeer(null); + } + + ZoomView oldView = view; + view = newView; + scaleFactor = 1.0; + viewPosition = new Point2D.Double(); + + if (view != null) { + viewListener = (ChangeListener) createViewListener(); + view.addChangeListener(viewListener); + view.setPeer(getPeer()); + } + + firePropertyChange("view", oldView, newView); + repaint(); + } + + /** + * Returns the current scale factor being applied to the view's drawing. + * + * @return the current scale factor + */ + public double getScaleFactor() { return scaleFactor; } + + /** + * Sets the current scale factor being applied to the view's drawing, then fires a state change. + * + * @param newScaleFactor the new scale factor to be applied to the view's drawing + * @throws IllegalArgumentException if {@code newScaleFactor <= 0} + */ + public void setScaleFactor(double newScaleFactor) { + if (newScaleFactor <= 0) { throw new IllegalArgumentException("newScaleFactor must be greater than 0."); } + if (view == null || newScaleFactor == scaleFactor) { return; } + + scaleFactor = newScaleFactor; + fireStateChanged(); + repaint(); + } + + /** + * Returns the size of the view in view coordinates. If there is no view, return {@code (0,0)}. + * + * @return {@inheritDoc} + */ + @Override + public Dimension getViewSize() { + if (view == null) { return new Dimension(); } + return view.getSize(); + } + + @Override + public void setViewSize(@NotNull Dimension newSize) { + ZoomView view = getZoomView(); + if (view != null && !view.getSize().equals(newSize)) { view.setSize(newSize); } + } + + /** + * Returns the view coordinates that appear in the upper left hand corner of the viewport in view + * coordinates, or {@code (0,0)} if there's no view. + * + * @return {@inheritDoc} + * @see #getPreciseViewPosition() + */ + @Override + public Point getViewPosition() { + Point2D precisePos = getPreciseViewPosition(); + return new Point((int) Math.floor(precisePos.getX()), (int) Math.floor(precisePos.getY())); + } + + /** + * Returns the view coordinates that appear in the upper left hand corner of the viewport in view + * coordinates, or {@code (0,0)} if there's no view. + * + * @return a {@code Point2D} object giving the upper left coordinates + */ + public Point2D getPreciseViewPosition() { + if (view == null) { return new Point2D.Double(); } + return toViewCoordinates(new Point2D.Double()); + } + + /** + * {@inheritDoc} + *

+ * It is recommended to use {@link #setViewPosition(Point2D)} instead for more precision. + * + * @param newViewPosition {@inheritDoc} + */ + @Override + public void setViewPosition(@NotNull Point newViewPosition) { setViewPosition((Point2D) newViewPosition); } + + /** + * Sets the view coordinates that appear in the upper left hand corner of the viewport, does nothing + * if there's no view. + * + * @param newViewPosition a {@code Point2D} object giving the upper left coordinates + */ + public void setViewPosition(@NotNull Point2D newViewPosition) { + if (view == null || newViewPosition.equals(viewPosition)) { return; } + viewPosition.setLocation(newViewPosition); + fireStateChanged(); + repaint(); + } + + /** + * {@inheritDoc} + * + * @return {@inheritDoc} + * @see #getPreciseViewRect() + */ + @Override + public @NotNull Rectangle getViewRect() { return super.getViewRect(); } + + /** + * Returns a rectangle whose origin is {@link #getPreciseViewPosition} and size is + * {@link #getPreciseExtentSize}. This is the visible part of the view in view coordinates. + * + * @return a {@code Rectangle2D} giving the visible part of the view using view coordinates + */ + public Rectangle2D getPreciseViewRect() { + Point2D pos = getPreciseViewPosition(); + Dimension2D size = getPreciseExtentSize(); + return new Rectangle2D.Double(pos.getX(), pos.getY(), size.getWidth(), size.getHeight()); + } + + /** + * Sets the view area of the viewport to the given {@code Rectangle2D} after first adjusting it + * to match the viewport's aspect ratio. + * + * @param viewRect the view area to set. + */ + public void setViewRect(@NotNull Rectangle2D viewRect) { + if (view == null) { return; } + + double widthRatio = getWidth() / viewRect.getWidth(); + double heightRatio = getHeight() / viewRect.getHeight(); + double minRatio = Math.min(widthRatio, heightRatio); + widthRatio /= minRatio; + heightRatio /= minRatio; + + double oldScaleFactor = scaleFactor; + Point2D oldViewPosition = viewPosition; + scaleFactor *= minRatio; + viewPosition = new Point2D.Double( + viewRect.getCenterX() - viewRect.getWidth() * widthRatio / 2, + viewRect.getCenterY() - viewRect.getHeight() * heightRatio / 2 + ); + + if (scaleFactor != oldScaleFactor || !viewPosition.equals(oldViewPosition)) { + fireStateChanged(); + repaint(); + } + } + + /** + * {@inheritDoc} Since the view inhabits space in double-precision, this method returns the size of the + * bounding box on the integer grid. + * + * @return {@inheritDoc} + */ + @Override + public Dimension getExtentSize() { + Point2D precisePos = getPreciseViewPosition(); + Dimension2D preciseSize = getPreciseExtentSize(); + return new Dimension( + (int) (Math.ceil(precisePos.getX() + preciseSize.getWidth()) - Math.floor(precisePos.getX())), + (int) (Math.ceil(precisePos.getY() + preciseSize.getHeight() - Math.floor(precisePos.getY()))) + ); + } + + /** + * Returns the size of the visible part of the view in view coordinates. + * + * @return a {@code Dimension2D} object giving the viewport's coverage of the view + */ + public Dimension2D getPreciseExtentSize() { + if (view == null) { return new Dimension2DDouble(); } + return toViewCoordinates((Dimension2D) getSize()); + } + + /** + * {@inheritDoc} + *

+ * It is recommended to use {@link #toViewCoordinates(Dimension2D)} instead for more precision. + * + * @param size {@inheritDoc} + * @return {@inheritDoc} + */ + @Override + public Dimension toViewCoordinates(@NotNull Dimension size) { + size.setSize(toViewCoordinates((Dimension2D) size)); + return size; + } + + /** + * Converts a size in pixel coordinates to view coordinates. + * + * @param size a {@code Dimension2D} object using pixel coordinates + * @return a {@code Dimension2D} object converted to view coordinates + */ + public Dimension2D toViewCoordinates(@NotNull Dimension2D size) { + return new Dimension2DDouble(size.getWidth() / scaleFactor, size.getHeight() / scaleFactor); + } + + /** + * {@inheritDoc} + *

+ * It is recommended to use {@link #toViewCoordinates(Point2D)} instead for more precision. + * + * @param p {@inheritDoc} + * @return {@inheritDoc} + */ + @Override + public Point toViewCoordinates(@NotNull Point p) { + p.setLocation(toViewCoordinates((Point2D) p)); + return p; + } + + /** + * Converts a point in pixel coordinates to view coordinates. + * + * @param p a {@code Point2D} object using pixel coordinates + * @return a {@code Point2D} object converted to view coordinates + */ + public Point2D toViewCoordinates(@NotNull Point2D p) { + return new Point2D.Double( + viewPosition.getX() + p.getX() / scaleFactor, + viewPosition.getY() + p.getY() / scaleFactor + ); + } + + /** + * {@inheritDoc} + *

+ * It is recommended to use {@link #setExtentSize(Dimension2D)} instead for more precision. + * + * @param newExtent {@inheritDoc} + */ + @Override + public void setExtentSize(@NotNull Dimension newExtent) { setExtentSize((Dimension2D) newExtent); } + + /** + * Sets the size of the visible part of the view using view coordinates. + * + * @param newExtent a {@code Dimension2D} object specifying the size of the view + */ + public void setExtentSize(@NotNull Dimension2D newExtent) { + if (view == null) { return; } + double widthRatio = getWidth() / newExtent.getWidth(); + double heightRatio = getHeight() / newExtent.getHeight(); + setScaleFactor(scaleFactor * Math.min(widthRatio, heightRatio)); + } + + /** A listener for the view. */ + protected class ZoomViewListener extends ViewListener implements ChangeListener { + + /** Constructs a {@code ZoomViewListener}. */ + protected ZoomViewListener() {} + + public void stateChanged(@NotNull ChangeEvent e) { + fireStateChanged(); + repaint(); + } + } + + /** + * Creates a listener for the view. + * + * @return a {@code ViewListener}. + */ + @Override + protected ViewListener createViewListener() { return new ZoomViewListener(); } + + @Override + protected LayoutManager createLayoutManager() { + return new LayoutManager() { + + public void addLayoutComponent(String name, Component comp) {} + public void removeLayoutComponent(Component comp) {} + + public Dimension preferredLayoutSize(Container parent) { return parent.getSize(); } + public Dimension minimumLayoutSize(Container parent) { return parent.getSize(); } + + public void layoutContainer(Container parent) { + ZoomViewport viewport = (ZoomViewport) parent; + viewport.getView().setBounds(0, 0, viewport.getWidth(), viewport.getHeight()); + } + }; + } + + /** + * Subclassers can override this to install a different peer in the constructor. Returns the + * {@code ZoomPeer} to use as the view of the {@code ZoomViewport}. + * + * @return a {@code ZoomPeer} + */ + protected ZoomPeer createPeer() { return new ZoomPeer(); } + + /** + * Sets the {@code ZoomViewport}'s component peer, which can be {@code null}. + * + * @param newPeer the viewport's new component peer + */ + public void setPeer(ZoomPeer newPeer) { setView(newPeer); } + + /** + * Returns the {@code ZoomViewport}'s component peer or {@code null}. + * + * @return this viewport's peer, or {@code null} if none exists + */ + public ZoomPeer getPeer() { return (ZoomPeer) getView(); } + + /** + * The component child of {@code ZoomViewport} that acts as a peer to the {@code ZoomView}. It provides + * the view with an unscaled surface on which to add popups and menus in addition to having installed the + * view's controllers. + */ + public class ZoomPeer extends JPanel { + + /** Identifies a component that should be laid out as a notification. */ + public static final String NOTIFICATION = "NOTIFICATION"; + + /** + * Mapping of controllers to the {@code MouseInputListeners} installed on this component that create + * {@code ControllerMouseEvent}s for the controllers. + */ + private final HashMap transformers; + + public ZoomPeer() { + setOpaque(false); + setFocusable(true); + setLayout(new ZoomPeerLayout()); + addMouseListener(new MouseAdapter() { + @Override + public void mousePressed(MouseEvent e) { + super.mousePressed(e); + requestFocusInWindow(); + } + }); + transformers = new HashMap<>(); + } + + /** + * Converts a point in view coordinates to pixel coordinates. + * + * @param p a {@code Point2D} object using view coordinates + * @return a {@code Point2D} object converted to pixel coordinates + */ + public Point2D fromViewCoordinates(@NotNull Point2D p) { + return new Point2D.Double( + (p.getX() - viewPosition.getX()) * scaleFactor, + (p.getY() - viewPosition.getY()) * scaleFactor + ); + } + + /** + * Converts a dimension in view coordinates to pixel coordinates. + * + * @param d a {@code Dimension2D} object using view coordinates + * @return a {@code Dimension2D} object converted to pixel coordinates + */ + public Dimension2D fromViewCoordinates(@NotNull Dimension2D d) { + return new Dimension2DDouble( + d.getWidth() * scaleFactor, + d.getHeight() * scaleFactor + ); + } + + /** @return {@code true} if the given point is inside the view, {@code false} otherwise */ + protected boolean pointInsideView(@NotNull Point2D p) { + Dimension viewSize = getViewSize(); + return p.getX() >= 0 && p.getX() <= viewSize.width && p.getY() >= 0 && p.getY() <= viewSize.height; + } + + /** + * Creates a {@code ControllerMouseEvent} with the location of the original transformed + * to view coordinates. + * + * @param e the base mouse event + * @return a controller mouse event in view coordinates + */ + private ControllerMouseEvent createControllerMouseEvent(@NotNull MouseEvent e) { + return new ControllerMouseEvent(this, e.getID(), e.getWhen(), e.getModifiersEx(), + toViewCoordinates((Point2D) e.getPoint()), e.getClickCount(), e.isPopupTrigger(), e.getButton()); + } + + /** + * Creates a {@code MouseInputListener} that transforms the location of mouse events into view + * coordinates before passing them to the supplied {@code ZoomViewController}. The controller-listener + * pair is added to the {@code transformers} field. + * + * @param c the controller to be given transformed mouse events + * @return a listener that supplies {@code c} with transformed mouse events + */ + private MouseInputListener transformController(ZoomViewController c) { + if (c == null) { return null; } + + MouseInputListener transformer = new MouseInputAdapter() { + + private boolean justInside; + + @Override + public void mouseClicked(MouseEvent e) { + ControllerMouseEvent cme = createControllerMouseEvent(e); + if (pointInsideView(cme.getPrecisePoint())) { c.mouseClicked(cme); } + } + + @Override + public void mousePressed(MouseEvent e) { + ControllerMouseEvent cme = createControllerMouseEvent(e); + if (pointInsideView(cme.getPrecisePoint())) { c.mousePressed(cme); } + } + + @Override + public void mouseReleased(MouseEvent e) { + ControllerMouseEvent cme = createControllerMouseEvent(e); + if (pointInsideView(cme.getPrecisePoint())) { c.mouseReleased(cme); } + } + + @Override + public void mouseExited(MouseEvent e) { + if (justInside) { + c.mouseExited(createControllerMouseEvent(e)); + justInside = false; + } + } + + @Override + public void mouseDragged(MouseEvent e) { c.mouseDragged(createControllerMouseEvent(e)); } + + @Override + public void mouseMoved(MouseEvent e) { + ControllerMouseEvent cme = createControllerMouseEvent(e); + c.mouseMoved(cme); + + if (pointInsideView(cme.getPrecisePoint())) { + if (!justInside) { + c.mouseEntered(cme); + justInside = true; + } + } + else if (justInside) { + c.mouseExited(cme); + justInside = false; + } + } + }; + + transformers.put(c, transformer); + return transformer; + } + + /** + * Adds the specified {@code ZoomViewController} to receive mouse input and motion events from this + * component. The position of events added via this method will be given in view coordinates. + * + * @param c the controller + */ + public void addZoomViewController(ZoomViewController c) { + MouseInputListener l = transformController(c); + addMouseListener(l); + addMouseMotionListener(l); + } + + /** + * Removes the specified {@code ZoomViewController} so that it no longer receives mouse input + * and motion events from this component. + * + * @param c the controller + */ + public void removeZoomViewController(ZoomViewController c) { + MouseInputListener l = transformers.remove(c); + removeMouseListener(l); + removeMouseMotionListener(l); + } + + /** + * Returns an array of all {@code ZoomViewController}s registered on this component. + * + * @return all of this component's {@code ZoomViewController}s + */ + public ZoomViewController[] getZoomViewControllers() { + return transformers.keySet().toArray(new ZoomViewController[0]); + } + + /** + * Layout manager for {@code ZoomPeer} that lays out components as notifications if they are added + * with {@link ZoomPeer#NOTIFICATION}. + */ + private static class ZoomPeerLayout implements LayoutManager { + + /** List of components to lay out as notifications. */ + private final List notifications = new ArrayList<>(); + + /** X coordinate to place each notification at. */ + private static final int NOTIFICATION_X = 10; + + /** Y coordinate to place the first notification at. */ + private static final int NOTIFICATION_Y = 10; + + /** Vertical gap between every notification. */ + private static final int NOTIFICATION_GAP = 5; + + @Override + public void addLayoutComponent(@Nullable String name, @NotNull Component comp) { + if (NOTIFICATION.equals(name)) { notifications.add(comp); } + } + + @Override + public void removeLayoutComponent(@NotNull Component comp) { notifications.remove(comp); } + + @Override + public Dimension preferredLayoutSize(@NotNull Container parent) { + return new Dimension(0, 0); + } + + @Override + public Dimension minimumLayoutSize(@NotNull Container parent) { + return new Dimension(0, 0); + } + + @Override + public void layoutContainer(@NotNull Container parent) { + + int y = NOTIFICATION_Y; + for (Component notification : notifications) { + if (notification.isVisible()) { + + Dimension size = notification.getPreferredSize(); + notification.setBounds(NOTIFICATION_X, y, size.width, size.height); + + y += size.height + NOTIFICATION_GAP; + } + } + } + } + } + + /** + * A {@code double}-precision {@code Dimension2D}. + *

+ * Unlike other classes in {@link java.awt.geom}, the {@code Dimension2D} class does not provide inner + * {@code Float} and {@code Double} classes. This class is a necessary {@code Dimension2D.Double} + * implementation. + */ + public static class Dimension2DDouble extends Dimension2D implements Serializable { + + /** The width of this {@code Dimension2D}.*/ + public double width; + + /** The height of this {@code Dimension2D}.*/ + public double height; + + /** Constructs and initializes a {@code Dimension2D} with lengths (0, 0). */ + public Dimension2DDouble() { setSize(0, 0); } + + /** + * Constructs and initializes a {@code Dimension2D} with the specified lengths. + * + * @param width the width of the newly constructed {@code Dimension2D}. + * @param height the height of the newly constructed {@code Dimension2D}. + */ + public Dimension2DDouble(double width, double height) { setSize(width, height); } + + + @Override + public double getWidth() { return width; } + + @Override + public double getHeight() { return height; } + + @Override + public void setSize(double width, double height) { + this.width = width; + this.height = height; + } + + /** + * Returns a {@code String} that represents the value of this {@code Dimension2D}. + * + * @return a string representation of this {@code Dimension2D}. + */ + public String toString() { return "Dimension2DDouble[" + width + ", " + height + "]"; } + + /** Use serialVersionUID from JDK 1.6 for interoperability. */ + @Serial + private static final long serialVersionUID = 6150783262733311327L; + } + + /** + * Returns a string representation of this {@code ZoomViewport}. This method is intended to be used only + * for debugging purposes, and the content and format of the returned string may vary between implementations. + * The returned string may be empty but may not be {@code null}. + * + * @return a string representation of this {@code ZoomViewport} + */ + protected String paramString() { + String paramString = super.paramString(); + paramString = paramString.substring(0, paramString.indexOf(",isViewSizeSet=")); + return paramString + + ",scaleFactor=" + scaleFactor + + ",viewPosition=" + viewPosition + + ",extentSize=" + getPreciseExtentSize() + + ",view=" + view; + } +} diff --git a/src/main/resources/edu/rpi/legup/themes/FlatLaf.properties b/src/main/resources/edu/rpi/legup/themes/FlatLaf.properties index 949cdce67..c45c9ab16 100644 --- a/src/main/resources/edu/rpi/legup/themes/FlatLaf.properties +++ b/src/main/resources/edu/rpi/legup/themes/FlatLaf.properties @@ -38,8 +38,18 @@ Tree.contradiction = #B20A10 Tree.selected = #1E88E5 Tree.selectedOutline = lighten($Tree.selected, 10%) Tree.selectedWidth = 2 -Tree.hover = $Puzzle.hover +Tree.hover = @hover Tree.hoverOutline = lighten($Tree.hover, 10%) +Tree.viewPadding = {Insets} 50,150,50,150 +Tree.nodeRadius = 25 +Tree.contradictionWidth = 5 +Tree.transitionTailGap = 60 +Tree.transitionArc = 12 +Tree.transitionHeadBase = 24 +Tree.transitionHeadHeight = 20 +Tree.transitionTailWeight = 6 +Tree.verticalGap = 20 +Tree.horizontalGap = 10 # Styles for individual puzzle types @@ -201,14 +211,37 @@ Expected.SvgIcon.text = #222222 Expected.SvgIcon.borderColor = #232323 +# Styles for custom components + +# ZoomPane +ZoomPaneUI = edu.rpi.legup.ui.zoompane.ZoomPaneUI +ZoomPane.smoothScrolling = true +ZoomPane.viewPadding = {insets} 10,10,10,10 + +# ZoomBar +ZoomBarUI = edu.rpi.legup.ui.zoompane.ZoomBarUI +ZoomBar.width = 16 +ZoomBar.minimumThumbSize = 12,12 +ZoomBar.trackInsets = 6,6,6,6 +ZoomBar.trackArc = 999 +ZoomBar.showButtons = true +ZoomBar.showFitButton = true +ZoomBar.fitButtonGap = 2 +ZoomBar.background = $ScrollBar.track +ZoomBar.thumb = if(@accentColor, @accentColor, @accentBase2Color) +ZoomBar.hoverThumbColor = darken($ZoomBar.thumb,5%,derived) +ZoomBar.pressedThumbColor = darken($ZoomBar.thumb,8%,derived) +ZoomBar.track = $Slider.disabledTrackColor +ZoomBar.hoverTrackColor = $Slider.trackColor + + # Styles for various UI elements [style]Label.welcome = font: 17 bold; border: 0,0,0,5; [style]Label.version = font: 10 italic; border: 0,0,0,5; -[style]Label.info = font: {font} 14; foreground: @foreground; background: @background; border: 5,15,5,15,mix(@foreground,@background,25%),3,10; -[style]Label.error = font: 14 italic; foreground: @invalid; background: @background; border: 5,15,5,15,mix(@invalid,@background,25%),3,10; +[style]Label.info = font: {font} 14; foreground: @foreground; background: changeAlpha(@background,90%); border: 5,15,5,15,changeAlpha(@foreground,40%),3,10; +[style]Label.error = font: 14 italic; foreground: @invalid; background: changeAlpha(@background,90%); border: 5,15,5,15,changeAlpha(@invalid,40%),3,10; [style]Label.prefsHeading = font: 14 bold; -[style]Label.zoomer = font: {font} 16; [style]Button.rule = font: 12 "Segoe UI"; [style]Button.smallRule = font: 10 "Segoe UI";