JAL-845 handling eXpand/Gather split frame views
authorgmungoc <g.m.carstairs@dundee.ac.uk>
Wed, 11 Feb 2015 12:26:31 +0000 (12:26 +0000)
committergmungoc <g.m.carstairs@dundee.ac.uk>
Wed, 11 Feb 2015 12:26:31 +0000 (12:26 +0000)
src/jalview/gui/AlignFrame.java
src/jalview/gui/AlignViewport.java
src/jalview/gui/Desktop.java
src/jalview/gui/SplitFrame.java
src/jalview/jbgui/GAlignFrame.java
src/jalview/jbgui/GSplitFrame.java
test/jalview/structure/StructureSelectionManagerTest.java [new file with mode: 0644]

index 5ef3da3..9f79999 100644 (file)
@@ -1480,22 +1480,17 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
   }
 
   /**
-   * close alignPanel2 and shuffle tabs appropriately.
+   * Close the specified panel and close up tabs appropriately.
    * 
-   * @param alignPanel2
+   * @param panelToClose
    */
-  public void closeView(AlignmentPanel alignPanel2)
+  public void closeView(AlignmentPanel panelToClose)
   {
     int index = tabbedPane.getSelectedIndex();
-    int closedindex = tabbedPane.indexOfComponent(alignPanel2);
-    alignPanels.remove(alignPanel2);
-    // Unnecessary
-    // if (viewport == alignPanel2.av)
-    // {
-    // viewport = null;
-    // }
-    alignPanel2.closePanel();
-    alignPanel2 = null;
+    int closedindex = tabbedPane.indexOfComponent(panelToClose);
+    alignPanels.remove(panelToClose);
+    panelToClose.closePanel();
+    panelToClose = null;
 
     tabbedPane.removeTabAt(closedindex);
     tabbedPane.validate();
@@ -1721,18 +1716,6 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
     viewport.getAlignment().moveSelectedSequencesByOne(sg,
             viewport.getHiddenRepSequences(), up);
     alignPanel.paintAlignment(true);
-
-    final AlignViewportI peer = viewport.getCodingComplement();
-    if (peer != null)
-    {
-      final SequenceGroup selectionGroup = peer.getSelectionGroup();
-      if (selectionGroup != null)
-      {
-        peer.getAlignment().moveSelectedSequencesByOne(
-                peer.getSelectionGroup(), peer.getHiddenRepSequences(), up);
-        ((AlignViewport) peer).getAlignPanel().paintAlignment(true);
-      }
-    }
   }
 
   synchronized void slideSequences(boolean right, int size)
@@ -2710,32 +2693,7 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
   @Override
   public void newView_actionPerformed(ActionEvent e)
   {
-    /*
-     * Note if the current view has a protein/cdna complementary view
-     */
-    AlignViewportI linkedView = this.viewport.getCodingComplement();
-
-    AlignmentPanel newPanel = newView(null, true);
-
-    /*
-     * If the original view has a protein/cdna linked view, make and link a new
-     * view there also.
-     */
-    // TODO refactor the hell out of this - move to a controller, lose the casts
-    // and direct member access, etc
-    if (linkedView != null)
-    {
-      AlignFrame linkedAlignFrame = ((AlignViewport) linkedView)
-              .getAlignPanel().alignFrame;
-      AlignmentPanel newLinkedPanel = linkedAlignFrame.newView(null, true);
-      newLinkedPanel.av.viewName = newPanel.av.viewName;
-      newPanel.av.setCodingComplement(newLinkedPanel.av);
-      final StructureSelectionManager ssm = StructureSelectionManager
-              .getStructureSelectionManager(Desktop.instance);
-      ssm.addCommandListener(newPanel.av);
-      ssm.addCommandListener(newLinkedPanel.av);
-
-    }
+    newView(null, true);
   }
 
   /**
@@ -5386,6 +5344,23 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
       avc.setViewportAndAlignmentPanel(viewport, alignPanel);
       setMenusFromViewport(viewport);
     }
+
+    /*
+     * If there is a frame linked to this one in a SplitPane, switch it to the
+     * same view tab index. No infinite recursion of calls should happen, since
+     * tabSelectionChanged() should not get invoked on setting the selected
+     * index to an unchanged value. Guard against setting an invalid index
+     * before the new view peer tab has been created.
+     */
+    final AlignViewportI peer = viewport.getCodingComplement();
+    if (peer != null)
+    {
+      AlignFrame linkedAlignFrame = ((AlignViewport) peer).getAlignPanel().alignFrame;
+      if (linkedAlignFrame.tabbedPane.getTabCount() > index)
+      {
+        linkedAlignFrame.tabbedPane.setSelectedIndex(index);
+      }
+    }
   }
 
   /**
@@ -5464,7 +5439,7 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
    * 
    * @param av
    */
