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;
45 import static java.util.Objects.requireNonNullElse;
47 public class WebServicesMenuManager
49 private final JMenu menu;
51 private final AlignFrame frame;
53 private JMenuItem inProgressItem = new JMenuItem("Service discovery in progress");
55 private JMenuItem noServicesItem = new JMenuItem("No services available");
57 inProgressItem.setEnabled(false);
58 inProgressItem.setVisible(false);
59 noServicesItem.setEnabled(false);
62 private Map<String, WeakReference<TaskI<?>>> interactiveTasks = new HashMap<>();
64 public WebServicesMenuManager(String name, AlignFrame frame)
67 menu = new JMenu(name);
68 menu.add(inProgressItem);
69 menu.add(noServicesItem);
72 public JMenu getMenu()
77 public void setNoServices(boolean noServices)
79 noServicesItem.setVisible(noServices);
82 public void setInProgress(boolean inProgress)
84 inProgressItem.setVisible(inProgress);
87 public void setServices(WebServiceProviderI services)
90 // services grouped by their category
91 Map<String, List<WebService<?>>> oneshotServices = new HashMap<>();
92 Map<String, List<WebService<?>>> interactiveServices = new HashMap<>();
93 for (WebService<?> service : services.getServices())
95 var map = service.isInteractive() ? interactiveServices : oneshotServices;
96 map.computeIfAbsent(service.getCategory(), k -> new ArrayList<>())
99 var allKeysSet = new HashSet<>(oneshotServices.keySet());
100 allKeysSet.addAll(interactiveServices.keySet());
101 var allKeys = new ArrayList<>(allKeysSet);
102 allKeys.sort(Comparator.naturalOrder());
103 for (String category : allKeys)
105 var categoryMenu = new JMenu(category);
106 var oneshot = oneshotServices.get(category);
108 addOneshotEntries(oneshot, categoryMenu);
109 var interactive = interactiveServices.get(category);
110 if (interactive != null)
113 categoryMenu.addSeparator();
114 addInteractiveEntries(interactive, categoryMenu);
116 menu.add(categoryMenu);
118 menu.add(inProgressItem);
119 menu.add(noServicesItem);
122 private void addOneshotEntries(List<WebService<?>> services, JMenu menu)
124 services.sort(Comparator
125 .<WebService<?>, String> comparing(s -> s.getUrl().toString())
126 .thenComparing(WebService::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 Objects.requireNonNullElse(action.getSubcategory(), ""),
150 k -> new ArrayList<>())
153 actionsByCategory.forEach((k, v) -> {
154 // create submenu named {subcategory} with {service} or use root menu
155 var atMenu = k.isEmpty() ? menu : new JMenu(String.format("%s with %s", k, service.getName()));
157 menu.add(atMenu); // add only if submenu
158 // sort actions by name pulling nulls to the front
159 v.sort(Comparator.comparing(
160 ActionI::getName, Comparator.nullsFirst(Comparator.naturalOrder())));
161 for (ActionI<?> action : v)
163 addEntriesForAction(action, atMenu, atMenu == menu);
169 private void addEntriesForAction(ActionI<?> action, JMenu menu, boolean isTopLevel)
171 var service = action.getWebService();
175 itemName = service.getName();
176 if (action.getName() != null && !action.getName().isEmpty())
177 itemName += " " + action.getName();
181 if (action.getName() == null || action.getName().isEmpty())
184 itemName = action.getName();
186 var datastore = service.getParamDatastore();
188 String text = itemName;
189 if (datastore.hasParameters() || datastore.hasPresets())
190 text += " with defaults";
191 JMenuItem item = new JMenuItem(text);
192 item.addActionListener(e -> {
193 runAction(action, frame.getCurrentView(), Collections.emptyList(),
194 Credentials.empty());
198 if (datastore.hasParameters())
200 JMenuItem item = new JMenuItem("Edit settings and run...");
201 item.addActionListener(e -> {
202 openEditParamsDialog(datastore, null, null).thenAccept(args -> {
204 runAction(action, frame.getCurrentView(), args, Credentials.empty());
209 var presets = datastore.getPresets();
210 if (presets != null && presets.size() > 0)
212 final var presetsMenu = new JMenu(MessageManager.formatMessage(
213 "label.run_with_preset_params", service.getName()));
214 final int dismissDelay = ToolTipManager.sharedInstance()
216 final int QUICK_TOOLTIP = 1500;
217 for (var preset : presets)
219 var item = new JMenuItem(preset.getName());
220 item.addMouseListener(new MouseAdapter()
223 public void mouseEntered(MouseEvent evt)
225 ToolTipManager.sharedInstance().setDismissDelay(QUICK_TOOLTIP);
229 public void mouseExited(MouseEvent evt)
231 ToolTipManager.sharedInstance().setDismissDelay(dismissDelay);
234 String tooltipTitle = MessageManager.getString(
235 preset.isModifiable() ? "label.user_preset" : "label.service_preset");
236 String tooltip = String.format("<strong>%s</strong><br/>%s",
237 tooltipTitle, preset.getDescription());
238 tooltip = JvSwingUtils.wrapTooltip(true, tooltip);
239 item.setToolTipText(tooltip);
240 item.addActionListener(event -> {
241 runAction(action, frame.getCurrentView(), preset.getArguments(),
242 Credentials.empty());
244 presetsMenu.add(item);
246 menu.add(presetsMenu);
250 private void addInteractiveEntries(List<WebService<?>> services, JMenu menu)
252 Map<String, List<WebService<?>>> byServiceName = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
253 for (var service : services)
255 byServiceName.computeIfAbsent(service.getName(), k -> new ArrayList<>())
258 for (var entry : byServiceName.entrySet())
260 var group = new InteractiveServiceEntryGroup(entry.getKey(), entry.getValue());
261 group.appendTo(menu);
265 private class InteractiveServiceEntryGroup
269 JMenuItem urlItem = new JMenuItem();
271 JCheckBoxMenuItem serviceItem = new JCheckBoxMenuItem();
273 JMenuItem editParamsItem = new JMenuItem("Edit parameters...");
275 JMenu presetsMenu = new JMenu("Change preset");
277 JMenu alternativesMenu = new JMenu("Choose action");
279 urlItem.setForeground(Color.BLUE);
280 urlItem.setVisible(false);
281 serviceItem.setVisible(false);
282 editParamsItem.setVisible(false);
283 presetsMenu.setVisible(false);
286 InteractiveServiceEntryGroup(String name, List<WebService<?>> services)
288 serviceLabel = new JLabel(name);
289 serviceLabel.setBorder(new EmptyBorder(0, 6, 0, 6));
290 buildAlternativesMenu(services);
293 private void buildAlternativesMenu(List<WebService<?>> services)
295 var menu = alternativesMenu;
296 services.sort(Comparator
297 .<WebService<?>, String> comparing(s -> s.getUrl().toString())
298 .thenComparing(s -> s.getName()));
300 for (var service : services)
302 // Adding url "separator" before each group
303 URL host = service.getUrl();
304 if (!host.equals(lastHost))
306 if (lastHost != null)
308 var item = new JMenuItem(host.toString());
309 item.setForeground(Color.BLUE);
310 item.addActionListener(e -> Desktop.showUrl(host.toString()));
315 var actionsByCategory = new TreeMap<String, List<ActionI<?>>>();
316 for (ActionI<?> action : service.getActions())
320 requireNonNullElse(action.getSubcategory(), ""),
321 k -> new ArrayList<>())
324 actionsByCategory.forEach((key, actions) -> {
325 var atMenu = key.isEmpty() ? menu : new JMenu(key + " with " + service.getName());
326 boolean topLevel = atMenu == menu;
329 actions.sort(Comparator.comparing(
331 Comparator.nullsFirst(Comparator.naturalOrder())));
332 for (ActionI<?> action : actions)
334 var item = new JMenuItem(action.getFullName());
335 item.addActionListener(e -> setAlternative(action));
342 private void setAlternative(ActionI<?> action)
344 final var arguments = new ArrayList<ArgumentI>();
345 final WsParamSetI[] lastPreset = { null };
347 // update selected url menu item
348 String url = action.getWebService().getUrl().toString();
349 urlItem.setText(url);
350 urlItem.setVisible(true);
351 for (var l : urlItem.getActionListeners())
352 urlItem.removeActionListener(l);
353 urlItem.addActionListener(e -> Desktop.showUrl(url));
355 // update selected service menu item
356 serviceItem.setText(action.getFullName());
357 serviceItem.setVisible(true);
358 for (var l : serviceItem.getActionListeners())
359 serviceItem.removeActionListener(l);
360 WebService<?> service = action.getWebService();
361 serviceItem.addActionListener(e -> {
362 if (serviceItem.getState())
364 cancelAndRunInteractive(action, frame.getCurrentView(), arguments,
365 Credentials.empty());
369 cancelInteractive(service.getName());
372 serviceItem.setSelected(true);
374 // update edit parameters menu item
375 var datastore = service.getParamDatastore();
376 editParamsItem.setVisible(datastore.hasParameters());
377 for (var l : editParamsItem.getActionListeners())
378 editParamsItem.removeActionListener(l);
379 if (datastore.hasParameters())
381 editParamsItem.addActionListener(e -> {
382 openEditParamsDialog(service.getParamDatastore(), lastPreset[0], arguments)
383 .thenAccept(args -> {
386 lastPreset[0] = null;
388 arguments.addAll(args);
389 cancelAndRunInteractive(action, frame.getCurrentView(),
390 arguments, Credentials.empty());
396 // update presets menu
397 presetsMenu.removeAll();
398 presetsMenu.setEnabled(datastore.hasPresets());
399 if (datastore.hasPresets())
401 for (WsParamSetI preset : datastore.getPresets())
403 var item = new JMenuItem(preset.getName());
404 item.addActionListener(e -> {
405 lastPreset[0] = preset;
406 cancelAndRunInteractive(action, frame.getCurrentView(),
407 preset.getArguments(), Credentials.empty());
409 presetsMenu.add(item);
413 cancelAndRunInteractive(action, frame.getCurrentView(), arguments,
414 Credentials.empty());
417 void appendTo(JMenu menu)
419 menu.add(serviceLabel);
421 menu.add(serviceItem);
422 menu.add(editParamsItem);
423 menu.add(presetsMenu);
424 menu.add(alternativesMenu);
428 private void cancelInteractive(String wsName)
430 var taskRef = interactiveTasks.get(wsName);
431 if (taskRef != null && taskRef.get() != null)
432 taskRef.get().cancel();
433 interactiveTasks.put(wsName, null);
436 private void cancelAndRunInteractive(ActionI<?> action,
437 AlignmentViewport viewport, List<ArgumentI> args, Credentials credentials)
439 var wsName = action.getWebService().getName();
440 cancelInteractive(wsName);
441 var task = runAction(action, viewport, args, credentials);
442 interactiveTasks.put(wsName, new WeakReference<>(task));
445 private TaskI<?> runAction(ActionI<?> action, AlignmentViewport viewport,
446 List<ArgumentI> args, Credentials credentials)
448 // casting and instance checks can be avoided with some effort,
449 // let them be for now.
450 if (action instanceof AlignmentAction)
452 // TODO: test if selection contains enough sequences
453 var _action = (AlignmentAction) action;
454 var handler = new AlignmentServiceGuiHandler(_action, frame);
455 return _action.perform(viewport, args, credentials, handler);
457 if (action instanceof AnnotationAction)
459 var _action = (AnnotationAction) action;
460 var handler = new AnnotationServiceGuiHandler(_action, frame);
461 return _action.perform(viewport, args, credentials, handler);
463 throw new IllegalArgumentException(
464 String.format("Illegal action type %s", action.getClass().getName()));
467 private static CompletionStage<List<ArgumentI>> openEditParamsDialog(
468 ParamDatastoreI paramStore, WsParamSetI preset, List<ArgumentI> arguments)
470 final WsJobParameters jobParams;
471 if (preset == null && arguments != null && arguments.size() > 0)
472 jobParams = new WsJobParameters(paramStore, preset, arguments);
474 jobParams = new WsJobParameters(paramStore, preset, null);
476 jobParams.setName(MessageManager.getString(
477 "label.adjusting_parameters_for_calculation"));
478 var stage = jobParams.showRunDialog();
479 return stage.thenApply(startJob -> {
481 return null; // null if cancelled
482 if (jobParams.getPreset() != null)
483 return jobParams.getPreset().getArguments();
484 if (jobParams.isServiceDefaults())
485 return Collections.emptyList();
487 return jobParams.getJobParams();