/** * 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; import java.awt.Color; import java.awt.Insets; import java.awt.Point; import java.awt.event.ActionEvent; import java.awt.event.MouseEvent; import java.awt.geom.Ellipse2D; import java.awt.geom.Rectangle2D; import javax.swing.Action; import javax.swing.JMenu; import javax.swing.JMenuItem; import javax.swing.JPopupMenu; import com.rapidminer.gui.actions.ToggleAction; import com.rapidminer.gui.dnd.OperatorTransferHandler; import com.rapidminer.gui.flow.FlowVisualizer; 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.AnnotationAlignment; import com.rapidminer.gui.flow.processrendering.annotations.style.AnnotationColor; import com.rapidminer.gui.flow.processrendering.annotations.style.AnnotationStyle; import com.rapidminer.gui.flow.processrendering.view.ProcessRendererView; import com.rapidminer.gui.tools.ResourceAction; import com.rapidminer.gui.tools.SwingTools; import com.rapidminer.operator.ExecutionUnit; import com.rapidminer.operator.Operator; import com.rapidminer.tools.I18N; /** * This class manages process annotations which can be added/edited in the * {@link ProcessRendererView}. * * @author Marco Boeck * @since 6.4.0 * */ public final class AnnotationsVisualizer { /** margin which removes the space reserved for checkbox icon on menu items */ private static final Insets MENU_ITEM_MARGIN = new Insets(2, -10, 2, 2); /** the process renderer */ private final ProcessRendererView view; private final FlowVisualizer flowVisualizer; /** the event hook and draw decorator */ private final AnnotationsDecorator decorator; /** the model backing the annotation decorator */ private final AnnotationsModel model; /** whether annotations are active or not */ private boolean active; /** action to toggle visibility of all notes */ private final ToggleAction toggleAnnotations = new ToggleAction(true, "workflow.annotation.toggle_visibility") { private static final long serialVersionUID = 1L; @Override public void actionToggled(ActionEvent e) { setActive(isSelected()); view.requestFocusInWindow(); } }; /** action to edit selected note */ private final ResourceAction editAnnotation = new ResourceAction(true, "workflow.annotation.edit") { private static final long serialVersionUID = 1L; @Override public void actionPerformed(ActionEvent e) { if (model.getSelected() != null) { decorator.editSelected(); } } }; /** * Creates the visualizer for {@link WorkflowAnnotation}s. * * @param view * the proces renderer instance * @param flowVisualizer * the flow visualizer instance */ public AnnotationsVisualizer(final ProcessRendererView view, final FlowVisualizer flowVisualizer) { this.view = view; this.model = new AnnotationsModel(view.getModel()); this.decorator = new AnnotationsDecorator(view, this, model); this.flowVisualizer = flowVisualizer; // start annotation decorators decorator.registerEventHooks(); // always show annotations by default toggleAnnotations.actionPerformed(new ActionEvent(view, 0, "")); } /** * Returns the workflow annotation backing model. * * @return the model instance, never {@code null} */ public AnnotationsModel getModel() { return model; } /** * Whether the annotations are active, i.e. are displayed and can be edited. * * @return {@code true} if they are active; {@code false} otherwise */ public boolean isActive() { return active && !flowVisualizer.isActive(); } /** * Sets whether the annotations are active or not. * * @param active * {@code true} if they are active; {@code false} otherwise */ public void setActive(final boolean active) { if (this.active != active) { this.active = active; if (!active) { model.reset(); decorator.reset(); } view.getModel().fireMiscChanged(); } } /** * Deletes the selected {@link WorkflowAnnotation}. Has no effect if no annotation has been * selected. * */ public void deleteSelected() { if (model.getSelected() != null) { model.deleteAnnotation(model.getSelected()); } } /** * Returns the toggle action for workflow annotations. * * @return the action, never {@code null} */ public ToggleAction getToggleAnnotationsAction() { return toggleAnnotations; } /** * Returns the action to edit the currently selected workflow annotation. * * @return the action, never {@code null} */ public ResourceAction getEditAnnotationAction() { return editAnnotation; } /** * Creates an action which can be used to add a new {@link OperatorAnnotation} (if an operator * is selected which does not yet have one) or a {@link ProcessAnnotation} at the top left * corner. * * @param process * the process for which to create the annotation. Can be {@code null} for first * process at action event time * @return the action, never {@code null} */ public ResourceAction makeAddAnnotationAction(final ExecutionUnit process) { ResourceAction addProcessAnnotation = new ResourceAction(true, "workflow.annotation.add") { private static final long serialVersionUID = 1L; @Override public void actionPerformed(ActionEvent e) { // do nothing if flow visualizer is active if (flowVisualizer.isActive()) { return; } // activate annotations if they are not active yet if (!isActive()) { getToggleAnnotationsAction().actionPerformed(null); } ExecutionUnit targetProcess = process; if (process == null) { targetProcess = view.getModel().getProcess(0); } // if we have a valid selected operator and it does not yet have an annotation if (!view.getModel().getSelectedOperators().isEmpty()) { Operator selOp = view.getModel().getSelectedOperators().get(0); if (!selOp.equals(view.getModel().getDisplayedChain())) { if (view.getModel().getOperatorAnnotations(selOp) == null || view.getModel().getOperatorAnnotations(selOp).isEmpty()) { Rectangle2D opRect = view.getModel().getOperatorRect(selOp); int x = (int) opRect.getCenterX() - OperatorAnnotation.DEFAULT_WIDTH / 2; int y = (int) opRect.getMaxY() + OperatorAnnotation.Y_OFFSET; AnnotationStyle style = new AnnotationStyle(AnnotationColor.TRANSPARENT, AnnotationAlignment.CENTER); OperatorAnnotation anno = new OperatorAnnotation( I18N.getGUILabel("workflow.annotation.default_text.label"), style, selOp, false, false, x, y, OperatorAnnotation.DEFAULT_WIDTH, OperatorAnnotation.DEFAULT_HEIGHT); model.addOperatorAnnotation(anno); decorator.editSelected(); return; } else { // the operator has anno so we want to add a process anno to its process targetProcess = selOp.getExecutionUnit(); } } } // not a valid operator selected or it is already annotated, create process anno ProcessAnnotation anno = new ProcessAnnotation(I18N.getGUILabel("workflow.annotation.default_text.label"), new AnnotationStyle(), targetProcess, false, false, new Rectangle2D.Double(ProcessAnnotation.MIN_X, ProcessAnnotation.MIN_Y, ProcessAnnotation.DEFAULT_WIDTH, ProcessAnnotation.DEFAULT_HEIGHT)); model.addProcessAnnotation(anno); decorator.editSelected(); } }; return addProcessAnnotation; } /** * Creates an action which can be used to add a new {@link ProcessAnnotation} at the given * point. * * @param process * the process for which to create the annotation. Can be {@code null} for first * process at action event time * @param origin * the x/y coordinates of the annotation. Can be {@code null} for default location * @return the action, never {@code null} */ public ResourceAction makeAddProcessAnnotationAction(final ExecutionUnit process, final Point origin) { ResourceAction addProcessAnnotation = new ResourceAction(true, "workflow.annotation.add") { private static final long serialVersionUID = 1L; @Override public void actionPerformed(ActionEvent e) { // activate annotations if they are not active yet if (!isActive()) { getToggleAnnotationsAction().actionPerformed(null); } ExecutionUnit targetProcess = process; Point point = origin; if (process == null) { targetProcess = view.getModel().getProcess(0); } if (origin == null) { point = new Point(WorkflowAnnotation.MIN_X, WorkflowAnnotation.MIN_Y); } ProcessAnnotation anno = new ProcessAnnotation(I18N.getGUILabel("workflow.annotation.default_text.label"), new AnnotationStyle(), targetProcess, false, false, new Rectangle2D.Double(point.getX(), point.getY(), ProcessAnnotation.DEFAULT_WIDTH, ProcessAnnotation.DEFAULT_HEIGHT)); model.addProcessAnnotation(anno); decorator.editSelected(); } }; return addProcessAnnotation; } /** * Creates an action which can be used to add a new {@link OperatorAnnotation} to the hovered * operator. * * @param operator * the operator for which to create the annotation * @return the action, never {@code null} */ public ResourceAction makeAddOperatorAnnotationAction(final Operator operator) { if (operator == null) { throw new IllegalArgumentException("operator must not be null!"); } ResourceAction addOperatorAnnotation = new ResourceAction(true, "workflow.annotation.attach") { private static final long serialVersionUID = 1L; @Override public void actionPerformed(ActionEvent e) { // activate annotations if they are not active yet if (!isActive()) { getToggleAnnotationsAction().actionPerformed(null); } Rectangle2D opRect = view.getModel().getOperatorRect(operator); int x = (int) opRect.getCenterX() - OperatorAnnotation.DEFAULT_WIDTH / 2; int y = (int) opRect.getMaxY() + OperatorAnnotation.Y_OFFSET; AnnotationStyle style = new AnnotationStyle(AnnotationColor.TRANSPARENT, AnnotationAlignment.CENTER); OperatorAnnotation anno = new OperatorAnnotation(I18N.getGUILabel("workflow.annotation.default_text.label"), style, operator, false, false, x, y, OperatorAnnotation.DEFAULT_WIDTH, OperatorAnnotation.DEFAULT_HEIGHT); model.addOperatorAnnotation(anno); decorator.editSelected(); } }; return addOperatorAnnotation; } /** * Creates an action which can be used to detach an existing {@link OperatorAnnotation} from the * hovered operator. * * @param operator * the operator for which to detach the annotation * @return the action, never {@code null} */ public ResourceAction makeDetachOperatorAnnotationAction(final Operator operator) { if (operator == null) { throw new IllegalArgumentException("operator must not be null!"); } ResourceAction detachOperatorAnnotation = new ResourceAction(true, "workflow.annotation.detach") { private static final long serialVersionUID = 1L; @Override public void actionPerformed(ActionEvent e) { // activate annotations if they are not active yet if (!isActive()) { getToggleAnnotationsAction().actionPerformed(null); } WorkflowAnnotations annotations = view.getModel().getOperatorAnnotations(operator); if (annotations != null) { for (WorkflowAnnotation anno : annotations.getAnnotationsDrawOrder()) { model.deleteAnnotation(anno); model.addProcessAnnotation(anno.createProcessAnnotation(anno.getProcess())); } } } }; return detachOperatorAnnotation; } /** * Creates an action which can be used to add bring an annotation to the front. * * @param anno * the annotation which should be brought to the front * @return the action, never {@code null} */ public ResourceAction makeToFrontAction(final WorkflowAnnotation anno) { if (anno == null) { throw new IllegalArgumentException("anno must not be null!"); } ResourceAction toFrontAnnotation = new ResourceAction(true, "workflow.annotation.order_to_front") { private static final long serialVersionUID = 1L; @Override public void actionPerformed(ActionEvent e) { model.toFront(anno); } }; return toFrontAnnotation; } /** * Creates an action which can be used to add send an annotation one layer forward. * * @param anno * the annotation which should be sent one layer forward * @return the action, never {@code null} */ public ResourceAction makeSendForwardAction(final WorkflowAnnotation anno) { if (anno == null) { throw new IllegalArgumentException("anno must not be null!"); } ResourceAction sendForwardAnnotation = new ResourceAction(true, "workflow.annotation.order_one_forward") { private static final long serialVersionUID = 1L; @Override public void actionPerformed(ActionEvent e) { model.sendForward(anno); } }; return sendForwardAnnotation; } /** * Creates an action which can be used to add send an annotation to the back. * * @param anno * the annotation which should be sent to the back * @return the action, never {@code null} */ public ResourceAction makeToBackAction(final WorkflowAnnotation anno) { if (anno == null) { throw new IllegalArgumentException("anno must not be null!"); } ResourceAction toBackAnnotation = new ResourceAction(true, "workflow.annotation.order_to_back") { private static final long serialVersionUID = 1L; @Override public void actionPerformed(ActionEvent e) { model.toBack(anno); } }; return toBackAnnotation; } /** * Creates an action which can be used to add send an annotation one layer back. * * @param anno * the annotation which should be sent one layer back * @return the action, never {@code null} */ public ResourceAction makeSendBackAction(final WorkflowAnnotation anno) { if (anno == null) { throw new IllegalArgumentException("anno must not be null!"); } ResourceAction sendBackAnnotation = new ResourceAction(true, "workflow.annotation.order_one_backward") { private static final long serialVersionUID = 1L; @Override public void actionPerformed(ActionEvent e) { model.sendBack(anno); } }; return sendBackAnnotation; } /** * Creates and displays the annotation popup menu if applicable. * * @param e * the mouse event * @return {@code true} if the context menu was shown; {@code false} otherwise */ public boolean showPopupMenu(final MouseEvent e) { if (e == null) { throw new IllegalArgumentException("e must not be null!"); } if (!isActive()) { return false; } if (model.getSelected() == null) { return false; } JPopupMenu menu = new JPopupMenu(); // edit action menu.add(new JMenuItem(getEditAnnotationAction())); // detach action (if applicable) if (model.getSelected() instanceof OperatorAnnotation) { menu.add(new JMenuItem(makeDetachOperatorAnnotationAction(((OperatorAnnotation) model.getSelected()) .getAttachedTo()))); } menu.addSeparator(); OperatorTransferHandler.installMenuItems(menu, true); menu.addSeparator(); // color change menu JMenu colorMenu = new JMenu(I18N.getGUILabel("workflow.annotation.color_select.label")); colorMenu.setIcon(SwingTools.createIcon("16/" + I18N.getGUILabel("workflow.annotation.color_select.icon"))); for (AnnotationColor color : AnnotationColor.values()) { Action action = color.makeColorChangeAction(model, model.getSelected()); JMenuItem item = new JMenuItem(action); Color borderColor = color.getColor(); if (color == AnnotationColor.TRANSPARENT) { borderColor = Color.LIGHT_GRAY; } item.setIcon(SwingTools.createIconFromColor(color.getColor(), borderColor, 16, 16, new Ellipse2D.Double(2, 2, 12, 12))); // this removes the space otherwise reserved for a checkbox item.setMargin(MENU_ITEM_MARGIN); colorMenu.add(item); } menu.add(colorMenu); // alignment change menu JMenu alignmentMenu = new JMenu(I18N.getGUILabel("workflow.annotation.alignment_select.label")); alignmentMenu.setIcon(SwingTools.createIcon("16/" + I18N.getGUILabel("workflow.annotation.alignment_select.icon"))); for (AnnotationAlignment align : AnnotationAlignment.values()) { Action action = align.makeAlignmentChangeAction(model, model.getSelected()); JMenuItem item = new JMenuItem(action); // this removes the space otherwise reserved for a checkbox item.setMargin(MENU_ITEM_MARGIN); alignmentMenu.add(item); } menu.add(alignmentMenu); // order menu if (model.getSelected() instanceof ProcessAnnotation) { JMenu orderMenu = new JMenu(I18N.getGUILabel("workflow.annotation.order_notes.label")); orderMenu.setIcon(SwingTools.createIcon("16/" + I18N.getGUILabel("workflow.annotation.order_notes.icon"))); Action action = makeToFrontAction(model.getSelected()); JMenuItem item = new JMenuItem(action); // this removes the space otherwise reserved for a checkbox item.setMargin(MENU_ITEM_MARGIN); orderMenu.add(item); action = makeToBackAction(model.getSelected()); item = new JMenuItem(action); // this removes the space otherwise reserved for a checkbox item.setMargin(MENU_ITEM_MARGIN); orderMenu.add(item); orderMenu.addSeparator(); action = makeSendForwardAction(model.getSelected()); item = new JMenuItem(action); // this removes the space otherwise reserved for a checkbox item.setMargin(MENU_ITEM_MARGIN); orderMenu.add(item); action = makeSendBackAction(model.getSelected()); item = new JMenuItem(action); // this removes the space otherwise reserved for a checkbox item.setMargin(MENU_ITEM_MARGIN); orderMenu.add(item); menu.add(orderMenu); } menu.addSeparator(); menu.add(getToggleAnnotationsAction().createMenuItem()); menu.show(view, e.getX(), e.getY()); return true; } }