-  public boolean closeView(AlignmentViewport av)
+  public boolean closeView(AlignViewportI av)
   {
     if (viewport == av)
     {
index 5698e0f..deba021 100644 (file)
@@ -1485,9 +1485,7 @@ public class AlignViewport extends AlignmentViewport implements
       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);
+      Desktop.addInternalFrame(splitFrame, linkedTitle, -1, -1);
 
       /*
        * Set the frames to listen for each other's edit and sort commands.
index 4bb828c..cb6bc6e 100644 (file)
@@ -20,6 +20,7 @@
  */
 package jalview.gui;
 
+import jalview.api.AlignmentViewPanel;
 import jalview.bin.Cache;
 import jalview.io.FileLoader;
 import jalview.io.FormatAdapter;
@@ -334,8 +335,7 @@ public class Desktop extends jalview.jbgui.GDesktop implements
             + jalview.bin.Cache.getProperty("VERSION") + "\n"
             + "Jalview Installation: "
             + jalview.bin.Cache.getDefault("INSTALLATION", "unknown")
-            + "\n"
-            + "Build Date: "
+            + "\n" + "Build Date: "
             + jalview.bin.Cache.getDefault("BUILD_DATE", "unknown") + "\n"
             + "Java version: " + System.getProperty("java.version") + "\n"
             + System.getProperty("os.arch") + " "
@@ -436,9 +436,10 @@ public class Desktop extends jalview.jbgui.GDesktop implements
     {
       ssm.setAddTempFacAnnot(jalview.bin.Cache.getDefault(
               Preferences.ADD_TEMPFACT_ANN, true));
-    ssm.setProcessSecondaryStructure(jalview.bin.Cache.getDefault(Preferences.STRUCT_FROM_PDB, true));
-    ssm.setSecStructServices(jalview.bin.Cache.getDefault(Preferences.USE_RNAVIEW,
-            true));
+      ssm.setProcessSecondaryStructure(jalview.bin.Cache.getDefault(
+              Preferences.STRUCT_FROM_PDB, true));
+      ssm.setSecStructServices(jalview.bin.Cache.getDefault(
+              Preferences.USE_RNAVIEW, true));
     }
     else
     {
@@ -484,7 +485,9 @@ public class Desktop extends jalview.jbgui.GDesktop implements
           public void run()
           {
             long instance = System.currentTimeMillis();
-            Desktop.instance.setProgressBar(MessageManager.getString("status.refreshing_news"), instance);
+            Desktop.instance.setProgressBar(
+                    MessageManager.getString("status.refreshing_news"),
+                    instance);
             jvnews.refreshNews();
             Desktop.instance.setProgressBar(null, instance);
             jvnews.showNews();
@@ -652,7 +655,6 @@ public class Desktop extends jalview.jbgui.GDesktop implements
     addInternalFrame(frame, title, true, w, h, true);
   }
 
-
   /**
    * Add an internal frame to the Jalview desktop
    * 
@@ -1197,8 +1199,7 @@ public class Desktop extends jalview.jbgui.GDesktop implements
     if (shortv)
     {
       message.append("<h1><strong>Version: "
-              + jalview.bin.Cache.getProperty("VERSION")
-              + "</strong></h1>");
+              + jalview.bin.Cache.getProperty("VERSION") + "</strong></h1>");
       message.append("<strong>Last Updated: <em>"
               + jalview.bin.Cache.getDefault("BUILD_DATE", "unknown")
               + "</em></strong>");
@@ -1244,8 +1245,7 @@ public class Desktop extends jalview.jbgui.GDesktop implements
     }
     message.append("<br>Authors:  "
             + jalview.bin.Cache
-                    .getDefault(
-                            "AUTHORFNAMES",
+                    .getDefault("AUTHORFNAMES",
                             "The Jalview Authors (See AUTHORS file for current list)")
             + "<br><br>Development managed by The Barton Group, University of Dundee, Scotland, UK.<br>"
             + "<br><br>For help, see the FAQ at <a href=\"http://www.jalview.org/faq\">www.jalview.org/faq</a> and/or join the jalview-discuss@jalview.org mailing list"
@@ -1482,8 +1482,9 @@ public class Desktop extends jalview.jbgui.GDesktop implements
         public void run()
         {
 
-          setProgressBar(MessageManager.formatMessage("label.saving_jalview_project", new Object[]{choice.getName()}),
-                  choice.hashCode());
+          setProgressBar(MessageManager.formatMessage(
+                  "label.saving_jalview_project", new Object[]
+                  { choice.getName() }), choice.hashCode());
           jalview.bin.Cache.setProperty("LAST_DIRECTORY",
                   choice.getParent());
           // TODO catch and handle errors for savestate
@@ -1500,10 +1501,11 @@ public class Desktop extends jalview.jbgui.GDesktop implements
             Cache.log.error(
                     "Problems whilst trying to save to " + choice.getName(),
                     ex);
-            JOptionPane.showMessageDialog(
-                    me,
-                    MessageManager.formatMessage("label.error_whilst_saving_current_state_to", new Object[]{ choice.getName()}),
-                    MessageManager.getString("label.couldnt_save_project"),
+            JOptionPane.showMessageDialog(me, MessageManager.formatMessage(
+                    "label.error_whilst_saving_current_state_to",
+                    new Object[]
+                    { choice.getName() }), MessageManager
+                    .getString("label.couldnt_save_project"),
                     JOptionPane.WARNING_MESSAGE);
           }
           setProgressBar(null, choice.hashCode());
@@ -1571,15 +1573,15 @@ public class Desktop extends jalview.jbgui.GDesktop implements
       final File selectedFile = chooser.getSelectedFile();
       setProjectFile(selectedFile);
       final String choice = selectedFile.getAbsolutePath();
-      jalview.bin.Cache.setProperty("LAST_DIRECTORY", selectedFile.getParent());
+      jalview.bin.Cache.setProperty("LAST_DIRECTORY",
+              selectedFile.getParent());
       new Thread(new Runnable()
       {
         public void run()
         {
           setProgressBar(MessageManager.formatMessage(
                   "label.loading_jalview_project", new Object[]
-                  { choice }),
-                  choice.hashCode());
+                  { choice }), choice.hashCode());
           try
           {
             new Jalview2XML().loadJalviewAlign(choice);
@@ -1590,13 +1592,13 @@ public class Desktop extends jalview.jbgui.GDesktop implements
           {
             Cache.log.error("Problems whilst loading project from "
                     + choice, ex);
-            JOptionPane.showMessageDialog(Desktop.desktop,
- MessageManager
+            JOptionPane.showMessageDialog(Desktop.desktop, MessageManager
                     .formatMessage(
                             "label.error_whilst_loading_project_from",
                             new Object[]
-                            { choice }),
-                    MessageManager.getString("label.couldnt_load_project"), JOptionPane.WARNING_MESSAGE);
+                            { choice }), MessageManager
+                    .getString("label.couldnt_load_project"),
+                    JOptionPane.WARNING_MESSAGE);
           }
           setProgressBar(null, choice.hashCode());
         }
@@ -1776,6 +1778,11 @@ public class Desktop extends jalview.jbgui.GDesktop implements
     return null;
   }
 
+  /**
+   * Explode the views in the given frame into separate AlignFrame
+   * 
+   * @param af
+   */
   public void explodeViews(AlignFrame af)
   {
     int size = af.alignPanels.size();
@@ -1805,6 +1812,14 @@ public class Desktop extends jalview.jbgui.GDesktop implements
 
   }
 
+  /**
+   * Gather expanded views (separate AlignFrame's) with the same sequence set
+   * identifier back in to this frame as additional views, and close the
+   * expanded views. Note the expanded frames may themselves have multiple
+   * views. We take the lot.
+   * 
+   * @param source
+   */
   public void gatherViews(AlignFrame source)
   {
     source.viewport.gatherViewsHere = true;
@@ -1851,7 +1866,8 @@ public class Desktop extends jalview.jbgui.GDesktop implements
               jalview.bin.Cache.getProperty("LAST_DIRECTORY"));
 
       chooser.setFileView(new JalviewFileView());
-      chooser.setDialogTitle(MessageManager.getString("label.open_saved_vamsas_session"));
+      chooser.setDialogTitle(MessageManager
+              .getString("label.open_saved_vamsas_session"));
       chooser.setToolTipText(MessageManager
               .getString("label.select_vamsas_session_opened_as_new_vamsas_session"));
 
@@ -1944,23 +1960,26 @@ public class Desktop extends jalview.jbgui.GDesktop implements
       return false;
     }
 
-    setProgressBar(MessageManager.formatMessage("status.importing_vamsas_session_from", new Object[]{file.getName()}),
-            file.hashCode());
+    setProgressBar(MessageManager.formatMessage(
+            "status.importing_vamsas_session_from", new Object[]
+            { file.getName() }), file.hashCode());
     try
     {
       v_client = new jalview.gui.VamsasApplication(this, file, null);
     } catch (Exception ex)
     {
-        setProgressBar(MessageManager.formatMessage("status.importing_vamsas_session_from", new Object[]{file.getName()}),
-                file.hashCode());
+      setProgressBar(MessageManager.formatMessage(
+              "status.importing_vamsas_session_from", new Object[]
+              { file.getName() }), file.hashCode());
       jalview.bin.Cache.log.error(
               "New vamsas session from existing session file failed:", ex);
       return false;
     }
     setupVamsasConnectedGui();
     v_client.initial_update(); // TODO: thread ?
-    setProgressBar(MessageManager.formatMessage("status.importing_vamsas_session_from", new Object[]{file.getName()}),
-            file.hashCode());
+    setProgressBar(MessageManager.formatMessage(
+            "status.importing_vamsas_session_from", new Object[]
+            { file.getName() }), file.hashCode());
     return v_client.inSession();
   }
 
