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.Collections;
9 import java.util.Comparator;
10 import java.util.HashMap;
11 import java.util.HashSet;
12 import java.util.List;
14 import java.util.TreeMap;
15 import java.util.concurrent.CompletionStage;
17 import javax.swing.JCheckBoxMenuItem;
18 import javax.swing.JLabel;
19 import javax.swing.JMenu;
20 import javax.swing.JMenuItem;
21 import javax.swing.ToolTipManager;
22 import javax.swing.border.EmptyBorder;
24 import jalview.gui.AlignFrame;
25 import jalview.gui.Desktop;
26 import jalview.gui.JvSwingUtils;
27 import jalview.gui.WsJobParameters;
28 import jalview.util.MessageManager;
29 import jalview.viewmodel.AlignmentViewport;
30 import jalview.ws.params.ArgumentI;
31 import jalview.ws.params.ParamDatastoreI;
32 import jalview.ws.params.WsParamSetI;
33 import jalview.ws2.actions.BaseTask;
34 import jalview.ws2.actions.PollingTaskExecutor;
35 import jalview.ws2.actions.alignment.AlignmentAction;
36 import jalview.ws2.actions.alignment.AlignmentResult;
37 import jalview.ws2.actions.annotation.AlignCalcWorkerAdapter;
38 import jalview.ws2.actions.annotation.AnnotationAction;
39 import jalview.ws2.actions.api.ActionI;
40 import jalview.ws2.actions.api.TaskI;
41 import jalview.ws2.api.Credentials;
42 import jalview.ws2.api.WebService;
43 import jalview.ws2.client.api.WebServiceProviderI;
45 public class WebServicesMenuManager
47 private final JMenu menu;
49 private final AlignFrame frame;
51 private JMenuItem inProgressItem = new JMenuItem("Service discovery in progress");
53 private JMenuItem noServicesItem = new JMenuItem("No services available");
55 inProgressItem.setEnabled(false);
56 inProgressItem.setVisible(false);
57 noServicesItem.setEnabled(false);
60 public WebServicesMenuManager(String name, AlignFrame frame)
63 menu = new JMenu(name);
64 menu.add(inProgressItem);
65 menu.add(noServicesItem);
68 public JMenu getMenu()
73 public void setNoServices(boolean noServices)
75 noServicesItem.setVisible(noServices);
78 public void setInProgress(boolean inProgress)
80 inProgressItem.setVisible(inProgress);
83 public void setServices(WebServiceProviderI services)
86 // services grouped by their category
87 Map<String, List<WebService<?>>> oneshotServices = new HashMap<>();
88 Map<String, List<WebService<?>>> interactiveServices = new HashMap<>();
89 for (WebService<?> service : services.getServices())
91 var map = service.isInteractive() ? interactiveServices : oneshotServices;
92 map.computeIfAbsent(service.getCategory(), k -> new ArrayList<>())
95 var allKeysSet = new HashSet<>(oneshotServices.keySet());
96 allKeysSet.addAll(interactiveServices.keySet());
97 var allKeys = new ArrayList<>(allKeysSet);
98 allKeys.sort(Comparator.naturalOrder());
99 for (String category : allKeys)
101 var categoryMenu = new JMenu(category);
102 var oneshot = oneshotServices.get(category);
104 addOneshotEntries(oneshot, categoryMenu);
105 var interactive = interactiveServices.get(category);
106 if (interactive != null)
109 categoryMenu.addSeparator();
110 addInteractiveEntries(interactive, categoryMenu);
112 menu.add(categoryMenu);
114 menu.add(inProgressItem);
115 menu.add(noServicesItem);
118 private void addOneshotEntries(List<WebService<?>> services, JMenu menu)
120 // Workaround. Comparator methods not working in j2s
121 services.sort((ws1, ws2) -> {
122 var res = ws1.getUrl().toString().compareTo(ws2.getUrl().toString());
124 res = ws1.getName().compareTo(ws2.getName());
128 for (WebService<?> service : services)
130 // if new host differs from the last one, add entry separating them
131 URL host = service.getUrl();
132 if (!host.equals(lastHost))
134 if (lastHost != null)
136 var item = new JMenuItem(host.toString());
137 item.setForeground(Color.BLUE);
138 item.addActionListener(e -> Desktop.showUrl(host.toString()));
143 // group actions by their subcategory, sorted
144 var actionsByCategory = new TreeMap<String, List<ActionI<?>>>();
145 for (ActionI<?> action : service.getActions())
149 action.getSubcategory() != null ? action.getSubcategory() : "",
150 k -> new ArrayList<>())
153 for (var entry : actionsByCategory.entrySet())
155 var category = entry.getKey();
156 var actions = entry.getValue();
157 // create submenu named {subcategory} with {service} or use root menu
158 var atMenu = category.isEmpty() ? menu : new JMenu(String.format("%s with %s", category, service.getName()));
160 menu.add(atMenu); // add only if submenu
161 // sort actions by name pulling nulls to the front
162 actions.sort(Comparator.comparing(
163 ActionI::getName, Comparator.nullsFirst(Comparator.naturalOrder())));
164 for (int i = 0; i < actions.size(); i++)
166 addEntriesForAction(actions.get(i), atMenu, atMenu == menu);
172 private void addEntriesForAction(ActionI<?> action, JMenu menu, boolean isTopLevel)
174 var service = action.getWebService();
178 itemName = service.getName();
179 if (action.getName() != null && !action.getName().isEmpty())
180 itemName += " " + action.getName();
184 if (action.getName() == null || action.getName().isEmpty())
187 itemName = action.getName();
189 var datastore = service.getParamDatastore();
191 String text = itemName;
192 if (datastore.hasParameters() || datastore.hasPresets())
193 text += " with defaults";
194 JMenuItem item = new JMenuItem(text);
195 item.addActionListener(e -> {
196 runAction(action, frame.getCurrentView(), Collections.emptyList(),
197 Credentials.empty());
201 if (datastore.hasParameters())
203 JMenuItem item = new JMenuItem("Edit settings and run...");
204 item.addActionListener(e -> {
205 openEditParamsDialog(datastore, null, null).thenAccept(args -> {
207 runAction(action, frame.getCurrentView(), args, Credentials.empty());
212 var presets = datastore.getPresets();
213 if (presets != null && presets.size() > 0)
215 final var presetsMenu = new JMenu(MessageManager.formatMessage(
216 "label.run_with_preset_params", service.getName()));
217 final int dismissDelay = ToolTipManager.sharedInstance()
219 final int QUICK_TOOLTIP = 1500;
220 for (var preset : presets)
222 var item = new JMenuItem(preset.getName());
223 item.addMouseListener(new MouseAdapter()
226 public void mouseEntered(MouseEvent evt)
228 ToolTipManager.sharedInstance().setDismissDelay(QUICK_TOOLTIP);
232 public void mouseExited(MouseEvent evt)
234 ToolTipManager.sharedInstance().setDismissDelay(dismissDelay);
237 String tooltipTitle = MessageManager.getString(
238 preset.isModifiable() ? "label.user_preset" : "label.service_preset");
239 String tooltip = String.format("<strong>%s</strong><br/>%s",
240 tooltipTitle, preset.getDescription());
241 tooltip = JvSwingUtils.wrapTooltip(true, tooltip);
242 item.setToolTipText(tooltip);
243 item.addActionListener(event -> {
244 runAction(action, frame.getCurrentView(), preset.getArguments(),
245 Credentials.empty());
247 presetsMenu.add(item);
249 menu.add(presetsMenu);
253 private void addInteractiveEntries(List<WebService<?>> services, JMenu menu)
255 Map<String, List<WebService<?>>> byServiceName = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
256 for (var service : services)
258 byServiceName.computeIfAbsent(service.getName(), k -> new ArrayList<>())
261 for (var entry : byServiceName.entrySet())
263 var group = new InteractiveServiceEntryGroup(entry.getKey(), entry.getValue());
264 group.appendTo(menu);
268 private class InteractiveServiceEntryGroup
272 JMenuItem urlItem = new JMenuItem();
274 JCheckBoxMenuItem serviceItem = new JCheckBoxMenuItem();
276 JMenuItem editParamsItem = new JMenuItem("Edit parameters...");
278 JMenu presetsMenu = new JMenu("Change preset");
280 JMenu alternativesMenu = new JMenu("Choose action");
282 urlItem.setForeground(Color.BLUE);
283 urlItem.setVisible(false);
284 serviceItem.setVisible(false);
285 editParamsItem.setVisible(false);
286 presetsMenu.setVisible(false);
289 InteractiveServiceEntryGroup(String name, List<WebService<?>> services)
291 serviceLabel = new JLabel(name);
292 serviceLabel.setBorder(new EmptyBorder(0, 6, 0, 6));
293 buildAlternativesMenu(services);
296 private void buildAlternativesMenu(List<WebService<?>> services)
298 var menu = alternativesMenu;
299 services.sort((ws1, ws2) -> {
300 var res = ws1.getUrl().toString().compareTo(ws2.getUrl().toString());
302 res = ws1.getName().compareTo(ws2.getName());
306 for (var service : services)
308 // Adding url "separator" before each group
309 URL host = service.getUrl();
310 if (!host.equals(lastHost))
312 if (lastHost != null)
314 var item = new JMenuItem(host.toString());
315 item.setForeground(Color.BLUE);
316 item.addActionListener(e -> Desktop.showUrl(host.toString()));
321 var actionsByCategory = new TreeMap<String, List<ActionI<?>>>();
322 for (ActionI<?> action : service.getActions())
326 action.getSubcategory() != null ? action.getSubcategory() : "",
327 k -> new ArrayList<>())
330 actionsByCategory.forEach((key, actions) -> {
331 var atMenu = key.isEmpty() ? menu : new JMenu(key + " with " + service.getName());
332 boolean topLevel = atMenu == menu;
335 actions.sort(Comparator.comparing(
337 Comparator.nullsFirst(Comparator.naturalOrder())));
338 for (ActionI<?> action : actions)
340 var item = new JMenuItem(action.getFullName());
341 item.addActionListener(e -> setAlternative(action));
348 private void setAlternative(ActionI<?> action)
350 final var arguments = new ArrayList<ArgumentI>();
351 final WsParamSetI[] lastPreset = { null };
353 // update selected url menu item
354 String url = action.getWebService().getUrl().toString();
355 urlItem.setText(url);
356 urlItem.setVisible(true);
357 for (var l : urlItem.getActionListeners())
358 urlItem.removeActionListener(l);
359 urlItem.addActionListener(e -> Desktop.showUrl(url));
361 // update selected service menu item
362 serviceItem.setText(action.getFullName());
363 serviceItem.setVisible(true);
364 for (var l : serviceItem.getActionListeners())
365 serviceItem.removeActionListener(l);
366 WebService<?> service = action.getWebService();
367 serviceItem.addActionListener(e -> {
368 runAction(action, frame.getCurrentView(), arguments,
369 Credentials.empty());
371 serviceItem.setSelected(true);
373 // update edit parameters menu item
374 var datastore = service.getParamDatastore();
375 editParamsItem.setVisible(datastore.hasParameters());
376 for (var l : editParamsItem.getActionListeners())
377 editParamsItem.removeActionListener(l);
378 if (datastore.hasParameters())
380 editParamsItem.addActionListener(e -> {
381 openEditParamsDialog(service.getParamDatastore(), lastPreset[0], arguments)
382 .thenAccept(args -> {
385 lastPreset[0] = null;
387 arguments.addAll(args);
388 runAction(action, frame.getCurrentView(),
389 arguments, Credentials.empty());
395 // update presets menu
396 presetsMenu.removeAll();
397 presetsMenu.setEnabled(datastore.hasPresets());
398 if (datastore.hasPresets())
400 for (WsParamSetI preset : datastore.getPresets())
402 var item = new JMenuItem(preset.getName());
403 item.addActionListener(e -> {
404 lastPreset[0] = preset;
405 runAction(action, frame.getCurrentView(),
406 preset.getArguments(), Credentials.empty());
408 presetsMenu.add(item);
412 runAction(action, frame.getCurrentView(), arguments,
413 Credentials.empty());
416 void appendTo(JMenu menu)
418 menu.add(serviceLabel);
420 menu.add(serviceItem);
421 menu.add(editParamsItem);
422 menu.add(presetsMenu);
423 menu.add(alternativesMenu);
428 private void runAction(ActionI<?> action, AlignmentViewport viewport,
429 List<ArgumentI> args, Credentials credentials)
431 // casting and instance checks can be avoided with some effort,
432 // let them be for now.
433 if (action instanceof AlignmentAction)
435 // TODO: test if selection contains enough sequences
436 var _action = (AlignmentAction) action;
437 var handler = new AlignmentServiceGuiHandler(_action, frame);
438 BaseTask<?, AlignmentResult> task = _action.createTask(viewport, args, credentials);
439 var executor = PollingTaskExecutor.fromPool(viewport.getServiceExecutor());
440 task.addTaskEventListener(handler);
441 var future = executor.submit(task);
442 task.setCancelAction(() -> { future.cancel(true); });
445 if (action instanceof AnnotationAction)
447 var calcManager = viewport.getCalcManager();
449 var _action = (AnnotationAction) action;
450 var worker = new AlignCalcWorkerAdapter(viewport, frame.alignPanel,
451 _action, args, credentials);
452 var handler = new AnnotationServiceGuiHandler(_action, frame);
453 worker.setWorkerListener(handler);
454 for (var w : calcManager.getWorkers())
456 if (worker.getCalcName() != null && worker.getCalcName().equals(w.getCalcName()))
458 calcManager.cancelWorker(w);
459 calcManager.removeWorker(w);
462 if (action.getWebService().isInteractive())
463 calcManager.registerWorker(worker);
465 calcManager.startWorker(worker);
468 throw new IllegalArgumentException(
469 String.format("Illegal action type %s", action.getClass().getName()));
472 private static CompletionStage<List<ArgumentI>> openEditParamsDialog(
473 ParamDatastoreI paramStore, WsParamSetI preset, List<ArgumentI> arguments)
475 final WsJobParameters jobParams;
476 if (preset == null && arguments != null && arguments.size() > 0)
477 jobParams = new WsJobParameters(paramStore, null, arguments);
479 jobParams = new WsJobParameters(paramStore, preset, null);
481 jobParams.setName(MessageManager.getString(
482 "label.adjusting_parameters_for_calculation"));
483 var stage = jobParams.showRunDialog();
484 return stage.thenApply(startJob -> {
486 return null; // null if cancelled
487 if (jobParams.getPreset() != null)
488 return jobParams.getPreset().getArguments();
489 if (jobParams.isServiceDefaults())
490 return Collections.emptyList();
492 return jobParams.getJobParams();