1 package jalview.ws2.gui;
4 import java.awt.event.MouseAdapter;
5 import java.awt.event.MouseEvent;
7 import java.util.ArrayList;
8 import java.util.Collection;
9 import java.util.Collections;
10 import java.util.Comparator;
11 import java.util.HashMap;
12 import java.util.HashSet;
13 import java.util.List;
15 import java.util.Objects;
16 import java.util.TreeMap;
17 import java.util.concurrent.CompletionStage;
19 import javax.swing.JMenu;
20 import javax.swing.JMenuItem;
21 import javax.swing.ToolTipManager;
23 import jalview.gui.AlignFrame;
24 import jalview.gui.Desktop;
25 import jalview.gui.JvSwingUtils;
26 import jalview.gui.WsJobParameters;
27 import jalview.util.MessageManager;
28 import jalview.viewmodel.AlignmentViewport;
29 import jalview.ws.params.ArgumentI;
30 import jalview.ws.params.ParamDatastoreI;
31 import jalview.ws.params.WsParamSetI;
32 import jalview.ws2.actions.alignment.AlignmentAction;
33 import jalview.ws2.actions.annotation.AnnotationAction;
34 import jalview.ws2.actions.api.ActionI;
35 import jalview.ws2.actions.api.TaskI;
36 import jalview.ws2.api.Credentials;
37 import jalview.ws2.api.WebService;
38 import jalview.ws2.client.api.WebServiceProviderI;
40 public class WebServicesMenuManager
42 private final JMenu menu;
44 private final AlignFrame frame;
46 private JMenuItem inProgressItem = new JMenuItem("Service discovery in progress");
48 private JMenuItem noServicesItem = new JMenuItem("No services available");
50 inProgressItem.setEnabled(false);
51 inProgressItem.setVisible(false);
52 noServicesItem.setEnabled(false);
55 public WebServicesMenuManager(String name, AlignFrame frame)
58 menu = new JMenu(name);
59 menu.add(inProgressItem);
60 menu.add(noServicesItem);
63 public JMenu getMenu()
68 public void setNoServices(boolean noServices)
70 noServicesItem.setVisible(noServices);
73 public void setInProgress(boolean inProgress)
75 inProgressItem.setVisible(inProgress);
78 public void setServices(WebServiceProviderI services)
81 // services grouped by their category
82 Map<String, List<WebService<?>>> oneshotServices = new HashMap<>();
83 Map<String, List<WebService<?>>> interactiveServices = new HashMap<>();
84 for (WebService<?> service : services.getServices())
86 var map = service.isInteractive() ? interactiveServices : oneshotServices;
87 map.computeIfAbsent(service.getCategory(), k -> new ArrayList<>())
90 var allKeysSet = new HashSet<>(oneshotServices.keySet());
91 allKeysSet.addAll(interactiveServices.keySet());
92 var allKeys = new ArrayList<>(allKeysSet);
93 allKeys.sort(Comparator.naturalOrder());
94 for (String category : allKeys)
96 var categoryMenu = new JMenu(category);
97 var oneshot = oneshotServices.get(category);
99 addOneshotEntries(oneshot, categoryMenu);
100 var interactive = interactiveServices.get(category);
101 if (interactive != null)
104 categoryMenu.addSeparator();
105 // addInteractiveEntries(interactive, categoryMenu);
107 menu.add(categoryMenu);
109 menu.add(inProgressItem);
110 menu.add(noServicesItem);
113 private void addOneshotEntries(List<WebService<?>> services, JMenu menu)
115 services.sort(Comparator
116 .<WebService<?>, String> comparing(s -> s.getUrl().toString())
117 .thenComparing(WebService::getName));
119 for (WebService<?> service : services)
121 // if new host differs from the last one, add entry separating them
122 URL host = service.getUrl();
123 if (!host.equals(lastHost))
125 if (lastHost != null)
127 var item = new JMenuItem(host.toString());
128 item.setForeground(Color.BLUE);
129 item.addActionListener(e -> Desktop.showUrl(host.toString()));
134 // group actions by their subcategory, sorted
135 var actionsByCategory = new TreeMap<String, List<ActionI<?>>>();
136 for (ActionI<?> action : service.getActions())
140 Objects.requireNonNullElse(action.getSubcategory(), ""),
141 k -> new ArrayList<>())
144 actionsByCategory.forEach((k, v) -> {
145 // create submenu named {subcategory} with {service} or use root menu
146 var atMenu = k.isEmpty() ? menu : new JMenu(String.format("%s with %s", k, service.getName()));
148 menu.add(atMenu); // add only if submenu
149 // sort actions by name pulling nulls to the front
150 v.sort(Comparator.comparing(
151 ActionI::getName, Comparator.nullsFirst(Comparator.naturalOrder())));
152 for (ActionI<?> action : v)
154 addEntriesForAction(action, atMenu, atMenu == menu);
160 private void addEntriesForAction(ActionI<?> action, JMenu menu, boolean isTopLevel)
162 var service = action.getWebService();
166 itemName = service.getName();
167 if (action.getName() != null && !action.getName().isEmpty())
168 itemName += " " + action.getName();
172 if (action.getName() == null || action.getName().isEmpty())
175 itemName = action.getName();
177 var datastore = service.getParamDatastore();
179 String text = itemName;
180 if (datastore.hasParameters() || datastore.hasPresets())
181 text += "with defaults";
182 JMenuItem item = new JMenuItem(text);
183 item.addActionListener(e -> {
184 runAction(action, frame.getCurrentView(), Collections.emptyList(),
185 Credentials.empty());
189 if (datastore.hasParameters())
191 JMenuItem item = new JMenuItem("Edit settings and run...");
192 item.addActionListener(e -> {
193 openEditParamsDialog(datastore, null, null).thenAccept(args -> {
195 runAction(action, frame.getCurrentView(), args, Credentials.empty());
200 var presets = datastore.getPresets();
201 if (presets != null && presets.size() > 0)
203 final var presetsMenu = new JMenu(MessageManager.formatMessage(
204 "label.run_with_preset_params", service.getName()));
205 final int dismissDelay = ToolTipManager.sharedInstance()
207 final int QUICK_TOOLTIP = 1500;
208 for (var preset : presets)
210 var item = new JMenuItem(preset.getName());
211 item.addMouseListener(new MouseAdapter()
214 public void mouseEntered(MouseEvent evt)
216 ToolTipManager.sharedInstance().setDismissDelay(QUICK_TOOLTIP);
220 public void mouseExited(MouseEvent evt)
222 ToolTipManager.sharedInstance().setDismissDelay(dismissDelay);
225 String tooltipTitle = MessageManager.getString(
226 preset.isModifiable() ? "label.user_preset" : "label.service_preset");
227 String tooltip = String.format("<strong>%s</strong><br/>%s",
228 tooltipTitle, preset.getDescription());
229 tooltip = JvSwingUtils.wrapTooltip(true, tooltip);
230 item.setToolTipText(tooltip);
231 item.addActionListener(event -> {
232 runAction(action, frame.getCurrentView(), preset.getArguments(),
233 Credentials.empty());
235 presetsMenu.add(item);
237 menu.add(presetsMenu);
241 private TaskI<?> runAction(ActionI<?> action, AlignmentViewport viewport,
242 List<ArgumentI> args, Credentials credentials)
244 // casting and instance checks can be avoided with some effort,
245 // let them be for now.
246 if (action instanceof AlignmentAction)
248 // TODO: test if selection contains enough sequences
249 var _action = (AlignmentAction) action;
250 var handler = new AlignmentServiceGuiHandler(_action, frame);
251 return _action.perform(viewport, args, credentials, handler);
253 if (action instanceof AnnotationAction)
255 var _action = (AnnotationAction) action;
256 var handler = new AnnotationServiceGuiHandler(_action, frame);
257 return _action.perform(viewport, args, credentials, handler);
259 throw new IllegalArgumentException(
260 String.format("Illegal action type %s", action.getClass().getName()));
263 private static CompletionStage<List<ArgumentI>> openEditParamsDialog(
264 ParamDatastoreI paramStore, WsParamSetI preset, List<ArgumentI> arguments)
266 final WsJobParameters jobParams;
267 if (preset == null && arguments != null && arguments.size() > 0)
268 jobParams = new WsJobParameters(paramStore, preset, arguments);
270 jobParams = new WsJobParameters(paramStore, preset, null);
272 jobParams.setName(MessageManager.getString(
273 "label.adjusting_parameters_for_calculation"));
274 var stage = jobParams.showRunDialog();
275 return stage.thenApply(startJob -> {
277 return null; // null if cancelled
278 if (jobParams.getPreset() != null)
279 return jobParams.getPreset().getArguments();
280 if (jobParams.isServiceDefaults())
281 return Collections.emptyList();
283 return jobParams.getJobParams();