@@ -1968,11 +1987,14 @@ public class Desktop extends jalview.jbgui.GDesktop implements
   {
     if (v_client != null)
     {
-      throw new Error(MessageManager.getString("error.try_join_vamsas_session_another"));
+      throw new Error(
+              MessageManager
+                      .getString("error.try_join_vamsas_session_another"));
     }
     if (mysesid == null)
     {
-      throw new Error(MessageManager.getString("error.invalid_vamsas_session_id"));
+      throw new Error(
+              MessageManager.getString("error.invalid_vamsas_session_id"));
     }
     v_client = new VamsasApplication(this, mysesid);
     setupVamsasConnectedGui();
@@ -2119,14 +2141,17 @@ public class Desktop extends jalview.jbgui.GDesktop implements
               { "Vamsas Document" }, "Vamsas Document");
 
       chooser.setFileView(new JalviewFileView());
-      chooser.setDialogTitle(MessageManager.getString("label.save_vamsas_document_archive"));
+      chooser.setDialogTitle(MessageManager
+              .getString("label.save_vamsas_document_archive"));
 
       int value = chooser.showSaveDialog(this);
 
       if (value == JalviewFileChooser.APPROVE_OPTION)
       {
         java.io.File choice = chooser.getSelectedFile();
-        JPanel progpanel = addProgressPanel(MessageManager.formatMessage("label.saving_vamsas_doc", new Object[]{choice.getName()}));
+        JPanel progpanel = addProgressPanel(MessageManager.formatMessage(
+                "label.saving_vamsas_doc", new Object[]
+                { choice.getName() }));
         jalview.bin.Cache.setProperty("LAST_DIRECTORY", choice.getParent());
         String warnmsg = null;
         String warnttl = null;
@@ -2178,7 +2203,8 @@ public class Desktop extends jalview.jbgui.GDesktop implements
     }
     if (b)
     {
-      vamUpdate = this.addProgressPanel(MessageManager.getString("label.updating_vamsas_session"));
+      vamUpdate = this.addProgressPanel(MessageManager
+              .getString("label.updating_vamsas_session"));
     }
     vamsasStart.setVisible(!b);
     vamsasStop.setVisible(!b);
@@ -2355,13 +2381,13 @@ public class Desktop extends jalview.jbgui.GDesktop implements
          * Also check for a split frame containing an AlignFrame
          */
         SplitFrame sf = (SplitFrame) frames[i];
-        if (sf.getTopComponent() instanceof AlignFrame)
+        if (sf.getTopFrame() instanceof AlignFrame)
         {
-          avp.add((AlignFrame) sf.getTopComponent());
+          avp.add((AlignFrame) sf.getTopFrame());
         }
-        if (sf.getBottomComponent() instanceof AlignFrame)
+        if (sf.getBottomFrame() instanceof AlignFrame)
         {
-          avp.add((AlignFrame) sf.getBottomComponent());
+          avp.add((AlignFrame) sf.getBottomFrame());
         }
       }
     }
@@ -2412,7 +2438,9 @@ public class Desktop extends jalview.jbgui.GDesktop implements
     // use reflection to avoid creating compilation dependency.
     if (!jalview.bin.Cache.groovyJarsPresent())
     {
-      throw new Error(MessageManager.getString("error.implementation_error_cannot_create_groovyshell"));
+      throw new Error(
+              MessageManager
+                      .getString("error.implementation_error_cannot_create_groovyshell"));
     }
     try
     {
@@ -2430,13 +2458,11 @@ public class Desktop extends jalview.jbgui.GDesktop implements
     } catch (Exception ex)
     {
       jalview.bin.Cache.log.error("Groovy Shell Creation failed.", ex);
-      JOptionPane
-              .showInternalMessageDialog(
-                      Desktop.desktop,
+      JOptionPane.showInternalMessageDialog(Desktop.desktop,
 
-                      MessageManager.getString("label.couldnt_create_groovy_shell"),
-                      MessageManager.getString("label.groovy_support_failed"),
-                      JOptionPane.ERROR_MESSAGE);
+      MessageManager.getString("label.couldnt_create_groovy_shell"),
+              MessageManager.getString("label.groovy_support_failed"),
+              JOptionPane.ERROR_MESSAGE);
     }
   }
 
@@ -2486,7 +2512,9 @@ public class Desktop extends jalview.jbgui.GDesktop implements
   {
     if (progressBarHandlers == null || !progressBars.contains(new Long(id)))
     {
-      throw new Error(MessageManager.getString("error.call_setprogressbar_before_registering_handler"));
+      throw new Error(
+              MessageManager
+                      .getString("error.call_setprogressbar_before_registering_handler"));
     }
     progressBarHandlers.put(new Long(id), handler);
     final JPanel progressPanel = progressBars.get(new Long(id));
@@ -2501,7 +2529,10 @@ public class Desktop extends jalview.jbgui.GDesktop implements
         public void actionPerformed(ActionEvent e)
         {
           handler.cancelActivity(id);
-          us.setProgressBar(MessageManager.formatMessage("label.cancelled_params", new Object[]{((JLabel) progressPanel.getComponent(0)).getText()}), id);
+          us.setProgressBar(MessageManager.formatMessage(
+                  "label.cancelled_params", new Object[]
+                  { ((JLabel) progressPanel.getComponent(0)).getText() }),
+                  id);
         }
       });
       progressPanel.add(cancel, BorderLayout.EAST);
