/** * DataCleaner (community edition) * Copyright (C) 2014 Neopost - Customer Information Management * * This copyrighted material is made available to anyone wishing to use, modify, * copy, or redistribute it subject to the terms and conditions of the GNU * Lesser General Public License, as published by the Free Software Foundation. * * 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 Lesser General Public License * for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this distribution; if not, write to: * Free Software Foundation, Inc. * 51 Franklin Street, Fifth Floor * Boston, MA 02110-1301 USA */ package org.datacleaner.widgets.visualization; import java.awt.Point; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.util.List; import java.util.Map; import java.util.Set; import javax.swing.Icon; import javax.swing.JMenu; import javax.swing.JMenuItem; import javax.swing.JPopupMenu; import org.apache.metamodel.schema.Table; import org.datacleaner.actions.DefaultRenameComponentActionListener; import org.datacleaner.actions.PreviewSourceDataActionListener; import org.datacleaner.actions.PreviewTransformedDataActionListener; import org.datacleaner.actions.RemoveComponentMenuItem; import org.datacleaner.actions.RemoveSourceTableMenuItem; import org.datacleaner.api.ComponentSuperCategory; import org.datacleaner.api.OutputDataStream; import org.datacleaner.bootstrap.WindowContext; import org.datacleaner.configuration.DataCleanerConfiguration; import org.datacleaner.connection.Datastore; import org.datacleaner.data.MetaModelInputColumn; import org.datacleaner.descriptors.RemoteTransformerDescriptor; import org.datacleaner.job.HasFilterOutcomes; import org.datacleaner.job.InputColumnSourceJob; import org.datacleaner.job.builder.AnalysisJobBuilder; import org.datacleaner.job.builder.ComponentBuilder; import org.datacleaner.job.builder.TransformerComponentBuilder; import org.datacleaner.metadata.HasMetadataProperties; import org.datacleaner.user.UsageLogger; import org.datacleaner.util.IconUtils; import org.datacleaner.util.ImageManager; import org.datacleaner.util.WidgetFactory; import org.datacleaner.widgets.ChangeRequirementMenu; import org.datacleaner.widgets.DescriptorMenuBuilder; import org.datacleaner.windows.MetadataDialog; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import edu.uci.ics.jung.algorithms.layout.AbstractLayout; import edu.uci.ics.jung.visualization.control.GraphMouseListener; import edu.uci.ics.jung.visualization.picking.PickedState; /** * Listener for mouse events on the {@link JobGraph}. * * Note that this class implements two interfaces and is thus added as two * distinct listener types: {@link MouseListener} and {@link GraphMouseListener} */ public class JobGraphMouseListener extends MouseAdapter implements GraphMouseListener<Object> { private static final Logger logger = LoggerFactory.getLogger(JobGraphMouseListener.class); private final JobGraphContext _graphContext; private final JobGraphLinkPainter _linkPainter; private final JobGraphActions _actions; private final WindowContext _windowContext; private final UsageLogger _usageLogger; // this is ugly, but a hack to make the graph mouse listener and the // regular mouse listener aware of each other's actions. private boolean _clickCaught = false; private Point _pressedPoint; public JobGraphMouseListener(final JobGraphContext graphContext, final JobGraphLinkPainter linkPainter, final JobGraphActions actions, final WindowContext windowContext, final UsageLogger usageLogger) { _graphContext = graphContext; _linkPainter = linkPainter; _actions = actions; _windowContext = windowContext; _usageLogger = usageLogger; } /** * Invoked when a component is double-clicked * * @param componentBuilder * @param me */ public void onComponentDoubleClicked(final ComponentBuilder componentBuilder, final MouseEvent me) { _actions.showConfigurationDialog(componentBuilder); } /** * Invoked when a {@link Table} is double clicked * * @param table * @param me */ public void onTableDoubleClicked(final Table table, final MouseEvent me) { _actions.showTableConfigurationDialog(table); } /** * Invoked when a {@link Table} is right-clicked * * @param table * @param me */ public void onTableRightClicked(final Table table, final MouseEvent me) { final JPopupMenu popup = new JPopupMenu(); popup.add(createLinkMenuItem(table)); final JMenuItem previewMenuItem = new JMenuItem("Preview data", ImageManager.get().getImageIcon(IconUtils.ACTION_PREVIEW, IconUtils.ICON_SIZE_SMALL)); final AnalysisJobBuilder analysisJobBuilder = _graphContext.getAnalysisJobBuilder(table); final Datastore datastore = analysisJobBuilder.getDatastore(); final List<MetaModelInputColumn> inputColumns = analysisJobBuilder.getSourceColumnsOfTable(table); previewMenuItem.addActionListener(new PreviewSourceDataActionListener(_windowContext, datastore, inputColumns)); popup.add(previewMenuItem); popup.addSeparator(); popup.add(new RemoveSourceTableMenuItem(analysisJobBuilder, table)); popup.show(_graphContext.getVisualizationViewer(), me.getX(), me.getY()); } /** * Invoked when a component is right-clicked * * @param componentBuilder * @param me */ public void onComponentRightClicked(final ComponentBuilder componentBuilder, final MouseEvent me) { final boolean isMultiStream = componentBuilder.getDescriptor().isMultiStreamComponent(); final JPopupMenu popup = new JPopupMenu(); final JMenuItem configureComponentMenuItem = new JMenuItem("Configure ...", ImageManager.get().getImageIcon(IconUtils.MENU_OPTIONS, IconUtils.ICON_SIZE_SMALL)); configureComponentMenuItem.addActionListener(e -> _actions.showConfigurationDialog(componentBuilder)); popup.add(configureComponentMenuItem); if (!isMultiStream && componentBuilder instanceof InputColumnSourceJob || componentBuilder instanceof HasFilterOutcomes) { popup.add(createLinkMenuItem(componentBuilder)); } for (final OutputDataStream dataStream : componentBuilder.getOutputDataStreams()) { final JobGraphLinkPainter.VertexContext vertexContext = new JobGraphLinkPainter.VertexContext(componentBuilder, componentBuilder.getOutputDataStreamJobBuilder(dataStream), dataStream); popup.add(createLinkMenuItem(vertexContext)); } final Icon renameIcon = ImageManager.get().getImageIcon(IconUtils.ACTION_RENAME, IconUtils.ICON_SIZE_SMALL); final JMenuItem renameMenuItem = WidgetFactory.createMenuItem("Rename component", renameIcon); renameMenuItem.addActionListener(new DefaultRenameComponentActionListener(componentBuilder, _graphContext)); popup.add(renameMenuItem); if (!isMultiStream && componentBuilder instanceof TransformerComponentBuilder) { final TransformerComponentBuilder<?> tjb = (TransformerComponentBuilder<?>) componentBuilder; final JMenuItem previewMenuItem = new JMenuItem("Preview data", ImageManager.get().getImageIcon(IconUtils.ACTION_PREVIEW, IconUtils.ICON_SIZE_SMALL)); if (tjb.getDescriptor() instanceof RemoteTransformerDescriptor) { previewMenuItem .addActionListener(new PreviewTransformedDataActionListener(_windowContext, null, tjb, 10)); } else { previewMenuItem.addActionListener(new PreviewTransformedDataActionListener(_windowContext, tjb)); } previewMenuItem.setEnabled(componentBuilder.isConfigured()); popup.add(previewMenuItem); } if (ChangeRequirementMenu.isRelevant(componentBuilder)) { popup.add(new ChangeRequirementMenu(componentBuilder)); } popup.addSeparator(); popup.add(new RemoveComponentMenuItem(componentBuilder)); popup.show(_graphContext.getVisualizationViewer(), me.getX(), me.getY()); } private JMenuItem createLinkMenuItem(final Object from) { return createLinkMenuItem( new JobGraphLinkPainter.VertexContext(from, _graphContext.getAnalysisJobBuilder(from), null)); } private JMenuItem createLinkMenuItem(final JobGraphLinkPainter.VertexContext from) { final ImageManager imageManager = ImageManager.get(); final String menuItemText; if (from.getOutputDataStream() == null) { menuItemText = "Link to ..."; } else { menuItemText = "Link \"" + from.getOutputDataStream().getName() + "\" to ..."; } final JMenuItem menuItem = new JMenuItem(menuItemText, imageManager.getImageIcon(IconUtils.ACTION_ADD_DARK, IconUtils.ICON_SIZE_SMALL)); menuItem.addActionListener(e -> _linkPainter.startLink(from)); return menuItem; } /** * Invoked when the canvas is right-clicked * * @param me */ public void onCanvasRightClicked(final MouseEvent me) { _linkPainter.cancelLink(); final JPopupMenu popup = new JPopupMenu(); final Point point = me.getPoint(); final AnalysisJobBuilder analysisJobBuilder = _graphContext.getMainAnalysisJobBuilder(); final DataCleanerConfiguration configuration = analysisJobBuilder.getConfiguration(); // add component options final Set<ComponentSuperCategory> superCategories = configuration.getEnvironment().getDescriptorProvider().getComponentSuperCategories(); for (final ComponentSuperCategory superCategory : superCategories) { final DescriptorMenuBuilder menuBuilder = new DescriptorMenuBuilder(analysisJobBuilder, _usageLogger, superCategory, point); final JMenu menu = new JMenu(superCategory.getName()); menu.setIcon(IconUtils.getComponentSuperCategoryIcon(superCategory, IconUtils.ICON_SIZE_MENU_ITEM)); menuBuilder.addItemsToMenu(menu); popup.add(menu); } // add (datastore and job) metadata option { final JMenuItem menuItem = WidgetFactory.createMenuItem("Job metadata", IconUtils.MODEL_METADATA); menuItem.addActionListener(e -> { final MetadataDialog dialog = new MetadataDialog(_windowContext, analysisJobBuilder); dialog.open(); }); popup.add(menuItem); } popup.show(_graphContext.getVisualizationViewer(), me.getX(), me.getY()); } @Override public void graphReleased(final Object v, final MouseEvent me) { logger.debug("Graph released"); if (isLeftClick(me)) { if (v instanceof ComponentBuilder) { final ComponentBuilder componentBuilder = (ComponentBuilder) v; final boolean ended = _linkPainter.endLink(componentBuilder, me); if (ended) { me.consume(); return; } } if (_pressedPoint != null && _pressedPoint.equals(me.getPoint())) { // avoid updating any coordinates when nothing has been moved return; } final PickedState<Object> pickedVertexState = _graphContext.getVisualizationViewer().getPickedVertexState(); final Object[] selectedObjects = pickedVertexState.getSelectedObjects(); final AbstractLayout<Object, JobGraphLink> graphLayout = _graphContext.getGraphLayout(); // update the coordinates metadata of the moved objects. for (final Object vertex : selectedObjects) { final Double x = graphLayout.getX(vertex); final Double y = graphLayout.getY(vertex); if (vertex instanceof HasMetadataProperties) { final Map<String, String> metadataProperties = ((HasMetadataProperties) vertex).getMetadataProperties(); metadataProperties.put(JobGraphMetadata.METADATA_PROPERTY_COORDINATES_X, "" + x.intValue()); metadataProperties.put(JobGraphMetadata.METADATA_PROPERTY_COORDINATES_Y, "" + y.intValue()); } else if (vertex instanceof Table) { final AnalysisJobBuilder analysisJobBuilder = _graphContext.getAnalysisJobBuilder(vertex); JobGraphMetadata.setPointForTable(analysisJobBuilder, (Table) vertex, x, y); } } if (selectedObjects.length > 0) { _graphContext.getJobGraph().refresh(); } } else if (isRightClick(me)) { if (v instanceof ComponentBuilder) { final ComponentBuilder componentBuilder = (ComponentBuilder) v; onComponentRightClicked(componentBuilder, me); } else if (v instanceof Table) { final Table table = (Table) v; onTableRightClicked(table, me); } } } @Override public void graphPressed(final Object v, final MouseEvent me) { logger.debug("graphPressed({}, {})", v, me); _pressedPoint = me.getPoint(); _clickCaught = false; if (isLeftClick(me)) { if (v instanceof ComponentBuilder) { final ComponentBuilder componentBuilder = (ComponentBuilder) v; if (me.getClickCount() == 2) { _clickCaught = true; onComponentDoubleClicked(componentBuilder, me); } } else if (v instanceof Table) { final Table table = (Table) v; if (me.getClickCount() == 2) { _clickCaught = true; onTableDoubleClicked(table, me); } } } else if (isRightClick(me)) { if (v instanceof ComponentBuilder) { _clickCaught = true; } else if (v instanceof Table) { _clickCaught = true; } } } @Override public void graphClicked(final Object v, final MouseEvent me) { logger.debug("graphClicked({}, {})", v, me); // We do nothing. We show the menu only when the mouse is released } @Override public void mouseReleased(final MouseEvent me) { logger.debug("mouseReleased({}) (clickCaught={})", me, _clickCaught); if (!_clickCaught) { if (isRightClick(me)) { onCanvasRightClicked(me); } } // reset the variable for next time _clickCaught = false; } private boolean isLeftClick(final MouseEvent me) { final int button = me.getButton(); return button == MouseEvent.BUTTON1 && (!me.isMetaDown()); } private boolean isRightClick(final MouseEvent me) { final int button = me.getButton(); if (button == MouseEvent.BUTTON2) { return true; } if (button == MouseEvent.BUTTON3) { return true; } // Mac OS X Cmd + click to achieve right-click if (button == MouseEvent.BUTTON1 && (me.isMetaDown())) { return true; } return false; } }