package beast.app.draw; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; import java.awt.Dimension; import java.awt.Image; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import javax.imageio.ImageIO; import javax.swing.BorderFactory; import javax.swing.Box; import javax.swing.BoxLayout; import javax.swing.JLabel; import javax.swing.JOptionPane; import javax.swing.JTextField; import javax.swing.SwingConstants; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; import beast.app.beauti.BeautiDoc; import beast.core.BEASTInterface; import beast.core.Input; import beast.core.util.Log; public class ListInputEditor extends InputEditor.Base { private static final long serialVersionUID = 1L; static Image DOWN_ICON; static Image RIGHT_ICON; { try { java.net.URL downURL = ClassLoader.getSystemResource(ModelBuilder.ICONPATH + "down.png"); DOWN_ICON = ImageIO.read(downURL); java.net.URL leftURL = ClassLoader.getSystemResource(ModelBuilder.ICONPATH + "right.png"); RIGHT_ICON = ImageIO.read(leftURL); } catch (Exception e) { e.printStackTrace(); } } protected ButtonStatus m_buttonStatus = ButtonStatus.ALL; /** * buttons for manipulating the list of inputs * */ protected SmallButton addButton; protected List<JTextField> m_entries; protected List<SmallButton> delButtonList; protected List<SmallButton> m_editButton; protected List<SmallLabel> m_validateLabels; protected Box m_listBox; protected ExpandOption m_bExpandOption; // the box containing any buttons protected Box buttonBox; static protected Set<String> g_collapsedIDs = new HashSet<>(); static Set<String> g_initiallyCollapsedIDs = new HashSet<>(); public abstract class ActionListenerObject implements ActionListener { public Object m_o; public ActionListenerObject(Object o) { super(); m_o = o; } } public abstract class ExpandActionListener implements ActionListener { Box m_box; BEASTInterface m_beastObject; public ExpandActionListener(Box box, BEASTInterface beastObject) { super(); m_box = box; m_beastObject = beastObject; } } //public ListInputEditor() {} public ListInputEditor(BeautiDoc doc) { super(doc); m_entries = new ArrayList<>(); delButtonList = new ArrayList<>(); m_editButton = new ArrayList<>(); m_validateLabels = new ArrayList<>(); m_bExpandOption = ExpandOption.FALSE; setLayout(new BoxLayout(this, BoxLayout.Y_AXIS)); } @Override public Class<?> type() { return ArrayList.class; } /** * return type of the list * */ public Class<?> baseType() { return BEASTInterface.class; } /** * construct an editor consisting of * o a label * o a button for selecting another plug-in * o a set of buttons for adding, deleting, editing items in the list */ @Override public void init(Input<?> input, BEASTInterface beastObject, int itemNr, ExpandOption isExpandOption, boolean addButtons) { m_bAddButtons = addButtons; m_bExpandOption = isExpandOption; m_input = input; m_beastObject = beastObject; this.itemNr = -1; addInputLabel(); if (m_inputLabel != null) { m_inputLabel.setMaximumSize(new Dimension(m_inputLabel.getSize().width, 1000)); m_inputLabel.setAlignmentY(1.0f); m_inputLabel.setVerticalAlignment(SwingConstants.TOP); m_inputLabel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); } m_listBox = Box.createVerticalBox(); // list of inputs for (Object o : (List<?>) input.get()) { if (o instanceof BEASTInterface) { BEASTInterface beastObject2 = (BEASTInterface) o; addSingleItem(beastObject2); } } setLayout(new BorderLayout()); add(m_listBox, BorderLayout.NORTH); buttonBox = Box.createHorizontalBox(); if (m_buttonStatus == ButtonStatus.ALL || m_buttonStatus == ButtonStatus.ADD_ONLY) { addButton = new SmallButton("+", true); addButton.setName("+"); addButton.setToolTipText("Add item to the list"); addButton.addActionListener(e -> addItem()); buttonBox.add(addButton); if (!doc.isExpertMode()) { // if nothing can be added, make add button invisible List<String> tabuList = new ArrayList<>(); for (int i = 0; i < m_entries.size(); i++) { tabuList.add(m_entries.get(i).getText()); } List<String> beastObjectNames = doc.getInputEditorFactory().getAvailablePlugins(m_input, m_beastObject, tabuList, doc); if (beastObjectNames.size() == 0) { addButton.setVisible(false); } } } // add validation label at the end of a list m_validateLabel = new SmallLabel("x", new Color(200, 0, 0)); if (m_bAddButtons) { buttonBox.add(m_validateLabel); m_validateLabel.setVisible(true); validateInput(); } buttonBox.add(Box.createHorizontalGlue()); m_listBox.add(buttonBox); updateState(); // // RRB: is there a better way to ensure lists are not spaced out across all available space? // JFrame frame = doc.getFrame(); // if (frame != null) { // m_listBox.add(Box.createVerticalStrut(frame.getHeight() - 150)); // } } // init protected void addSingleItem(BEASTInterface beastObject) { Box itemBox = Box.createHorizontalBox(); InputEditor editor = addPluginItem(itemBox, beastObject); SmallButton editButton = new SmallButton("e", true, SmallButton.ButtonType.square); editButton.setName(beastObject.getID() + ".editButton"); if (m_bExpandOption == ExpandOption.FALSE || m_bExpandOption == ExpandOption.IF_ONE_ITEM && ((List<?>) m_input.get()).size() > 1) { editButton.setToolTipText("Edit item in the list"); editButton.addActionListener(new ActionListenerObject(beastObject) { @Override public void actionPerformed(ActionEvent e) { m_o = editItem(m_o); } }); } else { editButton.setText(""); editButton.setToolTipText("Expand/collapse item in the list"); editButton.setButtonType(SmallButton.ButtonType.toolbar); } m_editButton.add(editButton); itemBox.add(editButton); SmallLabel validateLabel = new SmallLabel("x", new Color(200, 0, 0)); itemBox.add(validateLabel); validateLabel.setVisible(true); m_validateLabels.add(validateLabel); // AJD: This is not consistent with Mac OS X look and feel, and its not necessary //itemBox.setBorder(BorderFactory.createEtchedBorder()); if (m_bExpandOption == ExpandOption.TRUE || m_bExpandOption == ExpandOption.TRUE_START_COLLAPSED || (m_bExpandOption == ExpandOption.IF_ONE_ITEM && ((List<?>) m_input.get()).size() == 1)) { Box expandBox = Box.createVerticalBox(); //box.add(itemBox); doc.getInputEditorFactory().addInputs(expandBox, beastObject, editor, null, doc); //System.err.print(expandBox.getComponentCount()); if (expandBox.getComponentCount() > 1) { // only go here if it is worth showing expanded box //expandBox.setBorder(BorderFactory.createMatteBorder(1, 5, 1, 1, Color.gray)); //itemBox = box; Box box2 = Box.createVerticalBox(); box2.add(itemBox); itemBox.add(editButton, 0); box2.add(expandBox); // expandBox.setVisible(false); // //itemBox.remove(editButton); // editButton.setVisible(false); // } else { itemBox = box2; } else { editButton.setVisible(false); } editButton.addActionListener(new ExpandActionListener(expandBox, beastObject) { @Override public void actionPerformed(ActionEvent e) { SmallButton editButton = (SmallButton) e.getSource(); m_box.setVisible(!m_box.isVisible()); if (m_box.isVisible()) { try { editButton.setImg(DOWN_ICON); }catch (Exception e2) { // TODO: handle exception } g_collapsedIDs.remove(m_beastObject.getID()); } else { try { editButton.setImg(RIGHT_ICON); }catch (Exception e2) { // TODO: handle exception } g_collapsedIDs.add(m_beastObject.getID()); } } }); String id = beastObject.getID(); expandBox.setVisible(!g_collapsedIDs.contains(id)); try { if (expandBox.isVisible()) { editButton.setImg(DOWN_ICON); } else { editButton.setImg(RIGHT_ICON); } } catch (Exception e) { // TODO: handle exception } } else { if (BEASTObjectPanel.countInputs(beastObject, doc) == 0) { editButton.setVisible(false); } } if (m_validateLabel == null) { m_listBox.add(itemBox); } else { Component c = m_listBox.getComponent(m_listBox.getComponentCount() - 1); m_listBox.remove(c); m_listBox.add(itemBox); m_listBox.add(c); } } // addSingleItem /** * add components to box that are specific for the beastObject. * By default, this just inserts a label with the beastObject ID * * @param itemBox box to add components to * @param beastObject beastObject to add */ protected InputEditor addPluginItem(Box itemBox, BEASTInterface beastObject) { String name = beastObject.getID(); if (name == null || name.length() == 0) { name = beastObject.getClass().getName(); name = name.substring(name.lastIndexOf('.') + 1); } JLabel label = new JLabel(name); itemBox.add(Box.createRigidArea(new Dimension(5, 1))); itemBox.add(label); itemBox.add(Box.createHorizontalGlue()); return this; } class IDDocumentListener implements DocumentListener { BEASTInterface m_beastObject; JTextField m_entry; IDDocumentListener(BEASTInterface beastObject, JTextField entry) { m_beastObject = beastObject; m_entry = entry; } @Override public void removeUpdate(DocumentEvent e) { processEntry(); } @Override public void insertUpdate(DocumentEvent e) { processEntry(); } @Override public void changedUpdate(DocumentEvent e) { processEntry(); } void processEntry() { String oldID = m_beastObject.getID(); m_beastObject.setID(m_entry.getText()); BEASTObjectPanel.renamePluginID(m_beastObject, oldID, m_beastObject.getID(), doc); validateAllEditors(); m_entry.requestFocusInWindow(); } } protected void addItem() { List<String> tabuList = new ArrayList<>(); for (int i = 0; i < m_entries.size(); i++) { tabuList.add(m_entries.get(i).getText()); } List<BEASTInterface> beastObjects = pluginSelector(m_input, m_beastObject, tabuList); if (beastObjects != null) { for (BEASTInterface beastObject : beastObjects) { try { setValue(beastObject); //m_input.setValue(beastObject, m_beastObject); } catch (Exception ex) { Log.err.println(ex.getClass().getName() + " " + ex.getMessage()); } addSingleItem(beastObject); getDoc().addPlugin(beastObject); } validateInput(); updateState(); repaint(); } } // addItem protected Object editItem(Object o) { int i = ((List<?>) m_input.get()).indexOf(o); BEASTInterface beastObject = (BEASTInterface) ((List<?>) m_input.get()).get(i); BEASTObjectDialog dlg = new BEASTObjectDialog(beastObject, m_input.getType(), doc); if (dlg.showDialog()) { //m_labels.get(i).setText(dlg.m_panel.m_beastObject.getID()); if (m_entries.size() > i) { m_entries.get(i).setText(dlg.m_panel.m_beastObject.getID()); } //o = dlg.m_panel.m_beastObject; dlg.accept((BEASTInterface) o, doc); refreshPanel(); } BEASTObjectPanel.m_position.x -= 20; BEASTObjectPanel.m_position.y -= 20; //checkValidation(); validateAllEditors(); updateState(); doLayout(); return o; } // editItem protected void deleteItem(Object o) { int i = ((List<?>) m_input.get()).indexOf(o); m_listBox.remove(i); ((List<?>) m_input.get()).remove(i); //safeRemove(m_labels, i); safeRemove(m_entries, i); safeRemove(delButtonList, i); safeRemove(m_editButton, i); safeRemove(m_validateLabels, i); validateInput(); updateState(); doLayout(); repaint(); } // deleteItem private void safeRemove(List<?> list, int i) { if (list.size() > i) { list.remove(i); } } /** * Select existing plug-in, or create a new one. * Suppress existing plug-ins with IDs from the taboo list. * Return null if nothing is selected. */ protected List<BEASTInterface> pluginSelector(Input<?> input, BEASTInterface parent, List<String> tabooList) { List<BEASTInterface> selectedPlugins = new ArrayList<>(); List<String> beastObjectNames = doc.getInputEditorFactory().getAvailablePlugins(input, parent, tabooList, doc); /* select a beastObject **/ String className = null; if (beastObjectNames.size() == 1) { // if there is only one candidate, select that one className = beastObjectNames.get(0); } else if (beastObjectNames.size() == 0) { // no candidate => we cannot be in expert mode // create a new BEASTObject doc.setExpertMode(true); beastObjectNames = doc.getInputEditorFactory().getAvailablePlugins(input, parent, tabooList, doc); doc.setExpertMode(false); className = beastObjectNames.get(0); } else { // otherwise, pop up a list box className = (String) JOptionPane.showInputDialog(null, "Select a constant", "select", JOptionPane.PLAIN_MESSAGE, null, beastObjectNames.toArray(new String[0]), null); if (className == null) { return null; } } if (!className.startsWith("new ")) { /* return existing beastObject */ selectedPlugins.add(doc.pluginmap.get(className)); return selectedPlugins; } /* create new beastObject */ try { BEASTInterface beastObject = (BEASTInterface) Class.forName(className.substring(4)).newInstance(); BEASTObjectPanel.addPluginToMap(beastObject, doc); selectedPlugins.add(beastObject); return selectedPlugins; } catch (Exception ex) { JOptionPane.showMessageDialog(null, "Could not select beastObject: " + ex.getClass().getName() + " " + ex.getMessage() ); return null; } } // pluginSelector protected void updateState() { for (int i = 0; i < ((List<?>) m_input.get()).size(); i++) { try { BEASTInterface beastObject = (BEASTInterface) ((List<?>) m_input.get()).get(i); beastObject.validateInputs(); m_validateLabels.get(i).setVisible(false); } catch (IndexOutOfBoundsException e) { // happens when m_validateLabels is not large enough, so there is nothing to show } catch (Exception e) { // something went wrong, so show label if available if (m_validateLabels.size() > i) { m_validateLabels.get(i).setToolTipText(e.getMessage()); m_validateLabels.get(i).setVisible(true); } } } validateInput(); // this triggers properly re-layouting after an edit action setVisible(false); setVisible(true); } // updateState @Override public void startValidating(ValidationStatus state) { updateState(); } public void setButtonStatus(ButtonStatus buttonStatus) { m_buttonStatus = buttonStatus; } } // class ListPluginInputEditor