Merge remote-tracking branch 'origin/develop' into
[jalview.git] / src / jalview / gui / AlignViewport.java
index 1c105d0..5698e0f 100644 (file)
  */
 package jalview.gui;
 
+import jalview.analysis.AlignmentUtils;
+import jalview.analysis.AlignmentUtils.MappingResult;
 import jalview.analysis.AnnotationSorter.SequenceAnnotationOrder;
 import jalview.analysis.NJTree;
 import jalview.api.AlignViewportI;
 import jalview.bin.Cache;
 import jalview.commands.CommandI;
+import jalview.datamodel.Alignment;
 import jalview.datamodel.AlignmentI;
 import jalview.datamodel.ColumnSelection;
 import jalview.datamodel.PDBEntry;
@@ -51,9 +54,12 @@ import jalview.datamodel.SequenceGroup;
 import jalview.datamodel.SequenceI;
 import jalview.schemes.ColourSchemeProperty;
 import jalview.schemes.UserColourScheme;
+import jalview.structure.CommandListener;
 import jalview.structure.SelectionSource;
 import jalview.structure.StructureSelectionManager;
 import jalview.structure.VamsasSource;
+import jalview.util.MessageManager;
+import jalview.util.StringUtils;
 import jalview.viewmodel.AlignmentViewport;
 import jalview.ws.params.AutoCalcSetting;
 
@@ -61,11 +67,17 @@ import java.awt.Color;
 import java.awt.Container;
 import java.awt.Font;
 import java.awt.Rectangle;
+import java.io.File;
+import java.util.ArrayDeque;
 import java.util.ArrayList;
+import java.util.Deque;
 import java.util.Hashtable;
-import java.util.Stack;
+import java.util.Set;
 import java.util.Vector;
 
+import javax.swing.JInternalFrame;
+import javax.swing.JOptionPane;
+
 /**
  * DOCUMENT ME!
  * 
@@ -73,7 +85,7 @@ import java.util.Vector;
  * @version $Revision: 1.141 $
  */
 public class AlignViewport extends AlignmentViewport implements
-        SelectionSource, VamsasSource, AlignViewportI
+        SelectionSource, VamsasSource, AlignViewportI, CommandListener
 {
   int startRes;
 
@@ -129,9 +141,9 @@ public class AlignViewport extends AlignmentViewport implements
 
   boolean gatherViewsHere = false;
 
-  Stack<CommandI> historyList = new Stack<CommandI>();
+  private Deque<CommandI> historyList = new ArrayDeque<CommandI>();
 
-  Stack<CommandI> redoList = new Stack<CommandI>();
+  private Deque<CommandI> redoList = new ArrayDeque<CommandI>();
 
   int thresholdTextColour = 0;
 
@@ -1018,6 +1030,9 @@ public class AlignViewport extends AlignmentViewport implements
     return followSelection;
   }
 
