gt: 	  menuitem = new JMenuItem(generator.getTitle());
context: /*
   *   This program is free software: you can redistribute it and/or modify
   *   it under the terms of the GNU 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 General Public License for more details.
   *
   *   You should have received a copy of the GNU General Public License
   *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
   */
  
  /*
   * AttributeSelectionTab.java
   * Copyright (C) 2016-2021 University of Waikato, Hamilton, NZ
   */
  
  package adams.gui.tools.wekainvestigator.tab;
  
  import adams.core.ClassLister;
  import adams.core.MessageCollection;
  import adams.core.Properties;
  import adams.core.option.OptionUtils;
  import adams.gui.chooser.WekaFileChooser;
  import adams.gui.core.AbstractNamedHistoryPanel;
  import adams.gui.core.AbstractNamedHistoryPanel.HistoryEntryToolTipProvider;
  import adams.gui.core.BaseButton;
  import adams.gui.core.BaseComboBox;
  import adams.gui.core.BaseMenu;
  import adams.gui.core.BasePopupMenu;
  import adams.gui.core.BaseScrollPane;
  import adams.gui.core.BaseSplitPane;
  import adams.gui.core.BaseStatusBar;
  import adams.gui.core.ConsolePanel;
  import adams.gui.core.GUIHelper;
  import adams.gui.core.ParameterPanel;
  import adams.gui.dialog.ApprovalDialog;
  import adams.gui.event.WekaInvestigatorDataEvent;
  import adams.gui.goe.GenericArrayEditorDialog;
  import adams.gui.goe.GenericObjectEditorDialog;
  import adams.gui.goe.GenericObjectEditorPanel;
  import adams.gui.tools.wekainvestigator.InvestigatorPanel;
  import adams.gui.tools.wekainvestigator.data.MemoryContainer;
  import adams.gui.tools.wekainvestigator.history.AbstractHistoryPopupMenuItem;
  import adams.gui.tools.wekainvestigator.job.InvestigatorTabJob;
  import adams.gui.tools.wekainvestigator.output.RunInformationHelper;
  import adams.gui.tools.wekainvestigator.tab.attseltab.ResultItem;
  import adams.gui.tools.wekainvestigator.tab.attseltab.evaluation.AbstractAttributeSelectionEvaluation;
  import adams.gui.tools.wekainvestigator.tab.attseltab.output.AbstractOutputGenerator;
  import adams.gui.tools.wekainvestigator.tab.attseltab.output.TextStatistics;
  import com.github.fracpete.jclipboardhelper.ClipboardHelper;
  import weka.attributeSelection.ASEvaluation;
  import weka.attributeSelection.ASSearch;
  import weka.attributeSelection.BestFirst;
  import weka.attributeSelection.CfsSubsetEval;
  import weka.core.Instances;
  import weka.core.converters.AbstractFileSaver;
  
  import javax.swing.BorderFactory;
  import javax.swing.DefaultComboBoxModel;
  import javax.swing.JComponent;
  import javax.swing.JLabel;
  import javax.swing.JMenuItem;
  import javax.swing.JPanel;
  import javax.swing.SwingUtilities;
  import javax.swing.SwingWorker;
  import javax.swing.event.ChangeEvent;
  import java.awt.BorderLayout;
  import java.awt.Dialog.ModalityType;
  import java.awt.FlowLayout;
  import java.awt.GridLayout;
  import java.awt.event.ActionEvent;
  import java.awt.event.MouseEvent;
  import java.io.File;
  import java.util.ArrayList;
  import java.util.List;
  import java.util.Map;
  import java.util.Set;
  import java.util.logging.Level;
  
  /**
   * Tab for attribute selection.
   *
   * @author FracPete (fracpete at waikato dot ac dot nz)
   * @version $Revision$
   */
  public class AttributeSelectionTab
    extends AbstractInvestigatorTab {
  
    private static final long serialVersionUID = -4106630131554796889L;
  
    public static final String KEY_SEARCH = "search";
  
    public static final String KEY_EVALUATOR = "evaluator";
  
    public static final String KEY_EVALUATION = "evaluation";
  
    public static final String KEY_EVALUATION_PREFIX = "evaluation.";
  
    public static final String KEY_HISTORY = "history";
  
    /** the key for the output generators. */
    public final static String KEY_OUTPUTGENERATORS = "output generators";
  
    /**
     * Customized history panel.
     */
    public static class HistoryPanel
      extends AbstractNamedHistoryPanel<ResultItem>
      implements HistoryEntryToolTipProvider<ResultItem> {
  
      private static final long serialVersionUID = 8740813441072965573L;
  
      /** the owner. */
      protected AttributeSelectionTab m_Owner;
  
      /** the file chooser for models. */
      protected WekaFileChooser m_DatasetFileChooser;
  
      /**
       * Initializes the history.
       *
       * @param owner	the owning tab
       */
      public HistoryPanel(AttributeSelectionTab owner) {
        super();
        m_Owner = owner;
        setAllowRemove(true);
        setAllowRename(true);
        setButtonPanelVisible(true);
      }
  
      /**
       * Initializes the members.
       */
      @Override
      protected void initialize() {
        super.initialize();
  
        m_DatasetFileChooser = new WekaFileChooser();
      }
  
      /**
       * Gets called when a tooltip needs to get generated.
       *
       * @param history	the history
       * @param index 	the index in the history
       * @return		the generated tool tip, null if not available
       */
      public String createHistoryEntryToolTip(AbstractNamedHistoryPanel<ResultItem> history, int index) {
        String 		result;
        ResultItem 	item;
  
        result = null;
        item   = history.getEntry(index);
        if (item.hasRunInformation())
  	result = "<html>" + RunInformationHelper.toHTML(item.getRunInformation().toSpreadSheet()) + "</html>";
  
        return result;
      }
  
      /**
       * Sets whether to show tool tips.
       *
       * @param value	true if to show
       */
      public void setToolTipsEnabled(boolean value) {
        if (value)
  	setHistoryEntryToolTipProvider(this);
        else
  	setHistoryEntryToolTipProvider(null);
      }
  
      /**
       * Returns whether to show tool tips.
       *
       * @return		true if to show
       */
      public boolean getToolTipsEnabled() {
        return (getHistoryEntryToolTipProvider() != null);
      }
  
      /**
       * Removes all entries and payloads.
       */
      public void clear() {
        for (ResultItem item: m_Entries.values())
  	item.cleanUp();
        super.clear();
      }
  
      /**
       * Removes the specified entry.
       *
       * @param name	the name of the entry
       * @return		the entry that was stored under this name or null if
       * 			no entry was stored with this name
       */
      public ResultItem removeEntry(String name) {
        ResultItem	result;
  
        result = super.removeEntry(name);
        if (result != null)
  	result.cleanUp();
  
        return result;
      }
  
      /**
       * Displays the specified entry.
       *
       * @param name	the name of the entry, can be null to empty display
       */
      @Override
      protected void updateEntry(String name) {
        m_Owner.getPanelRight().removeAll();
        if (name != null) {
  	if (hasEntry(name))
  	  m_Owner.getPanelRight().add(getEntry(name).getTabbedPane());
        }
        m_Owner.getPanelRight().invalidate();
        m_Owner.getPanelRight().revalidate();
        m_Owner.getPanelRight().doLayout();
        m_Owner.getPanelRight().repaint();
      }
  
      /**
       * Generates the reduced data.
       *
       * @param item	the result item to use
       * @return		the reduced data
       */
      protected Instances generateReducedData(ResultItem item) {
        Instances		result;
  
        try {
  	result = item.getAttributeSelection().reduceDimensionality(item.getFull());
        }
        catch (Exception e) {
  	m_Owner.logError("Failed to reduce data!", e, "Data reduction");
  	result = null;
        }
  
        return result;
      }
  
      /**
       * Saves the reduced data to a file.
       *
       * @param item	the result item to use
       */
      protected void saveReducedData(ResultItem item) {
        Instances		reduced;
        int		retVal;
        AbstractFileSaver	writer;
        File		file;
  
        reduced = generateReducedData(item);
        if (reduced == null)
  	return;
  
        retVal = m_DatasetFileChooser.showSaveDialog(m_Owner);
        if (retVal != WekaFileChooser.APPROVE_OPTION)
  	return;
  
        file   = m_DatasetFileChooser.getSelectedFile();
        writer = m_DatasetFileChooser.getWriter();
        writer.setInstances(reduced);
        try {
  	writer.setFile(file);
  	writer.writeBatch();
        }
        catch (Exception e) {
  	m_Owner.logError("Failed to save reduce data to: " + file, e, "Save reduced data");
        }
      }
  
      /**
       * Makes the reduced data available as data container.
       *
       * @param item	the result item to use
       */
      protected void useReducedData(ResultItem item) {
        Instances		reduced;
        MemoryContainer	cont;
  
        reduced = generateReducedData(item);
        if (reduced == null)
  	return;
  
        cont = new MemoryContainer(reduced);
        SwingUtilities.invokeLater(() -> {
  	m_Owner.getData().add(cont);
  	m_Owner.logMessage("Added reduced data: " + reduced.relationName());
  	m_Owner.fireDataChange(new WekaInvestigatorDataEvent(m_Owner.getOwner(), WekaInvestigatorDataEvent.ROWS_ADDED, m_Owner.getData().size() - 1));
        });
      }
  
      /**
       * Regenerates the output.
       *
       * @param item	the result item to use
       */
      protected void regenerateOutput(final ResultItem item) {
        SwingWorker	worker;
  
        worker = new SwingWorker() {
  	@Override
  	protected Object doInBackground() throws Exception {
  	  item.getTabbedPane().removeAll();
  	  for (int i = 0; i < m_Owner.getOutputGenerators().length; i++) {
  	    try {
  	      m_Owner.getOutputGenerators()[i].generateOutput(item);
  	    }
  	    catch (Exception e) {
  	      m_Owner.logError("Failed to generate output with " + m_Owner.getOutputGenerators()[i].toCommandLine(), e, "Clusterer output generation");
  	    }
  	  }
  	  return null;
  	}
        };
        worker.execute();
      }
  
      /**
       * Prompts the user with a GOE for configuring the output generator and
       * returns it if accepted.
       *
       * @param generator		the generator to customize
       * @return			the customized generator, null if cancelled
       */
      protected AbstractOutputGenerator configureOutput(AbstractOutputGenerator generator) {
        GenericObjectEditorDialog		dialog;
  
        if (getParentDialog() != null)
          dialog = new GenericObjectEditorDialog(getParentDialog(), ModalityType.DOCUMENT_MODAL);
        else
          dialog = new GenericObjectEditorDialog(getParentFrame(), true);
        dialog.setDefaultCloseOperation(GenericArrayEditorDialog.DISPOSE_ON_CLOSE);
        dialog.setTitle("Configure output");
        dialog.getGOEEditor().setCanChangeClassInDialog(false);
        dialog.getGOEEditor().setClassType(AbstractOutputGenerator.class);
        dialog.setCurrent(generator);
        dialog.pack();
        dialog.setLocationRelativeTo(getParent());
        dialog.setVisible(true);
        if (dialog.getResult() != GenericObjectEditorDialog.APPROVE_OPTION)
          return null;
        else
          return (AbstractOutputGenerator) dialog.getCurrent();
      }
  
      /**
       * Prompts the user with a GOE for configuring the output generator and
       * then generates the output.
       *
       * @param generator		the generator to use customize
       * @param item		the result item to use
       */
      protected void generateOutput(AbstractOutputGenerator generator, ResultItem item) {
        final AbstractOutputGenerator current;
        SwingWorker			worker;
  
        current = configureOutput(generator);
        if (current == null)
  	return;
  
        worker = new SwingWorker() {
          @Override
          protected Object doInBackground() throws Exception {
            String msg = current.generateOutput(item);
  	  if (msg != null)
  	    m_Owner.logError(
  	      "Failed to generate output using: " + current.toCommandLine() + "\n" + msg,
  	      "Error generating output");
  	  return null;
          }
        };
        worker.execute();
      }
  
      /**
       * Prompts the user with a GOE for configuring the output generator and
       * then displays the generated outputs side by side.
       *
       * @param generator		the generator to use customize
       * @param indices		the indices of the items to display
       */
      protected void compareOutput(AbstractOutputGenerator generator, final int[] indices) {
        final AbstractOutputGenerator current;
        SwingWorker			worker;
  
        current = configureOutput(generator);
        if (current == null)
  	return;
  
        worker = new SwingWorker() {
          @Override
          protected Object doInBackground() throws Exception {
  	  List<JComponent> comps = new ArrayList<>();
  	  List<String> labels = new ArrayList<>();
  	  MessageCollection errors = new MessageCollection();
  	  for (int index: indices) {
  	    ResultItem item = getEntry(index);
  	    JComponent comp = current.createOutput(item, errors);
  	    if (comp != null) {
  	      comps.add(comp);
  	      labels.add(getEntryName(index));
  	    }
  	  }
  	  JPanel panel = null;
  	  if (comps.size() > 0) {
  	    panel = new JPanel(new GridLayout(1, comps.size()));
  	    for (int i = 0; i < comps.size(); i++) {
  	      JPanel subPanel = new JPanel(new BorderLayout());
  	      JLabel label = new JLabel(labels.get(i));
  	      subPanel.add(label, BorderLayout.NORTH);
  	      subPanel.add(comps.get(i), BorderLayout.CENTER);
  	      panel.add(subPanel);
  	    }
  	  }
  	  if (!errors.isEmpty())
  	    m_Owner.logError(
  	      "Failed to generate output using: " + current.toCommandLine() + "\n" + errors,
  	      "Error generating output");
  	  if (panel != null) {
  	    ApprovalDialog dialog;
  	    if (m_Owner.getParentDialog() != null)
  	      dialog = new ApprovalDialog(m_Owner.getParentDialog(), ModalityType.MODELESS);
  	    else
  	      dialog = new ApprovalDialog(m_Owner.getParentFrame(), false);
  	    dialog.setDefaultCloseOperation(ApprovalDialog.DISPOSE_ON_CLOSE);
  	    dialog.setTitle("Compare output");
  	    dialog.getContentPane().add(new BaseScrollPane(panel), BorderLayout.CENTER);
  	    dialog.setSize(GUIHelper.makeWider(GUIHelper.getDefaultDialogDimension()));
  	    dialog.setLocationRelativeTo(dialog.getParent());
  	    dialog.setVisible(true);
  	  }
  	  return null;
          }
        };
        worker.execute();
      }
  
      /**
       * Generates the right-click menu for the JList.
       *
       * @param e		the event that triggered the popup
       * @return		the generated menu
       * @see		#showPopup(MouseEvent)
       */
      protected BasePopupMenu createPopup(MouseEvent e) {
        BasePopupMenu		result;
        JMenuItem			menuitem;
        BaseMenu 			submenu;
        final int[]		indices;
        Class[]			classes;
  
        result  = super.createPopup(e);
        indices = getSelectedIndices();
  
        result.addSeparator();
  
        menuitem = new JMenuItem("Copy evaluator");
        menuitem.setEnabled((indices.length == 1));
        menuitem.addActionListener((ActionEvent ae) -> {
          String setup = OptionUtils.getCommandLine(getEntry(indices[0]).getEvaluator());
          ClipboardHelper.copyToClipboard(setup);
        });
        result.add(menuitem);
  
        menuitem = new JMenuItem("Copy search");
        menuitem.setEnabled((indices.length == 1));
        menuitem.addActionListener((ActionEvent ae) -> {
          String setup = OptionUtils.getCommandLine(getEntry(indices[0]).getSearch());
          ClipboardHelper.copyToClipboard(setup);
        });
        result.add(menuitem);
  
        menuitem = new JMenuItem("Save reduced data...");
        menuitem.setEnabled((indices.length == 1) && !getEntry(indices[0]).isCrossValidation() && getEntry(indices[0]).hasFull());
        menuitem.addActionListener((ActionEvent ae) -> saveReducedData(getEntry(indices[0])));
        result.add(menuitem);
  
        menuitem = new JMenuItem("Use reduced data...");
        menuitem.setEnabled((indices.length == 1) && !getEntry(indices[0]).isCrossValidation() && getEntry(indices[0]).hasFull());
        menuitem.addActionListener((ActionEvent ae) -> useReducedData(getEntry(indices[0])));
        result.add(menuitem);
  
        menuitem = new JMenuItem("Regenerate output" + (m_Owner.getOutputGenerators().length > 1 ? "s" : ""));
        menuitem.setEnabled((indices.length >= 1));
        menuitem.addActionListener((ActionEvent ae) -> { for (int index: indices) regenerateOutput(getEntry(index)); });
        result.add(menuitem);
  
        submenu = new BaseMenu("Additional output");
        classes = ClassLister.getSingleton().getClasses(AbstractOutputGenerator.class);
        for (Class cls: classes) {
  	try {
  	  final AbstractOutputGenerator generator = (AbstractOutputGenerator) cls.getDeclaredConstructor().newInstance();
  