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.gui.AlignFrame;
28 import jalview.gui.Desktop;
29 import jalview.gui.JvSwingUtils;
30 import jalview.gui.WsJobParameters;
31 import jalview.util.MessageManager;
32 import jalview.viewmodel.AlignmentViewport;
33 import jalview.ws.params.ArgumentI;
34 import jalview.ws.params.ParamDatastoreI;
35 import jalview.ws.params.WsParamSetI;
36 import jalview.ws2.actions.alignment.AlignmentAction;
37 import jalview.ws2.actions.annotation.AnnotationAction;
38 import jalview.ws2.actions.api.ActionI;
39 import jalview.ws2.actions.api.TaskI;
40 import jalview.ws2.api.Credentials;
41 import jalview.ws2.api.WebService;
42 import jalview.ws2.client.api.WebServiceProviderI;
44 import static java.lang.String.format;
46 public class WebServicesMenuManager
48 private final JMenu menu;
50 private final AlignFrame frame;
52 private JMenuItem inProgressItem = new JMenuItem("Service discovery in progress");
54 private JMenuItem noServicesItem = new JMenuItem("No services available");
56 inProgressItem.setEnabled(false);
57 inProgressItem.setVisible(false);
58 noServicesItem.setEnabled(false);
61 private Map<String, WeakReference<TaskI<?>>> interactiveTasks = new HashMap<>();
63 public WebServicesMenuManager(String name, AlignFrame frame)
66 menu = new JMenu(name);
67 menu.add(inProgressItem);
68 menu.add(noServicesItem);
71 public JMenu getMenu()
76 public void setNoServices(boolean noServices)
78 noServicesItem.setVisible(noServices);
81 public void setInProgress(boolean inProgress)
83 inProgressItem.setVisible(inProgress);
86 public void setServices(WebServiceProviderI services)
89 // services grouped by their category
90 Map<String, List<WebService<?>>> oneshotServices = new HashMap<>();
91 Map<String, List<WebService<?>>> interactiveServices = new HashMap<>();
92 for (WebService<?> service : services.getServices())
94 var map = service.isInteractive() ? interactiveServices : oneshotServices;
95 map.computeIfAbsent(service.getCategory(), k -> new ArrayList<>())
98 var allKeysSet = new HashSet<>(oneshotServices.keySet());
99 allKeysSet.addAll(interactiveServices.keySet());
100 var allKeys = new ArrayList<>(allKeysSet);
101 allKeys.sort(Comparator.naturalOrder());
102 for (String category : allKeys)
104 var categoryMenu = new JMenu(category);
105 var oneshot = oneshotServices.get(category);
107 addOneshotEntries(oneshot, categoryMenu);
108 var interactive = interactiveServices.get(category);
109 if (interactive != null)
112 categoryMenu.addSeparator();
113 addInteractiveEntries(interactive, categoryMenu);
115 menu.add(categoryMenu);
117 menu.add(inProgressItem);
118 menu.add(noServicesItem);
121 private void addOneshotEntries(List<WebService<?>> services, JMenu menu)
123 // Workaround. Comparator methods not working in j2s
124 services.sort((ws1, ws2) -> {
125 var res = ws1.getUrl().toString().compareTo(ws2.getUrl().toString());
127 res = ws1.getName().compareTo(ws2.getName());
131 for (WebService<?> service : services)
133 // if new host differs from the last one, add entry separating them
134 URL host = service.getUrl();
135 if (!host.equals(lastHost))
137 if (lastHost != null)
139 var item = new JMenuItem(host.toString());
140 item.setForeground(Color.BLUE);
141 item.addActionListener(e -> Desktop.showUrl(host.toString()));
146 // group actions by their subcategory, sorted
147 var actionsByCategory = new TreeMap<String, List<ActionI<?>>>();
148 for (ActionI<?> action : service.getActions())
152 action.getSubcategory() != null ? action.getSubcategory() : "",
153 k -> new ArrayList<>())
156 for (var entry : actionsByCategory.entrySet())
158 var category = entry.getKey();
159 var actions = entry.getValue();
160 // create submenu named {subcategory} with {service} or use root menu
161 var atMenu = category.isEmpty() ? menu : new JMenu(String.format("%s with %s", category, service.getName()));
163 menu.add(atMenu); // add only if submenu
164 // sort actions by name pulling nulls to the front
165 actions.sort(Comparator.comparing(
166 ActionI::getName, Comparator.nullsFirst(Comparator.naturalOrder())));
167 for (int i = 0; i < actions.size(); i++) {
168 addEntriesForAction(actions.get(i), atMenu, atMenu == menu);
174 private void addEntriesForAction(ActionI<?> action, JMenu menu, boolean isTopLevel)
176 var service = action.getWebService();
180 itemName = service.getName();
181 if (action.getName() != null && !action.getName().isEmpty())
182 itemName += " " + action.getName();
186 if (action.getName() == null || action.getName().isEmpty())
189 itemName = action.getName();
191 var datastore = service.getParamDatastore();
193 String text = itemName;
194 if (datastore.hasParameters() || datastore.hasPresets())
195 text += " with defaults";
196 JMenuItem item = new JMenuItem(text);
197 item.addActionListener(e -> {
198 runAction(action, frame.getCurrentView(), Collections.emptyList(),
199 Credentials.empty());
203 if (datastore.hasParameters())
205 JMenuItem item = new JMenuItem("Edit settings and run...");
206 item.addActionListener(e -> {
207 openEditParamsDialog(datastore, null, null).thenAccept(args -> {
209 runAction(action, frame.getCurrentView(), args, Credentials.empty());
214 var presets = datastore.getPresets();
215 if (presets != null && presets.size() > 0)
217 final var presetsMenu = new JMenu(MessageManager.formatMessage(
218 "label.run_with_preset_params", service.getName()));
219 final int dismissDelay = ToolTipManager.sharedInstance()
221 final int QUICK_TOOLTIP = 1500;
222 for (var preset : presets)
224 var item = new JMenuItem(preset.getName());
225 item.addMouseListener(new MouseAdapter()
228 public void mouseEntered(MouseEvent evt)
230 ToolTipManager.sharedInstance().setDismissDelay(QUICK_TOOLTIP);
234 public void mouseExited(MouseEvent evt)
236 ToolTipManager.sharedInstance().setDismissDelay(dismissDelay);
239 String tooltipTitle = MessageManager.getString(
240 preset.isModifiable() ? "label.user_preset" : "label.service_preset");
241 String tooltip = String.format("<strong>%s</strong><br/>%s",
242 tooltipTitle, preset.getDescription());
243 tooltip = JvSwingUtils.wrapTooltip(true, tooltip);
244 item.setToolTipText(tooltip);
245 item.addActionListener(event -> {
246 runAction(action, frame.getCurrentView(), preset.getArguments(),
247 Credentials.empty());
249 presetsMenu.add(item);
251 menu.add(presetsMenu);
255 private void addInteractiveEntries(List<WebService<?>> services, JMenu menu)
257 Map<String, List<WebService<?>>> byServiceName = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
258 for (var service : services)
260 byServiceName.computeIfAbsent(service.getName(), k -> new ArrayList<>())
263 for (var entry : byServiceName.entrySet())
265 var group = new InteractiveServiceEntryGroup(entry.getKey(), entry.getValue());
266 group.appendTo(menu);
270 private class InteractiveServiceEntryGroup
274 JMenuItem urlItem = new JMenuItem();
276 JCheckBoxMenuItem serviceItem = new JCheckBoxMenuItem();
278 JMenuItem editParamsItem = new JMenuItem("Edit parameters...");
280 JMenu presetsMenu = new JMenu("Change preset");
282 JMenu alternativesMenu = new JMenu("Choose action");
284 urlItem.setForeground(Color.BLUE);
285 urlItem.setVisible(false);
286 serviceItem.setVisible(false);
287 editParamsItem.setVisible(false);
288 presetsMenu.setVisible(false);
291 InteractiveServiceEntryGroup(String name, List<WebService<?>> services)
293 serviceLabel = new JLabel(name);
294 serviceLabel.setBorder(new EmptyBorder(0, 6, 0, 6));
295 buildAlternativesMenu(services);
298 private void buildAlternativesMenu(List<WebService<?>> services)
300 var menu = alternativesMenu;
301 services.sort((ws1, ws2) -> {
302 var res = ws1.getUrl().toString().compareTo(ws2.getUrl().toString());
304 res = ws1.getName().compareTo(ws2.getName());
308 for (var service : services)
310 // Adding url "separator" before each group
311 URL host = service.getUrl();
312 if (!host.equals(lastHost))
314 if (lastHost != null)
316 var item = new JMenuItem(host.toString());
317 item.setForeground(Color.BLUE);
318 item.addActionListener(e -> Desktop.showUrl(host.toString()));
323 var actionsByCategory = new TreeMap<String, List<ActionI<?>>>();
324 for (ActionI<?> action : service.getActions())
328 action.getSubcategory() != null ? action.getSubcategory() : "",
329 k -> new ArrayList<>())
332 actionsByCategory.forEach((key, actions) -> {
333 var atMenu = key.isEmpty() ? menu : new JMenu(key + " with " + service.getName());
334 boolean topLevel = atMenu == menu;
337 actions.sort(Comparator.comparing(
339 Comparator.nullsFirst(Comparator.naturalOrder())));
340 for (ActionI<?> action : actions)
342 var item = new JMenuItem(action.getFullName());
343 item.addActionListener(e -> setAlternative(action));
350 private void setAlternative(ActionI<?> action)
352 final var arguments = new ArrayList<ArgumentI>();
353 final WsParamSetI[] lastPreset = { null };
355 // update selected url menu item
356 String url = action.getWebService().getUrl().toString();
357 urlItem.setText(url);
358 urlItem.setVisible(true);
359 for (var l : urlItem.getActionListeners())
360 urlItem.removeActionListener(l);
361 urlItem.addActionListener(e -> Desktop.showUrl(url));
363 // update selected service menu item
364 serviceItem.setText(action.getFullName());
365 serviceItem.setVisible(true);
366 for (var l : serviceItem.getActionListeners())
367 serviceItem.removeActionListener(l);
368 WebService<?> service = action.getWebService();
369 serviceItem.addActionListener(e -> {
370 if (serviceItem.getState())
372 cancelAndRunInteractive(action, frame.getCurrentView(), arguments,
373 Credentials.empty());
377 cancelInteractive(service.getName());
380 serviceItem.setSelected(true);
382 // update edit parameters menu item
383 var datastore = service.getParamDatastore();
384 editParamsItem.setVisible(datastore.hasParameters());
385 for (var l : editParamsItem.getActionListeners())
386 editParamsItem.removeActionListener(l);
387 if (datastore.hasParameters())
389 editParamsItem.addActionListener(e -> {
390 openEditParamsDialog(service.getParamDatastore(), lastPreset[0], arguments)
391 .thenAccept(args -> {
394 lastPreset[0] = null;
396 arguments.addAll(args);
397 cancelAndRunInteractive(action, frame.getCurrentView(),
398 arguments, Credentials.empty());
404 // update presets menu
405 presetsMenu.removeAll();
406 presetsMenu.setEnabled(datastore.hasPresets());
407 if (datastore.hasPresets())
409 for (WsParamSetI preset : datastore.getPresets())
411 var item = new JMenuItem(preset.getName());
412 item.addActionListener(e -> {
413 lastPreset[0] = preset;
414 cancelAndRunInteractive(action, frame.getCurrentView(),
415 preset.getArguments(), Credentials.empty());
417 presetsMenu.add(item);
421 cancelAndRunInteractive(action, frame.getCurrentView(), arguments,
422 Credentials.empty());
425 void appendTo(JMenu menu)
427 menu.add(serviceLabel);
429 menu.add(serviceItem);
430 menu.add(editParamsItem);
431 menu.add(presetsMenu);
432 menu.add(alternativesMenu);
436 private void cancelInteractive(String wsName)
438 var taskRef = interactiveTasks.get(wsName);
439 if (taskRef != null && taskRef.get() != null)
440 taskRef.get().cancel();
441 interactiveTasks.put(wsName, null);
444 private void cancelAndRunInteractive(ActionI<?> action,
445 AlignmentViewport viewport, List<ArgumentI> args, Credentials credentials)
447 var wsName = action.getWebService().getName();
448 cancelInteractive(wsName);
449 var task = runAction(action, viewport, args, credentials);
450 interactiveTasks.put(wsName, new WeakReference<>(task));
453 private TaskI<?> runAction(ActionI<?> action, AlignmentViewport viewport,
454 List<ArgumentI> args, Credentials credentials)
456 // casting and instance checks can be avoided with some effort,
457 // let them be for now.
458 if (action instanceof AlignmentAction)
460 // TODO: test if selection contains enough sequences
461 var _action = (AlignmentAction) action;
462 var handler = new AlignmentServiceGuiHandler(_action, frame);
463 return _action.perform(viewport, args, credentials, handler);
465 if (action instanceof AnnotationAction)
467 var _action = (AnnotationAction) action;
468 var handler = new AnnotationServiceGuiHandler(_action, frame);
469 return _action.perform(viewport, args, credentials, handler);
471 throw new IllegalArgumentException(
472 String.format("Illegal action type %s", action.getClass().getName()));
475 private static CompletionStage<List<ArgumentI>> openEditParamsDialog(
476 ParamDatastoreI paramStore, WsParamSetI preset, List<ArgumentI> arguments)
478 final WsJobParameters jobParams;
479 if (preset == null && arguments != null && arguments.size() > 0)
480 jobParams = new WsJobParameters(paramStore, null, arguments);
482 jobParams = new WsJobParameters(paramStore, preset, null);
484 jobParams.setName(MessageManager.getString(
485 "label.adjusting_parameters_for_calculation"));
486 var stage = jobParams.showRunDialog();
487 return stage.thenApply(startJob -> {
489 return null; // null if cancelled
490 if (jobParams.getPreset() != null)
491 return jobParams.getPreset().getArguments();
492 if (jobParams.isServiceDefaults())
493 return Collections.emptyList();
495 return jobParams.getJobParams();