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.bin.Console;
25 import jalview.datamodel.AlignmentI;
26 import jalview.gui.AlignFrame;
27 import jalview.gui.Desktop;
28 import jalview.gui.JvSwingUtils;
29 import jalview.gui.WsJobParameters;
30 import jalview.util.MessageManager;
31 import jalview.viewmodel.AlignmentViewport;
32 import jalview.ws.params.ArgumentI;
33 import jalview.ws.params.ParamDatastoreI;
34 import jalview.ws.params.WsParamSetI;
35 import jalview.ws2.actions.BaseTask;
36 import jalview.ws2.actions.PollingTaskExecutor;
37 import jalview.ws2.actions.alignment.AlignmentAction;
38 import jalview.ws2.actions.alignment.AlignmentResult;
39 import jalview.ws2.actions.annotation.AlignCalcWorkerAdapter;
40 import jalview.ws2.actions.annotation.AnnotationAction;
41 import jalview.ws2.actions.api.ActionI;
42 import jalview.ws2.actions.api.TaskEventListener;
43 import jalview.ws2.actions.api.TaskI;
44 import jalview.ws2.actions.hmmer.PhmmerAction;
45 import jalview.ws2.api.Credentials;
46 import jalview.ws2.api.WebService;
47 import jalview.ws2.client.api.WebServiceProviderI;
49 public class WebServicesMenuManager
51 private final JMenu menu;
53 private final AlignFrame frame;
55 private JMenuItem inProgressItem = new JMenuItem("Service discovery in progress");
57 private JMenuItem noServicesItem = new JMenuItem("No services available");
59 inProgressItem.setEnabled(false);
60 inProgressItem.setVisible(false);
61 noServicesItem.setEnabled(false);
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 // Workaround. Comparator methods not working in j2s
125 services.sort((ws1, ws2) -> {
126 var res = ws1.getUrl().toString().compareTo(ws2.getUrl().toString());
128 res = ws1.getName().compareTo(ws2.getName());
132 for (WebService<?> service : services)
134 // if new host differs from the last one, add entry separating them
135 URL host = service.getUrl();
136 if (!host.equals(lastHost))
138 if (lastHost != null)
140 var item = new JMenuItem(host.toString());
141 item.setForeground(Color.BLUE);
142 item.addActionListener(e -> Desktop.showUrl(host.toString()));
147 // group actions by their subcategory, sorted
148 var actionsByCategory = new TreeMap<String, List<ActionI<?>>>();
149 for (ActionI<?> action : service.getActions())
153 action.getSubcategory() != null ? action.getSubcategory() : "",
154 k -> new ArrayList<>())
157 for (var entry : actionsByCategory.entrySet())
159 var category = entry.getKey();
160 var actions = entry.getValue();
161 // create submenu named {subcategory} with {service} or use root menu
162 var atMenu = category.isEmpty() ? menu : new JMenu(String.format("%s with %s", category, service.getName()));
164 menu.add(atMenu); // add only if submenu
165 // sort actions by name pulling nulls to the front
166 actions.sort(Comparator.comparing(
167 ActionI::getName, Comparator.nullsFirst(Comparator.naturalOrder())));
168 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 enabled = isActionEnabled(action);
179 var service = action.getWebService();
183 itemName = service.getName();
184 if (action.getName() != null && !action.getName().isEmpty())
185 itemName += " " + action.getName();
189 if (action.getName() == null || action.getName().isEmpty())
192 itemName = action.getName();
194 var datastore = service.getParamDatastore();
196 String text = itemName;
197 if (datastore.hasParameters() || datastore.hasPresets())
198 text += " with defaults";
199 JMenuItem item = new JMenuItem(text);
200 item.setEnabled(enabled);
201 item.addActionListener(e -> {
202 runAction(action, frame.getCurrentView(), Collections.emptyList(),
203 Credentials.empty());
207 if (datastore.hasParameters())
209 JMenuItem item = new JMenuItem("Edit settings and run...");
210 item.setEnabled(enabled);
211 item.addActionListener(e -> {
212 openEditParamsDialog(datastore, null, null).thenAccept(args -> {
214 runAction(action, frame.getCurrentView(), args, Credentials.empty());
219 var presets = datastore.getPresets();
220 if (presets != null && presets.size() > 0)
222 final var presetsMenu = new JMenu(MessageManager.formatMessage(
223 "label.run_with_preset_params", service.getName()));
224 presetsMenu.setEnabled(enabled);
225 final int dismissDelay = ToolTipManager.sharedInstance()
227 final int QUICK_TOOLTIP = 1500;
228 for (var preset : presets)
230 var item = new JMenuItem(preset.getName());
231 item.addMouseListener(new MouseAdapter()
234 public void mouseEntered(MouseEvent evt)
236 ToolTipManager.sharedInstance().setDismissDelay(QUICK_TOOLTIP);
240 public void mouseExited(MouseEvent evt)
242 ToolTipManager.sharedInstance().setDismissDelay(dismissDelay);
245 String tooltipTitle = MessageManager.getString(
246 preset.isModifiable() ? "label.user_preset" : "label.service_preset");
247 String tooltip = String.format("<strong>%s</strong><br/>%s",
248 tooltipTitle, preset.getDescription());
249 tooltip = JvSwingUtils.wrapTooltip(true, tooltip);
250 item.setToolTipText(tooltip);
251 item.addActionListener(event -> {
252 runAction(action, frame.getCurrentView(), preset.getArguments(),
253 Credentials.empty());
255 presetsMenu.add(item);
257 menu.add(presetsMenu);
261 private void addInteractiveEntries(List<WebService<?>> services, JMenu menu)
263 Map<String, List<WebService<?>>> byServiceName = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
264 for (var service : services)
266 byServiceName.computeIfAbsent(service.getName(), k -> new ArrayList<>())
269 for (var entry : byServiceName.entrySet())
271 var group = new InteractiveServiceEntryGroup(entry.getKey(), entry.getValue());
272 group.appendTo(menu);
276 private class InteractiveServiceEntryGroup
280 JMenuItem urlItem = new JMenuItem();
282 JCheckBoxMenuItem serviceItem = new JCheckBoxMenuItem();
284 JMenuItem editParamsItem = new JMenuItem("Edit parameters...");
286 JMenu presetsMenu = new JMenu("Change preset");
288 JMenu alternativesMenu = new JMenu("Choose action");
290 urlItem.setForeground(Color.BLUE);
291 urlItem.setVisible(false);
292 serviceItem.setVisible(false);
293 editParamsItem.setVisible(false);
294 presetsMenu.setVisible(false);
297 InteractiveServiceEntryGroup(String name, List<WebService<?>> services)
299 serviceLabel = new JLabel(name);
300 serviceLabel.setBorder(new EmptyBorder(0, 6, 0, 6));
301 buildAlternativesMenu(services);
304 private void buildAlternativesMenu(List<WebService<?>> services)
306 var menu = alternativesMenu;
307 services.sort((ws1, ws2) -> {
308 var res = ws1.getUrl().toString().compareTo(ws2.getUrl().toString());
310 res = ws1.getName().compareTo(ws2.getName());
314 for (var service : services)
316 // Adding url "separator" before each group
317 URL host = service.getUrl();
318 if (!host.equals(lastHost))
320 if (lastHost != null)
322 var item = new JMenuItem(host.toString());
323 item.setForeground(Color.BLUE);
324 item.addActionListener(e -> Desktop.showUrl(host.toString()));
329 var actionsByCategory = new TreeMap<String, List<ActionI<?>>>();
330 for (ActionI<?> action : service.getActions())
334 action.getSubcategory() != null ? action.getSubcategory() : "",
335 k -> new ArrayList<>())
338 actionsByCategory.forEach((key, actions) -> {
339 var atMenu = key.isEmpty() ? menu : new JMenu(key + " with " + service.getName());
340 boolean topLevel = atMenu == menu;
343 actions.sort(Comparator.comparing(
345 Comparator.nullsFirst(Comparator.naturalOrder())));
346 for (ActionI<?> action : actions)
348 var item = new JMenuItem(action.getFullName());
349 item.setEnabled(isActionEnabled(action));
350 item.addActionListener(e -> setAlternative(action));
357 private void setAlternative(ActionI<?> action)
359 final var arguments = new ArrayList<ArgumentI>();
360 final WsParamSetI[] lastPreset = { null };
362 // update selected url menu item
363 String url = action.getWebService().getUrl().toString();
364 urlItem.setText(url);
365 urlItem.setVisible(true);
366 for (var l : urlItem.getActionListeners())
367 urlItem.removeActionListener(l);
368 urlItem.addActionListener(e -> Desktop.showUrl(url));
370 // update selected service menu item
371 serviceItem.setText(action.getFullName());
372 serviceItem.setVisible(true);
373 for (var l : serviceItem.getActionListeners())
374 serviceItem.removeActionListener(l);
375 WebService<?> service = action.getWebService();
376 serviceItem.addActionListener(e -> {
377 runAction(action, frame.getCurrentView(), arguments,
378 Credentials.empty());
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 runAction(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 runAction(action, frame.getCurrentView(),
415 preset.getArguments(), Credentials.empty());
417 presetsMenu.add(item);
421 runAction(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 boolean isActionEnabled(ActionI<?> action)
438 var isNa = frame.getViewport().getAlignment().isNucleotide();
439 return ((isNa && action.doAllowNucleotide()) ||
440 (!isNa && action.doAllowProtein()));
443 private void runAction(ActionI<?> action, AlignmentViewport viewport,
444 List<ArgumentI> args, Credentials credentials)
446 // casting and instance checks can be avoided with some effort,
447 // let them be for now.
448 if (action instanceof AlignmentAction)
450 // TODO: test if selection contains enough sequences
451 var _action = (AlignmentAction) action;
452 var handler = new AlignmentServiceGuiHandler(_action, frame);
453 BaseTask<?, AlignmentResult> task = _action.createTask(viewport, args, credentials);
454 var executor = PollingTaskExecutor.fromPool(viewport.getServiceExecutor());
455 task.addTaskEventListener(handler);
456 var future = executor.submit(task);
457 task.setCancelAction(() -> { future.cancel(true); });
460 if (action instanceof AnnotationAction)
462 var calcManager = viewport.getCalcManager();
464 var _action = (AnnotationAction) action;
465 var worker = new AlignCalcWorkerAdapter(viewport, frame.alignPanel,
466 _action, args, credentials);
467 var handler = new AnnotationServiceGuiHandler(_action, frame);
468 worker.setWorkerListener(handler);
469 for (var w : calcManager.getWorkers())
471 if (worker.getCalcName() != null && worker.getCalcName().equals(w.getCalcName()))
473 calcManager.cancelWorker(w);
474 calcManager.removeWorker(w);
477 if (action.getWebService().isInteractive())
478 calcManager.registerWorker(worker);
480 calcManager.startWorker(worker);
483 if (action instanceof PhmmerAction)
485 var _action = (PhmmerAction) action;
486 var handler = new SearchServiceGuiHandler(_action, frame);
487 TaskI<AlignmentI> task = _action.createTask(viewport, args, credentials);
488 var executor = PollingTaskExecutor.fromPool(viewport.getServiceExecutor());
489 task.addTaskEventListener(handler);
490 _action.perform(viewport, args, credentials, handler);
493 Console.warn(String.format(
494 "No known handler for action type %s. All output will be discarded.",
495 action.getClass().getName()));
496 var task = action.createTask(viewport, args, credentials);
497 task.addTaskEventListener(TaskEventListener.nullListener());
498 PollingTaskExecutor.fromPool(viewport.getServiceExecutor())
502 private static CompletionStage<List<ArgumentI>> openEditParamsDialog(
503 ParamDatastoreI paramStore, WsParamSetI preset, List<ArgumentI> arguments)
505 final WsJobParameters jobParams;
506 if (preset == null && arguments != null && arguments.size() > 0)
507 jobParams = new WsJobParameters(paramStore, null, arguments);
509 jobParams = new WsJobParameters(paramStore, preset, null);
511 jobParams.setName(MessageManager.getString(
512 "label.adjusting_parameters_for_calculation"));
513 var stage = jobParams.showRunDialog();
514 return stage.thenApply(startJob -> {
516 return null; // null if cancelled
517 if (jobParams.getPreset() != null)
518 return jobParams.getPreset().getArguments();
519 if (jobParams.isServiceDefaults())
520 return Collections.emptyList();
522 return jobParams.getJobParams();