/**
* 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.event;
import java.awt.Cursor;
import java.awt.Point;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.awt.geom.Rectangle2D;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import javax.swing.SwingUtilities;
import com.rapidminer.gui.flow.processrendering.annotations.AnnotationDrawer;
import com.rapidminer.gui.flow.processrendering.annotations.AnnotationsDecorator;
import com.rapidminer.gui.flow.processrendering.annotations.AnnotationsVisualizer;
import com.rapidminer.gui.flow.processrendering.annotations.model.AnnotationResizeHelper;
import com.rapidminer.gui.flow.processrendering.annotations.model.AnnotationsModel;
import com.rapidminer.gui.flow.processrendering.annotations.model.OperatorAnnotation;
import com.rapidminer.gui.flow.processrendering.annotations.model.ProcessAnnotation;
import com.rapidminer.gui.flow.processrendering.annotations.model.WorkflowAnnotation;
import com.rapidminer.gui.flow.processrendering.annotations.model.WorkflowAnnotations;
import com.rapidminer.gui.flow.processrendering.annotations.style.AnnotationStyle;
import com.rapidminer.gui.flow.processrendering.event.ProcessRendererAnnotationEvent;
import com.rapidminer.gui.flow.processrendering.event.ProcessRendererEventListener;
import com.rapidminer.gui.flow.processrendering.event.ProcessRendererModelEvent;
import com.rapidminer.gui.flow.processrendering.event.ProcessRendererOperatorEvent;
import com.rapidminer.gui.flow.processrendering.model.ProcessRendererModel;
import com.rapidminer.gui.flow.processrendering.view.ProcessEventDecorator;
import com.rapidminer.gui.flow.processrendering.view.ProcessRendererView;
import com.rapidminer.gui.flow.processrendering.view.RenderPhase;
import com.rapidminer.gui.tools.SwingTools;
import com.rapidminer.operator.ExecutionUnit;
import com.rapidminer.operator.Operator;
import com.rapidminer.tools.I18N;
import com.rapidminer.tools.SystemInfoUtilities;
import com.rapidminer.tools.SystemInfoUtilities.OperatingSystem;
/**
* This class handles event hooks registered to the {@link ProcessRendererView} for workflow
* annotations.
*
* @author Marco Boeck
* @since 6.4.0
*
*/
public final class AnnotationEventHook {
/** the annotations decorator */
private final AnnotationsDecorator decorator;
/** the annotation handler */
private final AnnotationsVisualizer visualizer;
/** the annotations model */
private final AnnotationsModel model;
/** the annotation drawer */
private final AnnotationDrawer drawer;
/** the process renderer */
private final ProcessRendererView view;
/** the process renderer model */
private final ProcessRendererModel rendererModel;
/** handles events for non-selected process annotations */
private ProcessEventDecorator processAnnotationEvents = new ProcessEventDecorator() {
@Override
public void processMouseEvent(final ExecutionUnit process, final MouseEventType type, final MouseEvent e) {
if (!visualizer.isActive()) {
return;
}
Point point = rendererModel.getMousePositionRelativeToProcess();
if (point == null) {
point = e.getPoint();
}
switch (type) {
case MOUSE_CLICKED:
if (!SwingUtilities.isLeftMouseButton(e)) {
break;
}
if (process != null && e.getClickCount() >= 2) {
if (!AnnotationDrawer.isProcessInteractionHappening(rendererModel)) {
double x = Math.max(WorkflowAnnotation.MIN_X, point.getX());
double y = Math.max(WorkflowAnnotation.MIN_Y, point.getY());
ProcessAnnotation anno = new ProcessAnnotation(
I18N.getGUILabel("workflow.annotation.default_text.label"), new AnnotationStyle(),
process, false, false, new Rectangle2D.Double(x, y, ProcessAnnotation.DEFAULT_WIDTH,
ProcessAnnotation.DEFAULT_HEIGHT));
model.addProcessAnnotation(anno);
decorator.editSelected();
e.consume();
}
}
break;
case MOUSE_ENTERED:
case MOUSE_MOVED:
if (process != null) {
WorkflowAnnotations annotations = rendererModel.getProcessAnnotations(process);
if (updateHoveredStatus(point, process, annotations)) {
e.consume();
} else {
model.setHovered(null, null);
}
}
break;
case MOUSE_EXITED:
if (!SwingTools.isMouseEventExitedToChildComponents(view, e)) {
model.setHovered(null, null);
}
break;
case MOUSE_DRAGGED:
model.setHovered(null, null);
break;
case MOUSE_PRESSED:
if ((SwingTools.isControlOrMetaDown(e) || e.isShiftDown()) && e.getButton() == 1) {
return;
}
if (SwingUtilities.isLeftMouseButton(e) || SwingUtilities.isRightMouseButton(e)) {
if (model.getHovered() != null) {
model.setSelected(model.getHovered());
model.startDragOrResize(e, point, false);
e.consume();
// linux/mac only, otherwise the first click will only select
if (e.isPopupTrigger()) {
visualizer.showPopupMenu(e);
return;
}
} else {
if (model.getSelected() != null) {
model.setSelected(null);
// if context menu on process should open, don't prevent it
if (!e.isPopupTrigger()) {
e.consume();
}
}
}
}
break;
case MOUSE_RELEASED:
default:
break;
}
}
@Override
public void processKeyEvent(final ExecutionUnit process, final KeyEventType type, final KeyEvent e) {
// not interested
}
};
/** handles events for non-selected operator annotations */
private ProcessEventDecorator operatorAnnotationEvents = new ProcessEventDecorator() {
@Override
public void processMouseEvent(final ExecutionUnit process, final MouseEventType type, final MouseEvent e) {
if (!visualizer.isActive()) {
return;
}
Point point = rendererModel.getMousePositionRelativeToProcess();
if (point == null) {
point = e.getPoint();
}
switch (type) {
case MOUSE_CLICKED:
break;
case MOUSE_ENTERED:
case MOUSE_MOVED:
if (process != null) {
List<Operator> selectedOperators = rendererModel.getSelectedOperators();
// selected operators annotations are drawn over non selected ones, so
// handle them first
for (Operator selOp : selectedOperators) {
WorkflowAnnotations annotations = rendererModel.getOperatorAnnotations(selOp);
if (updateHoveredStatus(point, process, annotations)) {
e.consume();
return;
}
}
for (Operator op : process.getOperators()) {
if (selectedOperators.contains(op)) {
continue;
}
WorkflowAnnotations annotations = rendererModel.getOperatorAnnotations(op);
if (updateHoveredStatus(point, process, annotations)) {
e.consume();
return;
}
}
}
break;
case MOUSE_EXITED:
break;
case MOUSE_DRAGGED:
break;
case MOUSE_PRESSED:
if (SwingTools.isControlOrMetaDown(e) || e.isShiftDown()) {
return;
}
if (SwingUtilities.isLeftMouseButton(e) || SwingUtilities.isRightMouseButton(e)) {
if (model.getHovered() instanceof ProcessAnnotation) {
return;
}
if (model.getHovered() != null) {
model.setSelected(model.getHovered());
model.startDragOrResize(e, point, false);
e.consume();
// linux/mac only, otherwise the first click will only select
if (e.isPopupTrigger()) {
visualizer.showPopupMenu(e);
return;
}
} else {
if (model.getSelected() != null) {
model.setSelected(null);
if (!e.isPopupTrigger()) {
e.consume();
}
}
}
}
break;
case MOUSE_RELEASED:
default:
break;
}
}
@Override
public void processKeyEvent(final ExecutionUnit process, final KeyEventType type, final KeyEvent e) {
// not interested
}
};
/** handles events for selected annotations */
private ProcessEventDecorator workflowAnnotationSelectedEvents = new ProcessEventDecorator() {
@Override
public void processMouseEvent(final ExecutionUnit process, final MouseEventType type, final MouseEvent e) {
if (!visualizer.isActive()) {
return;
}
if (model.getSelected() == null) {
return;
}
Point point = rendererModel.getMousePositionRelativeToProcess();
if (point == null) {
point = e.getPoint();
}
switch (type) {
case MOUSE_ENTERED:
case MOUSE_EXITED:
case MOUSE_MOVED:
// only handle events over the selected annotation
if (!model.getSelected().getLocation().contains(point)
|| !model.getSelected().getProcess().equals(process)) {
return;
}
// always consume
e.consume();
if (process != null) {
WorkflowAnnotations annotations = rendererModel.getProcessAnnotations(process);
if (!updateHoveredStatus(point, process, annotations)) {
model.setHovered(null, null);
}
}
break;
case MOUSE_DRAGGED:
if (model.getDragged() != null || model.getResized() != null) {
model.updateDragOrResize(point);
// only consume if we actually started a drag
if (model.getDragged() != null && model.getDragged().isDragInProgress()) {
e.consume();
}
} else {
if (process != null) {
WorkflowAnnotations annotations = rendererModel.getProcessAnnotations(process);
if (!updateHoveredStatus(point, process, annotations)) {
model.setHovered(null, null);
}
}
}
break;
case MOUSE_CLICKED:
// only handle events over the selected annotation
if (!model.getSelected().getLocation().contains(point)
|| !model.getSelected().getProcess().equals(process)) {
return;
}
// always consume if we have a selected annotation
if (e.getClickCount() >= 2) {
decorator.editSelected();
e.consume();
}
break;
case MOUSE_PRESSED:
// only handle events over the selected annotation
if (!model.getSelected().getLocation().contains(point)
|| !model.getSelected().getProcess().equals(process)) {
return;
}
if (SwingUtilities.isLeftMouseButton(e) || SwingUtilities.isRightMouseButton(e)) {
// only allow popup trigger to pass through
if (e.isPopupTrigger()) {
if (visualizer.showPopupMenu(e)) {
e.consume();
}
return;
} else {
model.startDragOrResize(e, point, true);
e.consume();
}
}
break;
case MOUSE_RELEASED:
view.setCursor(Cursor.getDefaultCursor());
// always stop drag or resize at this point
model.stopDragOrResize(point);
// apart from that, only handle events over the selected annotation
if (!model.getSelected().getLocation().contains(point)
|| !model.getSelected().getProcess().equals(process)) {
return;
}
// only allow popup trigger to pass through
if (e.isPopupTrigger()) {
if (visualizer.showPopupMenu(e)) {
e.consume();
}
return;
} else {
e.consume();
}
break;
default:
break;
}
}
@Override
public void processKeyEvent(final ExecutionUnit process, final KeyEventType type, final KeyEvent e) {
if (!visualizer.isActive()) {
return;
}
if (type != KeyEventType.KEY_PRESSED) {
return;
}
if (model.getSelected() == null) {
return;
}
switch (e.getKeyCode()) {
case KeyEvent.VK_F2:
decorator.editSelected();
e.consume();
break;
case KeyEvent.VK_BACK_SPACE:
if (SystemInfoUtilities.getOperatingSystem() == OperatingSystem.OSX) {
model.deleteAnnotation(model.getSelected());
model.setResized(null);
model.setDragged(null);
e.consume();
}
break;
case KeyEvent.VK_DELETE:
model.deleteAnnotation(model.getSelected());
model.setResized(null);
model.setDragged(null);
e.consume();
break;
case KeyEvent.VK_ESCAPE:
model.setSelected(null);
model.stopDragOrResize(null);
e.consume();
break;
default:
break;
}
}
};
/** listener to be notified of process renderer model events, e.g. operator movements */
private ProcessRendererEventListener modelListener = new ProcessRendererEventListener() {
@Override
public void operatorsChanged(final ProcessRendererOperatorEvent e, final Collection<Operator> operators) {
switch (e.getEventType()) {
case OPERATORS_MOVED:
case PORTS_CHANGED:
List<WorkflowAnnotation> movedAnnos = positionOperatorAnnotations(operators);
rendererModel.fireAnnotationsMoved(movedAnnos);
break;
case SELECTED_OPERATORS_CHANGED:
model.setSelected(null);
break;
default:
break;
}
}
@Override
public void modelChanged(final ProcessRendererModelEvent e) {
switch (e.getEventType()) {
case DISPLAYED_CHAIN_CHANGED:
case DISPLAYED_PROCESSES_CHANGED:
model.reset();
decorator.reset();
drawer.reset();
List<WorkflowAnnotation> movedAnnos = positionOperatorAnnotations(
rendererModel.getDisplayedChain().getAllInnerOperators());
rendererModel.fireAnnotationsMoved(movedAnnos);
break;
case MISC_CHANGED:
case PROCESS_SIZE_CHANGED:
case PROCESS_ZOOM_CHANGED:
case DISPLAYED_CHAIN_WILL_CHANGE:
default:
break;
}
}
@Override
public void annotationsChanged(final ProcessRendererAnnotationEvent e,
final Collection<WorkflowAnnotation> annotations) {
// ignore
}
};
public AnnotationEventHook(final AnnotationsDecorator decorator, final AnnotationsModel model,
final AnnotationsVisualizer visualizer, final AnnotationDrawer drawer, final ProcessRendererView view,
final ProcessRendererModel rendererModel) {
this.decorator = decorator;
this.model = model;
this.visualizer = visualizer;
this.drawer = drawer;
this.view = view;
this.rendererModel = rendererModel;
}
/**
* Registers the event hooks and draw decorators to the process renderer.
*/
public void registerDecorators() {
view.addEventDecorator(processAnnotationEvents, RenderPhase.ANNOTATIONS);
view.addEventDecorator(operatorAnnotationEvents, RenderPhase.OPERATOR_ANNOTATIONS);
view.addEventDecorator(workflowAnnotationSelectedEvents, RenderPhase.OVERLAY);
rendererModel.registerEventListener(modelListener);
}
/**
* Removes the event hooks and draw decorators from the process renderer.
*/
public void unregisterEventHooks() {
view.removeEventDecorator(processAnnotationEvents, RenderPhase.ANNOTATIONS);
view.removeEventDecorator(operatorAnnotationEvents, RenderPhase.OPERATOR_ANNOTATIONS);
view.removeEventDecorator(workflowAnnotationSelectedEvents, RenderPhase.OVERLAY);
rendererModel.removeEventListener(modelListener);
}
/**
* Updates the hovered annotation.
*
* @param point
* the location of the mouse
* @param process
* the process being hovered
* @param annotations
* the annotations container, can be {@code null}
* @return {@code true} if we are hovering over an annotation; {@code false} otherwise
*/
private boolean updateHoveredStatus(final Point point, final ExecutionUnit process,
final WorkflowAnnotations annotations) {
if (annotations != null) {
// if we have a selected annotation, always check that first for hovering
if (model.getSelected() != null && model.getSelected().getProcess().equals(process)) {
if (model.getSelected().getLocation().contains(point)) {
model.setHovered(model.getSelected(),
AnnotationResizeHelper.getResizeDirectionOrNull(model.getSelected(), point));
return true;
}
}
// non-selected annotations
for (WorkflowAnnotation anno : annotations.getAnnotationsEventOrder()) {
// first one we find is hovered
if (anno.getLocation().contains(point)) {
model.setHovered(anno, AnnotationResizeHelper.getResizeDirectionOrNull(anno, point));
return true;
}
}
}
return false;
}
/**
* Updates the positions of all operator annotations for the given operators.
*
* @param operators
* the operators for which to reposition the annotations
* @return the list of annotations that have actually changed position
*/
private List<WorkflowAnnotation> positionOperatorAnnotations(final Collection<Operator> operators) {
List<WorkflowAnnotation> movedAnnos = new LinkedList<>();
for (Operator op : operators) {
WorkflowAnnotations annotations = rendererModel.getOperatorAnnotations(op);
if (annotations != null) {
Rectangle2D opRect = rendererModel.getOperatorRect(op);
for (WorkflowAnnotation anno : annotations.getAnnotationsDrawOrder()) {
Rectangle2D loc = anno.getLocation();
double annoCenter = loc.getCenterX();
double opCenter = opRect.getCenterX();
double newX = loc.getX() + (opCenter - annoCenter);
double newY = opRect.getMaxY() + OperatorAnnotation.Y_OFFSET;
// move if they really changed
if (loc.getX() != newX || loc.getY() != newY) {
anno.setLocation(new Rectangle2D.Double(newX, newY, loc.getWidth(), loc.getHeight()));
movedAnnos.add(anno);
}
}
}
}
return movedAnnos;
}
}