+  /**
+   * Send the current selection to be broadcast to any selection listeners.
+   */
   public void sendSelection()
   {
     jalview.structure.StructureSelectionManager
@@ -1038,7 +1053,6 @@ public class AlignViewport extends AlignmentViewport implements
   {
     AlignmentPanel[] aps = PaintRefresher.getAssociatedPanels(this
             .getSequenceSetId());
-    AlignmentPanel ap = null;
     for (int p = 0; aps != null && p < aps.length; p++)
     {
       if (aps[p].av == this)
@@ -1199,6 +1213,305 @@ public class AlignViewport extends AlignmentViewport implements
     this.showAutocalculatedAbove = showAutocalculatedAbove;
   }
 
+  /**
+   * Method called when another alignment's edit (or possibly other) command is
+   * broadcast to here.
+   *
+   * To allow for sequence mappings (e.g. protein to cDNA), we have to first
+   * 'unwind' the command on the source sequences (in simulation, not in fact),
+   * and then for each edit in turn:
+   * <ul>
+   * <li>compute the equivalent edit on the mapped sequences</li>
+   * <li>apply the mapped edit</li>
+   * <li>'apply' the source edit to the working copy of the source sequences</li>
+   * </ul>
+   * 
+   * @param command
+   * @param undo
+   * @param ssm
+   */
+  @Override
+  public void mirrorCommand(CommandI command, boolean undo,
+          StructureSelectionManager ssm, VamsasSource source)
+  {
+    /*
+     * ...work in progress... do nothing unless we are a 'complement' of the
+     * source May replace this with direct calls not via SSM.
+     */
+    if (source instanceof AlignViewportI
+            && ((AlignViewportI) source).getCodingComplement() == this)
+    {
+      // ok to continue;
+    }
+    else
+    {
+      return;
+    }
+
+    CommandI mappedCommand = ssm.mapCommand(command, undo, getAlignment(),
+            getGapCharacter());
+    if (mappedCommand != null)
+    {
+      AlignmentI[] views = getAlignPanel().alignFrame.getViewAlignments();
+      mappedCommand.doCommand(views);
+      getAlignPanel().alignmentChanged();
+    }
+  }
+
+  @Override
+  public VamsasSource getVamsasSource()
+  {
+    return this;
+  }
+
+  /**
+   * Add one command to the command history list.
+   * 
+   * @param command
+   */
+  public void addToHistoryList(CommandI command)
+  {
+    if (this.historyList != null)
+    {
+      this.historyList.push(command);
+      broadcastCommand(command, false);
+    }
+  }
+
+  protected void broadcastCommand(CommandI command, boolean undo)
+  {
+    getStructureSelectionManager().commandPerformed(command, undo, getVamsasSource());
+  }
+
+  /**
+   * Add one command to the command redo list.
+   * 
+   * @param command
+   */
+  public void addToRedoList(CommandI command)
+  {
+    if (this.redoList != null)
+    {
+      this.redoList.push(command);
+    }
+    broadcastCommand(command, true);
+  }
+
+  /**
+   * Clear the command redo list.
+   */
+  public void clearRedoList()
+  {
+    if (this.redoList != null)
+    {
+      this.redoList.clear();
+    }
+  }
+
+  public void setHistoryList(Deque<CommandI> list)
+  {
+    this.historyList = list;
+  }
+
+  public Deque<CommandI> getHistoryList()
+  {
+    return this.historyList;
+  }
+
+  public void setRedoList(Deque<CommandI> list)
+  {
+    this.redoList = list;
+  }
+
+  public Deque<CommandI> getRedoList()
+  {
+    return this.redoList;
+  }
+
+  /**
+   * Add the sequences from the given alignment to this viewport. Optionally,
+   * may give the user the option to open a new frame, or split panel, with cDNA
+   * and protein linked.
+   * 
+   * @param al
+   * @param title
+   */
+  public void addAlignment(AlignmentI al, String title)
+  {
+    // TODO: promote to AlignViewportI? applet CutAndPasteTransfer is different
+
+    // refactored from FileLoader / CutAndPasteTransfer / SequenceFetcher with
+    // this comment:
+    // TODO: create undo object for this JAL-1101
+
+    /*
+     * If one alignment is protein and one nucleotide, with at least one
+     * sequence name in common, offer to open a linked alignment.
+     */
+    if (getAlignment().isNucleotide() != al.isNucleotide())
+    {
+      final Set<String> sequenceNames = getAlignment().getSequenceNames();
+      sequenceNames.retainAll(al.getSequenceNames());
+      if (!sequenceNames.isEmpty()) // at least one sequence name in both
+      {
+        if (openLinkedAlignment(al, title))
+        {
+          return;
+        }
+      }
+    }
+
+    for (int i = 0; i < al.getHeight(); i++)
+    {
+      getAlignment().addSequence(al.getSequenceAt(i));
+    }
+    // TODO this call was done by SequenceFetcher but not FileLoader or
+    // CutAndPasteTransfer. Is it needed?
+    setEndSeq(getAlignment().getHeight());
+    firePropertyChange("alignment", null, getAlignment().getSequences());
+  }
+
+  /**
+   * Show a dialog with the option to open and link (cDNA <-> protein) as a new
+   * alignment. Returns true if the new alignment was opened, false if not,
+   * because the user declined the offer.
+   * 
+   * @param title
+   */
+  protected boolean openLinkedAlignment(AlignmentI al, String title)
+  {
+    String[] options = new String[]
+    { MessageManager.getString("action.no"),
+        MessageManager.getString("label.split_window"),
+        MessageManager.getString("label.new_window"), };
+    final String question = JvSwingUtils.wrapTooltip(true,
+            MessageManager.getString("label.open_linked_alignment?"));
+    int response = JOptionPane.showOptionDialog(Desktop.desktop, question,
+            MessageManager.getString("label.open_linked_alignment"),
+            JOptionPane.DEFAULT_OPTION, JOptionPane.PLAIN_MESSAGE, null,
+            options, options[0]);
+
+    if (response != 1 && response != 2)
+    {
+      return false;
+    }
+    final boolean openSplitPane = (response == 1);
+    final boolean openInNewWindow = (response == 2);
+
+    /*
+     * Create the AlignFrame first (which creates the new alignment's datasets),
+     * before attempting sequence mapping.
+     */
+    AlignFrame newAlignFrame = new AlignFrame(al, AlignFrame.DEFAULT_WIDTH,
+            AlignFrame.DEFAULT_HEIGHT);
+    newAlignFrame.setTitle(title);
+
+    /*
+     * Identify protein and dna alignments. Make a copy of this one if opening
+     * in a new split pane.
+     */
+    AlignmentI thisAlignment = openSplitPane ? new Alignment(getAlignment())
+            : getAlignment();
+    AlignmentI protein = al.isNucleotide() ? thisAlignment : al;
+    final AlignmentI cdna = al.isNucleotide() ? al : thisAlignment;
+
+    newAlignFrame.statusBar.setText(MessageManager.formatMessage(
+            "label.successfully_loaded_file", new Object[]
+            { title }));
+
+    // TODO if we want this (e.g. to enable reload of the alignment from file),
+    // we will need to add parameters to the stack.
+    // if (!protocol.equals(AppletFormatAdapter.PASTE))
+    // {
+    // alignFrame.setFileName(file, format);
+    // }
+
+    if (openInNewWindow)
+    {
+      Desktop.addInternalFrame(newAlignFrame, title,
+              AlignFrame.DEFAULT_WIDTH,
+              AlignFrame.DEFAULT_HEIGHT);
+    }
+
+    /*
+     * Try to find mappings for at least one sequence. Any mappings made will be
+     * added to the protein alignment.
+     */
+    MappingResult mapped = AlignmentUtils.mapProteinToCdna(protein, cdna);
+    final StructureSelectionManager ssm = StructureSelectionManager
+            .getStructureSelectionManager(Desktop.instance);
+    if (mapped != MappingResult.Mapped)
+    {
+      /*
+       * No mapping possible - warn the user, but leave window open.
+       */
+      final String msg = JvSwingUtils.wrapTooltip(true,
+              MessageManager.getString("label.mapping_failed"));
+      JOptionPane.showInternalMessageDialog(Desktop.desktop, msg,
+              MessageManager.getString("label.no_mappings"),
+              JOptionPane.WARNING_MESSAGE);
+    }
+
+    try
+    {
+      newAlignFrame.setMaximum(jalview.bin.Cache.getDefault(
+              "SHOW_FULLSCREEN",
+              false));
+    } catch (java.beans.PropertyVetoException ex)
+    {
+    }
+
+    if (openSplitPane)
+    {
+      /*
+       * Open in split pane. DNA sequence above, protein below.
+       */
+      AlignFrame copyMe = new AlignFrame(thisAlignment,
+              AlignFrame.DEFAULT_WIDTH, AlignFrame.DEFAULT_HEIGHT);
+      copyMe.setTitle(getAlignPanel().alignFrame.getTitle());
+      final AlignFrame proteinFrame = al.isNucleotide() ? copyMe
+              : newAlignFrame;
+      final AlignFrame cdnaFrame = al.isNucleotide() ? newAlignFrame
+              : copyMe;
+      protein = proteinFrame.viewport.getAlignment();
+
+      cdnaFrame.setVisible(true);
+      proteinFrame.setVisible(true);
+      String sep = String.valueOf(File.separatorChar);
+      String proteinShortName = StringUtils.getLastToken(
+              proteinFrame.getTitle(), sep);
+      String dnaShortName = StringUtils.getLastToken(cdnaFrame.getTitle(),
+              sep);
+      String linkedTitle = MessageManager.formatMessage(
+              "label.linked_view_title", dnaShortName, proteinShortName);
+      JInternalFrame splitFrame = new SplitFrame(cdnaFrame, proteinFrame);
+      Desktop.addInternalFrame(splitFrame, linkedTitle,
+              AlignFrame.DEFAULT_WIDTH,
+              AlignFrame.DEFAULT_HEIGHT);
+
+      /*
+       * Set the frames to listen for each other's edit and sort commands.
+       */
+      ssm.addCommandListener(cdnaFrame.getViewport());
+      ssm.addCommandListener(proteinFrame.getViewport());
+
+      /*
+       * 'Coding complement' (dna/protein) views will mirror each others' edits,
+       * selections, sorting etc as decided from time to time by the relevant
+       * authorities.
+       */
+      proteinFrame.getViewport().setCodingComplement(cdnaFrame.getViewport());
+    }
+
+    /*
+     * Register the mappings (held on the protein alignment) with the
+     * StructureSelectionManager (for mouseover linking).
+     */
+    ssm.addMappings(protein.getCodonFrames());
+
+    return true;
+  }
+
   public AnnotationColumnChooser getAnnotationColumnSelectionState()
   {
     return annotationColumnSelectionState;