--- /dev/null
+package jalview.ws2.gui;
+
+import java.awt.Color;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.TreeMap;
+import java.util.concurrent.CompletionStage;
+
+import javax.swing.JMenu;
+import javax.swing.JMenuItem;
+import javax.swing.ToolTipManager;
+
+import jalview.gui.AlignFrame;
+import jalview.gui.Desktop;
+import jalview.gui.JvSwingUtils;
+import jalview.gui.WsJobParameters;
+import jalview.util.MessageManager;
+import jalview.viewmodel.AlignmentViewport;
+import jalview.ws.params.ArgumentI;
+import jalview.ws.params.ParamDatastoreI;
+import jalview.ws.params.WsParamSetI;
+import jalview.ws2.actions.alignment.AlignmentAction;
+import jalview.ws2.actions.api.ActionI;
+import jalview.ws2.actions.api.TaskI;
+import jalview.ws2.api.Credentials;
+import jalview.ws2.api.WebService;
+import jalview.ws2.client.api.WebServiceProviderI;
+
+public class WebServicesMenuManager
+{
+ private final JMenu menu;
+
+ private final AlignFrame frame;
+
+ private JMenuItem inProgressItem = new JMenuItem("Service discovery in progress");
+
+ private JMenuItem noServicesItem = new JMenuItem("No services available");
+ {
+ inProgressItem.setEnabled(false);
+ inProgressItem.setVisible(false);
+ noServicesItem.setEnabled(false);
+ }
+
+ public WebServicesMenuManager(String name, AlignFrame frame)
+ {
+ this.frame = frame;
+ menu = new JMenu(name);
+ menu.add(inProgressItem);
+ menu.add(noServicesItem);
+ }
+
+ public JMenu getMenu()
+ {
+ return menu;
+ }
+
+ public void setNoServices(boolean noServices)
+ {
+ noServicesItem.setVisible(noServices);
+ }
+
+ public void setInProgress(boolean inProgress)
+ {
+ inProgressItem.setVisible(inProgress);
+ }
+
+ public void setServices(WebServiceProviderI services)
+ {
+ menu.removeAll();
+ // services grouped by their category
+ Map<String, List<WebService<?>>> oneshotServices = new HashMap<>();
+ Map<String, List<WebService<?>>> interactiveServices = new HashMap<>();
+ for (WebService<?> service : services.getServices())
+ {
+ var map = service.isInteractive() ? interactiveServices : oneshotServices;
+ map.computeIfAbsent(service.getCategory(), k -> new ArrayList<>())
+ .add(service);
+ }
+ var allKeysSet = new HashSet<>(oneshotServices.keySet());
+ allKeysSet.addAll(interactiveServices.keySet());
+ var allKeys = new ArrayList<>(allKeysSet);
+ allKeys.sort(Comparator.naturalOrder());
+ for (String category : allKeys)
+ {
+ var categoryMenu = new JMenu(category);
+ var oneshot = oneshotServices.get(category);
+ if (oneshot != null)
+ addOneshotEntries(oneshot, categoryMenu);
+ var interactive = interactiveServices.get(category);
+ if (interactive != null)
+ {
+ if (oneshot != null)
+ categoryMenu.addSeparator();
+ // addInteractiveEntries(interactive, categoryMenu);
+ }
+ menu.add(categoryMenu);
+ }
+ menu.add(inProgressItem);
+ menu.add(noServicesItem);
+ }
+
+ private void addOneshotEntries(List<WebService<?>> services, JMenu menu)
+ {
+ services.sort(Comparator
+ .<WebService<?>, String> comparing(s -> s.getUrl().toString())
+ .thenComparing(WebService::getName));
+ URL lastHost = null;
+ for (WebService<?> service : services)
+ {
+ // if new host differs from the last one, add entry separating them
+ URL host = service.getUrl();
+ if (!host.equals(lastHost))
+ {
+ if (lastHost != null)
+ menu.addSeparator();
+ var item = new JMenuItem(host.toString());
+ item.setForeground(Color.BLUE);
+ item.addActionListener(e -> Desktop.showUrl(host.toString()));
+ menu.add(item);
+ lastHost = host;
+ }
+ menu.addSeparator();
+ // group actions by their subcategory, sorted
+ var actionsByCategory = new TreeMap<String, List<ActionI<?>>>();
+ for (ActionI<?> action : service.getActions())
+ {
+ actionsByCategory
+ .computeIfAbsent(
+ Objects.requireNonNullElse(action.getSubcategory(), ""),
+ k -> new ArrayList<>())
+ .add(action);
+ }
+ actionsByCategory.forEach((k, v) -> {
+ // create submenu named {subcategory} with {service} or use root menu
+ var atMenu = k.isEmpty() ? menu : new JMenu(String.format("%s with %s", k, service.getName()));
+ if (atMenu != menu)
+ menu.add(atMenu); // add only if submenu
+ // sort actions by name pulling nulls to the front
+ v.sort(Comparator.comparing(
+ ActionI::getName, Comparator.nullsFirst(Comparator.naturalOrder())));
+ for (ActionI<?> action : v)
+ {
+ addEntriesForAction(action, atMenu, atMenu == menu);
+ }
+ });
+ }
+ }
+
+ private void addEntriesForAction(ActionI<?> action, JMenu menu, boolean isTopLevel)
+ {
+ var service = action.getWebService();
+ String itemName;
+ if (isTopLevel)
+ {
+ itemName = service.getName();
+ if (action.getName() != null && !action.getName().isEmpty())
+ itemName += " " + action.getName();
+ }
+ else
+ {
+ if (action.getName() == null || action.getName().isEmpty())
+ itemName = "Run";
+ else
+ itemName = action.getName();
+ }
+ var datastore = service.getParamDatastore();
+ {
+ String text = itemName;
+ if (datastore.hasParameters() || datastore.hasPresets())
+ text += "with defaults";
+ JMenuItem item = new JMenuItem(text);
+ item.addActionListener(e -> {
+ runAction(action, frame.getCurrentView(), Collections.emptyList(),
+ Credentials.empty());
+ });
+ menu.add(item);
+ }
+ if (datastore.hasParameters())
+ {
+ JMenuItem item = new JMenuItem("Edit settings and run...");
+ item.addActionListener(e -> {
+ openEditParamsDialog(datastore, null, null).thenAccept(args -> {
+ if (args != null)
+ runAction(action, frame.getCurrentView(), args, Credentials.empty());
+ });
+ });
+ menu.add(item);
+ }
+ var presets = datastore.getPresets();
+ if (presets != null && presets.size() > 0)
+ {
+ final var presetsMenu = new JMenu(MessageManager.formatMessage(
+ "label.run_with_preset_params", service.getName()));
+ final int dismissDelay = ToolTipManager.sharedInstance()
+ .getDismissDelay();
+ final int QUICK_TOOLTIP = 1500;
+ for (var preset : presets)
+ {
+ var item = new JMenuItem(preset.getName());
+ item.addMouseListener(new MouseAdapter()
+ {
+ @Override
+ public void mouseEntered(MouseEvent evt)
+ {
+ ToolTipManager.sharedInstance().setDismissDelay(QUICK_TOOLTIP);
+ }
+
+ @Override
+ public void mouseExited(MouseEvent evt)
+ {
+ ToolTipManager.sharedInstance().setDismissDelay(dismissDelay);
+ }
+ });
+ String tooltipTitle = MessageManager.getString(
+ preset.isModifiable() ? "label.user_preset" : "label.service_preset");
+ String tooltip = String.format("<strong>%s</strong><br/>%s",
+ tooltipTitle, preset.getDescription());
+ tooltip = JvSwingUtils.wrapTooltip(true, tooltip);
+ item.setToolTipText(tooltip);
+ item.addActionListener(event -> {
+ runAction(action, frame.getCurrentView(), preset.getArguments(),
+ Credentials.empty());
+ });
+ presetsMenu.add(item);
+ }
+ menu.add(presetsMenu);
+ }
+ }
+
+ private TaskI<?> runAction(ActionI<?> action, AlignmentViewport viewport,
+ List<ArgumentI> args, Credentials credentials)
+ {
+ // casting and instance checks can be avoided with some effort,
+ // let them be for now.
+ if (action instanceof AlignmentAction)
+ {
+ // TODO: test if selection contains enough sequences
+ var _action = (AlignmentAction) action;
+ var handler = new AlignmentServiceGuiHandler(_action, frame);
+ return _action.perform(viewport, args, credentials, handler);
+ }
+ throw new IllegalArgumentException(
+ String.format("Illegal action type %s", action.getClass().getName()));
+ }
+
+ private static CompletionStage<List<ArgumentI>> openEditParamsDialog(
+ ParamDatastoreI paramStore, WsParamSetI preset, List<ArgumentI> arguments)
+ {
+ final 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)
+ return null; // null if cancelled
+ if (jobParams.getPreset() != null)
+ return jobParams.getPreset().getArguments();
+ if (jobParams.isServiceDefaults())
+ return Collections.emptyList();
+ else
+ return jobParams.getJobParams();
+ });
+ }
+}