@@ -2763,17 +2794,18 @@ public class Desktop extends jalview.jbgui.GDesktop implements
         {
           if (progress != null)
           {
-            progress.setProgressBar(MessageManager.formatMessage("status.opening_params", new Object[]{url}), this.hashCode());
+            progress.setProgressBar(MessageManager.formatMessage(
+                    "status.opening_params", new Object[]
+                    { url }), this.hashCode());
           }
           jalview.util.BrowserLauncher.openURL(url);
         } catch (Exception ex)
         {
-          JOptionPane
-                  .showInternalMessageDialog(
-                          Desktop.desktop,
-                          MessageManager.getString("label.web_browser_not_found_unix"),
-                          MessageManager.getString("label.web_browser_not_found"),
-                          JOptionPane.WARNING_MESSAGE);
+          JOptionPane.showInternalMessageDialog(Desktop.desktop,
+                  MessageManager
+                          .getString("label.web_browser_not_found_unix"),
+                  MessageManager.getString("label.web_browser_not_found"),
+                  JOptionPane.WARNING_MESSAGE);
 
           ex.printStackTrace();
         }
@@ -2888,6 +2920,7 @@ public class Desktop extends jalview.jbgui.GDesktop implements
     dialogPause = false;
     block.release();
   }
+
   @Override
   protected void snapShotWindow_actionPerformed(ActionEvent e)
   {
@@ -2897,14 +2930,133 @@ public class Desktop extends jalview.jbgui.GDesktop implements
             "View of Desktop", getWidth(), getHeight(), of = new File(
                     "Jalview_snapshot" + System.currentTimeMillis()
                             + ".eps"), "View of desktop");
-    try {
+    try
+    {
       paintAll(im.getGraphics());
       im.writeImage();
     } catch (Exception q)
     {
-      Cache.log.error("Couldn't write snapshot to "+of.getAbsolutePath(),q);
+      Cache.log.error("Couldn't write snapshot to " + of.getAbsolutePath(),
+              q);
+      return;
+    }
+    Cache.log.info("Successfully written snapshot to file "
+            + of.getAbsolutePath());
+  }
+
+  /**
+   * Explode the views in the given frame into separate AlignFrame
+   * 
+   * @param sf
+   */
+  public void explodeViews(SplitFrame sf)
+  {
+    AlignFrame oldTopFrame = (AlignFrame) sf.getTopFrame();
+    AlignFrame oldBottomFrame = (AlignFrame) sf.getBottomFrame();
+    List<? extends AlignmentViewPanel> topPanels = oldTopFrame
+            .getAlignPanels();
+    List<? extends AlignmentViewPanel> bottomPanels = oldBottomFrame
+            .getAlignPanels();
+    int viewCount = topPanels.size();
+    if (viewCount < 2)
+    {
       return;
     }
-    Cache.log.info("Successfully written snapshot to file "+of.getAbsolutePath());
+
+    /*
+     * Processing in reverse order works, forwards order leaves the first panels
+     * not visible. I don't know why!
+     */
+    for (int i = viewCount - 1; i >= 0; i--)
+    {
+      /*
+       * Make new top and bottom frames. These take over the respective
+       * AlignmentPanel objects, including their AlignmentViewports, so the
+       * cdna/protein relationships between the viewports is carried over to the
+       * new split frames.
+       */
+      AlignFrame newTopFrame = new AlignFrame(
+              (AlignmentPanel) topPanels.get(i));
+      newTopFrame.setVisible(true);
+      AlignFrame newBottomFrame = new AlignFrame(
+              (AlignmentPanel) bottomPanels.get(i));
+      newBottomFrame.setVisible(true);
+      JInternalFrame splitFrame = new SplitFrame(newTopFrame,
+              newBottomFrame);
+      Desktop.addInternalFrame(splitFrame, sf.getTitle(), -1, -1);
+
+      // if (ap.av.explodedPosition != null
+      // && !ap.av.explodedPosition.equals(af.getBounds()))
+      // {
+      // newaf.setBounds(ap.av.explodedPosition);
+      // }
+      //
+      // ap.av.gatherViewsHere = false;
+    }
+
+    /*
+     * Clear references to the panels (now relocated in the new SplitFrames)
+     * before closing the old SplitFrame.
+     */
+    topPanels.clear();
+    bottomPanels.clear();
+    sf.close();
+  }
+
+  /**
+   * Gather expanded split frames, sharing the same pairs of sequence set ids,
+   * back into the given SplitFrame as additional views. Note that the gathered
+   * frames may themselves have multiple views.
+   * 
+   * @param source
+   */
+  public void gatherViews(SplitFrame source)
+  {
+    // source.viewport.gatherViewsHere = true;
+    // source.viewport.explodedPosition = source.getBounds();
+    AlignFrame myTopFrame = (AlignFrame) source.getTopFrame();
+    AlignFrame myBottomFrame = (AlignFrame) source.getBottomFrame();
+    String topViewId = myTopFrame.viewport.getSequenceSetId();
+    String bottomViewId = myBottomFrame.viewport.getSequenceSetId();
+  
+    JInternalFrame[] frames = desktop.getAllFrames();
+    for (JInternalFrame frame : frames)
+    {
+      if (frame instanceof SplitFrame && frame != source)
+      {
+        SplitFrame sf = (SplitFrame) frame;
+        AlignFrame topFrame = (AlignFrame) sf.getTopFrame();
+        AlignFrame bottomFrame = (AlignFrame) sf.getBottomFrame();
+        boolean gatherThis = false;
+        for (int a = 0; a < topFrame.alignPanels.size(); a++)
+        {
+          AlignmentPanel topPanel = topFrame.alignPanels.get(a);
+          AlignmentPanel bottomPanel = bottomFrame.alignPanels.get(a);
+          if (topViewId.equals(topPanel.av.getSequenceSetId())
+                  && bottomViewId.equals(bottomPanel.av.getSequenceSetId()))
+          {
+            gatherThis = true;
+            topPanel.av.gatherViewsHere = false;
+            bottomPanel.av.gatherViewsHere = false;
+            // topPanel.av.explodedPosition = af.getBounds();
+            myTopFrame.addAlignmentPanel(topPanel, false);
+            myBottomFrame.addAlignmentPanel(bottomPanel, false);
+          }
+        }
+  
+        if (gatherThis)
+        {
+          topFrame.getAlignPanels().clear();
+          bottomFrame.getAlignPanels().clear();
+          sf.close();
+        }
+      }
+    }
+
+    /*
+     * The dust settles...give focus to the tab we did this from.
+     */
+    myTopFrame.setDisplayedView(myTopFrame.alignPanel);
+
   }
 }
