/** * 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.processeditor; import java.awt.BorderLayout; import java.awt.Component; import java.awt.Dimension; import java.awt.Point; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.FocusEvent; import java.awt.event.FocusListener; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.util.Collections; import java.util.List; import javax.swing.Action; import javax.swing.BorderFactory; import javax.swing.ImageIcon; import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.JScrollPane; import javax.swing.JTree; import javax.swing.SwingUtilities; import javax.swing.Timer; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; import javax.swing.event.TreeSelectionEvent; import javax.swing.event.TreeSelectionListener; import javax.swing.tree.TreePath; import javax.swing.tree.TreeSelectionModel; import com.rapidminer.core.license.ProductConstraintManager; import com.rapidminer.gui.MainFrame; import com.rapidminer.gui.RapidMinerGUI; import com.rapidminer.gui.dnd.AbstractPatchedTransferHandler; import com.rapidminer.gui.dnd.OperatorTransferHandler; import com.rapidminer.gui.flow.processrendering.model.ProcessRendererModel; import com.rapidminer.gui.look.Colors; import com.rapidminer.gui.operatortree.actions.InfoOperatorAction; import com.rapidminer.gui.properties.PropertyPanel; import com.rapidminer.gui.tools.ExtendedJScrollPane; import com.rapidminer.gui.tools.FilterListener; import com.rapidminer.gui.tools.FilterTextField; import com.rapidminer.gui.tools.ResourceAction; import com.rapidminer.gui.tools.SelectionNavigationListener; import com.rapidminer.gui.tools.SwingTools; import com.rapidminer.gui.tools.TextFieldWithAction; import com.rapidminer.gui.tools.components.ToolTipWindow; import com.rapidminer.gui.tools.components.ToolTipWindow.TipProvider; import com.rapidminer.gui.tools.components.ToolTipWindow.TooltipLocation; import com.rapidminer.license.LicenseEvent; import com.rapidminer.license.LicenseEvent.LicenseEventType; import com.rapidminer.license.LicenseManagerListener; import com.rapidminer.operator.Operator; import com.rapidminer.operator.OperatorCreationException; import com.rapidminer.operator.OperatorDescription; import com.rapidminer.tools.GroupTree; import com.rapidminer.tools.I18N; import com.rapidminer.tools.ParameterService; import com.rapidminer.tools.usagestats.ActionStatisticsCollector; /** * This tree displays all groups and can be used to change the selected operators. * * @author Ingo Mierswa, Tobias Malbrecht, Sebastian Land */ public class NewOperatorGroupTree extends JPanel implements FilterListener, SelectionNavigationListener { private static final long serialVersionUID = 133086849304885475L; private final FilterTextField filterField = new FilterTextField(12); private transient final NewOperatorGroupTreeModel model = new NewOperatorGroupTreeModel(); private final JTree operatorGroupTree = new JTree(model); private transient final ResourceAction CLEAR_FILTER_ACTION = new ResourceAction(true, "clear_filter") { private static final long serialVersionUID = 3236281211064051583L; @Override public void actionPerformed(final ActionEvent e) { filterField.clearFilter(); filterField.requestFocusInWindow(); } }; private final ImageIcon CLEAR_FILTER_HOVERED_ICON = SwingTools.createIcon("16/x-mark_orange.png"); public transient final Action INFO_OPERATOR_ACTION = new InfoOperatorAction() { private static final long serialVersionUID = 7157100643209732656L; @Override protected Operator getOperator() { return getSelectedOperator(); } }; private NewOperatorGroupTreeRenderer renderer; /** * * @param editor * NewOperatorEditor is no longer used */ public NewOperatorGroupTree(final NewOperatorEditor editor) { setLayout(new BorderLayout()); operatorGroupTree.setShowsRootHandles(true); renderer = new NewOperatorGroupTreeRenderer(); operatorGroupTree.setCellRenderer(renderer); operatorGroupTree.expandRow(0); // because we let the renderer determine the height operatorGroupTree.setRowHeight(0); JScrollPane scrollPane = new ExtendedJScrollPane(operatorGroupTree); scrollPane.setBorder(BorderFactory.createMatteBorder(1, 0, 0, 0, Colors.TEXTFIELD_BORDER)); add(scrollPane, BorderLayout.CENTER); filterField.setToolTipText(I18N.getMessage(I18N.getGUIBundle(), "gui.field.filter_operators.tip")); filterField.addFilterListener(this); filterField.addSelectionNavigationListener(this); filterField.setDefaultFilterText(I18N.getMessage(I18N.getGUIBundle(), "gui.field.filter_operators.prompt")); filterField.addFocusListener(new FocusListener() { @Override public void focusLost(FocusEvent e) { // log focus lost for operator search statistics String text = filterField.getText(); if (!text.isEmpty()) { logSearchTerm("focus_lost", text); } } @Override public void focusGained(FocusEvent e) { // not needed } }); filterField.getDocument().addDocumentListener(new DocumentListener() { private Timer updateTimer; { updateTimer = new Timer(1000, new ActionListener() { @Override public void actionPerformed(ActionEvent e) { // log timeout for operator search statistics String text = filterField.getText(); if (!text.isEmpty()) { logSearchTerm("timeout", text); } } }); updateTimer.setRepeats(false); } @Override public void removeUpdate(final DocumentEvent e) { updateTimer.restart(); } @Override public void insertUpdate(final DocumentEvent e) { updateTimer.restart(); } @Override public void changedUpdate(final DocumentEvent e) { updateTimer.restart(); } }); JPanel headerBar = new JPanel(new BorderLayout()); TextFieldWithAction tf = new TextFieldWithAction(filterField, CLEAR_FILTER_ACTION, CLEAR_FILTER_HOVERED_ICON) { private static final long serialVersionUID = 1L; @Override public Dimension getPreferredSize() { return new Dimension(super.getPreferredSize().width, PropertyPanel.VALUE_CELL_EDITOR_HEIGHT); } }; headerBar.add(tf, BorderLayout.CENTER); headerBar.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); add(headerBar, BorderLayout.NORTH); operatorGroupTree.setRootVisible(false); operatorGroupTree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION); operatorGroupTree.setDragEnabled(true); operatorGroupTree.addTreeSelectionListener(new TreeSelectionListener() { @Override public void valueChanged(final TreeSelectionEvent e) { Operator op = getSelectedOperator(); if (op != null) { RapidMinerGUI.getMainFrame().getOperatorDocViewer().setDisplayedOperator(op); } } }); operatorGroupTree.setTransferHandler(new OperatorTransferHandler() { private static final long serialVersionUID = 1L; @Override protected List<Operator> getDraggedOperators() { Operator selectedOperator = NewOperatorGroupTree.this.getSelectedOperator(); if (selectedOperator == null) { return Collections.emptyList(); } else { logSearchTerm("inserted", filterField.getText()); return Collections.singletonList(selectedOperator); } } }); operatorGroupTree.addMouseListener(new MouseAdapter() { @Override public void mouseEntered(final MouseEvent e) { // don't do on resize drag if (SwingUtilities.isLeftMouseButton(e)) { return; } ProcessRendererModel modelRenderer = RapidMinerGUI.getMainFrame().getProcessPanel().getProcessRenderer() .getModel(); modelRenderer.setOperatorSourceHovered(true); modelRenderer.fireMiscChanged(); } @Override public void mouseExited(final MouseEvent e) { ProcessRendererModel modelRenderer = RapidMinerGUI.getMainFrame().getProcessPanel().getProcessRenderer() .getModel(); modelRenderer.setOperatorSourceHovered(false); modelRenderer.fireMiscChanged(); } @Override public void mouseClicked(final MouseEvent e) { if (e.getClickCount() == 2) { insertSelected(); } else { TreePath selPath = operatorGroupTree.getPathForLocation(e.getX(), e.getY()); if (selPath != null) { operatorGroupTree.setSelectionPath(selPath); } evaluatePopup(e); } } @Override public void mousePressed(final MouseEvent e) { TreePath selPath = operatorGroupTree.getPathForLocation(e.getX(), e.getY()); if (selPath != null) { operatorGroupTree.setSelectionPath(selPath); } evaluatePopup(e); } @Override public void mouseReleased(final MouseEvent e) { TreePath selPath = operatorGroupTree.getPathForLocation(e.getX(), e.getY()); if (selPath != null) { operatorGroupTree.setSelectionPath(selPath); } evaluatePopup(e); } }); operatorGroupTree.addKeyListener(new KeyListener() { @Override public void keyPressed(final KeyEvent e) {} @Override public void keyReleased(final KeyEvent e) { // insert selected operator upon press of enter or space switch (e.getKeyCode()) { case KeyEvent.VK_ENTER: case KeyEvent.VK_SPACE: if (getSelectedOperator() != null) { insertSelected(); } else { // folder or nothing selected TreePath path = operatorGroupTree.getSelectionPath(); if (path != null) { // folder selected if (operatorGroupTree.isExpanded(path)) { operatorGroupTree.collapsePath(path); } else { operatorGroupTree.expandPath(path); } } } e.consume(); return; } } @Override public void keyTyped(final KeyEvent e) {} }); // we need to know when the license changes because operators may become // supported/unsupported ProductConstraintManager.INSTANCE.registerLicenseManagerListener(new LicenseManagerListener() { @Override public <S, C> void handleLicenseEvent(final LicenseEvent<S, C> event) { if (event.getType() == LicenseEventType.ACTIVE_LICENSE_CHANGED) { operatorGroupTree.repaint(); } } }); new ToolTipWindow(new TipProvider() { @Override public String getTip(final Object o) { OperatorDescription opDesc; if (o instanceof OperatorDescription) { opDesc = (OperatorDescription) o; } else if (o instanceof GroupTree) { GroupTree groupTree = (GroupTree) o; return "<h3>" + groupTree.getName() + "</h3><p>" + groupTree.getDescription() + "</p>"; } else { return null; } StringBuilder b = new StringBuilder(); b.append("<h3>").append(opDesc.getName()).append("</h3><p>"); // b.append(opDesc.getLongDescriptionHTML()).append("</p>"); b.append(opDesc.getShortDescription()).append("</p>"); return b.toString(); } @Override public Object getIdUnder(final Point point) { TreePath path = operatorGroupTree.getPathForLocation((int) point.getX(), (int) point.getY()); if (path != null) { return path.getLastPathComponent(); } else { return null; } } @Override public Component getCustomComponent(final Object id) { return null; } }, operatorGroupTree, TooltipLocation.RIGHT); } @Override public void valueChanged(final String value) { TreePath[] selectionPaths = operatorGroupTree.getSelectionPaths(); List<TreePath> expandedPaths = model.applyFilter(value); for (TreePath expandedPath : expandedPaths) { int row = this.operatorGroupTree.getRowForPath(expandedPath); this.operatorGroupTree.expandRow(row); } GroupTree root = (GroupTree) this.operatorGroupTree.getModel().getRoot(); TreePath path = new TreePath(root); showNodes(root, path, expandedPaths); if (selectionPaths != null) { for (TreePath selectionPath : selectionPaths) { Object lastPathComponent = selectionPath.getLastPathComponent(); if (model.contains(lastPathComponent)) { operatorGroupTree.addSelectionPath(selectionPath); } } } } private void showNodes(final GroupTree tree, TreePath path, List<TreePath> expandedPaths) { if (tree.getSubGroups().size() == 0) { int row = this.operatorGroupTree.getRowForPath(path); this.operatorGroupTree.expandRow(row); } else if (tree.getSubGroups().size() > 0) { int row = this.operatorGroupTree.getRowForPath(path); this.operatorGroupTree.expandRow(row); for (GroupTree child : tree.getSubGroups()) { TreePath childPath = path.pathByAddingChild(child); if (expandedPaths.contains(childPath)) { showNodes(child, childPath, expandedPaths); } } } else { int row = this.operatorGroupTree.getRowForPath(path); this.operatorGroupTree.expandRow(row); } } public boolean shouldAutoConnectNewOperatorsInputs() { return "true".equals(ParameterService.getParameterValue(RapidMinerGUI.PROPERTY_AUTOWIRE_INPUT)); } public boolean shouldAutoConnectNewOperatorsOutputs() { return "true".equals(ParameterService.getParameterValue(RapidMinerGUI.PROPERTY_AUTOWIRE_OUTPUT)); } public JTree getTree() { return this.operatorGroupTree; } /** Creates a new popup menu for the selected operator. */ private JPopupMenu createOperatorPopupMenu() { JPopupMenu menu = new JPopupMenu(); menu.add(this.INFO_OPERATOR_ACTION); menu.addSeparator(); menu.add(new ResourceAction(true, "add_operator_now") { private static final long serialVersionUID = 4363124048356045034L; @Override public void actionPerformed(final ActionEvent e) { insertSelected(); } }); return menu; } /** * Checks if the given mouse event is a popup trigger and creates a new popup menu if necessary. */ private void evaluatePopup(final MouseEvent e) { if (e.isPopupTrigger()) { if (getSelectedOperator() != null) { createOperatorPopupMenu().show(operatorGroupTree, e.getX(), e.getY()); } } } private Operator getSelectedOperator() { if (operatorGroupTree.getSelectionPath() == null) { return null; } Object selectedOperator = operatorGroupTree.getSelectionPath().getLastPathComponent(); if (selectedOperator != null && selectedOperator instanceof OperatorDescription) { try { return ((OperatorDescription) selectedOperator).createOperatorInstance(); } catch (OperatorCreationException e) { e.printStackTrace(); return null; } } else { return null; } } private void insertSelected() { Operator operator = getSelectedOperator(); if (operator == null) { return; } MainFrame mainFrame = RapidMinerGUI.getMainFrame(); mainFrame.getActions().insert(Collections.singletonList(operator)); logSearchTerm("inserted", filterField.getText()); } @Override public void down() { int[] selectionRows = operatorGroupTree.getSelectionRows(); if (selectionRows != null) { if (selectionRows.length > 0 && selectionRows[0] < operatorGroupTree.getRowCount() - 1) { operatorGroupTree.setSelectionRow(selectionRows[0] + 1); } } else { if (operatorGroupTree.getRowCount() > 0) { operatorGroupTree.setSelectionRow(0); } } } @Override public void left() {} @Override public void right() {} @Override public void up() { int[] selectionRows = operatorGroupTree.getSelectionRows(); if (selectionRows != null) { if (selectionRows.length > 0 && selectionRows[0] > 0) { operatorGroupTree.setSelectionRow(selectionRows[0] - 1); } } else { if (operatorGroupTree.getRowCount() > 0) { operatorGroupTree.setSelectionRow(operatorGroupTree.getRowCount()); } } } @Override public void selected() { insertSelected(); } public AbstractPatchedTransferHandler getOperatorTreeTransferhandler() { return (AbstractPatchedTransferHandler) operatorGroupTree.getTransferHandler(); } /** * Logs the searchText under the given event. Shortens the searchText if is longer than 50 * characters. */ private void logSearchTerm(String event, String searchText) { if (searchText.length() > 50) { searchText = searchText.substring(0, 50) + "[...]"; } ActionStatisticsCollector.INSTANCE.log(ActionStatisticsCollector.TYPE_OPERATOR_SEARCH, event, searchText); } public FilterTextField getFilterField() { return filterField; } }