1 package jalview.ws2.gui;
4 import java.awt.event.MouseAdapter;
5 import java.awt.event.MouseEvent;
6 import java.lang.ref.WeakReference;
8 import java.util.ArrayList;
9 import java.util.Collection;
10 import java.util.Collections;
11 import java.util.Comparator;
12 import java.util.HashMap;
13 import java.util.HashSet;
14 import java.util.List;
16 import java.util.Objects;
17 import java.util.TreeMap;
18 import java.util.concurrent.CompletionStage;
20 import javax.swing.JCheckBoxMenuItem;
21 import javax.swing.JLabel;
22 import javax.swing.JMenu;
23 import javax.swing.JMenuItem;
24 import javax.swing.ToolTipManager;
25 import javax.swing.border.EmptyBorder;
27 import jalview.bin.Console;
28 import jalview.gui.AlignFrame;
29 import jalview.gui.Desktop;
30 import jalview.gui.JvSwingUtils;
31 import jalview.gui.WsJobParameters;
32 import jalview.util.MessageManager;
33 import jalview.viewmodel.AlignmentViewport;
34 import jalview.ws.params.ArgumentI;
35 import jalview.ws.params.ParamDatastoreI;
36 import jalview.ws.params.WsParamSetI;
37 import jalview.ws2.actions.alignment.AlignmentAction;
38 import jalview.ws2.actions.annotation.AnnotationAction;
39 import jalview.ws2.actions.api.ActionI;
40 import jalview.ws2.actions.api.TaskEventListener;
41 import jalview.ws2.actions.api.TaskI;
42 import jalview.ws2.api.Credentials;
43 import jalview.ws2.api.WebService;
44 import jalview.ws2.client.api.WebServiceProviderI;
46 import static java.lang.String.format;
48 public class WebServicesMenuManager
50 private final JMenu menu;
52 private final AlignFrame frame;
54 private JMenuItem inProgressItem = new JMenuItem("Service discovery in progress");
56 private JMenuItem noServicesItem = new JMenuItem("No services available");
58 inProgressItem.setEnabled(false);
59 inProgressItem.setVisible(false);
60 noServicesItem.setEnabled(false);
63 private Map<String, WeakReference<TaskI<?>>> interactiveTasks = new HashMap<>();
65 public WebServicesMenuManager(String name, AlignFrame frame)
68 menu = new JMenu(name);
69 menu.add(inProgressItem);
70 menu.add(noServicesItem);
73 public JMenu getMenu()
78 public void setNoServices(boolean noServices)
80 noServicesItem.setVisible(noServices);
83 public void setInProgress(boolean inProgress)
85 inProgressItem.setVisible(inProgress);
88 public void setServices(WebServiceProviderI services)
91 // services grouped by their category
92 Map<String, List<WebService<?>>> oneshotServices = new HashMap<>();
93 Map<String, List<WebService<?>>> interactiveServices = new HashMap<>();
94 for (WebService<?> service : services.getServices())
96 var map = service.isInteractive() ? interactiveServices : oneshotServices;
97 map.computeIfAbsent(service.getCategory(), k -> new ArrayList<>())
100 var allKeysSet = new HashSet<>(oneshotServices.keySet());
101 allKeysSet.addAll(interactiveServices.keySet());
102 var allKeys = new ArrayList<>(allKeysSet);
103 allKeys.sort(Comparator.naturalOrder());
104 for (String category : allKeys)
106 var categoryMenu = new JMenu(category);
107 var oneshot = oneshotServices.get(category);
109 addOneshotEntries(oneshot, categoryMenu);
110 var interactive = interactiveServices.get(category);
111 if (interactive != null)
114 categoryMenu.addSeparator();
115 addInteractiveEntries(interactive, categoryMenu);
117 menu.add(categoryMenu);
119 menu.add(inProgressItem);
120 menu.add(noServicesItem);
123 private void addOneshotEntries(List<WebService<?>> services, JMenu menu)
125 // Workaround. Comparator methods not working in j2s
126 services.sort((ws1, ws2) -> {
127 var res = ws1.getUrl().toString().compareTo(ws2.getUrl().toString());
129 res = ws1.getName().compareTo(ws2.getName());
133 for (WebService<?> service : services)
135 // if new host differs from the last one, add entry separating them
136 URL host = service.getUrl();
137 if (!host.equals(lastHost))
139 if (lastHost != null)
141 var item = new JMenuItem(host.toString());
142 item.setForeground(Color.BLUE);
143 item.addActionListener(e -> Desktop.showUrl(host.toString()));
148 // group actions by their subcategory, sorted
149 var actionsByCategory = new TreeMap<String, List<ActionI<?>>>();
150 for (ActionI<?> action : service.getActions())
154 action.getSubcategory() != null ? action.getSubcategory() : "",
155 k -> new ArrayList<>())
158 for (var entry : actionsByCategory.entrySet())
160 var category = entry.getKey();
161 var actions = entry.getValue();
162 // create submenu named {subcategory} with {service} or use root menu
163 var atMenu = category.isEmpty() ? menu : new JMenu(String.format("%s with %s", category, service.getName()));
165 menu.add(atMenu); // add only if submenu
166 // sort actions by name pulling nulls to the front
167 actions.sort(Comparator.comparing(
168 ActionI::getName, Comparator.nullsFirst(Comparator.naturalOrder())));
169 for (int i = 0; i < actions.size(); i++) {
170 addEntriesForAction(actions.get(i), atMenu, atMenu == menu);
176 private void addEntriesForAction(ActionI<?> action, JMenu menu, boolean isTopLevel)
178 var service = action.getWebService();
182 itemName = service.getName();
183 if (action.getName() != null && !action.getName().isEmpty())
184 itemName += " " + action.getName();
188 if (action.getName() == null || action.getName().isEmpty())
191 itemName = action.getName();
193 var datastore = service.getParamDatastore();
195 String text = itemName;
196 if (datastore.hasParameters() || datastore.hasPresets())
197 text += " with defaults";
198 JMenuItem item = new JMenuItem(text);
199 item.addActionListener(e -> {
200 runAction(action, frame.getCurrentView(), Collections.emptyList(),
201 Credentials.empty());
205 if (datastore.hasParameters())
207 JMenuItem item = new JMenuItem("Edit settings and run...");
208 item.addActionListener(e -> {
209 openEditParamsDialog(datastore, null, null).thenAccept(args -> {
211 runAction(action, frame.getCurrentView(), args, Credentials.empty());
216 var presets = datastore.getPresets();
217 if (presets != null && presets.size() > 0)
219 final var presetsMenu = new JMenu(MessageManager.formatMessage(
220 "label.run_with_preset_params", service.getName()));
221 final int dismissDelay = ToolTipManager.sharedInstance()
223 final int QUICK_TOOLTIP = 1500;
224 for (var preset : presets)
226 var item = new JMenuItem(preset.getName());
227 item.addMouseListener(new MouseAdapter()
230 public void mouseEntered(MouseEvent evt)
232 ToolTipManager.sharedInstance().setDismissDelay(QUICK_TOOLTIP);
236 public void mouseExited(MouseEvent evt)
238 ToolTipManager.sharedInstance().setDismissDelay(dismissDelay);
241 String tooltipTitle = MessageManager.getString(
242 preset.isModifiable() ? "label.user_preset" : "label.service_preset");
243 String tooltip = String.format("<strong>%s</strong><br/>%s",
244 tooltipTitle, preset.getDescription());
245 tooltip = JvSwingUtils.wrapTooltip(true, tooltip);
246 item.setToolTipText(tooltip);
247 item.addActionListener(event -> {
248 runAction(action, frame.getCurrentView(), preset.getArguments(),
249 Credentials.empty());
251 presetsMenu.add(item);
253 menu.add(presetsMenu);
257 private void addInteractiveEntries(List<WebService<?>> services, JMenu menu)
259 Map<String, List<WebService<?>>> byServiceName = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
260 for (var service : services)
262 byServiceName.computeIfAbsent(service.getName(), k -> new ArrayList<>())
265 for (var entry : byServiceName.entrySet())
267 var group = new InteractiveServiceEntryGroup(entry.getKey(), entry.getValue());
268 group.appendTo(menu);
272 private class InteractiveServiceEntryGroup
276 JMenuItem urlItem = new JMenuItem();
278 JCheckBoxMenuItem serviceItem = new JCheckBoxMenuItem();
280 JMenuItem editParamsItem = new JMenuItem("Edit parameters...");
282 JMenu presetsMenu = new JMenu("Change preset");
284 JMenu alternativesMenu = new JMenu("Choose action");
286 urlItem.setForeground(Color.BLUE);
287 urlItem.setVisible(false);
288 serviceItem.setVisible(false);
289 editParamsItem.setVisible(false);
290 presetsMenu.setVisible(false);
293 InteractiveServiceEntryGroup(String name, List<WebService<?>> services)
295 serviceLabel = new JLabel(name);
296 serviceLabel.setBorder(new EmptyBorder(0, 6, 0, 6));
297 buildAlternativesMenu(services);
300 private void buildAlternativesMenu(List<WebService<?>> services)
302 var menu = alternativesMenu;
303 services.sort((ws1, ws2) -> {
304 var res = ws1.getUrl().toString().compareTo(ws2.getUrl().toString());
306 res = ws1.getName().compareTo(ws2.getName());
310 for (var service : services)
312 // Adding url "separator" before each group
313 URL host = service.getUrl();
314 if (!host.equals(lastHost))
316 if (lastHost != null)
318 var item = new JMenuItem(host.toString());
319 item.setForeground(Color.BLUE);
320 item.addActionListener(e -> Desktop.showUrl(host.toString()));
325 var actionsByCategory = new TreeMap<String, List<ActionI<?>>>();
326 for (ActionI<?> action : service.getActions())
330 action.getSubcategory() != null ? action.getSubcategory() : "",
331 k -> new ArrayList<>())
334 actionsByCategory.forEach((key, actions) -> {
335 var atMenu = key.isEmpty() ? menu : new JMenu(key + " with " + service.getName());
336 boolean topLevel = atMenu == menu;
339 actions.sort(Comparator.comparing(
341 Comparator.nullsFirst(Comparator.naturalOrder())));
342 for (ActionI<?> action : actions)
344 var item = new JMenuItem(action.getFullName());
345 item.addActionListener(e -> setAlternative(action));
352 private void setAlternative(ActionI<?> action)
354 final var arguments = new ArrayList<ArgumentI>();
355 final WsParamSetI[] lastPreset = { null };
357 // update selected url menu item
358 String url = action.getWebService().getUrl().toString();
359 urlItem.setText(url);
360 urlItem.setVisible(true);
361 for (var l : urlItem.getActionListeners())
362 urlItem.removeActionListener(l);
363 urlItem.addActionListener(e -> Desktop.showUrl(url));
365 // update selected service menu item
366 serviceItem.setText(action.getFullName());
367 serviceItem.setVisible(true);
368 for (var l : serviceItem.getActionListeners())
369 serviceItem.removeActionListener(l);
370 WebService<?> service = action.getWebService();
371 serviceItem.addActionListener(e -> {
372 if (serviceItem.getState())
374 cancelAndRunInteractive(action, frame.getCurrentView(), arguments,
375 Credentials.empty());
379 cancelInteractive(service.getName());
382 serviceItem.setSelected(true);
384 // update edit parameters menu item
385 var datastore = service.getParamDatastore();
386 editParamsItem.setVisible(datastore.hasParameters());
387 for (var l : editParamsItem.getActionListeners())
388 editParamsItem.removeActionListener(l);
389 if (datastore.hasParameters())
391 editParamsItem.addActionListener(e -> {
392 openEditParamsDialog(service.getParamDatastore(), lastPreset[0], arguments)
393 .thenAccept(args -> {
396 lastPreset[0] = null;
398 arguments.addAll(args);
399 cancelAndRunInteractive(action, frame.getCurrentView(),
400 arguments, Credentials.empty());
406 // update presets menu
407 presetsMenu.removeAll();
408 presetsMenu.setEnabled(datastore.hasPresets());
409 if (datastore.hasPresets())
411 for (WsParamSetI preset : datastore.getPresets())
413 var item = new JMenuItem(preset.getName());
414 item.addActionListener(e -> {
415 lastPreset[0] = preset;
416 cancelAndRunInteractive(action, frame.getCurrentView(),
417 preset.getArguments(), Credentials.empty());
419 presetsMenu.add(item);
423 cancelAndRunInteractive(action, frame.getCurrentView(), arguments,
424 Credentials.empty());
427 void appendTo(JMenu menu)
429 menu.add(serviceLabel);
431 menu.add(serviceItem);
432 menu.add(editParamsItem);
433 menu.add(presetsMenu);
434 menu.add(alternativesMenu);
438 private void cancelInteractive(String wsName)
440 var taskRef = interactiveTasks.get(wsName);
441 if (taskRef != null && taskRef.get() != null)
442 taskRef.get().cancel();
443 interactiveTasks.put(wsName, null);
446 private void cancelAndRunInteractive(ActionI<?> action,
447 AlignmentViewport viewport, List<ArgumentI> args, Credentials credentials)
449 var wsName = action.getWebService().getName();
450 cancelInteractive(wsName);
451 var task = runAction(action, viewport, args, credentials);
452 interactiveTasks.put(wsName, new WeakReference<>(task));
455 private TaskI<?> runAction(ActionI<?> action, AlignmentViewport viewport,
456 List<ArgumentI> args, Credentials credentials)
458 // casting and instance checks can be avoided with some effort,
459 // let them be for now.
460 if (action instanceof AlignmentAction)
462 // TODO: test if selection contains enough sequences
463 var _action = (AlignmentAction) action;
464 var handler = new AlignmentServiceGuiHandler(_action, frame);
465 return _action.perform(viewport, args, credentials, handler);
467 if (action instanceof AnnotationAction)
469 var _action = (AnnotationAction) action;
470 var handler = new AnnotationServiceGuiHandler(_action, frame);
471 return _action.perform(viewport, args, credentials, handler);
473 Console.warn(String.format(
474 "No known handler for action type %s. All output will be discarded.",
475 action.getClass().getName()));
476 return action.perform(viewport, args, credentials,
477 TaskEventListener.nullListener());
480 private static CompletionStage<List<ArgumentI>> openEditParamsDialog(
481 ParamDatastoreI paramStore, WsParamSetI preset, List<ArgumentI> arguments)
483 final WsJobParameters jobParams;
484 if (preset == null && arguments != null && arguments.size() > 0)
485 jobParams = new WsJobParameters(paramStore, null, arguments);
487 jobParams = new WsJobParameters(paramStore, preset, null);
489 jobParams.setName(MessageManager.getString(
490 "label.adjusting_parameters_for_calculation"));
491 var stage = jobParams.showRunDialog();
492 return stage.thenApply(startJob -> {
494 return null; // null if cancelled
495 if (jobParams.getPreset() != null)
496 return jobParams.getPreset().getArguments();
497 if (jobParams.isServiceDefaults())
498 return Collections.emptyList();
500 return jobParams.getJobParams();