package jalview.ws2.gui; import java.awt.Color; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.lang.ref.WeakReference; 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.JCheckBoxMenuItem; import javax.swing.JLabel; import javax.swing.JMenu; import javax.swing.JMenuItem; import javax.swing.ToolTipManager; import javax.swing.border.EmptyBorder; 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.annotation.AnnotationAction; 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; import static java.lang.String.format; import static java.util.Objects.requireNonNullElse; 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); } private Map>> interactiveTasks = new HashMap<>(); 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 void addInteractiveEntries(List> services, JMenu menu) { Map>> byServiceName = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); for (var service : services) { byServiceName.computeIfAbsent(service.getName(), k -> new ArrayList<>()) .add(service); } for (var entry : byServiceName.entrySet()) { var group = new InteractiveServiceEntryGroup(entry.getKey(), entry.getValue()); group.appendTo(menu); } } private class InteractiveServiceEntryGroup { JLabel serviceLabel; JMenuItem urlItem = new JMenuItem(); JCheckBoxMenuItem serviceItem = new JCheckBoxMenuItem(); JMenuItem editParamsItem = new JMenuItem("Edit parameters..."); JMenu presetsMenu = new JMenu("Change preset"); JMenu alternativesMenu = new JMenu("Choose action"); { urlItem.setForeground(Color.BLUE); urlItem.setVisible(false); serviceItem.setVisible(false); editParamsItem.setVisible(false); presetsMenu.setVisible(false); } InteractiveServiceEntryGroup(String name, List> services) { serviceLabel = new JLabel(name); serviceLabel.setBorder(new EmptyBorder(0, 6, 0, 6)); buildAlternativesMenu(services); } private void buildAlternativesMenu(List> services) { var menu = alternativesMenu; services.sort(Comparator ., String> comparing(s -> s.getUrl().toString()) .thenComparing(s -> s.getName())); URL lastHost = null; for (var service : services) { // Adding url "separator" before each group 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(); var actionsByCategory = new TreeMap>>(); for (ActionI action : service.getActions()) { actionsByCategory .computeIfAbsent( requireNonNullElse(action.getSubcategory(), ""), k -> new ArrayList<>()) .add(action); } actionsByCategory.forEach((key, actions) -> { var atMenu = key.isEmpty() ? menu : new JMenu(key + " with " + service.getName()); boolean topLevel = atMenu == menu; if (!topLevel) menu.add(atMenu); actions.sort(Comparator.comparing( a -> a.getName(), Comparator.nullsFirst(Comparator.naturalOrder()))); for (ActionI action : actions) { var item = new JMenuItem(action.getFullName()); item.addActionListener(e -> setAlternative(action)); atMenu.add(item); } }); } } private void setAlternative(ActionI action) { final var arguments = new ArrayList(); final WsParamSetI[] lastPreset = { null }; // update selected url menu item String url = action.getWebService().getUrl().toString(); urlItem.setText(url); urlItem.setVisible(true); for (var l : urlItem.getActionListeners()) urlItem.removeActionListener(l); urlItem.addActionListener(e -> Desktop.showUrl(url)); // update selected service menu item serviceItem.setText(action.getFullName()); serviceItem.setVisible(true); for (var l : serviceItem.getActionListeners()) serviceItem.removeActionListener(l); WebService service = action.getWebService(); serviceItem.addActionListener(e -> { if (serviceItem.getState()) { cancelAndRunInteractive(action, frame.getCurrentView(), arguments, Credentials.empty()); } else { cancelInteractive(service.getName()); } }); serviceItem.setSelected(true); // update edit parameters menu item var datastore = service.getParamDatastore(); editParamsItem.setVisible(datastore.hasParameters()); for (var l : editParamsItem.getActionListeners()) editParamsItem.removeActionListener(l); if (datastore.hasParameters()) { editParamsItem.addActionListener(e -> { openEditParamsDialog(service.getParamDatastore(), lastPreset[0], arguments) .thenAccept(args -> { if (args != null) { lastPreset[0] = null; arguments.clear(); arguments.addAll(args); cancelAndRunInteractive(action, frame.getCurrentView(), arguments, Credentials.empty()); } }); }); } // update presets menu presetsMenu.removeAll(); presetsMenu.setEnabled(datastore.hasPresets()); if (datastore.hasPresets()) { for (WsParamSetI preset : datastore.getPresets()) { var item = new JMenuItem(preset.getName()); item.addActionListener(e -> { lastPreset[0] = preset; cancelAndRunInteractive(action, frame.getCurrentView(), preset.getArguments(), Credentials.empty()); }); presetsMenu.add(item); } } cancelAndRunInteractive(action, frame.getCurrentView(), arguments, Credentials.empty()); } void appendTo(JMenu menu) { menu.add(serviceLabel); menu.add(urlItem); menu.add(serviceItem); menu.add(editParamsItem); menu.add(presetsMenu); menu.add(alternativesMenu); } } private void cancelInteractive(String wsName) { var taskRef = interactiveTasks.get(wsName); if (taskRef != null && taskRef.get() != null) taskRef.get().cancel(); interactiveTasks.put(wsName, null); } private void cancelAndRunInteractive(ActionI action, AlignmentViewport viewport, List args, Credentials credentials) { var wsName = action.getWebService().getName(); cancelInteractive(wsName); var task = runAction(action, viewport, args, credentials); interactiveTasks.put(wsName, new WeakReference<>(task)); } 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); } if (action instanceof AnnotationAction) { var _action = (AnnotationAction) action; var handler = new AnnotationServiceGuiHandler(_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, null, 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(); }); } }