JAL-3878 Separate gui elements from operations.
[jalview.git] / src / jalview / ws2 / gui / AlignmentMenuBuilder.java
diff --git a/src/jalview/ws2/gui/AlignmentMenuBuilder.java b/src/jalview/ws2/gui/AlignmentMenuBuilder.java
new file mode 100644 (file)
index 0000000..f3dfb68
--- /dev/null
@@ -0,0 +1,377 @@
+package jalview.ws2.gui;
+
+import static java.lang.String.format;
+
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.CompletionStage;
+import java.util.function.Consumer;
+
+import javax.swing.JMenu;
+import javax.swing.JMenuItem;
+import javax.swing.ToolTipManager;
+
+import jalview.datamodel.Alignment;
+import jalview.datamodel.AlignmentI;
+import jalview.datamodel.AlignmentOrder;
+import jalview.datamodel.AlignmentView;
+import jalview.datamodel.HiddenColumns;
+import jalview.gui.AlignFrame;
+import jalview.gui.AlignViewport;
+import jalview.gui.Desktop;
+import jalview.gui.JvOptionPane;
+import jalview.gui.JvSwingUtils;
+import jalview.gui.WebserviceInfo;
+import jalview.gui.WsJobParameters;
+import jalview.util.MathUtils;
+import jalview.util.MessageManager;
+import jalview.ws.params.ArgumentI;
+import jalview.ws.params.ParamDatastoreI;
+import jalview.ws.params.WsParamSetI;
+import jalview.ws2.MenuEntryProviderI;
+import jalview.ws2.PollingTaskExecutor;
+import jalview.ws2.WSJob;
+import jalview.ws2.WebServiceInfoUpdater;
+import jalview.ws2.WebServiceWorkerI;
+import jalview.ws2.WebServiceWorkerListener;
+import jalview.ws2.operations.AlignmentOperation;
+import jalview.ws2.operations.AlignmentOperation.AlignmentResult;
+import jalview.ws2.operations.AlignmentOperation.AlignmentWorker;
+
+
+public class AlignmentMenuBuilder implements MenuEntryProviderI
+{
+  AlignmentOperation operation;
+  
+  public AlignmentMenuBuilder(AlignmentOperation operation)
+  {
+    this.operation = operation;
+  }
+  
+  public void buildMenu(JMenu parent, AlignFrame frame)
+  {
+    if (operation.canSubmitGaps())
+    {
+      var alignSubmenu = new JMenu(operation.getName());
+      buildMenu(alignSubmenu, frame, false);
+      parent.add(alignSubmenu);
+      var realignSubmenu = new JMenu(MessageManager.formatMessage(
+              "label.realign_with_params", operation.getName()));
+      realignSubmenu.setToolTipText(MessageManager
+              .getString("label.align_sequences_to_existing_alignment"));
+      buildMenu(realignSubmenu, frame, true);
+      parent.add(realignSubmenu);
+    }
+    else
+    {
+      buildMenu(parent, frame, false);
+    }
+  }
+
+  protected void buildMenu(JMenu parent, AlignFrame frame,
+          boolean submitGaps)
+  {
+    final String action = submitGaps ? "Align" : "Realign";
+    final var calcName = operation.getName();
+
+    {
+      var item = new JMenuItem(MessageManager.formatMessage(
+              "label.calcname_with_default_settings", calcName));
+      item.setToolTipText(MessageManager
+              .formatMessage("label.action_with_default_settings", action));
+      item.addActionListener((event) -> {
+        final AlignmentView msa = frame.gatherSequencesForAlignment();
+        if (msa != null)
+        {
+          startWorker(frame, msa, Collections.emptyList(), submitGaps);
+        }
+      });
+      parent.add(item);
+    }
+
+    if (operation.hasParameters())
+    {
+      var item = new JMenuItem(
+              MessageManager.getString("label.edit_settings_and_run"));
+      item.setToolTipText(MessageManager.getString(
+              "label.view_and_change_parameters_before_alignment"));
+      item.addActionListener((event) -> {
+        AlignmentView msa = frame.gatherSequencesForAlignment();
+        if (msa != null)
+        {
+          openEditParamsDialog(operation.getParamStore(), null, null)
+              .thenAcceptAsync((arguments) -> {
+                if (arguments != null)
+                {
+                  startWorker(frame, msa, arguments, submitGaps);
+                }
+              });
+        }
+      });
+      parent.add(item);
+    }
+
+    var presets = operation.getParamStore().getPresets();
+    if (presets != null && presets.size() > 0)
+    {
+      final var presetList = new JMenu(MessageManager
+              .formatMessage("label.run_with_preset_params", calcName));
+      final var showToolTipFor = ToolTipManager.sharedInstance()
+              .getDismissDelay();
+      for (final var preset : presets)
+      {
+        var item = new JMenuItem(preset.getName());
+        final int QUICK_TOOLTIP = 1500;
+        item.addMouseListener(new MouseAdapter()
+        {
+          @Override
+          public void mouseEntered(MouseEvent e)
+          {
+            ToolTipManager.sharedInstance().setDismissDelay(QUICK_TOOLTIP);
+          }
+
+          @Override
+          public void mouseExited(MouseEvent e)
+          {
+            ToolTipManager.sharedInstance().setDismissDelay(showToolTipFor);
+          }
+        });
+        String tooltip = JvSwingUtils.wrapTooltip(true,
+                format("<strong>%s</strong><br/>%s",
+                        MessageManager.getString(
+                                preset.isModifiable() ? "label.user_preset"
+                                        : "label.service_preset"),
+                        preset.getDescription()));
+        item.setToolTipText(tooltip);
+        item.addActionListener((event) -> {
+          AlignmentView msa = frame.gatherSequencesForAlignment();
+          startWorker(frame, msa, preset.getArguments(), submitGaps);
+        });
+        presetList.add(item);
+      }
+      parent.add(presetList);
+    }
+  }
+
+  private CompletionStage<List<ArgumentI>> openEditParamsDialog(
+          ParamDatastoreI paramStore, WsParamSetI preset,
+          List<ArgumentI> arguments)
+  {
+    WsJobParameters jobParams;
+    if (preset == null && arguments != null && arguments.size() > 0)
+      jobParams = new WsJobParameters(paramStore, preset, arguments);
+    else
+      jobParams = new WsJobParameters(paramStore, preset, null);
+    if (preset != null)
+    {
+      jobParams.setName(MessageManager.getString(
+          "label.adjusting_parameters_for_calculation"));
+    }
+    var stage = jobParams.showRunDialog();
+    return stage.thenApply((startJob) -> {
+      if (startJob)
+      {
+        if (jobParams.getPreset() == null)
+        {
+          return jobParams.getJobParams();
+        }
+        else
+        {
+          return jobParams.getPreset().getArguments();
+        }
+      }
+      else
+      {
+        return null;
+      }
+    });
+  }
+
+  private void startWorker(AlignFrame frame, AlignmentView msa,
+      List<ArgumentI> arguments, boolean submitGaps)
+  {
+    AlignViewport viewport = frame.getViewport();
+    PollingTaskExecutor executor = frame.getViewport().getWSExecutor();
+    if (msa != null)
+    {
+
+      String panelInfo = String.format("%s using service hosted at %s%n%s",
+              operation.getName(), operation.getHostName(),
+              Objects.requireNonNullElse(operation.getDescription(), ""));
+      var wsInfo = new WebserviceInfo(operation.getName(), panelInfo, false);
+      
+      final String alnTitle = frame.getTitle();
+      AlignmentWorker worker = operation.new AlignmentWorker(msa,
+          arguments, frame.getTitle(), submitGaps, true,
+          viewport);
+      String outputHeader = String.format("%s of %s%nJob details%n",
+          submitGaps ? "Re-alignment" : "Alignment", alnTitle);
+
+      var awl = new AlignmentWorkerListener(worker, wsInfo, frame,
+          outputHeader);
+      worker.setResultConsumer(awl);
+      worker.addListener(awl);
+      
+      executor.submit(worker);
+    }
+    
+  }
+  
+  private class AlignmentWorkerListener
+      implements WebServiceWorkerListener, Consumer<AlignmentResult>
+  {
+
+    final WebServiceWorkerI worker;
+    final WebserviceInfo wsInfo;
+    final AlignFrame frame;
+    WebServiceInfoUpdater updater;
+    String outputHeader;
+    final long progbarId = MathUtils.getUID();
+    
+    private AlignmentWorkerListener(WebServiceWorkerI worker, WebserviceInfo wsInfo,
+        AlignFrame frame, String header)
+    {
+      this.worker = worker;
+      this.wsInfo = wsInfo;
+      this.frame = frame;
+      this.outputHeader = header;
+      this.updater = new WebServiceInfoUpdater(worker, wsInfo);
+      updater.setOutputHeader(outputHeader);
+    }
+    
+    @Override
+    public void workerStarted(WebServiceWorkerI source)
+    {
+      // wsInfo.setThisService() should happen here
+      wsInfo.setVisible(true);
+    }
+    
+    @Override
+    public void workerNotStarted(WebServiceWorkerI source)
+    {
+      wsInfo.setVisible(false);
+      // TODO show notification dialog.
+       JvOptionPane.showMessageDialog(frame,
+           MessageManager.getString("info.invalid_msa_input_mininfo"),
+           MessageManager.getString("info.invalid_msa_notenough"),
+           JvOptionPane.INFORMATION_MESSAGE);
+    }
+
+    @Override
+    public void jobCreated(WebServiceWorkerI source, WSJob job)
+    {
+      int tabIndex = wsInfo.addJobPane();
+      wsInfo.setProgressName(String.format("region %d", job.getJobNum()), 
+          tabIndex);
+      wsInfo.setProgressText(tabIndex, outputHeader);
+      job.addPropertyChangeListener(updater);
+    }
+    
+    @Override
+    public void pollException(WebServiceWorkerI source, WSJob job, Exception e)
+    {
+      wsInfo.appendProgressText(job.getJobNum(),
+          MessageManager.formatMessage("info.server_exception",
+              operation.getName(), e.getMessage()));
+    }
+
+    @Override
+    public void workerCompleting(WebServiceWorkerI source)
+    {
+      // TODO Auto-generated method stub
+      wsInfo.setProgressBar(
+              MessageManager.getString("status.collecting_job_results"),
+              progbarId);
+    }
+    
+    @Override
+    public void workerCompleted(WebServiceWorkerI source)
+    {
+      wsInfo.removeProgressBar(progbarId);
+      
+    }
+
+    @Override
+    public void accept(AlignmentResult out)
+    {
+      if (out != null)
+      {
+        wsInfo.showResultsNewFrame.addActionListener(evt -> displayNewFrame(
+                new Alignment(out.getAln()), out.getAlorders(), out.getHidden()));
+        wsInfo.setResultsReady();
+      }
+      else
+      {
+        wsInfo.setFinishedNoResults();
+      }
+    }
+    
+    private void displayNewFrame(AlignmentI aln,
+        List<AlignmentOrder> alorders, HiddenColumns hidden)
+    {
+      AlignFrame frame = new AlignFrame(aln, hidden,
+              AlignFrame.DEFAULT_WIDTH, AlignFrame.DEFAULT_HEIGHT);
+      // TODO store feature renderer settings in worker object
+      // frame.getFeatureRenderer().transferSettings(featureSettings);
+      var regions = sortOrders(alorders);
+      if (alorders.size() == 1)
+      {
+        frame.addSortByOrderMenuItem(
+                format("%s Ordering", operation.getName()), alorders.get(0));
+      }
+      else
+      {
+        for (int i = 0; i < alorders.size(); i++)
+        {
+          final int j = i;
+          Iterable<String> iter = () -> regions.get(j).stream()
+                  .map(it -> Integer.toString(it)).iterator();
+          var orderName = format("%s Region %s Ordering", operation.getName(),
+                  String.join(",", iter));
+          frame.addSortByOrderMenuItem(orderName, alorders.get(i));
+        }
+      }
+    
+      /* TODO
+       * If alignment was requested from one half of a SplitFrame, show in a
+       * SplitFrame with the other pane similarly aligned.
+       */
+    
+      Desktop.addInternalFrame(frame, frame.getTitle(), AlignFrame.DEFAULT_WIDTH,
+              AlignFrame.DEFAULT_HEIGHT);
+    }
+
+
+    private List<List<Integer>> sortOrders(List<?> alorders)
+    {
+      List<List<Integer>> regions = new ArrayList<>();
+      for (int i = 0; i < alorders.size(); i++)
+      {
+        List<Integer> regs = new ArrayList<>();
+        regs.add(i);
+        int j = i + 1;
+        while (j < alorders.size())
+        {
+          if (alorders.get(i).equals(alorders.get(j)))
+          {
+            alorders.remove(j);
+            regs.add(j);
+          }
+          else
+          {
+            j++;
+          }
+        }
+        regions.add(regs);
+      }
+      return regions;
+    }
+    
+    
+  }
+}
+