/**
* Copyright (C) 2001-2017 by RapidMiner and the contributors
*
* Complete list of developers available at our web site:
*
* http://rapidminer.com
*
* This program is free software: you can redistribute it and/or modify it under the terms of the
* GNU Affero General Public License as published by the Free Software Foundation, either version 3
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
* even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License along with this program.
* If not, see http://www.gnu.org/licenses/.
*/
package com.rapidminer.gui.flow.processrendering.annotations.model;
import java.awt.Point;
import java.awt.event.MouseEvent;
import java.awt.geom.Rectangle2D;
import javax.swing.SwingUtilities;
import com.rapidminer.gui.flow.processrendering.annotations.AnnotationDrawUtils;
import com.rapidminer.gui.flow.processrendering.annotations.AnnotationsDecorator;
import com.rapidminer.gui.flow.processrendering.annotations.model.AnnotationResizeHelper.ResizeDirection;
import com.rapidminer.gui.flow.processrendering.annotations.style.AnnotationAlignment;
import com.rapidminer.gui.flow.processrendering.annotations.style.AnnotationColor;
import com.rapidminer.gui.flow.processrendering.model.ProcessRendererModel;
import com.rapidminer.operator.ExecutionUnit;
import com.rapidminer.operator.Operator;
/**
* The model backing the {@link AnnotationsDecorator}.
*
* @author Marco Boeck
* @since 6.4.0
*
*/
public class AnnotationsModel {
/** currently hovered annotation or {@code null} */
private WorkflowAnnotation hovered;
/** the resize direction currently being hovered or {@code null} */
private ResizeDirection hoveredResizeDirection;
/** currently selected annotation or {@code null} */
private WorkflowAnnotation selected;
/** the annotation currently being dragged or {@code null} */
private AnnotationDragHelper dragged;
/** the annotation currently being resized or {@code null} */
private AnnotationResizeHelper resized;
/** the process renderer model */
private ProcessRendererModel model;
/**
* Creates a new model backing the workflow annotations.
*
* @param model
* the process renderer model
*/
public AnnotationsModel(ProcessRendererModel model) {
this.model = model;
}
/**
* Returns the hovered {@link WorkflowAnnotation}.
*
* @return the hovered annotation or {@code null}
*/
public WorkflowAnnotation getHovered() {
return hovered;
}
/**
* Sets the hovered annotation. If it changes, fires a process renderer model misc event to
* trigger a repaint.
*
* @param hovered
* the new hovered annotation, can be {@code null}
* @param hoveredResizeDirection
* the hovered resize corner, can be {@code null}
*/
public void setHovered(final WorkflowAnnotation hovered, final ResizeDirection hoveredResizeDirection) {
if (hovered == null) {
if (this.hovered != null) {
this.hovered = null;
setHoveredResizeDirection(null);
model.fireAnnotationMiscChanged(null);
}
} else {
if (!hovered.equals(this.hovered)) {
this.hovered = hovered;
if (hovered.equals(selected)) {
setHoveredResizeDirection(hoveredResizeDirection);
} else {
setHoveredResizeDirection(null);
}
model.fireAnnotationMiscChanged(hovered);
} else {
if (hovered.equals(selected)) {
setHoveredResizeDirection(hoveredResizeDirection);
}
}
}
}
/**
* Returns the resize direction of the hovered annotation.
*
* @return the resize direction or {@code null}
*/
public ResizeDirection getHoveredResizeDirection() {
return hoveredResizeDirection;
}
/**
* Sets the hovered resize direction. If it changes, fires a process renderer model misc event
* to trigger a repaint.
*
* @param hoveredResizeDirection
* the hovered resize direction
*/
public void setHoveredResizeDirection(final ResizeDirection hoveredResizeDirection) {
if (hoveredResizeDirection != null) {
if (!hoveredResizeDirection.equals(this.hoveredResizeDirection)) {
this.hoveredResizeDirection = hoveredResizeDirection;
model.fireAnnotationMiscChanged(null);
}
} else {
if (this.hoveredResizeDirection != null) {
this.hoveredResizeDirection = null;
model.fireAnnotationMiscChanged(null);
}
}
}
/**
* Returns the selected {@link WorkflowAnnotation}.
*
* @return the selected annotation or {@code null}
*/
public WorkflowAnnotation getSelected() {
return selected;
}
/**
* Sets the selected {@link WorkflowAnnotation}. If it changes, fires a process renderer model
* annotation selection event to trigger a repaint.
*
* @param selected
* the selected annotation or {@code null}
*/
public void setSelected(WorkflowAnnotation selected) {
if (selected == null) {
if (getSelected() != null) {
this.selected = null;
model.fireAnnotationSelected(null);
}
} else {
if (!selected.equals(this.selected)) {
this.selected = selected;
model.fireAnnotationSelected(selected);
}
}
}
/**
* Returns the drag helper if an annotation is currently being dragged.
*
* @return the drag helper or {@code null}
*/
public AnnotationDragHelper getDragged() {
return dragged;
}
/**
* Sets the drag helper if an annotation is currently being dragged.
*
* @param dragged
* the drag helper or {@code null}
*/
public void setDragged(AnnotationDragHelper dragged) {
this.dragged = dragged;
}
/**
* Returns the resize helper if an annotation is currently being resized.
*
* @return the resize helper or {@code null}
*/
public AnnotationResizeHelper getResized() {
return resized;
}
/**
* Sets the resize helper if an annotation is currently being resized.
*
* @param resized
* the resize helper or {@code null}
*/
public void setResized(AnnotationResizeHelper resized) {
this.resized = resized;
}
/**
* Starts a drag or resizing of the selected annotation, depending on whether the drag starts on
* the annotation or one of the resize "knobs". If no annotation is selected, does nothing. If
* the triggering action was not a left-click, does nothing.
*
* @param e
* the mouse event triggering the drag/resize
* @param origin
* the origin of the drag/resize event
* @param allowResize
* if {@code true}, resize is allowed. Otherwise only a drag can be started
*/
public void startDragOrResize(final MouseEvent e, final Point origin, boolean allowResize) {
if (getSelected() == null) {
return;
}
if (!SwingUtilities.isLeftMouseButton(e)) {
return;
}
// manual resizing is NEVER permitted for operator annotations
if (getSelected() instanceof OperatorAnnotation) {
allowResize = false;
}
ResizeDirection direction = null;
if (allowResize) {
direction = AnnotationResizeHelper.getResizeDirectionOrNull(getSelected(), origin);
}
if (direction != null) {
resized = new AnnotationResizeHelper(selected, direction, origin);
} else {
dragged = new AnnotationDragHelper(selected, origin, model);
}
}
/**
* Updates the dragged position or the resizing of the selected annotation and fires a misc
* model change for the process renderer. If no drag and resizing is in progress, does nothing.
*
* @param point
* the current location
*/
public void updateDragOrResize(final Point point) {
if (dragged == null && resized == null) {
return;
}
if (dragged != null) {
dragged.handleDragEvent(point);
// fire moved event
model.fireAnnotationMoved(dragged.getDraggedAnnotation());
} else if (resized != null) {
resized.handleResizeEvent(point);
// fire moved event
model.fireAnnotationMoved(resized.getResized());
}
}
/**
* Stops the drag or resizing of the selected annotation. If neither was in progress, does
* nothing.
*
* @param destination
* the final destination of the drag/resize event. If {@code null}, dragging is
* assumed to be cancelled and no conversion from operator to process annotation or
* vice versa is performed.
*/
public void stopDragOrResize(final Point destination) {
if (dragged == null && resized == null) {
return;
}
if (destination != null) {
updateDragOrResize(destination);
}
// trigger process event so it becomes dirty
if (dragged != null) {
WorkflowAnnotation draggedAnno = dragged.getDraggedAnnotation();
// if we stop over an operator
if (dragged.getHoveredOperator() != null) {
if (draggedAnno instanceof ProcessAnnotation) {
if (destination != null) {
// delete process annotation if drag was not cancelled
deleteAnnotation(draggedAnno);
addOperatorAnnotation(draggedAnno.createOperatorAnnotation(dragged.getHoveredOperator()));
} else {
moveProcessAnnoToPoint((ProcessAnnotation) draggedAnno, dragged.getOrigin(),
dragged.getStartingPoint());
}
} else if (draggedAnno instanceof OperatorAnnotation) {
OperatorAnnotation opAnno = (OperatorAnnotation) draggedAnno;
if (destination != null) {
// remove from original operator
model.removeOperatorAnnotation(opAnno);
moveOperatorAnnoToOperator(opAnno, dragged.getHoveredOperator());
// attach to new operator
opAnno.setAttachedTo(dragged.getHoveredOperator());
model.addOperatorAnnotation(opAnno);
} else {
// destination is null = cancelled dragging
moveOperatorAnnoToOperator(opAnno, opAnno.getAttachedTo());
}
opAnno.fireUpdate();
}
} else {
// we did not stop over an operator and dragged an annotation
if (destination == null) {
// we cancelled dragging via ESC -> reset to original position
if (draggedAnno instanceof OperatorAnnotation) {
moveOperatorAnnoToOperator((OperatorAnnotation) draggedAnno,
((OperatorAnnotation) draggedAnno).getAttachedTo());
} else if (draggedAnno instanceof ProcessAnnotation) {
moveProcessAnnoToPoint((ProcessAnnotation) draggedAnno, dragged.getOrigin(),
dragged.getStartingPoint());
}
} else {
if (draggedAnno instanceof OperatorAnnotation && dragged.isUnsnapped()) {
// an operator annotation was dragged away from an operator
// convert to process annotation
deleteAnnotation(draggedAnno);
addProcessAnnotation(draggedAnno.createProcessAnnotation(draggedAnno.getProcess()));
} else if (draggedAnno instanceof ProcessAnnotation) {
// notify process of change
draggedAnno.fireUpdate();
}
}
}
// otherwise we might end up with a selection rectangle here
model.setSelectionRectangle(null);
} else if (resized != null) {
resized.getResized().fireUpdate();
}
if (resized != null) {
WorkflowAnnotation anno = resized.getResized();
int prefHeight = AnnotationDrawUtils.getContentHeight(AnnotationDrawUtils.createStyledCommentString(
anno.getComment(), anno.getStyle()), (int) anno.getLocation().getWidth());
boolean overflowing = false;
if (prefHeight > anno.getLocation().getHeight()) {
overflowing = true;
}
anno.setOverflowing(overflowing);
}
// reset
dragged = null;
resized = null;
}
/**
* Deletes the given annotation and fires updates.
*
* @param toDelete
* the annotation to delete
*
*/
public void deleteAnnotation(final WorkflowAnnotation toDelete) {
if (toDelete == null) {
throw new IllegalArgumentException("toDelete must not be null!");
}
if (toDelete instanceof OperatorAnnotation) {
OperatorAnnotation anno = (OperatorAnnotation) toDelete;
model.removeOperatorAnnotation(anno);
} else if (toDelete instanceof ProcessAnnotation) {
ProcessAnnotation anno = (ProcessAnnotation) toDelete;
model.removeProcessAnnotation(anno);
}
setSelected(null);
fireProcessUpdate(toDelete);
model.fireAnnotationMiscChanged(null);
}
/**
* Adds the given operator annotation and fires updates.
*
* @param anno
* the annotation to add
*/
public void addOperatorAnnotation(final OperatorAnnotation anno) {
if (anno == null) {
throw new IllegalArgumentException("anno must not be null!");
}
model.addOperatorAnnotation(anno);
setSelected(anno);
fireProcessUpdate(anno);
model.fireAnnotationMoved(anno);
}
/**
* Adds the given process annotation and fires updates.
*
* @param anno
* the annotation to add
*/
public void addProcessAnnotation(final ProcessAnnotation anno) {
if (anno == null) {
throw new IllegalArgumentException("anno must not be null!");
}
model.addProcessAnnotation(anno);
setSelected(anno);
fireProcessUpdate(anno);
model.fireAnnotationMoved(anno);
}
/**
* Sets the color of the annotation and fires an event afterwards.
*
* @param anno
* the annotation which will have its color changed
* @param color
* the new color
*/
public void setAnnotationColor(final WorkflowAnnotation anno, final AnnotationColor color) {
if (anno == null) {
throw new IllegalArgumentException("anno must not be null!");
}
if (color == null) {
throw new IllegalArgumentException("color must not be null!");
}
anno.getStyle().setAnnotationColor(color);
anno.setColored();
fireProcessUpdate(anno);
model.fireAnnotationMiscChanged(anno);
}
/**
* Sets the alignment of the annotation and fires an event afterwards.
*
* @param anno
* the annotation which will have its alignment changed
* @param alignment
* the new alignment
*/
public void setAnnotationAlignment(final WorkflowAnnotation anno, final AnnotationAlignment alignment) {
if (anno == null) {
throw new IllegalArgumentException("anno must not be null!");
}
if (alignment == null) {
throw new IllegalArgumentException("alignment must not be null!");
}
anno.getStyle().setAnnotationAlignment(alignment);
fireProcessUpdate(anno);
model.fireAnnotationMiscChanged(anno);
}
/**
* Sets the comment of the annotation and fires an event afterwards.
*
* @param anno
* the annotation which will have its comment changed
* @param comment
* the new comment
*/
public void setAnnotationComment(final WorkflowAnnotation anno, final String comment) {
if (anno == null) {
throw new IllegalArgumentException("anno must not be null!");
}
if (comment == null) {
throw new IllegalArgumentException("comment must not be null!");
}
anno.setComment(comment);
fireProcessUpdate(anno);
model.fireAnnotationMoved(anno);
}
/**
* Bring the given annotation to the front. That annotation will be drawn over all other
* annotations as well as receive events first.
*
* @param anno
* the annotation to bring to the front
*/
public void toFront(final WorkflowAnnotation anno) {
if (anno == null) {
throw new IllegalArgumentException("anno must not be null!");
}
model.getProcessAnnotations(anno.getProcess()).toFront(anno);
fireProcessUpdate(anno);
model.fireAnnotationMiscChanged(anno);
}
/**
* Brings the given annotation one layer forward.
*
* @param anno
* the annotation to bring forward
*/
public void sendForward(final WorkflowAnnotation anno) {
if (anno == null) {
throw new IllegalArgumentException("anno must not be null!");
}
model.getProcessAnnotations(anno.getProcess()).sendForward(anno);
fireProcessUpdate(anno);
model.fireAnnotationMiscChanged(anno);
}
/**
* Bring the given annotation to the back. That annotation will be drawn behind all other
* annotations as well as receive events last.
*
* @param anno
* the annotation to bring to the front
*/
public void toBack(final WorkflowAnnotation anno) {
if (anno == null) {
throw new IllegalArgumentException("anno must not be null!");
}
model.getProcessAnnotations(anno.getProcess()).toBack(anno);
fireProcessUpdate(anno);
model.fireAnnotationMiscChanged(anno);
}
/**
* Sends the given annotation one layer backward.
*
* @param anno
* the annotation to send backward
*/
public void sendBack(final WorkflowAnnotation anno) {
if (anno == null) {
throw new IllegalArgumentException("anno must not be null!");
}
model.getProcessAnnotations(anno.getProcess()).sendBack(anno);
fireProcessUpdate(anno);
model.fireAnnotationMiscChanged(anno);
}
/**
* Resets model status as if the model was newly created
*/
public void reset() {
this.hovered = null;
this.hoveredResizeDirection = null;
this.selected = null;
this.dragged = null;
this.resized = null;
}
/**
* Moves the {@link OperatorAnnotation} to the target {@link Operator}. Only updates the
* location!
*
* @param opAnno
* the annotation to move
* @param target
* the operator to which the annotation should be moved
*/
private void moveOperatorAnnoToOperator(final OperatorAnnotation opAnno, final Operator target) {
int x = (int) (model.getOperatorRect(target).getCenterX() - opAnno.getLocation().getWidth() / 2);
int y = (int) model.getOperatorRect(target).getMaxY() + OperatorAnnotation.Y_OFFSET;
opAnno.setLocation(new Rectangle2D.Double(x, y, opAnno.getLocation().getWidth(), opAnno.getLocation().getHeight()));
}
/**
* Moves the {@link ProcessAnnotation} to the target {@link Point}. Only updates the location!
*
* @param processAnno
* the annotation to move
* @param current
* the current absolute point of the annotation
* @param target
* the new absolute point of the annotation
*/
private void moveProcessAnnoToPoint(final ProcessAnnotation processAnno, final Point current, final Point target) {
processAnno.setLocation(new Rectangle2D.Double(target.getX(), target.getY(), processAnno.getLocation().getWidth(),
processAnno.getLocation().getHeight()));
}
/**
* Fires an update for a process. If the annotation is not attached to any process, does
* nothing.
*
* @param anno
* the annotation which triggered the update
*/
private void fireProcessUpdate(final WorkflowAnnotation anno) {
ExecutionUnit process = anno.getProcess();
if (process != null) {
// dirty hack to trigger a process update
process.getEnclosingOperator().rename(process.getEnclosingOperator().getName());
}
}
}