package beast.app.beauti; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; import java.awt.Dimension; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.io.File; import java.util.ArrayList; import java.util.EventObject; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import javax.swing.BorderFactory; import javax.swing.Box; import javax.swing.DefaultCellEditor; import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JComboBox; import javax.swing.JComponent; import javax.swing.JOptionPane; import javax.swing.JScrollPane; import javax.swing.JTable; import javax.swing.JTextField; import javax.swing.ListSelectionModel; import javax.swing.SwingUtilities; import javax.swing.UIManager; import javax.swing.border.Border; import javax.swing.event.CellEditorListener; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; import javax.swing.table.TableCellEditor; import javax.swing.table.TableCellRenderer; import javax.swing.table.TableColumn; import beast.app.draw.ListInputEditor; import beast.app.draw.SmallButton; import beast.app.util.FileDrop; import beast.core.BEASTInterface; import beast.core.Input; import beast.core.MCMC; import beast.core.State; import beast.core.StateNode; import beast.core.util.CompoundDistribution; import beast.core.util.Log; import beast.evolution.alignment.Alignment; import beast.evolution.alignment.FilteredAlignment; import beast.evolution.alignment.Taxon; import beast.evolution.branchratemodel.BranchRateModel; import beast.evolution.likelihood.GenericTreeLikelihood; import beast.evolution.sitemodel.SiteModel; import beast.evolution.sitemodel.SiteModelInterface; import beast.evolution.tree.TreeInterface; // TODO: add useAmbiguities flag // TODO: add warning if useAmbiguities=false and nr of patterns=1 (happens when all data is ambiguous) public class AlignmentListInputEditor extends ListInputEditor { private static final long serialVersionUID = 1L; final static int NAME_COLUMN = 0; final static int FILE_COLUMN = 1; final static int TAXA_COLUMN = 2; final static int SITES_COLUMN = 3; final static int TYPE_COLUMN = 4; final static int SITEMODEL_COLUMN = 5; final static int CLOCKMODEL_COLUMN = 6; final static int TREE_COLUMN = 7; final static int USE_AMBIGUITIES_COLUMN = 8; final static int NR_OF_COLUMNS = 9; final static int STRUT_SIZE = 5; /** * alignments that form a partition. These can be FilteredAlignments * */ List<Alignment> alignments; int partitionCount; GenericTreeLikelihood[] likelihoods; Object[][] tableData; JTable table; JTextField nameEditor; List<JButton> linkButtons; List<JButton> unlinkButtons; JButton splitButton; /** * The button for deleting an alignment in the alignment list. */ JButton delButton; protected SmallButton replaceButton; private JScrollPane scrollPane; public AlignmentListInputEditor(BeautiDoc doc) { super(doc); } @Override public Class<?> type() { return List.class; } @Override public Class<?> baseType() { return Alignment.class; } @Override public Class<?>[] types() { Class<?>[] types = new Class[2]; types[0] = List.class; types[1] = Alignment.class; return types; } @Override @SuppressWarnings("unchecked") public void init(Input<?> input, BEASTInterface beastObject, int itemNr, ExpandOption isExpandOption, boolean addButtons) { this.itemNr = itemNr; if (input.get() instanceof List) { alignments = (List<Alignment>) input.get(); } else { // we just have a single Alignment alignments = new ArrayList<>(); alignments.add((Alignment) input.get()); } linkButtons = new ArrayList<>(); unlinkButtons = new ArrayList<>(); partitionCount = alignments.size(); // override BoxLayout in superclass setLayout(new BorderLayout()); add(createLinkButtons(), BorderLayout.NORTH); add(createListBox(), BorderLayout.CENTER); //Box box = Box.createVerticalBox(); //box.add(Box.createVerticalStrut(STRUT_SIZE)); //box.add(createLinkButtons()); //box.add(Box.createVerticalStrut(STRUT_SIZE)); //box.add(createListBox()); //box.add(Box.createVerticalStrut(STRUT_SIZE)); //box.add(Box.createVerticalGlue()); //add(box, BorderLayout.CENTER); Color focusColor = UIManager.getColor("Focus.color"); Border focusBorder = BorderFactory.createMatteBorder(2, 2, 2, 2, focusColor); new FileDrop(null, scrollPane, focusBorder, new FileDrop.Listener() { @Override public void filesDropped(java.io.File[] files) { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { addItem(files); } }); } // end filesDropped }); // end FileDrop.Listener // this should place the add/remove/split buttons at the bottom of the window. add(createAddRemoveSplitButtons(), BorderLayout.SOUTH); updateStatus(); } /** * Creates the link/unlink button component * @return a box containing three link/unlink button pairs. */ private JComponent createLinkButtons() { Box box = Box.createHorizontalBox(); addLinkUnlinkPair(box, "Site Models"); box.add(Box.createHorizontalStrut(STRUT_SIZE)); addLinkUnlinkPair(box, "Clock Models"); box.add(Box.createHorizontalStrut(STRUT_SIZE)); addLinkUnlinkPair(box, "Trees"); box.add(Box.createHorizontalGlue()); return box; } private JComponent createAddRemoveSplitButtons() { Box buttonBox = Box.createHorizontalBox(); addButton = new SmallButton("+", true, SmallButton.ButtonType.square); addButton.setName("+"); addButton.setToolTipText("Add item to the list"); addButton.addActionListener(e -> addItem()); buttonBox.add(Box.createHorizontalStrut(STRUT_SIZE)); buttonBox.add(addButton); buttonBox.add(Box.createHorizontalStrut(STRUT_SIZE)); delButton = new SmallButton("-", true, SmallButton.ButtonType.square); delButton.setName("-"); delButton.setToolTipText("Delete selected items from the list"); delButton.addActionListener(e -> { if (doc.hasLinkedAtLeastOnce) { JOptionPane.showMessageDialog(null, "Cannot delete partition while parameters are linked"); return; } delItem(); }); buttonBox.add(delButton); buttonBox.add(Box.createHorizontalStrut(STRUT_SIZE)); replaceButton = new SmallButton("r", true, SmallButton.ButtonType.square); replaceButton.setName("r"); replaceButton.setToolTipText("Replace alignment by one loaded from file"); replaceButton.addActionListener(e -> replaceItem()); buttonBox.add(Box.createHorizontalStrut(STRUT_SIZE)); buttonBox.add(replaceButton); buttonBox.add(Box.createHorizontalStrut(STRUT_SIZE)); splitButton = new JButton("Split"); splitButton.setName("Split"); splitButton.setToolTipText("Split alignment into partitions, for example, codon positions"); splitButton.addActionListener(e -> splitItem()); buttonBox.add(splitButton); buttonBox.add(Box.createHorizontalGlue()); return buttonBox; } /** * This method just adds the two buttons (with add()) and does not add any glue or struts before or after. * @param box * @param label */ private void addLinkUnlinkPair(Box box, String label) { //JLabel label = new JLabel(label+":"); //box.add(label); JButton linkSModelButton = new JButton("Link " + label); linkSModelButton.setName("Link " + label); linkSModelButton.addActionListener(e -> { JButton button = (JButton) e.getSource(); link(columnLabelToNr(button.getText())); table.repaint(); }); box.add(linkSModelButton); linkSModelButton.setEnabled(!getDoc().hasLinkedAtLeastOnce); JButton unlinkSModelButton = new JButton("Unlink " + label); unlinkSModelButton.setName("Unlink " + label); unlinkSModelButton.addActionListener(e -> { JButton button = (JButton) e.getSource(); unlink(columnLabelToNr(button.getText())); table.repaint(); }); box.add(unlinkSModelButton); unlinkSModelButton.setEnabled(!getDoc().hasLinkedAtLeastOnce); linkButtons.add(linkSModelButton); unlinkButtons.add(unlinkSModelButton); } private int columnLabelToNr(String column) { int columnNr; if (column.contains("Tree")) { columnNr = TREE_COLUMN; } else if (column.contains("Clock")) { columnNr = CLOCKMODEL_COLUMN; } else { columnNr = SITEMODEL_COLUMN; } return columnNr; } private void link(int columnNr) { int[] selected = getTableRowSelection(); // do the actual linking for (int i = 1; i < selected.length; i++) { int rowNr = selected[i]; link(columnNr, rowNr, selected[0]); } } /** links partition in row "rowToLink" with partition in "rowToLinkWith" so that * after linking there is only one partition for context "columnNr", namely that * of "rowToLinkWith" */ private void link(int columnNr, int rowToLink, int rowToLinkWith) { Object old = tableData[rowToLink][columnNr]; tableData[rowToLink][columnNr] = tableData[rowToLinkWith][columnNr]; try { updateModel(columnNr, rowToLink); } catch (Exception ex) { Log.warning.println(ex.getMessage()); // unlink if we could not link tableData[rowToLink][columnNr] = old; try { updateModel(columnNr, rowToLink); } catch (Exception ex2) { // ignore } } MRCAPriorInputEditor.customConnector(doc); } private void unlink(int columnNr) { int[] selected = getTableRowSelection(); for (int i = 1; i < selected.length; i++) { int rowNr = selected[i]; tableData[rowNr][columnNr] = getDoc().partitionNames.get(rowNr).partition; try { updateModel(columnNr, rowNr); } catch (Exception ex) { Log.err.println(ex.getMessage()); ex.printStackTrace(); } } } int[] getTableRowSelection() { return table.getSelectedRows(); } /** set partition of type columnNr to partition model nr rowNr **/ void updateModel(int columnNr, int rowNr) { Log.warning.println("updateModel: " + rowNr + " " + columnNr + " " + table.getSelectedRow() + " " + table.getSelectedColumn()); for (int i = 0; i < partitionCount; i++) { Log.warning.println(i + " " + tableData[i][0] + " " + tableData[i][SITEMODEL_COLUMN] + " " + tableData[i][CLOCKMODEL_COLUMN] + " " + tableData[i][TREE_COLUMN]); } getDoc(); String partition = (String) tableData[rowNr][columnNr]; // check if partition needs renaming String oldName = null; boolean isRenaming = false; try { switch (columnNr) { case SITEMODEL_COLUMN: if (!doc.pluginmap.containsKey("SiteModel.s:" + partition)) { String id = ((BEASTInterface)likelihoods[rowNr].siteModelInput.get()).getID(); oldName = BeautiDoc.parsePartition(id); doc.renamePartition(BeautiDoc.SITEMODEL_PARTITION, oldName, partition); isRenaming = true; } break; case CLOCKMODEL_COLUMN: { String id = likelihoods[rowNr].branchRateModelInput.get().getID(); String clockModelName = id.substring(0, id.indexOf('.')) + ".c:" + partition; if (!doc.pluginmap.containsKey(clockModelName)) { oldName = BeautiDoc.parsePartition(id); doc.renamePartition(BeautiDoc.CLOCKMODEL_PARTITION, oldName, partition); isRenaming = true; } } break; case TREE_COLUMN: if (!doc.pluginmap.containsKey("Tree.t:" + partition)) { String id = likelihoods[rowNr].treeInput.get().getID(); oldName = BeautiDoc.parsePartition(id); doc.renamePartition(BeautiDoc.TREEMODEL_PARTITION, oldName, partition); isRenaming = true; } break; } } catch (Exception e) { JOptionPane.showMessageDialog(this, "Cannot rename item: " + e.getMessage()); tableData[rowNr][columnNr] = oldName; return; } if (isRenaming) { doc.determinePartitions(); initTableData(); setUpComboBoxes(); table.repaint(); return; } int partitionID = BeautiDoc.ALIGNMENT_PARTITION; switch (columnNr) { case SITEMODEL_COLUMN: partitionID = BeautiDoc.SITEMODEL_PARTITION; break; case CLOCKMODEL_COLUMN: partitionID = BeautiDoc.CLOCKMODEL_PARTITION; break; case TREE_COLUMN: partitionID = BeautiDoc.TREEMODEL_PARTITION; break; } int partitionNr = doc.getPartitionNr(partition, partitionID); GenericTreeLikelihood treeLikelihood = null; if (partitionNr >= 0) { // we ar linking treeLikelihood = likelihoods[partitionNr]; } // (TreeLikelihood) doc.pluginmap.get("treeLikelihood." + // tableData[rowNr][NAME_COLUMN]); boolean needsRePartition = false; PartitionContext oldContext = new PartitionContext(this.likelihoods[rowNr]); switch (columnNr) { case SITEMODEL_COLUMN: { SiteModelInterface siteModel = null; if (treeLikelihood != null) { // getDoc().getPartitionNr(partition, // BeautiDoc.SITEMODEL_PARTITION) != // rowNr) { siteModel = treeLikelihood.siteModelInput.get(); } else { siteModel = (SiteModel) doc.pluginmap.get("SiteModel.s:" + partition); if (siteModel != likelihoods[rowNr].siteModelInput.get()) { PartitionContext context = getPartitionContext(rowNr); try { siteModel = (SiteModel.Base) BeautiDoc.deepCopyPlugin((BEASTInterface) likelihoods[rowNr].siteModelInput.get(), likelihoods[rowNr], (MCMC) doc.mcmc.get(), oldContext, context, doc, null); } catch (RuntimeException e) { JOptionPane.showMessageDialog(this, "Could not clone site model: " + e.getMessage()); return; } } } SiteModelInterface target = this.likelihoods[rowNr].siteModelInput.get(); if (target instanceof SiteModel.Base && siteModel instanceof SiteModel.Base) { if (!((SiteModel.Base)target).substModelInput.canSetValue(((SiteModel.Base)siteModel).substModelInput.get(), (SiteModel.Base) target)) { throw new IllegalArgumentException("Cannot link site model: substitution models are incompatible"); } } else { throw new IllegalArgumentException("Don't know how to link this site model"); } needsRePartition = (this.likelihoods[rowNr].siteModelInput.get() != siteModel); this.likelihoods[rowNr].siteModelInput.setValue(siteModel, this.likelihoods[rowNr]); partition = ((BEASTInterface)likelihoods[rowNr].siteModelInput.get()).getID(); partition = BeautiDoc.parsePartition(partition); getDoc().setCurrentPartition(BeautiDoc.SITEMODEL_PARTITION, rowNr, partition); } break; case CLOCKMODEL_COLUMN: { BranchRateModel clockModel = null; if (treeLikelihood != null) { // getDoc().getPartitionNr(partition, // BeautiDoc.CLOCKMODEL_PARTITION) // != rowNr) { clockModel = treeLikelihood.branchRateModelInput.get(); } else { clockModel = getDoc().getClockModel(partition); if (clockModel != likelihoods[rowNr].branchRateModelInput.get()) { PartitionContext context = getPartitionContext(rowNr); try { clockModel = (BranchRateModel) BeautiDoc.deepCopyPlugin(likelihoods[rowNr].branchRateModelInput.get(), likelihoods[rowNr], (MCMC) doc.mcmc.get(), oldContext, context, doc, null); } catch (RuntimeException e) { JOptionPane.showMessageDialog(this, "Could not clone clock model: " + e.getMessage()); return; } } } // make sure that *if* the clock model has a tree as input, it is // the same as // for the likelihood TreeInterface tree = null; for (Input<?> input : ((BEASTInterface) clockModel).listInputs()) { if (input.getName().equals("tree")) { tree = (TreeInterface) input.get(); } } if (tree != null && tree != this.likelihoods[rowNr].treeInput.get()) { JOptionPane.showMessageDialog(this, "Cannot link clock model with different trees"); throw new IllegalArgumentException("Cannot link clock model with different trees"); } needsRePartition = (this.likelihoods[rowNr].branchRateModelInput.get() != clockModel); this.likelihoods[rowNr].branchRateModelInput.setValue(clockModel, this.likelihoods[rowNr]); partition = likelihoods[rowNr].branchRateModelInput.get().getID(); partition = BeautiDoc.parsePartition(partition); getDoc().setCurrentPartition(BeautiDoc.CLOCKMODEL_PARTITION, rowNr, partition); } break; case TREE_COLUMN: { TreeInterface tree = null; if (treeLikelihood != null) { // getDoc().getPartitionNr(partition, // BeautiDoc.TREEMODEL_PARTITION) != // rowNr) { tree = treeLikelihood.treeInput.get(); } else { tree = (TreeInterface) doc.pluginmap.get("Tree.t:" + partition); if (tree != likelihoods[rowNr].treeInput.get()) { PartitionContext context = getPartitionContext(rowNr); try { tree = (TreeInterface) BeautiDoc.deepCopyPlugin((BEASTInterface) likelihoods[rowNr].treeInput.get(), likelihoods[rowNr], (MCMC) doc.mcmc.get(), oldContext, context, doc, null); } catch (RuntimeException e) { JOptionPane.showMessageDialog(this, "Could not clone tree model: " + e.getMessage()); return; } State state = ((MCMC) doc.mcmc.get()).startStateInput.get(); List<StateNode> stateNodes = new ArrayList<>(); stateNodes.addAll(state.stateNodeInput.get()); for (StateNode s : stateNodes) { if (s.getID().endsWith(".t:" + oldContext.tree) && !(s instanceof TreeInterface)) { try { @SuppressWarnings("unused") StateNode copy = (StateNode) BeautiDoc.deepCopyPlugin(s, likelihoods[rowNr], (MCMC) doc.mcmc.get(), oldContext, context, doc, null); } catch (RuntimeException e) { JOptionPane.showMessageDialog(this, "Could not clone tree model: " + e.getMessage()); return; } } } } } // sanity check: make sure taxon sets are compatible Taxon.assertSameTaxa(tree.getID(), tree.getTaxonset().getTaxaNames(), likelihoods[rowNr].dataInput.get().getID(), likelihoods[rowNr].dataInput.get().getTaxaNames()); needsRePartition = (this.likelihoods[rowNr].treeInput.get() != tree); Log.warning.println("needsRePartition = " + needsRePartition); if (needsRePartition) { TreeInterface oldTree = this.likelihoods[rowNr].treeInput.get(); List<TreeInterface> tModels = new ArrayList<>(); for (GenericTreeLikelihood likelihood : likelihoods) { if (likelihood.treeInput.get() == oldTree) { tModels.add(likelihood.treeInput.get()); } } if (tModels.size() == 1) { // remove old tree from model ((BEASTInterface)oldTree).setInputValue("estimate", false); // use toArray to prevent ConcurrentModificationException for (Object beastObject : BEASTInterface.getOutputs(oldTree).toArray()) { //.toArray(new BEASTInterface[0])) { for (Input<?> input : ((BEASTInterface)beastObject).listInputs()) { try { if (input.get() == oldTree) { if (input.getRule() != Input.Validate.REQUIRED) { input.setValue(tree/*null*/, (BEASTInterface) beastObject); //} else { //input.setValue(tree, (BEASTInterface) beastObject); } } else if (input.get() instanceof List) { @SuppressWarnings("unchecked") List<TreeInterface> list = (List<TreeInterface>) input.get(); if (list.contains(oldTree)) { // && input.getRule() != Validate.REQUIRED) { list.remove(oldTree); if (!list.contains(tree)) { list.add(tree); } } } } catch (Exception e) { e.printStackTrace(); } } } } } likelihoods[rowNr].treeInput.setValue(tree, likelihoods[rowNr]); // TreeDistribution d = getDoc().getTreePrior(partition); // CompoundDistribution prior = (CompoundDistribution) // doc.pluginmap.get("prior"); // if (!getDoc().posteriorPredecessors.contains(d)) { // prior.pDistributions.setValue(d, prior); // } partition = likelihoods[rowNr].treeInput.get().getID(); partition = BeautiDoc.parsePartition(partition); getDoc().setCurrentPartition(BeautiDoc.TREEMODEL_PARTITION, rowNr, partition); } } tableData[rowNr][columnNr] = partition; if (needsRePartition) { List<BeautiSubTemplate> templates = new ArrayList<>(); templates.add(doc.beautiConfig.partitionTemplate.get()); templates.addAll(doc.beautiConfig.subTemplates); // keep applying rules till model does not change doc.setUpActivePlugins(); int n; do { n = doc.posteriorPredecessors.size(); doc.applyBeautiRules(templates, false, oldContext); doc.setUpActivePlugins(); } while (n != doc.posteriorPredecessors.size()); doc.determinePartitions(); } if (treeLikelihood == null) { initTableData(); setUpComboBoxes(); } updateStatus(); } private PartitionContext getPartitionContext(int rowNr) { PartitionContext context = new PartitionContext( tableData[rowNr][NAME_COLUMN].toString(), tableData[rowNr][SITEMODEL_COLUMN].toString(), tableData[rowNr][CLOCKMODEL_COLUMN].toString(), tableData[rowNr][TREE_COLUMN].toString()); return context; } @Override protected void addInputLabel() { } void initTableData() { this.likelihoods = new GenericTreeLikelihood[partitionCount]; if (tableData == null) { tableData = new Object[partitionCount][NR_OF_COLUMNS]; } CompoundDistribution likelihoods = (CompoundDistribution) doc.pluginmap.get("likelihood"); for (int i = 0; i < partitionCount; i++) { Alignment data = alignments.get(i); // partition name tableData[i][NAME_COLUMN] = data; // alignment name if (data instanceof FilteredAlignment) { tableData[i][FILE_COLUMN] = ((FilteredAlignment) data).alignmentInput.get(); } else { tableData[i][FILE_COLUMN] = data; } // # taxa tableData[i][TAXA_COLUMN] = data.getTaxonCount(); // # sites tableData[i][SITES_COLUMN] = data.getSiteCount(); // Data type tableData[i][TYPE_COLUMN] = data.getDataType(); // site model GenericTreeLikelihood likelihood = (GenericTreeLikelihood) likelihoods.pDistributions.get().get(i); assert (likelihood != null); this.likelihoods[i] = likelihood; tableData[i][SITEMODEL_COLUMN] = getPartition(likelihood.siteModelInput); // clock model tableData[i][CLOCKMODEL_COLUMN] = getPartition(likelihood.branchRateModelInput); // tree tableData[i][TREE_COLUMN] = getPartition(likelihood.treeInput); // useAmbiguities tableData[i][USE_AMBIGUITIES_COLUMN] = null; try { if (hasUseAmbiguitiesInput(i)) { tableData[i][USE_AMBIGUITIES_COLUMN] = likelihood.getInputValue("useAmbiguities"); } } catch (Exception e) { // ignore } } } private boolean hasUseAmbiguitiesInput(int i) { try { for (Input<?> input : likelihoods[i].listInputs()) { if (input.getName().equals("useAmbiguities")) { return true; } } } catch (Exception e) { // ignore } return false; } private String getPartition(Input<?> input) { BEASTInterface beastObject = (BEASTInterface) input.get(); String id = beastObject.getID(); String partition = BeautiDoc.parsePartition(id); return partition; } protected Component createListBox() { String[] columnData = new String[] { "Name", "File", "Taxa", "Sites", "Data Type", "Site Model", "Clock Model", "Tree", "Ambiguities" }; initTableData(); // set up table. // special features: background shading of rows // custom editor allowing only Date column to be edited. table = new JTable(tableData, columnData) { private static final long serialVersionUID = 1L; // method that induces table row shading @Override public Component prepareRenderer(TableCellRenderer renderer, int Index_row, int Index_col) { Component comp = super.prepareRenderer(renderer, Index_row, Index_col); // even index, selected or not selected if (isCellSelected(Index_row, Index_col)) { comp.setBackground(Color.gray); } else if (Index_row % 2 == 0 && !isCellSelected(Index_row, Index_col)) { comp.setBackground(new Color(237, 243, 255)); } else { comp.setBackground(Color.white); } JComponent jcomp = (JComponent) comp; switch (Index_col) { case NAME_COLUMN: case CLOCKMODEL_COLUMN: case TREE_COLUMN: case SITEMODEL_COLUMN: jcomp.setToolTipText("Set " + table.getColumnName(Index_col).toLowerCase() + " for this partition"); break; case FILE_COLUMN: case TAXA_COLUMN: case SITES_COLUMN: case TYPE_COLUMN: jcomp.setToolTipText("Report " + table.getColumnName(Index_col).toLowerCase() + " for this partition"); break; case USE_AMBIGUITIES_COLUMN: jcomp.setToolTipText("<html>Flag whether to use ambiguities.<br>" + "If not set, the treelikelihood will treat ambiguities in the<br>" + "data as unknowns<br>" + "If set, the treelikelihood will use ambiguities as equally<br>" + "likely values for the tips.<br>" + "This will make the computation twice as slow.</html>"); break; default: jcomp.setToolTipText(null); } updateStatus(); return comp; } }; int size = table.getFont().getSize(); table.setRowHeight(25 * size/13); table.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); table.setColumnSelectionAllowed(false); table.setRowSelectionAllowed(true); table.setName("alignmenttable"); setUpComboBoxes(); TableColumn col = table.getColumnModel().getColumn(NAME_COLUMN); nameEditor = new JTextField(); nameEditor.getDocument().addDocumentListener(new DocumentListener() { @Override public void removeUpdate(DocumentEvent e) { processPartitionName(); } @Override public void insertUpdate(DocumentEvent e) { processPartitionName(); } @Override public void changedUpdate(DocumentEvent e) { processPartitionName(); } }); col.setCellEditor(new DefaultCellEditor(nameEditor)); // // set up editor that makes sure only doubles are accepted as entry // // and only the Date column is editable. table.setDefaultEditor(Object.class, new TableCellEditor() { JTextField m_textField = new JTextField(); int m_iRow, m_iCol; @Override public boolean stopCellEditing() { //Log.warning.println("stopCellEditing()"); table.removeEditor(); String text = m_textField.getText(); try { Double.parseDouble(text); } catch (Exception e) { return false; } tableData[m_iRow][m_iCol] = text; return true; } @Override public boolean isCellEditable(EventObject anEvent) { //Log.warning.println("isCellEditable()"); return table.getSelectedColumn() == 0; } @Override public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int rowNr, int colNr) { return null; } @Override public boolean shouldSelectCell(EventObject anEvent) { return false; } @Override public void removeCellEditorListener(CellEditorListener l) { } @Override public Object getCellEditorValue() { return null; } @Override public void cancelCellEditing() { } @Override public void addCellEditorListener(CellEditorListener l) { } }); // show alignment viewer when double clicking a row table.addMouseListener(new MouseListener() { @Override public void mouseReleased(MouseEvent e) { } @Override public void mousePressed(MouseEvent e) { } @Override public void mouseExited(MouseEvent e) { } @Override public void mouseEntered(MouseEvent e) { } @Override public void mouseClicked(MouseEvent e) { if (e.getClickCount() > 1) { try { int alignmemt = table.rowAtPoint(e.getPoint()); Alignment alignment = alignments.get(alignmemt); int best = 0; BeautiAlignmentProvider provider = null; for (BeautiAlignmentProvider provider2 : doc.beautiConfig.alignmentProvider) { int match = provider2.matches(alignment); if (match > best) { best = match; provider = provider2; } } provider.editAlignment(alignment, doc); } catch (Exception e1) { e1.printStackTrace(); } updateStatus(); } else if (e.getButton() == e.BUTTON3) { int alignmemt = table.rowAtPoint(e.getPoint()); Alignment alignment = alignments.get(alignmemt); int result = JOptionPane.showConfirmDialog(null, "Do you want to replace alignment " + alignment.getID()); if (result == JOptionPane.YES_OPTION) { replaceItem(alignment); } } } }); scrollPane = new JScrollPane(table); int rowsToDisplay = 3; Dimension d = table.getPreferredSize(); scrollPane.setPreferredSize( new Dimension(d.width,table.getRowHeight()*rowsToDisplay+table.getTableHeader().getHeight())); return scrollPane; } // createListBox void setUpComboBoxes() { // set up comboboxes @SuppressWarnings("unchecked") Set<String>[] partitionNames = new HashSet[3]; for (int i = 0; i < 3; i++) { partitionNames[i] = new HashSet<>(); } for (int i = 0; i < partitionCount; i++) { partitionNames[0].add(((BEASTInterface) likelihoods[i].siteModelInput.get()).getID()); partitionNames[1].add(likelihoods[i].branchRateModelInput.get().getID()); partitionNames[2].add(likelihoods[i].treeInput.get().getID()); } String[][] partitionNameStrings = new String[3][]; for (int i = 0; i < 3; i++) { partitionNameStrings[i] = partitionNames[i].toArray(new String[0]); } for (int j = 0; j < 3; j++) { for (int i = 0; i < partitionNameStrings[j].length; i++) { partitionNameStrings[j][i] = BeautiDoc.parsePartition(partitionNameStrings[j][i]); } } TableColumn col = table.getColumnModel().getColumn(SITEMODEL_COLUMN); JComboBox<String> siteModelComboBox = new JComboBox<>(partitionNameStrings[0]); siteModelComboBox.setEditable(true); siteModelComboBox.addActionListener(new ComboActionListener(SITEMODEL_COLUMN)); col.setCellEditor(new DefaultCellEditor(siteModelComboBox)); // If the cell should appear like a combobox in its // non-editing state, also set the combobox renderer col.setCellRenderer(new MyComboBoxRenderer(partitionNameStrings[0])); col = table.getColumnModel().getColumn(CLOCKMODEL_COLUMN); JComboBox<String> clockModelComboBox = new JComboBox<>(partitionNameStrings[1]); clockModelComboBox.setEditable(true); clockModelComboBox.addActionListener(new ComboActionListener(CLOCKMODEL_COLUMN)); col.setCellEditor(new DefaultCellEditor(clockModelComboBox)); col.setCellRenderer(new MyComboBoxRenderer(partitionNameStrings[1])); col = table.getColumnModel().getColumn(TREE_COLUMN); JComboBox<String> treeComboBox = new JComboBox<>(partitionNameStrings[2]); treeComboBox.setEditable(true); treeComboBox.addActionListener(new ComboActionListener(TREE_COLUMN)); col.setCellEditor(new DefaultCellEditor(treeComboBox)); col.setCellRenderer(new MyComboBoxRenderer(partitionNameStrings[2])); col = table.getColumnModel().getColumn(TAXA_COLUMN); col.setPreferredWidth(30); col = table.getColumnModel().getColumn(SITES_COLUMN); col.setPreferredWidth(30); col = table.getColumnModel().getColumn(USE_AMBIGUITIES_COLUMN); JCheckBox checkBox = new JCheckBox(); checkBox.addActionListener(e -> { if (table.getSelectedRow() >= 0 && table.getSelectedColumn() >= 0) { Log.warning.println(" " + table.getValueAt(table.getSelectedRow(), table.getSelectedColumn())); } try { int row = table.getSelectedRow(); if (hasUseAmbiguitiesInput(row)) { likelihoods[row].setInputValue("useAmbiguities", checkBox.isSelected()); tableData[row][USE_AMBIGUITIES_COLUMN] = checkBox.isSelected(); } else { if (checkBox.isSelected()) { checkBox.setSelected(false); } } } catch (Exception ex) { // TODO: handle exception } }); col.setCellEditor(new DefaultCellEditor(checkBox)); col.setCellRenderer(new MyCheckBoxRenderer()); col.setPreferredWidth(20); col.setMaxWidth(20); } void processPartitionName() { Log.warning.println("processPartitionName"); Log.warning.println(table.getSelectedColumn() + " " + table.getSelectedRow()); String oldName = tableData[table.getSelectedRow()][table.getSelectedColumn()].toString(); String newName = nameEditor.getText(); if (!oldName.equals(newName) && newName.indexOf(".") >= 0) { // prevent full stops to be used in IDs newName = newName.replaceAll("\\.", ""); table.setValueAt(newName, table.getSelectedRow(), table.getSelectedColumn()); table.repaint(); } if (!oldName.equals(newName)) { try { int partitionID = -2; switch (table.getSelectedColumn()) { case NAME_COLUMN: partitionID = BeautiDoc.ALIGNMENT_PARTITION; break; case SITEMODEL_COLUMN: partitionID = BeautiDoc.SITEMODEL_PARTITION; break; case CLOCKMODEL_COLUMN: partitionID = BeautiDoc.CLOCKMODEL_PARTITION; break; case TREE_COLUMN: partitionID = BeautiDoc.TREEMODEL_PARTITION; break; default: throw new IllegalArgumentException("Cannot rename item in column"); } getDoc().renamePartition(partitionID, oldName, newName); table.setValueAt(newName, table.getSelectedRow(), table.getSelectedColumn()); setUpComboBoxes(); } catch (Exception e) { JOptionPane.showMessageDialog(null, "Renaming failed: " + e.getMessage()); } } // debugging code: //for (int i = 0; i < partitionCount; i++) { // Log.warning.println(i + " " + tableData[i][0]); //} } class ComboActionListener implements ActionListener { int m_nColumn; public ComboActionListener(int columnNr) { m_nColumn = columnNr; } @Override public void actionPerformed(ActionEvent e) { // SwingUtilities.invokeLater(new Runnable() { // @Override // public void run() { Log.warning.println("actionPerformed "); Log.warning.println(table.getSelectedRow() + " " + table.getSelectedColumn()); if (table.getSelectedRow() >= 0 && table.getSelectedColumn() >= 0) { Log.warning.println(" " + table.getValueAt(table.getSelectedRow(), table.getSelectedColumn())); } for (int i = 0; i < partitionCount; i++) { try { updateModel(m_nColumn, i); } catch (Exception ex) { Log.warning.println(ex.getMessage()); } } // }}); } } public class MyComboBoxRenderer extends JComboBox<String> implements TableCellRenderer { private static final long serialVersionUID = 1L; public MyComboBoxRenderer(String[] items) { super(items); setOpaque(true); } @Override public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { if (isSelected) { // setForeground(table.getSelectionForeground()); super.setBackground(table.getSelectionBackground()); } else { setForeground(table.getForeground()); setBackground(table.getBackground()); } // Select the current value setSelectedItem(value); return this; } } public class MyCheckBoxRenderer extends JCheckBox implements TableCellRenderer { private static final long serialVersionUID = 1L; public MyCheckBoxRenderer() { super(); setOpaque(true); } @Override public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { if (hasUseAmbiguitiesInput(row)) { if (isSelected) { // setForeground(table.getSelectionForeground()); super.setBackground(table.getSelectionBackground()); } else { setForeground(table.getForeground()); setBackground(table.getBackground()); } setEnabled(true); setSelected((Boolean) value); } else { setEnabled(false); } return this; } } @Override protected void addSingleItem(BEASTInterface beastObject) { initTableData(); repaint(); } @Override protected void addItem() { addItem(null); } // addItem private void addItem(File[] fileArray) { List<BEASTInterface> beastObjects = doc.beautiConfig.selectAlignments(doc, this, fileArray); // Component c = this; if (beastObjects != null) { refreshPanel(); } } void delItem() { int[] selected = getTableRowSelection(); if (selected.length == 0) { JOptionPane.showMessageDialog(this, "Select partitions to delete, before hitting the delete button"); } // do the actual deleting for (int i = selected.length - 1; i >= 0; i--) { int rowNr = selected[i]; // before deleting, unlink site model, clock model and tree // check whether any of the models are linked BranchRateModel.Base clockModel = likelihoods[rowNr].branchRateModelInput.get(); SiteModelInterface siteModel = likelihoods[rowNr].siteModelInput.get(); TreeInterface tree = likelihoods[rowNr].treeInput.get(); List<GenericTreeLikelihood> cModels = new ArrayList<>(); List<GenericTreeLikelihood> models = new ArrayList<>(); List<GenericTreeLikelihood> tModels = new ArrayList<>(); for (GenericTreeLikelihood likelihood : likelihoods) { if (likelihood != likelihoods[rowNr]) { if (likelihood.branchRateModelInput.get() == clockModel) { cModels.add(likelihood); } if (likelihood.siteModelInput.get() == siteModel) { models.add(likelihood); } if (likelihood.treeInput.get() == tree) { tModels.add(likelihood); } } } try { if (cModels.size() > 0) { // clock model is linked, so we need to unlink if (doc.getPartitionNr(clockModel) != rowNr) { tableData[rowNr][CLOCKMODEL_COLUMN] = getDoc().partitionNames.get(rowNr).partition; } else { int freePartition = doc.getPartitionNr(cModels.get(0)); tableData[rowNr][CLOCKMODEL_COLUMN] = getDoc().partitionNames.get(freePartition).partition; } updateModel(CLOCKMODEL_COLUMN, rowNr); } if (models.size() > 0) { // site model is linked, so we need to unlink if (doc.getPartitionNr((BEASTInterface) siteModel) != rowNr) { tableData[rowNr][SITEMODEL_COLUMN] = getDoc().partitionNames.get(rowNr).partition; } else { int freePartition = doc.getPartitionNr(models.get(0)); tableData[rowNr][SITEMODEL_COLUMN] = getDoc().partitionNames.get(freePartition).partition; } updateModel(SITEMODEL_COLUMN, rowNr); } if (tModels.size() > 0) { // tree is linked, so we need to unlink if (doc.getPartitionNr((BEASTInterface) tree) != rowNr) { tableData[rowNr][TREE_COLUMN] = getDoc().partitionNames.get(rowNr).partition; } else { int freePartition = doc.getPartitionNr(tModels.get(0)); tableData[rowNr][TREE_COLUMN] = getDoc().partitionNames.get(freePartition).partition; } updateModel(TREE_COLUMN, rowNr); } getDoc().delAlignmentWithSubnet(alignments.get(rowNr)); alignments.remove(rowNr); // remove deleted likelihood from likelihoods array GenericTreeLikelihood[] tmp = new GenericTreeLikelihood[likelihoods.length - 1]; int k = 0; for (int j = 0; j < likelihoods.length; j++) { if (j != rowNr) { tmp[k] = likelihoods[j]; k++; } } likelihoods = tmp; partitionCount--; } catch (Exception e) { JOptionPane.showMessageDialog(null, "Deletion failed: " + e.getMessage()); e.printStackTrace(); } } MRCAPriorInputEditor.customConnector(doc); refreshPanel(); } // delItem void replaceItem() { int [] selected = getTableRowSelection(); if (selected.length != 1) { // don't know how to replace multiple alignments at the same time // should never get here (button is disabled) return; } Alignment alignment = alignments.get(selected[0]); replaceItem(alignment); } private void replaceItem(Alignment alignment) { BeautiAlignmentProvider provider = new BeautiAlignmentProvider(); List<BEASTInterface> list = provider.getAlignments(doc); List<Alignment> alignments = new ArrayList<>(); for (BEASTInterface o : list) { if (o instanceof Alignment) { alignments.add((Alignment) o); } } Alignment replacement = null; if (alignments.size() > 1) { JComboBox<Alignment> jcb = new JComboBox<Alignment>(alignments.toArray(new Alignment[]{})); JOptionPane.showMessageDialog( null, jcb, "Select a replacement alignment", JOptionPane.QUESTION_MESSAGE); replacement = (Alignment) jcb.getSelectedItem(); } else if (alignments.size() == 1) { replacement = alignments.get(0); } if (replacement != null) { if (!replacement.getDataType().getClass().getName().equals(alignment.getDataType().getClass().getName())) { JOptionPane.showMessageDialog(null, "Data types do not match, so alignment cannot be replaced: " + replacement.getID() + " " + replacement.getDataType().getClass().getName() + " != " + alignment.getID() + " " + alignment.getDataType().getClass().getName()); return; } // replace alignment Set<BEASTInterface> outputs = new LinkedHashSet<>(); outputs.addAll(alignment.getOutputs()); for (BEASTInterface o : outputs) { for (Input<?> input : o.listInputs()) { if (input.get() == alignment) { input.setValue(replacement, o); replacement.getOutputs().add(o); } else if (input.get() instanceof List) { @SuppressWarnings("rawtypes") List inputlist = (List) input.get(); int i = inputlist.indexOf(alignment); if (i >= 0) { inputlist.set(i, replacement); replacement.getOutputs().add(o); } } } } int i = doc.alignments.indexOf(alignment); doc.alignments.set(i, replacement); refreshPanel(); } } // replaceItem void splitItem() { int[] selected = getTableRowSelection(); if (selected.length == 0) { JOptionPane.showMessageDialog(this, "Select partitions to split, before hitting the split button"); return; } String[] options = { "{1,2} + 3", "{1,2} + 3 frame 2", "{1,2} + 3 frame 3", "1 + 2 + 3", "1 + 2 + 3 frame 2", "1 + 2 + 3 frame 3"}; String option = (String)JOptionPane.showInputDialog(null, "Split selected alignments into partitions", "Option", JOptionPane.WARNING_MESSAGE, null, options, "1 + 2 + 3"); if (option == null) { return; } String[] filters = null; String[] ids = null; if (option.equals(options[0])) { filters = new String[] { "1::3,2::3", "3::3" }; ids = new String[] { "_1,2", "_3" }; } else if (option.equals(options[1])) { filters = new String[] { "1::3,3::3", "2::3" }; ids = new String[] { "_1,2", "_3" }; } else if (option.equals(options[2])) { filters = new String[] { "2::3,3::3", "1::3" }; ids = new String[] { "_1,2", "_3" }; } else if (option.equals(options[3])) { filters = new String[] { "1::3", "2::3", "3::3" }; ids = new String[] { "_1", "_2", "_3" }; } else if (option.equals(options[4])) { filters = new String[] { "2::3", "3::3", "1::3" }; ids = new String[] { "_1", "_2", "_3" }; } else if (option.equals(options[5])) { filters = new String[] { "3::3", "1::3", "2::3" }; ids = new String[] { "_1", "_2", "_3" }; } else { return; } for (int i = selected.length - 1; i >= 0; i--) { int rowNr = selected[i]; Alignment alignment = alignments.remove(rowNr); getDoc().delAlignmentWithSubnet(alignment); try { List<Alignment> newAlignments = new ArrayList<>(); for (int j = 0; j < filters.length; j++) { FilteredAlignment f = new FilteredAlignment(); f.initByName("data", alignment, "filter", filters[j], "dataType", alignment.dataTypeInput.get()); f.setID(alignment.getID() + ids[j]); getDoc().addAlignmentWithSubnet(f, getDoc().beautiConfig.partitionTemplate.get()); newAlignments.add(f); } alignments.addAll(newAlignments); partitionCount = alignments.size(); tableData = null; initTableData(); if (newAlignments.size() == 2) { link(TREE_COLUMN, alignments.size() - 1, alignments.size() - 2); } else { link(TREE_COLUMN, alignments.size() - 2, alignments.size() - 3); tableData = null; initTableData(); link(TREE_COLUMN, alignments.size() - 1, alignments.size() - 2); } } catch (Exception e) { e.printStackTrace(); } } refreshPanel(); } // splitItem /** enable/disable buttons, etc **/ void updateStatus() { boolean status = (alignments.size() > 1); if (alignments.size() >= 2 && getTableRowSelection().length == 0) { status = false; } for (JButton button : linkButtons) { button.setEnabled(status); } for (JButton button : unlinkButtons) { button.setEnabled(status); } status = (getTableRowSelection().length > 0); splitButton.setEnabled(status); delButton.setEnabled(status); replaceButton.setEnabled(getTableRowSelection().length == 1); } } // class AlignmentListInputEditor