index f98eea7..ab4596e 100644 (file)
@@ -1,19 +1,24 @@
 package jalview.gui;
 
+import jalview.jbgui.GAlignFrame;
 import jalview.jbgui.GSplitFrame;
+import jalview.structure.StructureSelectionManager;
 
 import java.awt.Component;
 import java.awt.MouseInfo;
 import java.awt.Point;
 import java.awt.Rectangle;
+import java.awt.Toolkit;
 import java.awt.event.ActionEvent;
 import java.awt.event.ActionListener;
 import java.awt.event.KeyAdapter;
 import java.awt.event.KeyEvent;
 import java.awt.event.KeyListener;
+import java.beans.PropertyVetoException;
 import java.util.Map.Entry;
 
 import javax.swing.AbstractAction;
+import javax.swing.InputMap;
 import javax.swing.JComponent;
 import javax.swing.JMenuItem;
 import javax.swing.KeyStroke;
@@ -24,7 +29,7 @@ public class SplitFrame extends GSplitFrame
 {
   private static final long serialVersionUID = 1L;
 
-  public SplitFrame(JComponent top, JComponent bottom)
+  public SplitFrame(GAlignFrame top, GAlignFrame bottom)
   {
     super(top, bottom);
     init();
@@ -42,7 +47,6 @@ public class SplitFrame extends GSplitFrame
     addKeyListener();
 
     addKeyBindings();
-
   }
 
   /**
@@ -55,14 +59,14 @@ public class SplitFrame extends GSplitFrame
       @Override
       public void internalFrameClosed(InternalFrameEvent evt)
       {
-        if (getTopComponent() instanceof AlignFrame)
+        if (getTopFrame() instanceof AlignFrame)
         {
-          ((AlignFrame) getTopComponent())
+          ((AlignFrame) getTopFrame())
                   .closeMenuItem_actionPerformed(true);
         }
-        if (getBottomComponent() instanceof AlignFrame)
+        if (getBottomFrame() instanceof AlignFrame)
         {
-          ((AlignFrame) getBottomComponent())
+          ((AlignFrame) getBottomFrame())
                   .closeMenuItem_actionPerformed(true);
         }
       };
@@ -75,18 +79,24 @@ public class SplitFrame extends GSplitFrame
    */
   protected void addKeyListener()
   {
-    // TODO Key Bindings rather than KeyListener are recommended for Swing
     addKeyListener(new KeyAdapter() {
 
       @Override
       public void keyPressed(KeyEvent e)
       {
-        Component c = getComponentAtMouse();
-        if (c != null)
+        AlignFrame af = (AlignFrame) getFrameAtMouse();
+
+        /*
+         * Intercept and override any keys here if wanted.
+         */
+        if (!overrideKey(e, af))
         {
-          for (KeyListener kl : c.getKeyListeners())
+          if (af != null)
           {
-            kl.keyPressed(e);
+            for (KeyListener kl : af.getKeyListeners())
+            {
+              kl.keyPressed(e);
+            }
           }
         }
       }
@@ -94,7 +104,7 @@ public class SplitFrame extends GSplitFrame
       @Override
       public void keyReleased(KeyEvent e)
       {
-        Component c = getComponentAtMouse();
+        Component c = getFrameAtMouse();
         if (c != null)
         {
           for (KeyListener kl : c.getKeyListeners())
@@ -108,20 +118,68 @@ public class SplitFrame extends GSplitFrame
   }
 
   /**
+   * Returns true if the key event is overriden and actioned (or ignored) here,
+   * else returns false, indicating it should be delegated to the AlignFrame's
+   * usual handler.
+   * <p>
+   * We can't handle Cmd-Key combinations here, instead this is done by
+   * overriding key bindings.
+   * 
+   * @see addKeyOverrides
+   * @param e
+   * @param af
+   * @return
+   */
+  protected boolean overrideKey(KeyEvent e, AlignFrame af)
+  {
+    boolean actioned = false;
+    int keyCode = e.getKeyCode();
+    switch (keyCode)
+    {
+    case KeyEvent.VK_DOWN:
+      if (e.isAltDown() || !af.viewport.cursorMode)
+      {
+        /*
+         * Key down (or Alt-key-down in cursor mode) - move selected sequences
+         */
+        ((AlignFrame) getTopFrame()).moveSelectedSequences(false);
+        ((AlignFrame) getBottomFrame()).moveSelectedSequences(false);
+        actioned = true;
+        e.consume();
+      }
+      break;
+    case KeyEvent.VK_UP:
+      if (e.isAltDown() || !af.viewport.cursorMode)
+      {
+        /*
+         * Key up (or Alt-key-up in cursor mode) - move selected sequences
+         */
+        ((AlignFrame) getTopFrame()).moveSelectedSequences(true);
+        ((AlignFrame) getBottomFrame()).moveSelectedSequences(true);
+        actioned = true;
+        e.consume();
+      }
+    default:
+    }
+    return actioned;
+  }
+
+  /**
    * Returns the split pane component the mouse is in, or null if neither.
    * 
    * @return
    */
-  protected Component getComponentAtMouse()
+  protected GAlignFrame getFrameAtMouse()
   {
     Point loc = MouseInfo.getPointerInfo().getLocation();
     
-    if (isIn(loc, getTopComponent())) {
-      return getTopComponent();
+    if (isIn(loc, getTopFrame()))
+    {
+      return getTopFrame();
     }
-    else if (isIn(loc, getBottomComponent()))
+    else if (isIn(loc, getBottomFrame()))
     {
-      return getBottomComponent();
+      return getBottomFrame();
     }
     return null;
   }
@@ -134,86 +192,299 @@ public class SplitFrame extends GSplitFrame
   }
 
   /**
-   * Set key bindings (recommended for Swing over key accelerators). For now,
-   * delegate to the corresponding key accelerator for the AlignFrame that the
-   * mouse is in. Hopefully can be simplified in future if AlignFrame is changed
-   * to use key bindings rather than accelerators.
+   * Set key bindings (recommended for Swing over key accelerators).
    */
   private void addKeyBindings()
   {
-    if (getTopComponent() instanceof AlignFrame)
+    overrideDelegatedKeyBindings();
+
+    overrideImplementedKeyBindings();
+  }
+
+  /**
+   * Override key bindings with alternative action methods implemented in this
+   * class.
+   */
+  protected void overrideImplementedKeyBindings()
+  {
+    overrideNewView();
+    overrideCloseView();
+    overrideExpandViews();
+    overrideGatherViews();
+  }
+
+  /**
+   * Replace Cmd-W close view action with our version.
+   */
+  protected void overrideCloseView()
+  {
+    AbstractAction action;
+    /*
+     * Ctrl-W / Cmd-W - close view or window
+     */
+    KeyStroke key_cmdW = KeyStroke.getKeyStroke(KeyEvent.VK_W, Toolkit
+            .getDefaultToolkit().getMenuShortcutKeyMask(), false);
+    action = new AbstractAction()
+    {
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        closeView_actionPerformed();
+      }
+    };
+    overrideKeyBinding(key_cmdW, action);
+  }
+
+  /**
+   * Replace Cmd-T new view action with our version.
+   */
+  protected void overrideNewView()
+  {
+    /*
+     * Ctrl-T / Cmd-T open new view
+     */
+    KeyStroke key_cmdT = KeyStroke.getKeyStroke(KeyEvent.VK_T, Toolkit
+            .getDefaultToolkit().getMenuShortcutKeyMask(), false);
+    AbstractAction action = new AbstractAction()
+    {
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        newView_actionPerformed();
+      }
+    };
+    overrideKeyBinding(key_cmdT, action);
+  }
+
+  /**
+   * For now, delegates key events to the corresponding key accelerator for the
+   * AlignFrame that the mouse is in. Hopefully can be simplified in future if
+   * AlignFrame is changed to use key bindings rather than accelerators.
+   */
+  protected void overrideDelegatedKeyBindings()
+  {
+    if (getTopFrame() instanceof AlignFrame)
     {
-      for (Entry<KeyStroke, JMenuItem> acc : ((AlignFrame) getTopComponent())
+      /*
+       * Get all accelerator keys in the top frame (the bottom should be
+       * identical) and override each one.
+       */
+      for (Entry<KeyStroke, JMenuItem> acc : ((AlignFrame) getTopFrame())
               .getAccelerators().entrySet())
       {
+        overrideKeyBinding(acc);
+      }
+    }
+  }
 
-        final KeyStroke ks = acc.getKey();
-        this.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(ks, ks);
-        this.getActionMap().put(ks, new AbstractAction()
+  /**
+   * Overrides an AlignFrame key accelerator with our version which delegates to
+   * the action listener in whichever frame has the mouse (and does nothing if
+   * neither has).
+   * 
+   * @param acc
+   */
+  private void overrideKeyBinding(Entry<KeyStroke, JMenuItem> acc)
+  {
+    final KeyStroke ks = acc.getKey();
+    InputMap inputMap = this.getInputMap(JComponent.WHEN_FOCUSED);
+    inputMap.put(ks, ks);
+    this.getActionMap().put(ks, new AbstractAction()
+    {
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        Component c = getFrameAtMouse();
+        if (c != null && c instanceof AlignFrame)
         {
-          @Override
-          public void actionPerformed(ActionEvent e)
+          for (ActionListener a : ((AlignFrame) c).getAccelerators()
+                  .get(ks).getActionListeners())
           {
-            Component c = getComponentAtMouse();
-            if (c instanceof AlignFrame)
-            {
-              for (ActionListener a : ((AlignFrame) c).getAccelerators()
-                      .get(ks).getActionListeners())
-              {
-
-                a.actionPerformed(null);
-              }
-            }
+            a.actionPerformed(null);
           }
-        });
+        }
       }
-      /*
-       * Disable unwanted here
-       */
-      // X expand views - wrecks the split pane view
-      KeyStroke key_X = KeyStroke.getKeyStroke(KeyEvent.VK_X, 0, false);
-      disableAccelerator(key_X);
+    });
+  }
+
+  /**
+   * Replace an accelerator key's action with the specified action.
+   * 
+   * @param ks
+   */
+  protected void overrideKeyBinding(KeyStroke ks, AbstractAction action)
+  {
+    this.getActionMap().put(ks, action);
+    overrideMenuItem(ks, action);
+  }
+
+  /**
+   * Create and link new views (with matching names) in both panes.
+   * <p>
+   * Note this is _not_ multiple tabs, each hosting a split pane view, rather it
+   * is a single split pane with each split holding multiple tabs which are
+   * linked in pairs.
+   */
+  protected void newView_actionPerformed()
+  {
+    System.out.println("newView " + this.hashCode());
+    AlignFrame topFrame = (AlignFrame) getTopFrame();
+    AlignFrame bottomFrame = (AlignFrame) getBottomFrame();
+
+    AlignmentPanel newTopPanel = topFrame.newView(null, true);
+    AlignmentPanel newBottomPanel = bottomFrame.newView(null, true);
+
+    /*
+     * This currently (for the first new view only) leaves the top pane on tab 0
+     * but the bottom on tab 1. This results from 'setInitialTabVisible' echoing
+     * from the bottom back to the first frame. Next line is a fudge to work
+     * around this. TODO find a better way.
+     */
+    if (topFrame.getTabIndex() != bottomFrame.getTabIndex())
+    {
+      topFrame.setDisplayedView(newTopPanel);
+    }
+
+    newBottomPanel.av.viewName = newTopPanel.av.viewName;
+    newTopPanel.av.setCodingComplement(newBottomPanel.av);
+
+    final StructureSelectionManager ssm = StructureSelectionManager
+            .getStructureSelectionManager(Desktop.instance);
+    ssm.addCommandListener(newTopPanel.av);
+    ssm.addCommandListener(newBottomPanel.av);
+  }
+
+  /**
+   * Close the currently selected view in both panes. If there is only one view,
+   * close this split frame.
+   */
+  protected void closeView_actionPerformed()
+  {
+    int viewCount = ((AlignFrame) getTopFrame()).getAlignPanels().size();
+    if (viewCount < 2)
+    {
+      close();
+      return;
+    }
+
+    AlignmentPanel topPanel = ((AlignFrame) getTopFrame()).alignPanel;
+    AlignmentPanel bottomPanel = ((AlignFrame) getBottomFrame()).alignPanel;
+
+    ((AlignFrame) getTopFrame()).closeView(topPanel);
+    ((AlignFrame) getBottomFrame()).closeView(bottomPanel);
+
+  }
+
+  /**
+   * Close child frames and this split frame.
+   */
+  public void close()
+  {
+    ((AlignFrame) getTopFrame()).closeMenuItem_actionPerformed(true);
+    ((AlignFrame) getBottomFrame()).closeMenuItem_actionPerformed(true);
+    try
+    {
+      this.setClosed(true);
+    } catch (PropertyVetoException e)
+    {
+      // ignore
     }
   }
 
   /**
-   * Ugly hack for Proof of Concept that disables the key binding in this frame
-   * _and_ disables the bound menu item _and_ removes the key accelerator in the
-   * child frames.
+   * Replace AlignFrame 'expand views' action with SplitFrame version.
+   */
+  protected void overrideExpandViews()
+  {
+    KeyStroke key_X = KeyStroke.getKeyStroke(KeyEvent.VK_X, 0, false);
+    AbstractAction action = new AbstractAction()
+    {
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        expandViews_actionPerformed();
+      }
+    };
+    overrideMenuItem(key_X, action);
+  }
+
+  /**
+   * Replace AlignFrame 'gather views' action with SplitFrame version.
+   */
+  protected void overrideGatherViews()
+  {
+    KeyStroke key_G = KeyStroke.getKeyStroke(KeyEvent.VK_G, 0, false);
+    AbstractAction action = new AbstractAction()
+    {
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        gatherViews_actionPerformed();
+      }
+    };
+    overrideMenuItem(key_G, action);
+  }
+
+  /**
+   * Override the menu action associated with the keystroke in the child frames,
+   * replacing it with the given action.
    * 
-   * @param key
+   * @param ks
+   * @param action
    */
