Java tutorial
/** * Copyright (C) 2013, University of Manchester and University of Southampton * * Licensed under the GNU Lesser General Public License v2.1 * See the "LICENSE" file that is distributed with the source code for license terms. */ package uk.ac.soton.mib104.t2.workbench.ui; import java.awt.BasicStroke; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; import java.awt.Dimension; import java.awt.FlowLayout; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Rectangle; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.FocusAdapter; import java.awt.event.FocusEvent; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.util.Collections; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.TreeMap; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.swing.AbstractAction; import javax.swing.AbstractButton; import javax.swing.BorderFactory; import javax.swing.Icon; import javax.swing.JButton; import javax.swing.JComponent; import javax.swing.JLabel; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JTabbedPane; import javax.swing.JTextField; import javax.swing.KeyStroke; import javax.swing.SwingUtilities; import javax.swing.border.Border; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import javax.swing.plaf.basic.BasicButtonUI; import net.sf.taverna.t2.workbench.icons.WorkbenchIcons; import net.sf.taverna.t2.workbench.ui.views.contextualviews.activity.ActivityConfigurationPanel; import net.sf.taverna.t2.workflowmodel.processor.activity.Activity; import org.apache.commons.lang.ObjectUtils; /** * Activity configuration panel (with input port and output port components.) * * @author Mark Borkum * @version 0.0.1-SNAPSHOT * * @param <ACTIVITY> the type of the activity * @param <CONFIG> the type of the configuration bean for the activity * @param <IN_CONFIG> the type of the configuration bean for input ports * @param <OUT_CONFIG> the type of the configuration bean for output ports * @param <IN_COMPONENT> the type of the component for input ports * @param <OUT_COMPONENT> the type of the component for output ports */ public abstract class ActivityConfigurationPanelWithInputPortAndOutputPortComponents<ACTIVITY extends Activity<CONFIG>, CONFIG, IN_CONFIG, OUT_CONFIG, IN_COMPONENT extends Component, OUT_COMPONENT extends Component> extends ActivityConfigurationPanel<ACTIVITY, CONFIG> { private static abstract class AbstractJTabbedPaneWithTabTitleAdapterAndButtonTabComponent extends JTabbedPane { /** * * @link http://docs.oracle.com/javase/tutorial/uiswing/examples/components/TabComponentsDemoProject/src/components/ButtonTabComponent.java */ private static final class ButtonTabComponent extends JPanel { private class TabButton extends JButton implements ActionListener { private static final int delta = 6; private static final long serialVersionUID = 5702371651363313007L; private static final int size = 17; public TabButton() { this.setPreferredSize(new Dimension(size, size)); this.setToolTipText("close this tab"); this.setUI(new BasicButtonUI()); this.setContentAreaFilled(false); this.setFocusable(false); this.setBorder(BorderFactory.createEtchedBorder()); this.setBorderPainted(false); this.addMouseListener(buttonMouseListener); this.setRolloverEnabled(true); this.addActionListener(this); } @Override public void actionPerformed(final ActionEvent e) { final int index = tabbedPane.indexOfTabComponent(ButtonTabComponent.this); if (index != -1) { final String title = tabbedPane.getTitleAt(index); switch (JOptionPane.showConfirmDialog(SwingUtilities.getWindowAncestor(tabbedPane), String.format( "<html><body width=\"380\">You have configured the tab \"%s\". If you close it, your changes will be lost. Do you want to close it anyway?</body></html>", title), "Are you sure you want to close this tab?", JOptionPane.YES_NO_CANCEL_OPTION)) { case JOptionPane.YES_OPTION: break; default: return; } tabbedPane.removeTabAt(index); } } @Override protected void paintComponent(final Graphics g) { super.paintComponent(g); final Graphics2D g2 = (Graphics2D) g.create(); if (this.getModel().isPressed()) { g2.translate(1, 1); } g2.setStroke(new BasicStroke(2)); g2.setColor(Color.BLACK); if (this.getModel().isRollover()) { g2.setColor(Color.RED); } g2.drawLine(delta, delta, getWidth() - delta - 1, getHeight() - delta - 1); g2.drawLine(getWidth() - delta - 1, delta, delta, getHeight() - delta - 1); g2.dispose(); } @Override public void updateUI() { return; } } private final static MouseListener buttonMouseListener = new MouseAdapter() { @Override public void mouseEntered(final MouseEvent e) { final Component component = e.getComponent(); if (component instanceof AbstractButton) { final AbstractButton button = (AbstractButton) component; button.setBorderPainted(true); } } @Override public void mouseExited(final MouseEvent e) { final Component component = e.getComponent(); if (component instanceof AbstractButton) { final AbstractButton button = (AbstractButton) component; button.setBorderPainted(false); } } }; private static final Border defaultButtonBorder = BorderFactory.createEmptyBorder(2, 0, 0, 0); private static final Border defaultLabelBorder = BorderFactory.createEmptyBorder(0, 7, 0, 5); private static final long serialVersionUID = -2426393757304130401L; private final JTabbedPane tabbedPane; public ButtonTabComponent(final JTabbedPane tabbedPane) { super(new FlowLayout(FlowLayout.LEFT, 0, 0)); if (tabbedPane == null) { throw new IllegalArgumentException(new NullPointerException("tabbedPane")); } this.tabbedPane = tabbedPane; this.setBorder(defaultButtonBorder); this.setOpaque(false); final JLabel label = new JLabel() { private static final long serialVersionUID = 7209206022355197846L; @Override public String getText() { final int index = tabbedPane.indexOfTabComponent(ButtonTabComponent.this); if (index != -1) { return tabbedPane.getTitleAt(index); } return null; } @Override public void setIcon(final Icon icon) { if (icon != null) { throw new UnsupportedOperationException(); } } }; label.setBorder(defaultLabelBorder); this.add(label); this.add(new TabButton()); } } /** * * @link http://java-swing-tips.googlecode.com/svn/trunk/TabTitleEditor/src/java/example/MainPanel.java */ private static final class TabTitleAdapter extends MouseAdapter implements ChangeListener { private Component currentComponent = null; private Dimension currentDimension = null; private int currentIndex = -1; private int currentLength = -1; private final JTextField editor = new JTextField(); private final AbstractJTabbedPaneWithTabTitleAdapterAndButtonTabComponent tabbedPane; public TabTitleAdapter(final AbstractJTabbedPaneWithTabTitleAdapterAndButtonTabComponent tabbedPane) { super(); if (tabbedPane == null) { throw new IllegalArgumentException(new NullPointerException("tabbedPane")); } this.tabbedPane = tabbedPane; tabbedPane.addChangeListener(this); tabbedPane.addMouseListener(this); editor.setBorder(BorderFactory.createEmptyBorder()); editor.addFocusListener(new FocusAdapter() { @Override public void focusLost(final FocusEvent e) { // renameTabTitle(); cancelEditing(); } }); editor.addKeyListener(new KeyAdapter() { @Override public void keyPressed(final KeyEvent e) { switch (e.getKeyCode()) { case KeyEvent.VK_ENTER: renameTabTitle(); break; case KeyEvent.VK_ESCAPE: cancelEditing(); break; default: editor.setPreferredSize( (editor.getText().length() > currentLength) ? null : currentDimension); tabbedPane.revalidate(); tabbedPane.repaint(); } } }); tabbedPane.getInputMap(JComponent.WHEN_FOCUSED).put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "start-editing"); tabbedPane.getActionMap().put("start-editing", new AbstractAction() { private static final long serialVersionUID = 4657327152920108200L; @Override public void actionPerformed(final ActionEvent e) { startEditing(); } }); } private void cancelEditing() { if (currentIndex == -1) { return; } tabbedPane.setTabComponentAt(currentIndex, currentComponent); currentIndex = -1; currentLength = -1; currentComponent = null; editor.setVisible(false); editor.setPreferredSize(null); tabbedPane.requestFocusInWindow(); } @Override public void mouseClicked(final MouseEvent e) { final Rectangle r = tabbedPane.getUI().getTabBounds(tabbedPane, tabbedPane.getSelectedIndex()); if ((r != null) && r.contains(e.getPoint()) && e.getClickCount() == 2) { startEditing(); } else { // renameTabTitle(); cancelEditing(); } } private void renameTabTitle() { final String newTitle = editor.getText().trim(); if (currentIndex >= 0) { if (tabbedPane.isTabTitleValid(newTitle)) { for (int index = 0, length = tabbedPane.getTabCount(); index < length; index++) { if ((index != currentIndex) && tabbedPane.getTitleAt(index).equals(newTitle)) { cancelEditing(); JOptionPane.showMessageDialog(SwingUtilities.getWindowAncestor(tabbedPane), String.format("Duplicate name: %s", newTitle), "Unable to rename tab", JOptionPane.ERROR_MESSAGE); return; } } tabbedPane.setTitleAt(currentIndex, newTitle); } else { cancelEditing(); JOptionPane.showMessageDialog(SwingUtilities.getWindowAncestor(tabbedPane), String.format("Invalid name: %s", newTitle), "Unable to rename tab", JOptionPane.ERROR_MESSAGE); return; } } cancelEditing(); } private void startEditing() { currentIndex = tabbedPane.getSelectedIndex(); currentComponent = tabbedPane.getTabComponentAt(currentIndex); tabbedPane.setTabComponentAt(currentIndex, editor); editor.setVisible(true); editor.setText(tabbedPane.getTitleAt(currentIndex)); editor.selectAll(); editor.requestFocusInWindow(); currentLength = editor.getText().length(); currentDimension = editor.getPreferredSize(); editor.setMinimumSize(currentDimension); } @Override public void stateChanged(final ChangeEvent e) { // renameTabTitle(); cancelEditing(); } } private static final long serialVersionUID = -7092855656003233391L; public AbstractJTabbedPaneWithTabTitleAdapterAndButtonTabComponent() { super(); new TabTitleAdapter(this); } @Override public void addTab(final String title, final Component component) { this.addTab(title, null, component); } @Override public void addTab(final String title, final Icon icon, final Component component) { super.addTab(title, icon, component, "double-click to rename this tab (save changes by pressing ENTER key)"); final int index = this.indexOfComponent(component); if (index != -1) { this.setTabComponentAt(index, new ButtonTabComponent(this)); this.tabAdded(index, title, component); } } @Override public void addTab(final String title, final Icon icon, final Component component, final String tip) { throw new UnsupportedOperationException(); } protected boolean isTabTitleValid(final String title) { return true; } @Override public void removeTabAt(final int index) throws IndexOutOfBoundsException { final String title = this.getTitleAt(index); final Component component = this.getComponentAt(index); super.removeTabAt(index); this.tabRemoved(index, title, component); } @Override public void setTitleAt(final int index, final String newTitle) throws IndexOutOfBoundsException { final String oldTitle = this.getTitleAt(index); final Component component = this.getComponentAt(index); super.setTitleAt(index, newTitle); if (!oldTitle.equals(newTitle)) { this.tabTitleChanged(index, oldTitle, newTitle, component); } } protected abstract void tabAdded(int index, String title, final Component component); protected abstract void tabRemoved(int index, String title, final Component component); protected abstract void tabTitleChanged(int index, String oldTitle, String newTitle, final Component component); } private static final Icon defaultAddInputPortButtonIcon = null; private static final String defaultAddInputPortButtonText = "Add input port"; private static final String defaultAddInputPortButtonTip = null; private static final Icon defaultAddOutputPortButtonIcon = null; private static final String defaultAddOutputPortButtonText = "Add output port"; private static final String defaultAddOutputPortButtonTip = null; private static final String defaultEmptyInputPortsText = "No input ports"; private static final String defaultEmptyOutputPortsText = "No output ports"; private static final String defaultInputPortName = "in"; private static final Icon defaultInputPortsTabIcon = WorkbenchIcons.inputPortIcon; private static final String defaultInputPortsTabText = "Input ports"; private static final String defaultInputPortsTabTip = null; private static final String defaultOutputPortName = "out"; private static final Icon defaultOutputPortsTabIcon = WorkbenchIcons.outputPortIcon; private static final String defaultOutputPortsTabText = "Output ports"; private static final String defaultOutputPortsTabTip = null; private static final long serialVersionUID = -3693674617612423237L; /** * Returns the next port name with the specified <code>prefix</code>, given the set of existing <code>portNames</code>. * * @param prefix The prefix. * @param portNames The set of existing port names. * @return The next port name. * @throws IllegalArgumentException If <code>prefix == null</code>. */ private static final String nextPortName(final String prefix, final Set<String> portNames) throws IllegalArgumentException { if (prefix == null) { throw new IllegalArgumentException(new NullPointerException("portName")); } final Set<Integer> indices = new HashSet<Integer>(); if (portNames != null) { // Compile a regular expression for the prefix followed by an integer. final Pattern pattern = Pattern.compile(String.format("^\\s*?%s(\\d+)\\s*?$", Pattern.quote(prefix))); for (final String nextPortName : portNames) { final Matcher matcher = pattern.matcher(nextPortName); if (matcher.matches()) { // If the regular expression matches the 'nextPortName', then add the integer (capture group 1) to the set. indices.add(Integer.parseInt(matcher.group(1))); } } } // The next index is the successor of the maximum index. final int nextIndex = 1 + (indices.isEmpty() ? 0 : Collections.max(indices)); return String.format("%s%d", prefix, nextIndex); } private final ACTIVITY activity; private CONFIG configBean; /** * The label that is displayed when there are no input ports. */ private final JLabel emptyInputPortsLabel = new JLabel(); /** * The label that is displayed when there are no output ports. */ private final JLabel emptyOutputPortsLabel = new JLabel(); /** * The panel that contains either the tabbed pane of input ports, or the empty label. */ private final JPanel inputPortsPane = new JPanel(); /** * The tabbed pane for input ports. */ private final JTabbedPane inputPortsTabbedPane = new AbstractJTabbedPaneWithTabTitleAdapterAndButtonTabComponent() { private static final long serialVersionUID = 3161996013853282097L; @Override protected boolean isTabTitleValid(final String title) { return ActivityConfigurationPanelWithInputPortAndOutputPortComponents.this.isPortName(title); } @Override protected void tabAdded(final int index, final String title, final Component component) { // If the count is unity, then we have just added the first tab. Hence, // we need to swap the contents of the pane, re-validate and re-paint. if (this.getTabCount() == 1) { inputPortsPane.remove(emptyInputPortsLabel); inputPortsPane.add(this, BorderLayout.CENTER); inputPortsPane.revalidate(); inputPortsPane.repaint(); } // If this component is not refreshing, then select the new tab. if (!refreshing) { this.setSelectedIndex(index); } @SuppressWarnings("unchecked") final IN_COMPONENT inputPortComponent = (IN_COMPONENT) component; ActivityConfigurationPanelWithInputPortAndOutputPortComponents.this.inputPortAdded(title, inputPortComponent); } @Override protected void tabRemoved(final int index, final String title, final Component component) { // If the count is zero, then we have just removed the last tab. Hence, // we need to swap the contents of the pane, re-validate and re-paint. if (this.getTabCount() == 0) { inputPortsPane.remove(this); inputPortsPane.add(emptyInputPortsLabel, BorderLayout.CENTER); inputPortsPane.revalidate(); inputPortsPane.repaint(); } @SuppressWarnings("unchecked") final IN_COMPONENT inputPortComponent = (IN_COMPONENT) component; ActivityConfigurationPanelWithInputPortAndOutputPortComponents.this.inputPortRemoved(title, inputPortComponent); } @Override protected void tabTitleChanged(final int index, final String oldTitle, final String newTitle, final Component component) { @SuppressWarnings("unchecked") final IN_COMPONENT inputPortComponent = (IN_COMPONENT) component; ActivityConfigurationPanelWithInputPortAndOutputPortComponents.this.inputPortNameChanged(oldTitle, newTitle, inputPortComponent); } }; /** * The panel that contains either the tabbed pane of output ports, or the empty label. */ private final JPanel outputPortsPane = new JPanel(); /** * The tabbed pane for output ports. */ private final JTabbedPane outputPortsTabbedPane = new AbstractJTabbedPaneWithTabTitleAdapterAndButtonTabComponent() { private static final long serialVersionUID = -7885377287285294474L; @Override protected boolean isTabTitleValid(final String title) { return ActivityConfigurationPanelWithInputPortAndOutputPortComponents.this.isPortName(title); } @Override protected void tabAdded(final int index, final String title, final Component component) { // If the count is unity, then we have just added the first tab. Hence, // we need to swap the contents of the pane, re-validate and re-paint. if (this.getTabCount() == 1) { outputPortsPane.remove(emptyOutputPortsLabel); outputPortsPane.add(this, BorderLayout.CENTER); outputPortsPane.revalidate(); outputPortsPane.repaint(); } // If this component is not refreshing, then select the new tab. if (!refreshing) { this.setSelectedIndex(index); } @SuppressWarnings("unchecked") final OUT_COMPONENT outputPortComponent = (OUT_COMPONENT) component; ActivityConfigurationPanelWithInputPortAndOutputPortComponents.this.outputPortAdded(title, outputPortComponent); } @Override protected void tabRemoved(final int index, final String title, final Component component) { // If the count is zero, then we have just removed the last tab. Hence, // we need to swap the contents of the pane, re-validate and re-paint. if (this.getTabCount() == 0) { outputPortsPane.remove(this); outputPortsPane.add(emptyOutputPortsLabel, BorderLayout.CENTER); outputPortsPane.revalidate(); outputPortsPane.repaint(); } @SuppressWarnings("unchecked") final OUT_COMPONENT outputPortComponent = (OUT_COMPONENT) component; ActivityConfigurationPanelWithInputPortAndOutputPortComponents.this.outputPortRemoved(title, outputPortComponent); } @Override protected void tabTitleChanged(final int index, final String oldTitle, final String newTitle, final Component component) { @SuppressWarnings("unchecked") final OUT_COMPONENT outputPortComponent = (OUT_COMPONENT) component; ActivityConfigurationPanelWithInputPortAndOutputPortComponents.this.outputPortNameChanged(oldTitle, newTitle, outputPortComponent); } }; /** * Mutex variable that is used to synchronize "refreshConfiguration" method. * <p> * This class is a user-interface component. Hence, it is not safe to synchronize * on "this" instance, as it could block the thread[s] for the renderer. Instead, * we synchronize on a unique object. */ private final Object refreshConfigurationLock = new Object(); /** * Flag that indicates whether or not this component is refreshing, i.e., the refreshConfiguration method is being executed. * <p> * This attribute is declared as "volatile" to ensure that it is accessed by a single thread at a time. */ private volatile boolean refreshing = false; /** * The "master" tabbed pane for this component. */ private final JTabbedPane tabbedPane = new JTabbedPane(); /** * Sole constructor. * * @param activity The activity. * @throws IllegalArgumentException If <code>activity == null</code>. */ public ActivityConfigurationPanelWithInputPortAndOutputPortComponents(final ACTIVITY activity) throws IllegalArgumentException { super(); if (activity == null) { throw new IllegalArgumentException(new NullPointerException("activity")); } this.activity = activity; // Call the "removeAll" method before the "initGui" method to ensure that this // panel contains no components before it is initialized. this.removeAll(); this.initGui(); // Call the "refreshConfiguration" method after the "initGui" method to ensure // that this panel is initialized before the configuration bean is refreshed // for the first time. this.refreshConfiguration(); } /** * Adds a <code>component</code> represented by a <code>title</code> and no icon. * * @param title the title to be displayed in this tab * @param component the component to be displayed when this tab is clicked */ public final void addTab(final String title, final Component component) { this.addTab(title, null, component); } /** * Adds a <code>component</code> represented by a <code>title</code> and/or <code>icon</code>, either of which can be <code>null</code>. * * @param title the title to be displayed in this tab * @param icon the icon to be displayed in this tab * @param component the component to be displayed when this tab is clicked */ public final void addTab(final String title, final Icon icon, final Component component) { this.addTab(title, icon, component, null); } /** * Adds a <code>component</code> and <code>tip</code> represented by a <code>title</code> and/or <code>icon</code>, either of which can be <code>null</code>. * * @param title the title to be displayed in this tab * @param icon the icon to be displayed in this tab * @param component the component to be displayed when this tab is clicked * @param tip the tooltip to be displayed for this tab */ public final void addTab(final String title, final Icon icon, final Component component, final String tip) { final int index = tabbedPane.indexOfComponent(component); if (index == -1) { tabbedPane.addTab(title, icon, component, tip); } } /** * Validates the values of an input port component. * * @param inputPortComponent the input port component to be checked * @return <code>true</code> if the values are valid, otherwise, <code>false</code>. */ protected abstract boolean checkInputPortComponentValues(IN_COMPONENT inputPortComponent); /** * Validates the values of an output port component. * * @param inputPortComponent the output port component to be checked * @return <code>true</code> if the values are valid, otherwise, <code>false</code>. */ protected abstract boolean checkOutputPortComponentValues(OUT_COMPONENT outputPortComponent); @Override public boolean checkValues() { // Check each input port component. for (final IN_COMPONENT inputPortComponent : this.getInputPortComponents().values()) { if (!this.checkInputPortComponentValues(inputPortComponent)) { return false; } } // Check each output port component. for (final OUT_COMPONENT outputPortComponent : this.getOutputPortComponents().values()) { if (!this.checkOutputPortComponentValues(outputPortComponent)) { return false; } } // Success!! return true; } /** * Returns the activity for this configuration panel. * * @return The activity for this configuration panel. * @throws IllegalStateException If <code>activity == null</code>. */ public final ACTIVITY getActivity() throws IllegalStateException { if (activity == null) { throw new IllegalStateException(new NullPointerException("activity")); } return activity; } @Override public final CONFIG getConfiguration() { return configBean; } /** * Returns the tab icon for the given <code>component</code>. * * @param component the component being queried * @return the icon for <code>component</code> * @throws IndexOutOfBoundsException if component is not found */ public final Icon getIcon(final Component component) throws IndexOutOfBoundsException { final int index = tabbedPane.indexOfComponent(component); return tabbedPane.getIconAt(index); } /** * Returns an immutable map of port names to input port components. * <p> * Equivalent to: <code>this.getInputPortComponents(Cardinality.MANY);</code> * * @return an immutable map of port names to input port components. */ public final Map<String, IN_COMPONENT> getInputPortComponents() { return this.getInputPortComponents(Cardinality.MANY); } /** * Returns an immutable map of port names to input port components, restricted by the given <code>cardinality</code>. * <p> * The behavior of this method is controlled by the <code>cardinality</code> parameter: * <ul> * <li><code>Cardinality.ZERO</code> - Map is empty</li> * <li><code>Cardinality.ONE</code> - Map contains only selected input port component</li> * <li><code>Cardinality.MANY</code> - Map contains all input port components</li> * </ul> * * @param cardinality the cardinality * @return an immutable map of port names to input port components. */ public final Map<String, IN_COMPONENT> getInputPortComponents(final Cardinality cardinality) { final Map<String, IN_COMPONENT> inputPortComponents = new TreeMap<String, IN_COMPONENT>(); switch (cardinality) { case ZERO: break; case ONE: final int selectedIndex = inputPortsTabbedPane.getSelectedIndex(); if (selectedIndex != -1) { final String portName = inputPortsTabbedPane.getTitleAt(selectedIndex); @SuppressWarnings("unchecked") final IN_COMPONENT inputPortComponent = (IN_COMPONENT) inputPortsTabbedPane .getComponentAt(selectedIndex); inputPortComponents.put(portName, inputPortComponent); } break; case MANY: for (int index = 0, length = inputPortsTabbedPane.getTabCount(); index < length; index++) { final String portName = inputPortsTabbedPane.getTitleAt(index); @SuppressWarnings("unchecked") final IN_COMPONENT inputPortComponent = (IN_COMPONENT) inputPortsTabbedPane.getComponentAt(index); inputPortComponents.put(portName, inputPortComponent); } break; default: break; } return Collections.unmodifiableMap(inputPortComponents); } /** * Returns a map of port names to input port configuration beans for the given <code>configBean</code>. * * @param configBean the activity configuration bean * @return map of port names to input port configuration beans. */ protected abstract Map<String, IN_CONFIG> getInputPortConfigurations(CONFIG configBean); /** * Returns an immutable map of port names to output port components. * <p> * Equivalent to: <code>this.getOutputPortComponents(Cardinality.MANY);</code> * * @return an immutable map of port names to output port components. */ public final Map<String, OUT_COMPONENT> getOutputPortComponents() { return this.getOutputPortComponents(Cardinality.MANY); } /** * Returns an immutable map of port names to output port components, restricted by the given <code>cardinality</code>. * <p> * The behavior of this method is controlled by the <code>cardinality</code> parameter: * <ul> * <li><code>Cardinality.ZERO</code> - Map is empty</li> * <li><code>Cardinality.ONE</code> - Map contains only selected output port component</li> * <li><code>Cardinality.MANY</code> - Map contains all output port components</li> * </ul> * * @param cardinality the cardinality * @return an immutable map of port names to output port components. */ public final Map<String, OUT_COMPONENT> getOutputPortComponents(final Cardinality cardinality) { final Map<String, OUT_COMPONENT> outputPortComponents = new TreeMap<String, OUT_COMPONENT>(); switch (cardinality) { case ZERO: break; case ONE: final int selectedIndex = outputPortsTabbedPane.getSelectedIndex(); if (selectedIndex != -1) { final String portName = outputPortsTabbedPane.getTitleAt(selectedIndex); @SuppressWarnings("unchecked") final OUT_COMPONENT outputPortComponent = (OUT_COMPONENT) outputPortsTabbedPane .getComponentAt(selectedIndex); outputPortComponents.put(portName, outputPortComponent); } break; case MANY: for (int index = 0, length = outputPortsTabbedPane.getTabCount(); index < length; index++) { final String portName = outputPortsTabbedPane.getTitleAt(index); @SuppressWarnings("unchecked") final OUT_COMPONENT outputPortComponent = (OUT_COMPONENT) outputPortsTabbedPane .getComponentAt(index); outputPortComponents.put(portName, outputPortComponent); } break; default: break; } return Collections.unmodifiableMap(outputPortComponents); } /** * Returns a map of port names to output port configuration beans for the given <code>configBean</code>. * * @param configBean the activity configuration bean * @return map of port names to output port configuration beans. */ protected abstract Map<String, OUT_CONFIG> getOutputPortConfigurations(CONFIG configBean); /** * Returns the tab title for the given <code>component</code>. * * @param component the component being queried * @return the title for <code>component</code> * @throws IndexOutOfBoundsException if component is not found */ public final String getTitle(final Component component) throws IndexOutOfBoundsException { final int index = tabbedPane.indexOfComponent(component); return tabbedPane.getTitleAt(index); } /** * Returns the tooltip text for the given <code>component</code>. * * @param component the component being queried * @return the tooltip text for <code>component</code> * @throws IndexOutOfBoundsException if component is not found */ public final String getToolTipText(final Component component) throws IndexOutOfBoundsException { final int index = tabbedPane.indexOfComponent(component); return tabbedPane.getToolTipTextAt(index); } /** * Initializes the graphical user interface (GUI). * <p> * Subclasses may refine the implementation of this method. */ protected void initGui() { this.setDoubleBuffered(false); this.setLayout(new BorderLayout()); tabbedPane.setBorder(BorderFactory.createEmptyBorder()); tabbedPane.addTab(defaultInputPortsTabText, defaultInputPortsTabIcon, inputPortsPane, defaultInputPortsTabTip); tabbedPane.addTab(defaultOutputPortsTabText, defaultOutputPortsTabIcon, outputPortsPane, defaultOutputPortsTabTip); this.add(tabbedPane, BorderLayout.CENTER); inputPortsPane.setBorder(BorderFactory.createEmptyBorder()); inputPortsPane.setLayout(new BorderLayout()); emptyInputPortsLabel.setHorizontalAlignment(JLabel.CENTER); emptyInputPortsLabel.setText(defaultEmptyInputPortsText); final JButton addInputPortButton = new JButton(); addInputPortButton.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(final MouseEvent e) { final String portName = nextPortName(defaultInputPortName, ActivityConfigurationPanelWithInputPortAndOutputPortComponents.this.getInputPortComponents() .keySet()); final IN_CONFIG inputPortConfigBean = ActivityConfigurationPanelWithInputPortAndOutputPortComponents.this .newInputPortConfiguration(); final IN_COMPONENT inputPortComponent = ActivityConfigurationPanelWithInputPortAndOutputPortComponents.this .toInputPortComponent(portName, inputPortConfigBean); inputPortsTabbedPane.addTab(portName, inputPortComponent); } }); addInputPortButton.setIcon(defaultAddInputPortButtonIcon); addInputPortButton.setText(defaultAddInputPortButtonText); addInputPortButton.setToolTipText(defaultAddInputPortButtonTip); inputPortsPane.add(emptyInputPortsLabel, BorderLayout.CENTER); inputPortsPane.add(addInputPortButton, BorderLayout.SOUTH); outputPortsPane.setBorder(BorderFactory.createEmptyBorder()); outputPortsPane.setLayout(new BorderLayout()); emptyOutputPortsLabel.setHorizontalAlignment(JLabel.CENTER); emptyOutputPortsLabel.setText(defaultEmptyOutputPortsText); final JButton addOutputPortButton = new JButton(); addOutputPortButton.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(final MouseEvent e) { final String portName = nextPortName(defaultOutputPortName, ActivityConfigurationPanelWithInputPortAndOutputPortComponents.this .getOutputPortComponents().keySet()); final OUT_CONFIG outputPortConfigBean = ActivityConfigurationPanelWithInputPortAndOutputPortComponents.this .newOutputPortConfiguration(); final OUT_COMPONENT outputPortComponent = ActivityConfigurationPanelWithInputPortAndOutputPortComponents.this .toOutputPortComponent(portName, outputPortConfigBean); outputPortsTabbedPane.addTab(portName, outputPortComponent); } }); addOutputPortButton.setIcon(defaultAddOutputPortButtonIcon); addOutputPortButton.setText(defaultAddOutputPortButtonText); addOutputPortButton.setToolTipText(defaultAddOutputPortButtonTip); outputPortsPane.add(emptyOutputPortsLabel, BorderLayout.CENTER); outputPortsPane.add(addOutputPortButton, BorderLayout.SOUTH); } /** * Called when an input port tab is added. * * @param portName the title of the tab * @param inputPortComponent the component */ protected void inputPortAdded(final String portName, final IN_COMPONENT inputPortComponent) { return; } /** * Called when the title of an input port tab is changed. * * @param oldPortName the original title of the tab * @param newPortName the new title of the tab * @param inputPortComponent the component */ protected void inputPortNameChanged(final String oldPortName, final String newPortName, final IN_COMPONENT inputPortComponent) { return; } /** * Called when an input port tab is removed. * * @param portName the title of the tab * @param inputPortComponent the component */ protected void inputPortRemoved(final String portName, final IN_COMPONENT inputPortComponent) { return; } @Override public final boolean isConfigurationChanged() { // This line assumes that the configuration bean implements the Object#equals(Object) method. return !ObjectUtils.equals(this.getConfiguration(), this.toConfiguration()); } /** * Tests if the given <code>input</code> contains a valid port name. * * @param input the character sequence * @return <code>true</code> if the <code>input</code> contains a valid port name, otherwise, <code>false</code>. */ protected boolean isPortName(final CharSequence input) { return Pattern.matches("^\\w+$", input); } /** * Returns a new activity configuration bean. * * @return a new activity configuration bean */ protected abstract CONFIG newConfiguration(); /** * Returns a new input port configuration bean. * * @return a new input port configuration bean */ protected abstract IN_CONFIG newInputPortConfiguration(); /** * Returns a new output port configuration bean. * * @return a new output port configuration bean */ protected abstract OUT_CONFIG newOutputPortConfiguration(); @Override public final void noteConfiguration() { configBean = this.toConfiguration(); } /** * Called when an output port tab is added. * * @param portName the title of the tab * @param outputPortComponent the component */ protected void outputPortAdded(final String portName, final OUT_COMPONENT outputPortComponent) { return; } /** * Called when the title of an output port tab is changed. * * @param oldPortName the original title of the tab * @param newPortName the new title of the tab * @param outputPortComponent the component */ protected void outputPortNameChanged(final String oldPortName, final String newPortName, final OUT_COMPONENT outputPortComponent) { return; } /** * Called when an output port tab is removed. * * @param portName the title of the tab * @param outputPortComponent the component */ protected void outputPortRemoved(final String portName, final OUT_COMPONENT outputPortComponent) { return; } @Override public void refreshConfiguration() { if (refreshing) { // If we're already refreshing, then there is no work to do. return; } synchronized (refreshConfigurationLock) { if (refreshing) { // Re-test the flag; defensive programming against race conditions. If we're // already refreshing, then something has gone wrong... throw new IllegalStateException("refreshing"); } else { // Otherwise, raise the flag. refreshing = true; } try { inputPortsTabbedPane.removeAll(); outputPortsTabbedPane.removeAll(); configBean = this.getActivity().getConfiguration(); final Map<String, IN_CONFIG> inputPortConfigBeans = this.getInputPortConfigurations(configBean); if (inputPortConfigBeans != null) { for (final Map.Entry<String, IN_CONFIG> entry : (new TreeMap<String, IN_CONFIG>( inputPortConfigBeans)).entrySet()) { final String portName = entry.getKey(); final IN_CONFIG inputPortConfigBean = entry.getValue(); final IN_COMPONENT inputPortComponent = this.toInputPortComponent(portName, inputPortConfigBean); inputPortsTabbedPane.addTab(portName, inputPortComponent); } } final Map<String, OUT_CONFIG> outputPortConfigBeans = this.getOutputPortConfigurations(configBean); if (outputPortConfigBeans != null) { for (final Map.Entry<String, OUT_CONFIG> entry : (new TreeMap<String, OUT_CONFIG>( outputPortConfigBeans)).entrySet()) { final String portName = entry.getKey(); final OUT_CONFIG outputPortConfigBean = entry.getValue(); final OUT_COMPONENT outputPortComponent = this.toOutputPortComponent(portName, outputPortConfigBean); outputPortsTabbedPane.addTab(portName, outputPortComponent); } } } finally { // Ensure that the flag is lowered, even if an exception is raised inside the "try" block. refreshing = false; } } } /** * Removes the tab for the specified <code>component</code> * * @param component the component whose tab is to be removed * @throws IndexOutOfBoundsException if component is not found */ public final void removeTab(final Component component) throws IndexOutOfBoundsException { final int index = tabbedPane.indexOfComponent(component); tabbedPane.removeTabAt(index); } /** * Sets the icon for the <code>component</code> to <code>icon</code>, which may be <code>null</code>. * * @param component the component whose icon is to be set * @param icon the icon to be displayed in the tab * @throws IndexOutOfBoundsException if component is not found */ public final void setIcon(final Component component, final Icon icon) throws IndexOutOfBoundsException { final int index = tabbedPane.indexOfComponent(component); tabbedPane.setIconAt(index, icon); } /** * Sets the map of port names to input port configuration beans for the given <code>configBean</code>. * * @param configBean the activity configuration bean * @param inputPortConfigBeans the new map of port names to input port configuration beans */ protected abstract void setInputPortConfigurations(CONFIG configBean, Map<String, IN_CONFIG> inputPortConfigBeans); /** * Sets the map of port names to output port configuration beans for the given <code>configBean</code>. * * @param configBean the activity configuration bean * @param outputPortConfigBeans the new map of port names to input port configuration beans */ protected abstract void setOutputPortConfigurations(CONFIG configBean, Map<String, OUT_CONFIG> outputPortConfigBeans); /** * Sets the title for the <code>component</code> to <code>title</code>. * * @param component the component whose title is to be set * @param title the title to be displayed in the tab * @throws IndexOutOfBoundsException if component is not found */ public final void setTitle(final Component component, final String title) throws IndexOutOfBoundsException { final int index = tabbedPane.indexOfComponent(component); tabbedPane.setTitleAt(index, title); } /** * Sets the tooltip text for the <code>component</code> to <code>tip</code>, which may be <code>null</code>. * * @param component the component whose title is to be set * @param tip the tooltip text to be displayed in the tab * @throws IndexOutOfBoundsException if component is not found */ public final void setToolTipText(final Component component, final String tip) throws IndexOutOfBoundsException { final int index = tabbedPane.indexOfComponent(component); tabbedPane.setToolTipTextAt(index, tip); } /** * Returns this configuration panel as an activity configuration bean. * <p> * Equivalent to: <code>this.toConfiguration(Cardinality.MANY, Cardinality.MANY);</code> * * @return an activity configuration bean */ public final CONFIG toConfiguration() { return this.toConfiguration(Cardinality.MANY, Cardinality.MANY); } /** * Returns this configuration panel as an activity configuration bean, whose input and output ports are limited by the specified cardinalities. * * @param inputPortCardinality the cardinality for the selection of input ports * @param outputPortCardinality the cardinality for the selection of output ports * @return an activity configuration bean * @see #getInputPortComponents(Cardinality) * @see #getOutputPortComponents(Cardinality) */ public CONFIG toConfiguration(final Cardinality inputPortCardinality, final Cardinality outputPortCardinality) { final CONFIG configBean = this.newConfiguration(); final Map<String, IN_CONFIG> inputPortConfigBeans = new TreeMap<String, IN_CONFIG>(); for (final Map.Entry<String, IN_COMPONENT> entry : this.getInputPortComponents(inputPortCardinality) .entrySet()) { final String portName = entry.getKey(); final IN_COMPONENT inputPortComponent = entry.getValue(); final IN_CONFIG inputPortConfigBean = this.toInputPortConfiguration(portName, inputPortComponent); inputPortConfigBeans.put(portName, inputPortConfigBean); } this.setInputPortConfigurations(configBean, Collections.unmodifiableMap(inputPortConfigBeans)); final Map<String, OUT_CONFIG> outputPortConfigBeans = new TreeMap<String, OUT_CONFIG>(); for (final Map.Entry<String, OUT_COMPONENT> entry : this.getOutputPortComponents(outputPortCardinality) .entrySet()) { final String portName = entry.getKey(); final OUT_COMPONENT outputPortComponent = entry.getValue(); final OUT_CONFIG outputPortConfigBean = this.toOutputPortConfiguration(portName, outputPortComponent); outputPortConfigBeans.put(portName, outputPortConfigBean); } this.setOutputPortConfigurations(configBean, Collections.unmodifiableMap(outputPortConfigBeans)); return configBean; } /** * Returns the given input port configuration bean as an input port component. * * @param portName the port name * @param inputPortConfigBean the input port configuration bean * @return the input port component */ protected abstract IN_COMPONENT toInputPortComponent(String portName, IN_CONFIG inputPortConfigBean); /** * Returns the given input port component as an input port configuration bean. * * @param portName the port name * @param inputPortComponent the input port component * @return the input port configuration bean */ protected abstract IN_CONFIG toInputPortConfiguration(String portName, IN_COMPONENT inputPortComponent); /** * Returns the given output port configuration bean as an output port component. * * @param portName the port name * @param outputPortConfigBean the output port configuration bean * @return the output port component */ protected abstract OUT_COMPONENT toOutputPortComponent(String portName, OUT_CONFIG outputPortConfigBean); /** * Returns the given output port component as an output port configuration bean. * * @param portName the port name * @param outputPortComponent the output port component * @return the output port configuration bean */ protected abstract OUT_CONFIG toOutputPortConfiguration(String portName, OUT_COMPONENT outputPortComponent); }