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.BaseAction;
36 import jalview.ws2.actions.BaseTask;
37 import jalview.ws2.actions.PollingTaskExecutor;
38 import jalview.ws2.actions.alignment.AlignmentAction;
39 import jalview.ws2.actions.alignment.AlignmentResult;
40 import jalview.ws2.actions.annotation.AlignCalcWorkerAdapter;
41 import jalview.ws2.actions.annotation.AnnotationAction;
42 import jalview.ws2.actions.api.ActionI;
43 import jalview.ws2.actions.api.TaskEventListener;
44 import jalview.ws2.actions.api.TaskI;
45 import jalview.ws2.actions.hmmer.PhmmerAction;
46 import jalview.ws2.actions.secstructpred.SecStructPredAction;
47 import jalview.ws2.api.Credentials;
48 import jalview.ws2.api.WebService;
49 import jalview.ws2.client.api.WebServiceProviderI;
51 public class WebServicesMenuManager
53 private final JMenu menu;
55 private final AlignFrame frame;
57 private JMenuItem inProgressItem = new JMenuItem("Service discovery in progress");
59 private JMenuItem noServicesItem = new JMenuItem("No services available");
61 inProgressItem.setEnabled(false);
62 inProgressItem.setVisible(false);
63 noServicesItem.setEnabled(false);
66 public WebServicesMenuManager(String name, AlignFrame frame)
69 menu = new JMenu(name);
70 menu.add(inProgressItem);
71 menu.add(noServicesItem);
74 public JMenu getMenu()
79 public void setNoServices(boolean noServices)
81 noServicesItem.setVisible(noServices);
84 public void setInProgress(boolean inProgress)
86 inProgressItem.setVisible(inProgress);
89 public void setServices(WebServiceProviderI services)
92 // services grouped by their category
93 Map<String, List<WebService<?>>> oneshotServices = new HashMap<>();
94 Map<String, List<WebService<?>>> interactiveServices = new HashMap<>();
95 for (WebService<?> service : services.getServices())
97 var map = service.isInteractive() ? interactiveServices : oneshotServices;
98 map.computeIfAbsent(service.getCategory(), k -> new ArrayList<>())
101 var allKeysSet = new HashSet<>(oneshotServices.keySet());
102 allKeysSet.addAll(interactiveServices.keySet());
103 var allKeys = new ArrayList<>(allKeysSet);
104 allKeys.sort(Comparator.naturalOrder());
105 for (String category : allKeys)
107 var categoryMenu = new JMenu(category);
108 var oneshot = oneshotServices.get(category);
110 addOneshotEntries(oneshot, categoryMenu);
111 var interactive = interactiveServices.get(category);
112 if (interactive != null)
115 categoryMenu.addSeparator();
116 addInteractiveEntries(interactive, categoryMenu);
118 menu.add(categoryMenu);
120 menu.add(inProgressItem);
121 menu.add(noServicesItem);
124 private void addOneshotEntries(List<WebService<?>> services, JMenu menu)
126 // Workaround. Comparator methods not working in j2s
127 services.sort((ws1, ws2) -> {
128 var res = ws1.getUrl().toString().compareTo(ws2.getUrl().toString());
130 res = ws1.getName().compareTo(ws2.getName());
134 for (WebService<?> service : services)
136 // if new host differs from the last one, add entry separating them
137 URL host = service.getUrl();
138 if (!host.equals(lastHost))
140 if (lastHost != null)
142 var item = new JMenuItem(host.toString());
143 item.setForeground(Color.BLUE);
144 item.addActionListener(e -> Desktop.showUrl(host.toString()));
149 // group actions by their subcategory, sorted
150 var actionsByCategory = new TreeMap<String, List<ActionI<?>>>();
151 for (ActionI<?> action : service.getActions())
155 action.getSubcategory() != null ? action.getSubcategory() : "",
156 k -> new ArrayList<>())
159 for (var entry : actionsByCategory.entrySet())
161 var category = entry.getKey();
162 var actions = entry.getValue();
163 // create submenu named {subcategory} with {service} or use root menu
164 var atMenu = category.isEmpty() ? menu : new JMenu(String.format("%s with %s", category, service.getName()));
166 menu.add(atMenu); // add only if submenu
167 // sort actions by name pulling nulls to the front
168 actions.sort(Comparator.comparing(
169 ActionI::getName, Comparator.nullsFirst(Comparator.naturalOrder())));
170 for (int i = 0; i < actions.size(); i++)
172 addEntriesForAction(actions.get(i), atMenu, atMenu == menu);
178 private void addEntriesForAction(ActionI<?> action, JMenu menu, boolean isTopLevel)
180 var enabled = isActionEnabled(action);
181 var service = action.getWebService();
185 itemName = service.getName();
186 if (action.getName() != null && !action.getName().isEmpty())
187 itemName += " " + action.getName();
191 if (action.getName() == null || action.getName().isEmpty())
194 itemName = action.getName();
196 var datastore = service.getParamDatastore();
198 String text = itemName;
199 if (datastore.hasParameters() || datastore.hasPresets())
200 text += " with defaults";
201 JMenuItem item = new JMenuItem(text);
202 item.setEnabled(enabled);
203 item.addActionListener(e -> {
204 runAction(action, frame.getCurrentView(), Collections.emptyList(),
205 Credentials.empty());
209 if (datastore.hasParameters())
211 JMenuItem item = new JMenuItem("Edit settings and run...");
212 item.setEnabled(enabled);
213 item.addActionListener(e -> {
214 openEditParamsDialog(datastore, null, null).thenAccept(args -> {
216 runAction(action, frame.getCurrentView(), args, Credentials.empty());
221 var presets = datastore.getPresets();
222 if (presets != null && presets.size() > 0)
224 final var presetsMenu = new JMenu(MessageManager.formatMessage(
225 "label.run_with_preset_params", service.getName()));
226 presetsMenu.setEnabled(enabled);
227 final int dismissDelay = ToolTipManager.sharedInstance()
229 final int QUICK_TOOLTIP = 1500;
230 for (var preset : presets)
232 var item = new JMenuItem(preset.getName());
233 item.addMouseListener(new MouseAdapter()
236 public void mouseEntered(MouseEvent evt)
238 ToolTipManager.sharedInstance().setDismissDelay(QUICK_TOOLTIP);
242 public void mouseExited(MouseEvent evt)
244 ToolTipManager.sharedInstance().setDismissDelay(dismissDelay);
247 String tooltipTitle = MessageManager.getString(
248 preset.isModifiable() ? "label.user_preset" : "label.service_preset");
249 String tooltip = String.format("<strong>%s</strong><br/>%s",
250 tooltipTitle, preset.getDescription());
251 tooltip = JvSwingUtils.wrapTooltip(true, tooltip);
252 item.setToolTipText(tooltip);
253 item.addActionListener(event -> {
254 runAction(action, frame.getCurrentView(), preset.getArguments(),
255 Credentials.empty());
257 presetsMenu.add(item);
259 menu.add(presetsMenu);
263 private void addInteractiveEntries(List<WebService<?>> services, JMenu menu)
265 Map<String, List<WebService<?>>> byServiceName = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
266 for (var service : services)
268 byServiceName.computeIfAbsent(service.getName(), k -> new ArrayList<>())
271 for (var entry : byServiceName.entrySet())
273 var group = new InteractiveServiceEntryGroup(entry.getKey(), entry.getValue());
274 group.appendTo(menu);
278 private class InteractiveServiceEntryGroup
282 JMenuItem urlItem = new JMenuItem();
284 JCheckBoxMenuItem serviceItem = new JCheckBoxMenuItem();
286 JMenuItem editParamsItem = new JMenuItem("Edit parameters...");
288 JMenu presetsMenu = new JMenu("Change preset");
290 JMenu alternativesMenu = new JMenu("Choose action");
292 urlItem.setForeground(Color.BLUE);
293 urlItem.setVisible(false);
294 serviceItem.setVisible(false);
295 editParamsItem.setVisible(false);
296 presetsMenu.setVisible(false);
299 InteractiveServiceEntryGroup(String name, List<WebService<?>> services)
301 serviceLabel = new JLabel(name);
302 serviceLabel.setBorder(new EmptyBorder(0, 6, 0, 6));
303 buildAlternativesMenu(services);
306 private void buildAlternativesMenu(List<WebService<?>> services)
308 var menu = alternativesMenu;
309 services.sort((ws1, ws2) -> {
310 var res = ws1.getUrl().toString().compareTo(ws2.getUrl().toString());
312 res = ws1.getName().compareTo(ws2.getName());
316 for (var service : services)
318 // Adding url "separator" before each group
319 URL host = service.getUrl();
320 if (!host.equals(lastHost))
322 if (lastHost != null)
324 var item = new JMenuItem(host.toString());
325 item.setForeground(Color.BLUE);
326 item.addActionListener(e -> Desktop.showUrl(host.toString()));
331 var actionsByCategory = new TreeMap<String, List<ActionI<?>>>();
332 for (ActionI<?> action : service.getActions())
336 action.getSubcategory() != null ? action.getSubcategory() : "",
337 k -> new ArrayList<>())
340 actionsByCategory.forEach((key, actions) -> {
341 var atMenu = key.isEmpty() ? menu : new JMenu(key + " with " + service.getName());
342 boolean topLevel = atMenu == menu;
345 actions.sort(Comparator.comparing(
347 Comparator.nullsFirst(Comparator.naturalOrder())));
348 for (ActionI<?> action : actions)
350 var item = new JMenuItem(action.getFullName());
351 item.setEnabled(isActionEnabled(action));
352 item.addActionListener(e -> setAlternative(action));
359 private void setAlternative(ActionI<?> action)
361 final var arguments = new ArrayList<ArgumentI>();
362 final WsParamSetI[] lastPreset = { null };
364 // update selected url menu item
365 String url = action.getWebService().getUrl().toString();
366 urlItem.setText(url);
367 urlItem.setVisible(true);
368 for (var l : urlItem.getActionListeners())
369 urlItem.removeActionListener(l);
370 urlItem.addActionListener(e -> Desktop.showUrl(url));
372 // update selected service menu item
373 serviceItem.setText(action.getFullName());
374 serviceItem.setVisible(true);
375 for (var l : serviceItem.getActionListeners())
376 serviceItem.removeActionListener(l);
377 WebService<?> service = action.getWebService();
378 serviceItem.addActionListener(e -> {
379 runAction(action, frame.getCurrentView(), arguments,
380 Credentials.empty());
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 runAction(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 runAction(action, frame.getCurrentView(),
417 preset.getArguments(), Credentials.empty());
419 presetsMenu.add(item);
423 runAction(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 boolean isActionEnabled(ActionI<?> action)
440 var isNa = frame.getViewport().getAlignment().isNucleotide();
441 return ((isNa && action.doAllowNucleotide()) ||
442 (!isNa && action.doAllowProtein()));
445 private void 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 BaseTask<?, AlignmentResult> task = _action.createTask(viewport, args, credentials);
456 var executor = PollingTaskExecutor.fromPool(viewport.getServiceExecutor());
457 task.addTaskEventListener(handler);
458 var future = executor.submit(task);
459 task.setCancelAction(() -> { future.cancel(true); });
462 if (action instanceof AnnotationAction)
464 var calcManager = viewport.getCalcManager();
466 var _action = (AnnotationAction) action;
467 var worker = new AlignCalcWorkerAdapter(viewport, frame.alignPanel,
468 _action, args, credentials);
469 var handler = new AnnotationServiceGuiHandler(_action, frame);
470 worker.setWorkerListener(handler);
471 for (var w : calcManager.getWorkers())
473 if (worker.getCalcName() != null && worker.getCalcName().equals(w.getCalcName()))
475 calcManager.cancelWorker(w);
476 calcManager.removeWorker(w);
479 if (action.getWebService().isInteractive())
480 calcManager.registerWorker(worker);
482 calcManager.startWorker(worker);
485 if (action instanceof PhmmerAction || action instanceof SecStructPredAction)
487 var _action = (BaseAction<AlignmentI>) action;
488 var handler = new SearchServiceGuiHandler(_action, frame);
489 var task = (BaseTask<?, AlignmentI>) _action // FIXME: unsafe cast
490 .createTask(viewport, args, credentials);
491 var executor = PollingTaskExecutor.fromPool(viewport.getServiceExecutor());
492 task.addTaskEventListener(handler);
493 var future = executor.submit(task);
494 task.setCancelAction(() -> {
499 Console.warn(String.format(
500 "No known handler for action type %s. All output will be discarded.",
501 action.getClass().getName()));
502 var task = action.createTask(viewport, args, credentials);
503 task.addTaskEventListener(TaskEventListener.nullListener());
504 PollingTaskExecutor.fromPool(viewport.getServiceExecutor())
508 private static CompletionStage<List<ArgumentI>> openEditParamsDialog(
509 ParamDatastoreI paramStore, WsParamSetI preset, List<ArgumentI> arguments)
511 final WsJobParameters jobParams;
512 if (preset == null && arguments != null && arguments.size() > 0)
513 jobParams = new WsJobParameters(paramStore, null, arguments);
515 jobParams = new WsJobParameters(paramStore, preset, null);
517 jobParams.setName(MessageManager.getString(
518 "label.adjusting_parameters_for_calculation"));
519 var stage = jobParams.showRunDialog();
520 return stage.thenApply(startJob -> {
522 return null; // null if cancelled
523 if (jobParams.getPreset() != null)
524 return jobParams.getPreset().getArguments();
525 if (jobParams.isServiceDefaults())
526 return Collections.emptyList();
528 return jobParams.getJobParams();