-  protected void disableAccelerator(KeyStroke key)
+  private void overrideMenuItem(KeyStroke ks, AbstractAction action)
   {
-    disableAccelerator(key, getTopComponent());
-    disableAccelerator(key, getBottomComponent());
-    this.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).remove(key);
+    overrideMenuItem(ks, action, getTopFrame());
+    overrideMenuItem(ks, action, getBottomFrame());
   }
 
   /**
-   * Disable the menu item for which this key is the accelerator, also removes
-   * its action listeners to prevent key accelerator working.
+   * Override the menu action associated with the keystroke in one child frame,
+   * replacing it with the given action. Mwahahahaha.
    * 
    * @param key
+   * @param action
    * @param comp
    */
-  private void disableAccelerator(KeyStroke key, JComponent comp)
+  private void overrideMenuItem(KeyStroke key, final AbstractAction action,
+          JComponent comp)
   {
-    // HACKED ONLY FOR PROOF OF CONCEPT
-    // Proper solution might involve explicit 'configure menu' method on
-    // AlignFrame, or
-    // changing key listeners to key bindings in AlignFrame, or both
     if (comp instanceof AlignFrame)
     {
       JMenuItem mi = ((AlignFrame) comp).getAccelerators().get(key);
       if (mi != null)
       {
-        mi.setEnabled(false);
         for (ActionListener al : mi.getActionListeners())
         {
           mi.removeActionListener(al);
         }
+        mi.addActionListener(new ActionListener()
+        {
+          @Override
+          public void actionPerformed(ActionEvent e)
+          {
+            action.actionPerformed(e);
+          }
+        });
       }
     }
   }
