package beast.app.draw; import java.awt.Color; import java.awt.Component; import java.awt.Dimension; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import javax.swing.BorderFactory; import javax.swing.Box; import javax.swing.BoxLayout; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JTextField; import javax.swing.SwingConstants; import javax.swing.border.Border; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; import javax.swing.event.ListSelectionListener; import beast.app.beauti.BeautiDoc; import beast.app.beauti.BeautiPanel; import beast.app.beauti.BeautiPanelConfig; import beast.core.BEASTInterface; import beast.core.Input; import beast.core.util.Log; /** * Base class for editors that provide a GUI for manipulating an Input for a BEASTObject. * The idea is that for every type of Input there will be a dedicated editor, e.g. * for a String Input, there will be an edit field, for a Boolean Input, there will * be a checkbox in the editor. * <p/> * The default just provides an edit field and uses toString() on Input to get its value. * To change the behaviour, override * public void init(Input<?> input, BEASTObject beastObject, int itemNr, ExpandOption isExpandOption, boolean addButtons) */ /** note that it is assumed that any InputEditor is a java.awt.Component **/ public interface InputEditor { final public static String NO_VALUE = "<none>"; public enum ExpandOption {TRUE, TRUE_START_COLLAPSED, FALSE, IF_ONE_ITEM} public enum ButtonStatus {ALL, NONE, DELETE_ONLY, ADD_ONLY} public enum ValidationStatus { IS_VALID, IS_INVALID, HAS_INVALIDMEMBERS } /** type of BEASTObject to which this editor can be used **/ Class<?> type(); /** list of types of BEASTObjects to which this editor can be used **/ Class<?>[] types(); /** initialise InputEditor * @param input to be edited * @param beastObject parent beastObject containing the input * @param itemNr if the input is a list, itemNr indicates which item to edit in the list * @param isExpandOption start state of input editor * @param addButtons button status of input editor */ void init(Input<?> input, BEASTInterface beastObject, int itemNr, ExpandOption isExpandOption, boolean addButtons); /** set document with the model containing the input **/ void setDoc(BeautiDoc doc); /** * set decoration. This method is deprecated, because decoration can be handled by the JComponent with setBorder method on **/ //@Deprecated //void setBorder(Border border); /** prepare to validate input **/ void startValidating(ValidationStatus state); /** validate input and update status of input editor if necessary **/ void validateInput(); /** add input editor to listen for changes **/ void addValidationListener(InputEditor validateListener); /** propagate status of predecessor inputs through list of beastObjects **/ void notifyValidationListeners(ValidationStatus state); Component getComponent(); public abstract class Base extends JPanel implements InputEditor { private static final long serialVersionUID = 1L; /** * the input to be edited * */ protected Input<?> m_input; /** * parent beastObject * */ protected BEASTInterface m_beastObject; /** * text field used for primitive input editors * */ protected JTextField m_entry; protected int itemNr; public JTextField getEntry() { return m_entry; } JLabel m_inputLabel; protected static Dimension PREFERRED_SIZE = new Dimension(200, 25); protected static Dimension MAX_SIZE = new Dimension(1024, 25); /** * flag to indicate label, edit and validate buttons/labels should be added * */ protected boolean m_bAddButtons = true; /** * label that shows up when validation fails * */ protected SmallLabel m_validateLabel; /** * document that we are editing * */ protected BeautiDoc doc; /** * list of objects that want to be notified of the validation state when it changes * */ List<InputEditor> m_validateListeners; @Override public void addValidationListener(InputEditor validateListener) { if (m_validateListeners == null) { m_validateListeners = new ArrayList<>(); } m_validateListeners.add(validateListener); } @Override public void notifyValidationListeners(ValidationStatus state) { if (m_validateListeners != null) { for (InputEditor listener : m_validateListeners) { listener.startValidating(state); } } } // TODO this should not be static. Better if it was an instance variable, // TODO since its currently set by an input of BeautiPanelConfig, which can be different for each BeautiPanel. public static int g_nLabelWidth = 150; public Base(BeautiDoc doc) { setLayout(new BoxLayout(this, BoxLayout.X_AXIS)); this.doc = doc; if (doc != null) { doc.currentInputEditors.add(this); } } // c'tor protected BeautiDoc getDoc() { if (doc == null) { Component c = this; while (c.getParent() != null) { c = c.getParent(); if (c instanceof BeautiPanel) { doc = ((BeautiPanel) c).getDoc(); } } } return doc; } /** * return class the editor is suitable for. * Either implement type() or types() if multiple * types are supported * */ @Override abstract public Class<?> type(); @Override public Class<?>[] types() { Class<?>[] types = new Class<?>[1]; types[0] = type(); return types; } /** * construct an editor consisting of a label and input entry * */ @Override public void init(Input<?> input, BEASTInterface beastObject, int itemNr, ExpandOption isExpandOption, boolean addButtons) { m_bAddButtons = addButtons; m_input = input; m_beastObject = beastObject; this.itemNr= itemNr; addInputLabel(); setUpEntry(); add(m_entry); add(Box.createHorizontalGlue()); addValidationLabel(); } // init void setUpEntry() { m_entry = new JTextField(); m_entry.setName(m_input.getName()); m_entry.setColumns(10); // Dimension prefDim = new Dimension(PREFERRED_SIZE.width, m_entry.getPreferredSize().height); // Dimension maxDim = new Dimension(MAX_SIZE.width, m_entry.getPreferredSize().height); // m_entry.setMinimumSize(prefDim); // m_entry.setPreferredSize(prefDim); // m_entry.setSize(prefDim); initEntry(); m_entry.setToolTipText(m_input.getHTMLTipText()); Dimension maxDim = m_entry.getPreferredSize(); m_entry.setMaximumSize(maxDim); m_entry.getDocument().addDocumentListener(new DocumentListener() { @Override public void removeUpdate(DocumentEvent e) { processEntry(); } @Override public void insertUpdate(DocumentEvent e) { processEntry(); } @Override public void changedUpdate(DocumentEvent e) { processEntry(); } }); } protected void initEntry() { if (m_input.get() != null) { m_entry.setText(m_input.get().toString()); } } @SuppressWarnings({ "unchecked", "rawtypes" }) protected void setValue(Object o) { if (itemNr < 0) { m_input.setValue(o, m_beastObject); } else { // set value of an item in a list List list = (List) m_input.get(); Object other = list.get(itemNr); if (other != o) { if (other instanceof BEASTInterface) { BEASTInterface.getOutputs(other).remove(m_beastObject); } list.set(itemNr, o); if (o instanceof BEASTInterface) { BEASTInterface.getOutputs(o).add(m_beastObject); } } } } protected void processEntry() { try { setValue(m_entry.getText()); validateInput(); m_entry.requestFocusInWindow(); } catch (Exception ex) { // JOptionPane.showMessageDialog(null, "Error while setting " + m_input.getName() + ": " + ex.getMessage() + // " Leaving value at " + m_input.get()); // m_entry.setText(m_input.get() + ""); if (m_validateLabel != null) { m_validateLabel.setVisible(true); m_validateLabel.setToolTipText("<html><p>Parsing error: " + ex.getMessage() + ". Value was left at " + m_input.get() + ".</p></html>"); m_validateLabel.m_circleColor = Color.orange; } repaint(); } } protected void addInputLabel() { if (m_bAddButtons) { String name = formatName(m_input.getName()); addInputLabel(name, m_input.getHTMLTipText()); } } protected String formatName(String name) { if (doc.beautiConfig.inputLabelMap.containsKey(m_beastObject.getClass().getName() + "." + name)) { name = doc.beautiConfig.inputLabelMap.get(m_beastObject.getClass().getName() + "." + name); } else { name = name.replaceAll("([a-z])([A-Z])", "$1 $2"); name = name.substring(0, 1).toUpperCase() + name.substring(1); } return name; } protected void addInputLabel(String label, String tipText) { if (m_bAddButtons) { m_inputLabel = new JLabel(label); m_inputLabel.setToolTipText(tipText); m_inputLabel.setHorizontalTextPosition(SwingConstants.RIGHT); //Dimension size = new Dimension(g_nLabelWidth, 20); int fontsize = m_inputLabel.getFont().getSize(); Dimension size = new Dimension(200 * fontsize / 13, 20 * fontsize / 13); m_inputLabel.setMaximumSize(size); m_inputLabel.setMinimumSize(size); m_inputLabel.setPreferredSize(size); m_inputLabel.setBorder(BorderFactory.createEmptyBorder(0, 5, 0, 5)); // m_inputLabel.setSize(size); // m_inputLabel.setBorder(BorderFactory.createLineBorder(Color.RED, 2)); // RRB: temporary //m_inputLabel.setBorder(BorderFactory.createBevelBorder(BevelBorder.RAISED)); add(m_inputLabel); } } protected void addValidationLabel() { if (m_bAddButtons) { m_validateLabel = new SmallLabel("x", new Color(200, 0, 0)); add(m_validateLabel); m_validateLabel.setVisible(true); validateInput(); } } /* check the input is valid, continue checking recursively */ protected void validateAllEditors() { for (InputEditor editor : doc.currentInputEditors) { editor.validateInput(); } } @Override public void validateInput() { try { m_input.validate(); if (m_entry != null && !m_input.canSetValue(m_entry.getText(), m_beastObject)) { throw new IllegalArgumentException("invalid value"); } // recurse try { validateRecursively(m_input, new HashSet<>()); } catch (Exception e) { notifyValidationListeners(ValidationStatus.HAS_INVALIDMEMBERS); if (m_validateLabel != null) { m_validateLabel.setVisible(true); m_validateLabel.setToolTipText("<html><p>Recursive error in " + e.getMessage() + "</p></html>"); m_validateLabel.m_circleColor = Color.orange; } repaint(); return; } if (m_validateLabel != null) { m_validateLabel.setVisible(false); } notifyValidationListeners(ValidationStatus.IS_VALID); } catch (Exception e) { Log.err.println("Validation message: " + e.getMessage()); if (m_validateLabel != null) { m_validateLabel.setToolTipText(e.getMessage()); m_validateLabel.m_circleColor = Color.red; m_validateLabel.setVisible(true); } notifyValidationListeners(ValidationStatus.IS_INVALID); } repaint(); } /* Recurse in any of the input beastObjects * and validate its inputs */ void validateRecursively(Input<?> input, Set<Input<?>> done) { if (done.contains(input)) { // this prevent cycles to lock up validation return; } else { done.add(input); } if (input.get() != null) { if (input.get() instanceof BEASTInterface) { BEASTInterface beastObject = ((BEASTInterface) input.get()); for (Input<?> input2 : beastObject.listInputs()) { try { input2.validate(); } catch (Exception e) { throw new IllegalArgumentException(((BEASTInterface) input.get()).getID() + "</p><p> " + e.getMessage()); } validateRecursively(input2, done); } } if (input.get() instanceof List<?>) { for (Object o : (List<?>) input.get()) { if (o != null && o instanceof BEASTInterface) { BEASTInterface beastObject = (BEASTInterface) o; for (Input<?> input2 : beastObject.listInputs()) { try { input2.validate(); } catch (Exception e) { throw new IllegalArgumentException(((BEASTInterface) o).getID() + " " + e.getMessage()); } validateRecursively(input2, done); } } } } } } // validateRecursively @Override public void startValidating(ValidationStatus state) { validateInput(); } public void refreshPanel() { Component c = this; while (c.getParent() != null) { c = c.getParent(); if (c instanceof ListSelectionListener) { ((ListSelectionListener) c).valueChanged(null); } } } /** * synchronise values in panel with current network * */ protected void sync() { Component c = this; while (c.getParent() != null) { c = c.getParent(); if (c instanceof BeautiPanel) { BeautiPanel panel = (BeautiPanel) c; BeautiPanelConfig cfgPanel = panel.config; cfgPanel.sync(panel.partitionIndex); } } } // we should leave it to the component to set its own border @Override @Deprecated public void setBorder(Border border) { super.setBorder(border); } @Override public void setDoc(BeautiDoc doc) { this.doc = doc; } // what is this method for? We should leave repainting to the standard mechanism // RRB: Did not always work in the past. The following should suffice (though perhaps // slightly less efficient to also revalidate, but have not noticed any difference) @Override public void repaint() { // tell Swing that an area of the window is dirty super.repaint(); // tell the layout manager to recalculate the layout super.revalidate(); } @Override public Component getComponent() { return this; } } // class InputEditor.Base } // InputEditor interface