/*
* Copyright (c) 2003-2012 Fred Hutchinson Cancer Research Center
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.fhcrc.cpl.toolbox.gui;
import org.fhcrc.cpl.toolbox.commandline.arguments.*;
import org.fhcrc.cpl.toolbox.commandline.CLMUserManualGenerator;
import org.fhcrc.cpl.toolbox.TextProvider;
import org.fhcrc.cpl.toolbox.ApplicationContext;
import org.fhcrc.cpl.toolbox.filehandler.TempFileManager;
import org.fhcrc.cpl.toolbox.commandline.CommandLineModule;
import org.fhcrc.cpl.toolbox.commandline.CommandLineModuleUtilities;
import org.apache.log4j.Logger;
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.util.Map;
import java.util.HashMap;
import java.util.prefs.Preferences;
import java.util.prefs.BackingStoreException;
import java.io.*;
/**
* JFrame class for specifying command-line arguments for a given module
*
* Override the method createArgumentFieldPanel() in a subclass to add handling for more arg types
*
* TODO: use swiXML? Layout is variable, but some bits are constant, like the buttons
*/
public class InteractiveModuleFrame extends JDialog
{
static Logger _log = Logger.getLogger(InteractiveModuleFrame.class);
//module to invoke
protected CommandLineModule module;
//map from arguments to the components that will hold their values
protected Map<CommandLineArgumentDefinition,JComponent> argComponentMap;
//for storing previously specified values
protected Preferences prefs = Preferences.userNodeForPackage(InteractiveModuleFrame.class);
protected static final int MAX_FIELDPANE_HEIGHT = 600;
protected static final int ARG_HELP_PANEL_HEIGHT = 90;
public static final int DEFAULT_MAX_ARG_LABEL_LENGTH = 50;
protected int maxArgLabelLength = DEFAULT_MAX_ARG_LABEL_LENGTH;
//help with a specific argument
// protected JDialog argHelpDialog;
protected JTextArea argHelpTextArea;
// protected JPanel argHelpPanel;
protected JScrollPane argHelpScrollPane;
//dialog box providing full help for the command
protected JDialog helpDialog;
protected Map<String, String> moduleArgMap;
protected CLMUserManualGenerator userManualGenerator = new CLMUserManualGenerator();
protected ListenerHelper helper = new ListenerHelper(this);
protected JPanel buttonPanel = null;
protected GridBagConstraints buttonGBC = null;
//arbitrary
public int width = 650;
//will be overridden
public int height = 300;
public int fieldViewportHeight = 300;
public int fieldPaneHeight = 300;
public int fieldPanelWidth = width;
//are we done with the frame?
public boolean done = false;
//has the user specified arguments?
public boolean argsSpecified = false;
//to hang listeners off of, to notify things that we're done. not displayed
protected JButton fakeButton;
protected boolean shouldStoreAlgValues = true;
public InteractiveModuleFrame(CommandLineModule module, Map<String, String> moduleArgMap)
{
this(module, moduleArgMap, DEFAULT_MAX_ARG_LABEL_LENGTH);
}
/**
*
* @param module
* @param moduleArgMap initial values for specified args
*/
public InteractiveModuleFrame(CommandLineModule module, Map<String, String> moduleArgMap, int maxArgLabelLength)
{
super();
this.maxArgLabelLength = maxArgLabelLength;
setTitle(TextProvider.getText("ARGUMENTS_FOR_COMMAND_COMMAND",module.getCommandName()));
this.setModalityType(ModalityType.APPLICATION_MODAL);
fakeButton = new JButton("fake");
this.module = module;
this.moduleArgMap = moduleArgMap;
//buttons
JButton buttonExecute = new JButton(TextProvider.getText("EXECUTE"));
JButton buttonCancel = new JButton(TextProvider.getText("CANCEL"));
JButton buttonShowHelp = new JButton(TextProvider.getText("HELP"));
helper.addListener(buttonExecute, "buttonExecute_actionPerformed");
helper.addListener(buttonCancel, "buttonCancel_actionPerformed");
helper.addListener(buttonShowHelp, "buttonShowHelp_actionPerformed");
//default action is execute
getRootPane().setDefaultButton(buttonExecute);
//set up the panel
JPanel contentPanel = new JPanel();
GridBagConstraints contentPanelGBC = new GridBagConstraints();
contentPanelGBC.gridwidth = GridBagConstraints.REMAINDER;
contentPanel.setLayout(new GridBagLayout());
add(contentPanel);
//add all argument fields
addFieldsForArguments(contentPanel, helper);
//add buttons
buttonPanel = new JPanel();
GridBagConstraints buttonPanelGBC = new GridBagConstraints();
buttonPanelGBC.gridwidth = GridBagConstraints.REMAINDER;
contentPanel.add(buttonPanel, buttonPanelGBC);
buttonGBC = new GridBagConstraints();
buttonGBC.insets = new Insets(10, 0, 10, 0);
buttonPanel.add(buttonExecute, buttonGBC);
GridBagConstraints secondToLastButtonGBC = new GridBagConstraints();
secondToLastButtonGBC.insets = new Insets(10, 0, 10, 0);
secondToLastButtonGBC.gridwidth = GridBagConstraints.RELATIVE;
buttonPanel.add(buttonCancel, secondToLastButtonGBC);
GridBagConstraints lastButtonGBC = new GridBagConstraints();
lastButtonGBC.insets = new Insets(10, 0, 10, 0);
lastButtonGBC.gridwidth = GridBagConstraints.REMAINDER;
buttonPanel.add(buttonShowHelp, lastButtonGBC);
//add help
argHelpTextArea = new JTextArea();
argHelpTextArea.setEditable(false);
argHelpTextArea.setOpaque(false);
argHelpTextArea.setBounds(0,0,500,200);
argHelpTextArea.setLineWrap(true);
argHelpTextArea.setWrapStyleWord(true);
argHelpScrollPane = new JScrollPane();
argHelpScrollPane.setBorder(BorderFactory.createTitledBorder("Argument Help"));
argHelpScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
argHelpScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
argHelpScrollPane.setPreferredSize(new Dimension(width, ARG_HELP_PANEL_HEIGHT));
argHelpScrollPane.setViewportView(argHelpTextArea);
contentPanel.add(argHelpScrollPane, buttonPanelGBC);
//20 * the number of text fields, plus the height of the button area,
//plus some padding
height = fieldPaneHeight + 100 + 70 + 15;
setPreferredSize(new Dimension(width+30, height));
setSize(new Dimension(width+30, height));
setMinimumSize(new Dimension(width+30, height));
Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
int centerH = screenSize.width / 2;
int centerV = screenSize.height / 2;
this.setLocation(centerH - width / 2, centerV - height / 2);
setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
}
public void addDoneListener(ActionListener listener)
{
fakeButton.addActionListener(listener);
}
/**
* Wait for argument specification
* @return
*/
public boolean collectArguments()
{
setVisible(true);
return argsSpecified;
}
/**
* Notify any listeners that we're done
* @param event
*/
protected void notifyDone(ActionEvent event)
{
ActionListener[] fakeButtonListeners = fakeButton.getActionListeners();
if (fakeButtonListeners != null)
{
for (ActionListener listener : fakeButtonListeners)
listener.actionPerformed(event);
}
}
/**
* Add fields representing each of the module's arguments. Each arg handled separately by another method
* TODO: move field creation into the argument classes? That would be more modular. Also more work
* @param contentPanel
* @param helper
*/
protected void addFieldsForArguments(JPanel contentPanel, ListenerHelper helper)
{
//set up scrollpane UI
boolean firstArg = true;
JScrollPane fieldScrollPane = new JScrollPane();
fieldScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
fieldScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
GridBagConstraints fieldPaneGBC = new GridBagConstraints();
fieldPaneGBC.gridwidth=GridBagConstraints.REMAINDER;
JPanel panelInViewport = new JPanel();
panelInViewport.setLayout(new GridBagLayout());
fieldScrollPane.setViewportView(panelInViewport);
JPanel allFieldsPanel = new JPanel();
allFieldsPanel.setLayout(new GridBagLayout());
GridBagConstraints allFieldsPanelGBC = new GridBagConstraints();
allFieldsPanelGBC.gridwidth=GridBagConstraints.REMAINDER;
argComponentMap = new HashMap<CommandLineArgumentDefinition,JComponent>();
//For each argument, create the GUI components that will capture the value.
//Basic args first
for (CommandLineArgumentDefinition argDef :
module.getBasicArgumentDefinitions())
{
addComponentsForArg(argDef, firstArg, allFieldsPanel);
firstArg = false;
}
//add advanced args, after a separator
int extraAdvancedArgsHeight = 0; //extra height, not from the args themselves, but from the section title
CommandLineArgumentDefinition[] advancedArgs = module.getAdvancedArgumentDefinitions();
if (advancedArgs != null && advancedArgs.length > 0)
{
extraAdvancedArgsHeight = 25;
JSeparator visibleSeparator = new JSeparator();
visibleSeparator.setPreferredSize(new Dimension(fieldPanelWidth-10, 1));
allFieldsPanel.add(visibleSeparator, allFieldsPanelGBC);
JLabel advancedArgsLabel = new JLabel("Advanced Arguments");
advancedArgsLabel.setToolTipText("The default values of these arguments are appropriate for " +
"most use cases. Do not alter these values unless you know what you're doing.");
allFieldsPanel.add(advancedArgsLabel, allFieldsPanelGBC);
JSeparator visibleSeparator2 = new JSeparator();
visibleSeparator2.setPreferredSize(new Dimension(fieldPanelWidth-10, 1));
allFieldsPanel.add(visibleSeparator2, allFieldsPanelGBC);
for (CommandLineArgumentDefinition argDef : advancedArgs)
addComponentsForArg(argDef, false, allFieldsPanel);
}
//20 * the number of text fields, plus the height of the button area, plus whatever from the advanced area,
//plus some padding
fieldViewportHeight = 41 * argComponentMap.size() + 25 + extraAdvancedArgsHeight;
fieldPaneHeight = Math.min(fieldViewportHeight, MAX_FIELDPANE_HEIGHT);
fieldPaneHeight = Math.max(300, fieldPaneHeight);
allFieldsPanel.setMinimumSize(new Dimension(fieldPanelWidth, fieldViewportHeight));
allFieldsPanel.setPreferredSize(new Dimension(fieldPanelWidth, fieldViewportHeight));
fieldScrollPane.setMinimumSize(new Dimension(fieldPanelWidth, fieldPaneHeight));
fieldScrollPane.setPreferredSize(new Dimension(fieldPanelWidth, fieldPaneHeight));
contentPanel.add(fieldScrollPane, fieldPaneGBC);
panelInViewport.add(allFieldsPanel, allFieldsPanelGBC);
}
/**
* Add the components that represent a particular argument. This will include labels.
* Inidividual handling for each arg type is done in yet another method, createArgumentFieldPanel
* @param argDef
* @param firstArg
* @param allFieldsPanel
*/
protected void addComponentsForArg(CommandLineArgumentDefinition argDef, boolean firstArg,
JPanel allFieldsPanel)
{
String labelText = argDef.getArgumentDisplayName();
if (CommandLineModuleUtilities.isUnnamedSeriesArgument(argDef) ||
CommandLineModuleUtilities.isUnnamedArgument(argDef))
labelText = argDef.getHelpText();
if (labelText.length() > maxArgLabelLength)
labelText = labelText.substring(0, maxArgLabelLength-3) + "...";
JLabel argLabel = new JLabel(labelText);
String toolTipText = argDef.getHelpText();
if (toolTipText == null)
toolTipText = "";
if (toolTipText.length() > 50)
toolTipText = toolTipText.substring(0, 46) + "....";
argLabel.setToolTipText(toolTipText);
JComponent argComponent;
String fieldValue = null;
//if module argument map is provided, don't use prefs for defaulting
if (moduleArgMap != null)
{
if (moduleArgMap.containsKey(argDef.getArgumentName()))
{
fieldValue = moduleArgMap.get(argDef.getArgumentName());
}
}
else
{
fieldValue = prefs.get(module.getCommandName() + ":" + argDef.getArgumentName(), null);
}
if (fieldValue == null || fieldValue.length() == 0)
{
if (argDef.hasDefaultValue())
{
//TODO: this may bomb on more complicated custom datatypes
fieldValue = argDef.getDefaultValueAsString();
}
}
JPanel fieldPanel = new JPanel();
//System.err.println("Handling " + argDef.getArgumentName());
//treat unnamed series parameters differently
if (CommandLineModuleUtilities.isUnnamedSeriesArgument(argDef))
argComponent = argDef.addComponentsForGUISeries(fieldPanel, this, fieldValue);
else
argComponent = argDef.addComponentsForGUI(fieldPanel, this, fieldValue);
argComponentMap.put(argDef, argComponent);
JPanel labelPanel = new JPanel();
GridBagConstraints labelPanelGBC = new GridBagConstraints();
labelPanelGBC.gridwidth=GridBagConstraints.RELATIVE;
labelPanelGBC.anchor = GridBagConstraints.LINE_END;
labelPanelGBC.insets = new Insets(0,0,0,0);
GridBagConstraints labelGBC = new GridBagConstraints();
labelGBC.anchor = GridBagConstraints.LINE_END;
labelGBC.gridwidth=GridBagConstraints.RELATIVE;
GridBagConstraints helpGBC = new GridBagConstraints();
helpGBC.anchor = GridBagConstraints.LINE_START;
labelGBC.insets = new Insets(0, 0, 0, 0);
if (argDef.isRequired())
{
JLabel requiredLabel = new JLabel("*");
requiredLabel.setForeground(Color.BLUE);
requiredLabel.setToolTipText("This argument is required");
requiredLabel.addMouseListener(new ArgRequiredListener());
labelPanel.add(requiredLabel);
}
labelPanel.add(argLabel, labelGBC);
//only add help link if there's help text to show
if (argDef.getHelpText() != null && argDef.getHelpText().length() > 0)
{
JLabel helpLabel = new JLabel("?");
helpLabel.setToolTipText("Click for argument help");
helpLabel.setForeground(Color.BLUE);
helpLabel.addMouseListener(new ArgHelpListener(argDef));
labelPanel.add(helpLabel, helpGBC);
}
allFieldsPanel.add(labelPanel, labelPanelGBC);
GridBagConstraints fieldGBC = new GridBagConstraints();
fieldGBC.gridwidth=GridBagConstraints.REMAINDER;
fieldGBC.anchor = GridBagConstraints.LINE_START;
fieldGBC.insets = new Insets(0,0,0,0);
allFieldsPanel.add(fieldPanel, fieldGBC);
}
/**
* Tells the user that an arg is required
*/
protected class ArgRequiredListener implements MouseListener
{
public void mouseClicked(MouseEvent e)
{
infoMessage("This argument is required");
}
public void mousePressed(MouseEvent e) {}
public void mouseReleased(MouseEvent e) {}
public void mouseEntered(MouseEvent e) {}
public void mouseExited(MouseEvent e) {}
}
/**
* Display arg help in a box
*/
protected class ArgHelpListener implements MouseListener
{
protected CommandLineArgumentDefinition argDef;
public ArgHelpListener(CommandLineArgumentDefinition argDef)
{
this.argDef = argDef;
}
public void mouseClicked(MouseEvent e)
{
/*
//dhmay getting rid of separate argument help dialog, replacing with an always-there text area
if (argHelpDialog != null && argHelpDialog.isVisible())
{
//help already initialized and visible
}
else
{
argHelpTextArea = new JTextArea();
argHelpTextArea.setEditable(false);
argHelpTextArea.setOpaque(false);
argHelpTextArea.setBounds(0,0,500,200);
argHelpTextArea.setLineWrap(true);
argHelpTextArea.setWrapStyleWord(true);
argHelpDialog = new JDialog();
argHelpDialog.add(argHelpTextArea);
argHelpDialog.setVisible(true);
Point thisLocation = getLocation();
argHelpDialog.setLocation((int) thisLocation.getX() + 10, (int) thisLocation.getY() + 5);
argHelpDialog.setSize(600,150);
argHelpDialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
argHelpDialog.setModalityType(ModalityType.DOCUMENT_MODAL);
// argHelpDialog.setAlwaysOnTop(true);
}
argHelpDialog.setTitle("Help for argument '" + argName + "'");
argHelpTextArea.setVisible(true);
argHelpDialog.setVisible(true);
*/
argHelpTextArea.setText(argDef.getHelpText());
argHelpScrollPane.setBorder(BorderFactory.createTitledBorder("Argument Help for '" +
argDef.getArgumentDisplayName() + "'"));
argHelpScrollPane.getVerticalScrollBar().setValue(argHelpScrollPane.getVerticalScrollBar().getMinimum());
}
public void mousePressed(MouseEvent e) {}
public void mouseReleased(MouseEvent e) {}
public void mouseEntered(MouseEvent e) {}
public void mouseExited(MouseEvent e) {}
}
/**
* grab all argument field values
* @return
*/
protected Map<String,String> parseFieldArguments()
{
Map<String,String> argNameValueMap = new HashMap<String,String>();
String PROTECTED_SPACE_SEP = "MYSEP_PROTECTEDSPACE_MYSEP";
for (CommandLineArgumentDefinition argDef : argComponentMap.keySet())
{
JComponent argComponent = argComponentMap.get(argDef);
String argValue = argDef.getValueFromGUIComponent(argComponent);
if (argValue != null && argValue.length() > 0)
{
if (argDef.getArgumentName().equals(
CommandLineArgumentDefinition.UNNAMED_PARAMETER_VALUE_SERIES_ARGUMENT))
{
_log.debug("Unnamed series. Value before:\n" + argValue);
argValue = argValue.trim();
if (FileArgumentDefinition.class.isAssignableFrom(argDef.getClass()) ||
FileToReadListArgumentDefinition.class.isAssignableFrom(argDef.getClass()))
{
//First, look for " symbols. Protect any unprotected spaces between them with a \
StringBuffer alteredArg = new StringBuffer();
boolean insideQuote = false;
boolean afterBackslash = false;
for (byte curByte : argValue.getBytes())
{
if (curByte == '\\')
afterBackslash = true;
else
{
if (curByte == '\"')
{
insideQuote = !insideQuote;
}
else if (curByte == ' ' && !afterBackslash && insideQuote)
alteredArg.append('\\');
afterBackslash = false;
}
alteredArg.append((char) curByte);
}
argValue = alteredArg.toString();
_log.debug("After in-quote space protection: " + argValue);
//dhmay adding protection for a backslash followed by a space, convention on Linux for escaping
//a space
argValue = argValue.replaceAll("\\\\ ", PROTECTED_SPACE_SEP);
_log.debug("After protect:\n" + argValue);
argValue = argValue.replaceAll(" ", CommandLineModule.UNNAMED_ARG_SERIES_SEPARATOR);
argValue = argValue.replaceAll(PROTECTED_SPACE_SEP, " ");
}
else
{
argValue = argValue.replaceAll(" ", CommandLineModule.UNNAMED_ARG_SERIES_SEPARATOR);
}
_log.debug("After:\n" + argValue);
}
argNameValueMap.put(argDef.getArgumentName(), argValue);
if (shouldStoreAlgValues)
{
prefs.put(module.getCommandName() + ":" + argDef.getArgumentName(), argValue);
try
{
prefs.flush();
}
catch (BackingStoreException e)
{
_log.debug("BackingStoreException saving prefs for " + module.getCommandName() +
":" + argDef.getArgumentName(), e);
}
}
}
else
{
prefs.remove(module.getCommandName() + ":" + argDef.getArgumentName());
}
}
return argNameValueMap;
}
/**
* Show a dialog box with help for this command
* @param event
*/
public void buttonShowHelp_actionPerformed(ActionEvent event)
{
File tempFile = null;
try
{
tempFile = TempFileManager.createTempFile("help_" + module.getCommandName() + ".html", this);
PrintWriter outPW = new PrintWriter(tempFile);
userManualGenerator.generateCommandManualEntry(module, outPW);
outPW.flush();
HtmlViewerPanel.showFileInDialog(tempFile, "Manual for commmand '" + module.getCommandName() + "'");
}
catch (IOException e)
{
ApplicationContext.infoMessage("Failed to open browser, HTML is in " +
tempFile.getAbsolutePath());
}
}
/**
* Try to execute command
* @param event
*/
public void buttonExecute_actionPerformed(ActionEvent event)
{
Map<String,String> argNameValueMap = parseFieldArguments();
try
{
_log.debug("Executing action. Arguments:");
for (String argName : argNameValueMap.keySet())
{
_log.debug("\t" + argName + "=" + argNameValueMap.get(argName));
}
module.digestArguments(argNameValueMap);
}
catch (ArgumentValidationException e)
{
infoMessage(TextProvider.getText("FAILED_ARGUMENT_VALIDATION") + "\n" +
TextProvider.getText("ERROR") + ": " + e.getMessage());
return;
}
_log.debug("Done digesting arguments");
argsSpecified = true;
notifyDone(event);
disposeAllComponents();
}
/**
* Get rid of everything
*/
protected void disposeAllComponents()
{
// if (argHelpDialog != null)
// {
// argHelpDialog.setVisible(false);
// argHelpDialog.dispose();
// }
if (helpDialog != null)
{
helpDialog.setVisible(false);
helpDialog.dispose();
}
this.setVisible(false);
this.dispose();
}
protected void infoMessage(String message)
{
JOptionPane.showMessageDialog(ApplicationContext.getFrame(), message, "Information", JOptionPane.INFORMATION_MESSAGE);
}
protected void errorMessage(String message, Throwable t)
{
if (null != t)
{
message = message + "\n" + t.getMessage() + "\n";
StringWriter sw = new StringWriter();
PrintWriter w = new PrintWriter(sw);
t.printStackTrace(w);
w.flush();
message += "\n";
message += sw.toString();
}
JOptionPane.showMessageDialog(ApplicationContext.getFrame(), message, "Information", JOptionPane.INFORMATION_MESSAGE);
}
/**
* Cancel, close window
* @param e
*/
public void buttonCancel_actionPerformed(ActionEvent e)
{
disposeAllComponents();
done=true;
notifyDone(e);
}
public CLMUserManualGenerator getUserManualGenerator()
{
return userManualGenerator;
}
public void setUserManualGenerator(CLMUserManualGenerator userManualGenerator)
{
this.userManualGenerator = userManualGenerator;
}
public int getMaxArgLabelLength()
{
return maxArgLabelLength;
}
public void setMaxArgLabelLength(int maxArgLabelLength)
{
this.maxArgLabelLength = maxArgLabelLength;
}
public boolean isShouldStoreAlgValues()
{
return shouldStoreAlgValues;
}
public void setShouldStoreAlgValues(boolean shouldStoreAlgValues)
{
this.shouldStoreAlgValues = shouldStoreAlgValues;
}
}