+
+  /**
+   * Expand any multiple views (which are always in pairs) into separate split
+   * frames.
+   */
+  protected void expandViews_actionPerformed()
+  {
+    Desktop.instance.explodeViews(this);
+  }
+
+  /**
+   * Gather any other SplitFrame views of this alignment back in as multiple
+   * (pairs of) views in this SplitFrame.
+   */
+  protected void gatherViews_actionPerformed()
+  {
+    Desktop.instance.gatherViews(this);
+  }
 }
index 1cfc73c..2d23e6b 100755 (executable)
@@ -3253,4 +3253,15 @@ public class GAlignFrame extends JInternalFrame
   {
     return this.accelerators;
   }
+
+  /**
+   * Returns the selected index of the tabbed pane, or -1 if none selected
+   * (including the case where the tabbed pane has not been made visible).
+   * 
+   * @return
+   */
+  public int getTabIndex()
+  {
+    return tabbedPane.getSelectedIndex();
+  }
 }
index 281a93e..40cce0d 100644 (file)
@@ -1,6 +1,5 @@
 package jalview.jbgui;
 
-import javax.swing.JComponent;
 import javax.swing.JInternalFrame;
 import javax.swing.JSplitPane;
 
@@ -8,9 +7,9 @@ public class GSplitFrame extends JInternalFrame
 {
   private static final long serialVersionUID = 1L;
 
-  private JComponent topComponent;
+  private GAlignFrame topFrame;
 
-  private JComponent bottomComponent;
+  private GAlignFrame bottomFrame;
 
   /**
    * Constructor
@@ -18,10 +17,10 @@ public class GSplitFrame extends JInternalFrame
    * @param top
    * @param bottom
    */
-  public GSplitFrame(JComponent top, JComponent bottom)
+  public GSplitFrame(GAlignFrame top, GAlignFrame bottom)
   {
-    this.topComponent = top;
-    this.bottomComponent = bottom;
+    this.topFrame = top;
+    this.bottomFrame = bottom;
     JSplitPane splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, top,
             bottom);
     splitPane.setVisible(true);
@@ -31,13 +30,13 @@ public class GSplitFrame extends JInternalFrame
     splitPane.setDividerSize(0);
   }
 
-  public JComponent getTopComponent()
+  public GAlignFrame getTopFrame()
   {
-    return topComponent;
+    return topFrame;
   }
 
-  public JComponent getBottomComponent()
+  public GAlignFrame getBottomFrame()
   {
-    return bottomComponent;
+    return bottomFrame;
   }
 }
