/** * 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.properties; import java.awt.Color; import java.awt.Cursor; import java.awt.Dimension; import java.awt.FlowLayout; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.Insets; import java.awt.event.ActionEvent; import java.awt.event.ComponentAdapter; import java.awt.event.ComponentEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import javax.swing.Icon; import javax.swing.ImageIcon; import javax.swing.JButton; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JTextArea; import javax.swing.SwingConstants; import javax.swing.SwingUtilities; import com.rapidminer.gui.tools.AttributeGuiTools; import com.rapidminer.gui.tools.ResourceAction; import com.rapidminer.gui.tools.SwingTools; import com.rapidminer.tools.AbstractObservable; import com.rapidminer.tools.I18N; import com.rapidminer.tools.Observable; import com.rapidminer.tools.Observer; import com.rapidminer.tools.Ontology; import com.rapidminer.tools.SystemInfoUtilities; import com.rapidminer.tools.SystemInfoUtilities.OperatingSystem; import com.rapidminer.tools.expression.FunctionDescription; /** * Panel which displays a {@link FunctionDescription}. * * @author Sabrina Kirstein * @since 6.5.0 */ public class FunctionDescriptionPanel extends JPanel { private static final long serialVersionUID = 3290719075570794252L; /** * As the FunctionDescriptionPanel is a Panel, it cannot be an observable. It owns an * {@link Observable}, which informs the observers about click changes. * * @author Sabrina Kirstein */ private class PrivateObservable extends AbstractObservable<FunctionDescription> { @Override public void fireUpdate() { fireUpdate(functionEntry); } } private JLabel lblReturnTypeIcon; private JLabel lblFunctionName; private JButton btShowInfo; private JTextArea textareaInfoText; /** panel which contains the {@link #btShowInfo} button */ private JPanel buttonPanel; /** panel which contains a label and the info of the {@link FunctionDescription} */ private JPanel infoPanel; private Color defaultBackground; private MouseListener hoverMouseListener; private MouseListener dispatchMouseListener; private FunctionDescription functionEntry; private boolean isExpanded = false; private boolean initialized = false; private PrivateObservable observable = new PrivateObservable(); /** dummy text area used to calculate the height of the panel */ private static JTextArea dummyTextArea = new JTextArea(); /** Parameter types that should be highlighted in the function name with parameters */ private static final String[] PARAMETER_TYPES = { "Condition", "Attribute_value", "Nominal", "Numeric", "Integer", "Constant", "Date" }; private static final ImageIcon INFO_ICON = SwingTools.createIcon("13/" + I18N.getGUILabel("function_description.info.icon")); private static final ImageIcon INFO_ICON_HOVERED = SwingTools.createIcon("13/" + I18N.getGUILabel("function_description.info.hovered.icon")); private static final ImageIcon ICON_ATTRIBUTE_VALUE = SwingTools.createIcon("16/question.png"); private static final Color COLOR_LABEL = Color.DARK_GRAY; private static final Color COLOR_HIGHLIGHT = new Color(225, 225, 225); private static final Dimension DIMENSION_LABEL = new Dimension(100, 25); private static final int FIRST_ROW_HEIGHT = 35; private static final int ROW_HEIGHT = 22; private static final String HTML_TAB = " "; /** defines when the width of function names is cropped */ private static int MAX_WIDTH_OF_TEXT = 600; /** * Creates a panel for the given {@link FunctionDescription}. When the panel is expanded, the * extra information is shown. * * @param functionEntry */ public FunctionDescriptionPanel(FunctionDescription functionEntry) { this.functionEntry = functionEntry; initGUI(); if (functionEntry != null) { updateFunctionEntry(functionEntry); showMoreInformation(isExpanded); } else { showMoreInformation(false); } registerMouseListener(); addComponentListener(new ComponentAdapter() { @Override public void componentResized(ComponentEvent arg0) { updateHeight(); } }); initialized = true; } /** * Register an observer to react on click events */ public void registerObserver(Observer<FunctionDescription> observer) { observable.addObserver(observer, false); } public static void updateMaximalWidth(int maxWidth) { MAX_WIDTH_OF_TEXT = maxWidth - 15; } /** * initializes the graphical user interface */ private void initGUI() { isExpanded = false; setDefaultBackground(getBackground()); setLayout(new GridBagLayout()); GridBagConstraints gbc = new GridBagConstraints(); gbc.gridx = 0; gbc.weighty = 0.3; gbc.gridy = 0; gbc.insets = new Insets(8, 7, 7, 7); gbc.anchor = GridBagConstraints.WEST; lblReturnTypeIcon = new JLabel(""); add(lblReturnTypeIcon, gbc); gbc.weightx = 1; gbc.insets = new Insets(7, 0, 7, 7); gbc.gridx += 1; lblFunctionName = new JLabel(""); lblFunctionName.setAlignmentX(LEFT_ALIGNMENT); gbc.fill = GridBagConstraints.HORIZONTAL; add(lblFunctionName, gbc); // Button panel gbc.gridx += 1; gbc.weightx = 0; gbc.anchor = GridBagConstraints.EAST; buttonPanel = new JPanel(); buttonPanel.setLayout(new FlowLayout(FlowLayout.RIGHT, 0, 0)); buttonPanel.setOpaque(false); btShowInfo = new JButton(new ResourceAction(true, "function_description.more_information") { private static final long serialVersionUID = 1L; @Override public void actionPerformed(ActionEvent e) { toggleMoreInformation(); } }); btShowInfo.setIcon(INFO_ICON); btShowInfo.setContentAreaFilled(false); btShowInfo.setBorderPainted(false); btShowInfo.addMouseListener(new MouseAdapter() { @Override public void mouseExited(MouseEvent e) { highlightInfoButton(false); } @Override public void mouseEntered(MouseEvent e) { highlightInfoButton(true); } }); buttonPanel.add(btShowInfo); add(btShowInfo, gbc); setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); // Info panel infoPanel = new JPanel(); infoPanel.setOpaque(false); infoPanel.setLayout(new GridBagLayout()); textareaInfoText = new JTextArea() { private static final long serialVersionUID = 1L; @Override public Dimension getMinimumSize() { return FunctionDescriptionPanel.DIMENSION_LABEL; } @Override public Dimension getPreferredSize() { return getMinimumSize(); }; }; textareaInfoText.setForeground(COLOR_LABEL); textareaInfoText.setAlignmentX(SwingConstants.LEFT); textareaInfoText.setBackground(getBackground()); textareaInfoText.setEditable(false); textareaInfoText.setBorder(null); textareaInfoText.setLineWrap(true); textareaInfoText.setWrapStyleWord(true); textareaInfoText.setFocusable(false); dummyTextArea.setBorder(null); dummyTextArea.setAlignmentX(SwingConstants.LEFT); dummyTextArea.setEditable(false); dummyTextArea.setLineWrap(true); dummyTextArea.setWrapStyleWord(true); gbc.gridx = 0; gbc.gridy = 0; gbc.gridwidth = 3; gbc.fill = GridBagConstraints.BOTH; gbc.anchor = GridBagConstraints.NORTHWEST; gbc.weightx = 1; gbc.weighty = 1; gbc.insets = new Insets(0, 7, 7, 7); infoPanel.add(textareaInfoText, gbc); gbc.gridy += 1; gbc.weighty = 0.7; add(infoPanel, gbc); addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { // add the input name to the expression observable.fireUpdate(); } @Override public void mouseExited(MouseEvent e) { highlightFunctionName(false); } @Override public void mouseEntered(MouseEvent e) { highlightFunctionName(true); } }); updateHeight(); } /** * registers the mouse listener to show more information and to enable row highlighting */ private void registerMouseListener() { this.addMouseListener(createOrGetHoverMouseListener()); textareaInfoText.addMouseListener(createOrGetDispatchMouseListener()); lblReturnTypeIcon.addMouseListener(createOrGetDispatchMouseListener()); lblFunctionName.addMouseListener(createOrGetDispatchMouseListener()); infoPanel.addMouseListener(createOrGetDispatchMouseListener()); } private void setDefaultBackground(Color color) { defaultBackground = color; } /** * updates the displayed {@link FunctionDescription} * * @param functionEntry */ private void updateFunctionEntry(final FunctionDescription functionEntry) { if (functionEntry == null) { return; } // the tt tags are a hack to ensure that the label stays on the same location when the input // panel is toggled. Do not delete them! String croppedText = "<html>" + SwingTools .getStrippedJComponentText(this, HTML_TAB + HTML_TAB + HTML_TAB + functionEntry.getFunctionNameWithParameters(), MAX_WIDTH_OF_TEXT, 0) + "<tt> </tt></html>"; for (String parameterType : PARAMETER_TYPES) { croppedText = croppedText.replaceAll(parameterType, "<tt>" + parameterType + "</tt>"); } lblFunctionName.setText(croppedText); String helpText = "<html><b>" + functionEntry.getHelpTextName() + "</b>: " + (functionEntry.getFunctionNameWithParameters() != null ? functionEntry.getFunctionNameWithParameters() : functionEntry.getDisplayName()) + "</html>"; lblFunctionName.setToolTipText(helpText); Icon icon = null; if (functionEntry.getReturnType() == Ontology.ATTRIBUTE_VALUE) { icon = ICON_ATTRIBUTE_VALUE; } else { icon = AttributeGuiTools.getIconForValueType(functionEntry.getReturnType(), true); } lblReturnTypeIcon.setIcon(icon); lblReturnTypeIcon.setToolTipText(Ontology.ATTRIBUTE_VALUE_TYPE.mapIndexToDisplayName(functionEntry.getReturnType())); String infoText = getFunctionInfo(); textareaInfoText.setText(infoText); textareaInfoText.setToolTipText(infoText); infoPanel.setVisible(isExpanded); setVisible(true); updateHeight(); } /** * shows the info text in the {@link FunctionDescriptionPanel} if <code>show</code> is true * * @param show */ private void showMoreInformation(boolean show) { infoPanel.setVisible(show); updateHeight(); } /** * Toggles the visibility of the info text in the {@link FunctionDescriptionPanel} */ private void toggleMoreInformation() { isExpanded = !isExpanded; showMoreInformation(isExpanded); } /** * Creates the {@link MouseListener} which toggles the advanced information. If it is already * created this will return the current instance. * * @return */ private MouseListener createOrGetHoverMouseListener() { if (hoverMouseListener == null) { hoverMouseListener = new MouseAdapter() { @Override public void mouseExited(MouseEvent e) { if (!SwingTools.isMouseEventExitedToChildComponents(FunctionDescriptionPanel.this, e)) { highlight(false); } } @Override public void mouseEntered(MouseEvent e) { highlight(true); } }; } return hoverMouseListener; } /** * Creates the {@link MouseListener} which delivers {@link MouseEvent}s to the * {@link FunctionDescriptionPanel}. Some GUI elements, like {@link JLabel} with Tooltips, may * consume all events and does not inform the parent component. * * @return */ private MouseListener createOrGetDispatchMouseListener() { if (dispatchMouseListener == null) { dispatchMouseListener = new MouseListener() { @Override public void mouseClicked(MouseEvent e) { FunctionDescriptionPanel.this.dispatchEvent(SwingUtilities.convertMouseEvent(e.getComponent(), e, FunctionDescriptionPanel.this)); } @Override public void mousePressed(MouseEvent e) { FunctionDescriptionPanel.this.dispatchEvent(SwingUtilities.convertMouseEvent(e.getComponent(), e, FunctionDescriptionPanel.this)); } @Override public void mouseReleased(MouseEvent e) { FunctionDescriptionPanel.this.dispatchEvent(SwingUtilities.convertMouseEvent(e.getComponent(), e, FunctionDescriptionPanel.this)); } @Override public void mouseEntered(MouseEvent e) { FunctionDescriptionPanel.this.dispatchEvent(SwingUtilities.convertMouseEvent(e.getComponent(), e, FunctionDescriptionPanel.this)); } @Override public void mouseExited(MouseEvent e) { FunctionDescriptionPanel.this.dispatchEvent(SwingUtilities.convertMouseEvent(e.getComponent(), e, FunctionDescriptionPanel.this)); } }; } return dispatchMouseListener; } /** * Highlight the info button and also the containing {@link FunctionDescriptionPanel}. * * @param highlight */ private void highlightInfoButton(boolean highlight) { if (highlight) { btShowInfo.setIcon(INFO_ICON_HOVERED); } else { btShowInfo.setIcon(INFO_ICON); } highlight(highlight); } /** * Highlight the {@link #lblFunctionName}. * * @param highlight */ private void highlightFunctionName(boolean highlight) { if (highlight) { lblFunctionName.setForeground(SwingTools.RAPIDMINER_ORANGE); } else { lblFunctionName.setForeground(Color.BLACK); } highlight(highlight); } /** * Highlight the {@link FunctionDescriptionPanel}. * * @param highlight */ private void highlight(boolean highlight) { if (highlight) { setBackground(COLOR_HIGHLIGHT); textareaInfoText.setBackground(COLOR_HIGHLIGHT); } else { if (defaultBackground != null) { setBackground(defaultBackground); textareaInfoText.setBackground(defaultBackground); } } } /** * Updates the max and the min height of the {@link FunctionDescriptionPanel} */ private void updateHeight() { int totalHeight = FIRST_ROW_HEIGHT; if (isVisible() && textareaInfoText.getText() != null && !textareaInfoText.getText().isEmpty() && functionEntry != null && isExpanded && initialized) { double numberOfLines = Math.ceil(getContentHeight(textareaInfoText.getText(), getWidth()) / 16.0); totalHeight += numberOfLines * ROW_HEIGHT; // add magic number 7 for one line descriptions if (numberOfLines == 1) { totalHeight += 7; } } infoPanel.setMinimumSize(new Dimension(getPreferredSize().width, totalHeight - FIRST_ROW_HEIGHT)); infoPanel.setPreferredSize(new Dimension(getPreferredSize().width, totalHeight - FIRST_ROW_HEIGHT)); infoPanel.setMaximumSize(new Dimension(getMaximumSize().width, totalHeight - FIRST_ROW_HEIGHT)); setMinimumSize(new Dimension(getPreferredSize().width, totalHeight)); setPreferredSize(new Dimension(getPreferredSize().width, totalHeight)); setMaximumSize(new Dimension(getMaximumSize().width, totalHeight)); } /** * Gives the additional function information * * @return function information */ private String getFunctionInfo() { return functionEntry.getDescription(); } /** * Calculates the preferred height of an text area with the given fixed width for the specified * string. * * @param info * the description of the {@link FunctionDescription} * @param width * the width of the content * @return the preferred height given the comment */ private static int getContentHeight(final String info, final int width) { if (info == null) { throw new IllegalArgumentException("info must not be null!"); } dummyTextArea.setText(info); dummyTextArea.setSize(width, Short.MAX_VALUE); // height is not exact. Multiply by magic number to get a more fitting value... if (SystemInfoUtilities.getOperatingSystem() == OperatingSystem.OSX || SystemInfoUtilities.getOperatingSystem() == OperatingSystem.UNIX || SystemInfoUtilities.getOperatingSystem() == OperatingSystem.SOLARIS) { return (int) (dummyTextArea.getPreferredSize().getHeight() * 1.05f); } else { return (int) dummyTextArea.getPreferredSize().getHeight(); } } }