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(); }); } }