diff --git a/test/jalview/structure/StructureSelectionManagerTest.java b/test/jalview/structure/StructureSelectionManagerTest.java
new file mode 100644 (file)
index 0000000..487ef2c
--- /dev/null
@@ -0,0 +1,175 @@
+package jalview.structure;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import jalview.datamodel.AlignedCodonFrame;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import org.junit.Before;
+import org.junit.Test;
+
+public class StructureSelectionManagerTest
+{
+  private StructureSelectionManager ssm;
+
+  @Before
+  public void setUp()
+  {
+    ssm = new StructureSelectionManager();
+  }
+
+  @Test
+  public void testAddMapping()
+  {
+    AlignedCodonFrame acf1 = new AlignedCodonFrame();
+    AlignedCodonFrame acf2 = new AlignedCodonFrame();
+
+    /*
+     * One mapping only.
+     */
+    ssm.addMapping(acf1);
+    assertEquals(1, ssm.seqmappings.size());
+    assertTrue(ssm.seqmappings.contains(acf1));
+    assertEquals(1, ssm.seqMappingRefCounts.size());
+    assertEquals(1, ssm.seqMappingRefCounts.get(acf1).intValue());
+
+    /*
+     * A second mapping.
+     */
+    ssm.addMapping(acf2);
+    assertEquals(2, ssm.seqmappings.size());
+    assertTrue(ssm.seqmappings.contains(acf1));
+    assertTrue(ssm.seqmappings.contains(acf2));
+    assertEquals(2, ssm.seqMappingRefCounts.size());
+    assertEquals(1, ssm.seqMappingRefCounts.get(acf1).intValue());
+    assertEquals(1, ssm.seqMappingRefCounts.get(acf2).intValue());
+
+    /*
+     * A second reference to the first mapping.
+     */
+    ssm.addMapping(acf1);
+    assertEquals(2, ssm.seqmappings.size());
+    assertTrue(ssm.seqmappings.contains(acf1));
+    assertTrue(ssm.seqmappings.contains(acf2));
+    assertEquals(2, ssm.seqMappingRefCounts.size());
+    assertEquals(2, ssm.seqMappingRefCounts.get(acf1).intValue());
+    assertEquals(1, ssm.seqMappingRefCounts.get(acf2).intValue());
+  }
+
+  @Test
+  public void testAddMappings()
+  {
+    AlignedCodonFrame acf1 = new AlignedCodonFrame();
+    AlignedCodonFrame acf2 = new AlignedCodonFrame();
+    AlignedCodonFrame acf3 = new AlignedCodonFrame();
+
+    Set<AlignedCodonFrame> set1 = new HashSet<AlignedCodonFrame>();
+    set1.add(acf1);
+    set1.add(acf2);
+    Set<AlignedCodonFrame> set2 = new HashSet<AlignedCodonFrame>();
+    set2.add(acf2);
+    set2.add(acf3);
+
+    /*
+     * Adding both sets adds acf2 twice and acf1 and acf3 once each.
+     */
+    ssm.addMappings(set1);
+    ssm.addMappings(set2);
+
+    assertEquals(3, ssm.seqmappings.size());
+    assertTrue(ssm.seqmappings.contains(acf1));
+    assertTrue(ssm.seqmappings.contains(acf2));
+    assertTrue(ssm.seqmappings.contains(acf3));
+    assertEquals(3, ssm.seqMappingRefCounts.size());
+    assertEquals(1, ssm.seqMappingRefCounts.get(acf1).intValue());
+    assertEquals(2, ssm.seqMappingRefCounts.get(acf2).intValue());
+    assertEquals(1, ssm.seqMappingRefCounts.get(acf3).intValue());
+  }
+
+  @Test
+  public void testRemoveMapping()
+  {
+    AlignedCodonFrame acf1 = new AlignedCodonFrame();
+    AlignedCodonFrame acf2 = new AlignedCodonFrame();
+    ssm.addMapping(acf1);
+
+    /*
+     * Add one and remove it.
+     */
+    ssm.removeMapping(acf1);
+    ssm.removeMapping(acf2);
+    assertEquals(0, ssm.seqmappings.size());
+    assertEquals(0, ssm.seqMappingRefCounts.size());
+
+    /*
+     * Add one twice and remove it once.
+     */
+    ssm.addMapping(acf1);
+    ssm.addMapping(acf2);
+    ssm.addMapping(acf1);
+    ssm.removeMapping(acf1);
+    assertEquals(2, ssm.seqmappings.size());
+    assertTrue(ssm.seqmappings.contains(acf1));
+    assertTrue(ssm.seqmappings.contains(acf2));
+    assertEquals(2, ssm.seqMappingRefCounts.size());
+    assertEquals(1, ssm.seqMappingRefCounts.get(acf1).intValue());
+    assertEquals(1, ssm.seqMappingRefCounts.get(acf2).intValue());
+
+    /*
+     * Remove both once more to clear the set.
+     */
+    ssm.removeMapping(acf1);
+    ssm.removeMapping(acf2);
+    assertEquals(0, ssm.seqmappings.size());
+    assertEquals(0, ssm.seqMappingRefCounts.size());
+  }
+
+  @Test
+  public void testRemoveMappings()
+  {
+    AlignedCodonFrame acf1 = new AlignedCodonFrame();
+    AlignedCodonFrame acf2 = new AlignedCodonFrame();
+    AlignedCodonFrame acf3 = new AlignedCodonFrame();
+
+    /*
+     * Initial ref counts are 3/2/1:
+     */
+    ssm.addMapping(acf1);
+    ssm.addMapping(acf1);
+    ssm.addMapping(acf1);
+    ssm.addMapping(acf2);
+    ssm.addMapping(acf2);
+    ssm.addMapping(acf3);
+
+    Set<AlignedCodonFrame> set1 = new HashSet<AlignedCodonFrame>();
+    set1.add(acf1);
+    set1.add(acf2);
+    Set<AlignedCodonFrame> set2 = new HashSet<AlignedCodonFrame>();
+    set2.add(acf2);
+    set2.add(acf3);
+
+    /*
+     * Remove one ref each to acf1, acf2, counts are now 2/1/1:
+     */
+    ssm.removeMappings(set1);
+    assertEquals(3, ssm.seqmappings.size());
+    assertTrue(ssm.seqmappings.contains(acf1));
+    assertTrue(ssm.seqmappings.contains(acf2));
+    assertTrue(ssm.seqmappings.contains(acf3));
+    assertEquals(3, ssm.seqMappingRefCounts.size());
+    assertEquals(2, ssm.seqMappingRefCounts.get(acf1).intValue());
+    assertEquals(1, ssm.seqMappingRefCounts.get(acf2).intValue());
+    assertEquals(1, ssm.seqMappingRefCounts.get(acf3).intValue());
+
+    /*
+     * Remove one ref each to acf2, acf3 - they are removed
+     */
+    ssm.removeMappings(set2);
+    assertEquals(1, ssm.seqmappings.size());
+    assertTrue(ssm.seqmappings.contains(acf1));
+    assertEquals(1, ssm.seqMappingRefCounts.size());
+    assertEquals(2, ssm.seqMappingRefCounts.get(acf1).intValue());
+  }
+}