From 778f0fc457ac635f1dfdd829e3cc37ab5a5a284b Mon Sep 17 00:00:00 2001 From: Mateusz Warowny Date: Fri, 11 Mar 2022 15:46:17 +0100 Subject: [PATCH] JAL-3878 Create web service menu manager. --- src/jalview/ws2/api/Credentials.java | 8 +- src/jalview/ws2/gui/WebServicesMenuManager.java | 279 +++++++++++++++++++++++ 2 files changed, 286 insertions(+), 1 deletion(-) create mode 100644 src/jalview/ws2/gui/WebServicesMenuManager.java diff --git a/src/jalview/ws2/api/Credentials.java b/src/jalview/ws2/api/Credentials.java index 462c048..cc7c714 100644 --- a/src/jalview/ws2/api/Credentials.java +++ b/src/jalview/ws2/api/Credentials.java @@ -7,10 +7,16 @@ public final class Credentials String username = null; String email = null; String password = null; - + private static final Credentials EMPTY = new Credentials(); + private Credentials() { } + public static final Credentials empty() + { + return EMPTY; + } + public static final Credentials usingEmail(String email) { Objects.requireNonNull(email); if (email.isEmpty()) diff --git a/src/jalview/ws2/gui/WebServicesMenuManager.java b/src/jalview/ws2/gui/WebServicesMenuManager.java new file mode 100644 index 0000000..188f587 --- /dev/null +++ b/src/jalview/ws2/gui/WebServicesMenuManager.java @@ -0,0 +1,279 @@ +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>> oneshotServices = new HashMap<>(); + Map>> 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> services, JMenu menu) + { + services.sort(Comparator + ., 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>>(); + 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("%s
%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 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> openEditParamsDialog( + ParamDatastoreI paramStore, WsParamSetI preset, List 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(); + }); + } +} -- 1.7.10.2