JAL-4199 Fix cancel button not appearing on progress bar
[jalview.git] / src / jalview / ws2 / gui / WebServicesMenuManager.java
1 package jalview.ws2.gui;
2
3 import java.awt.Color;
4 import java.awt.event.MouseAdapter;
5 import java.awt.event.MouseEvent;
6 import java.net.URL;
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;
13 import java.util.Map;
14 import java.util.TreeMap;
15 import java.util.concurrent.CompletionStage;
16
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;
23
24 import jalview.gui.AlignFrame;
25 import jalview.gui.Desktop;
26 import jalview.gui.JvSwingUtils;
27 import jalview.gui.WsJobParameters;
28 import jalview.util.MessageManager;
29 import jalview.viewmodel.AlignmentViewport;
30 import jalview.ws.params.ArgumentI;
31 import jalview.ws.params.ParamDatastoreI;
32 import jalview.ws.params.WsParamSetI;
33 import jalview.ws2.actions.BaseTask;
34 import jalview.ws2.actions.PollingTaskExecutor;
35 import jalview.ws2.actions.alignment.AlignmentAction;
36 import jalview.ws2.actions.alignment.AlignmentResult;
37 import jalview.ws2.actions.annotation.AlignCalcWorkerAdapter;
38 import jalview.ws2.actions.annotation.AnnotationAction;
39 import jalview.ws2.actions.api.ActionI;
40 import jalview.ws2.actions.api.TaskI;
41 import jalview.ws2.api.Credentials;
42 import jalview.ws2.api.WebService;
43 import jalview.ws2.client.api.WebServiceProviderI;
44
45 public class WebServicesMenuManager
46 {
47   private final JMenu menu;
48
49   private final AlignFrame frame;
50
51   private JMenuItem inProgressItem = new JMenuItem("Service discovery in progress");
52
53   private JMenuItem noServicesItem = new JMenuItem("No services available");
54   {
55     inProgressItem.setEnabled(false);
56     inProgressItem.setVisible(false);
57     noServicesItem.setEnabled(false);
58   }
59
60   public WebServicesMenuManager(String name, AlignFrame frame)
61   {
62     this.frame = frame;
63     menu = new JMenu(name);
64     menu.add(inProgressItem);
65     menu.add(noServicesItem);
66   }
67
68   public JMenu getMenu()
69   {
70     return menu;
71   }
72
73   public void setNoServices(boolean noServices)
74   {
75     noServicesItem.setVisible(noServices);
76   }
77
78   public void setInProgress(boolean inProgress)
79   {
80     inProgressItem.setVisible(inProgress);
81   }
82
83   public void setServices(WebServiceProviderI services)
84   {
85     menu.removeAll();
86     // services grouped by their category
87     Map<String, List<WebService<?>>> oneshotServices = new HashMap<>();
88     Map<String, List<WebService<?>>> interactiveServices = new HashMap<>();
89     for (WebService<?> service : services.getServices())
90     {
91       var map = service.isInteractive() ? interactiveServices : oneshotServices;
92       map.computeIfAbsent(service.getCategory(), k -> new ArrayList<>())
93           .add(service);
94     }
95     var allKeysSet = new HashSet<>(oneshotServices.keySet());
96     allKeysSet.addAll(interactiveServices.keySet());
97     var allKeys = new ArrayList<>(allKeysSet);
98     allKeys.sort(Comparator.naturalOrder());
99     for (String category : allKeys)
100     {
101       var categoryMenu = new JMenu(category);
102       var oneshot = oneshotServices.get(category);
103       if (oneshot != null)
104         addOneshotEntries(oneshot, categoryMenu);
105       var interactive = interactiveServices.get(category);
106       if (interactive != null)
107       {
108         if (oneshot != null)
109           categoryMenu.addSeparator();
110         addInteractiveEntries(interactive, categoryMenu);
111       }
112       menu.add(categoryMenu);
113     }
114     menu.add(inProgressItem);
115     menu.add(noServicesItem);
116   }
117
118   private void addOneshotEntries(List<WebService<?>> services, JMenu menu)
119   {
120     // Workaround. Comparator methods not working in j2s
121     services.sort((ws1, ws2) -> {
122       var res = ws1.getUrl().toString().compareTo(ws2.getUrl().toString());
123       if (res == 0)
124         res = ws1.getName().compareTo(ws2.getName());
125       return res;
126     });
127     URL lastHost = null;
128     for (WebService<?> service : services)
129     {
130       // if new host differs from the last one, add entry separating them
131       URL host = service.getUrl();
132       if (!host.equals(lastHost))
133       {
134         if (lastHost != null)
135           menu.addSeparator();
136         var item = new JMenuItem(host.toString());
137         item.setForeground(Color.BLUE);
138         item.addActionListener(e -> Desktop.showUrl(host.toString()));
139         menu.add(item);
140         lastHost = host;
141       }
142       menu.addSeparator();
143       // group actions by their subcategory, sorted
144       var actionsByCategory = new TreeMap<String, List<ActionI<?>>>();
145       for (ActionI<?> action : service.getActions())
146       {
147         actionsByCategory
148             .computeIfAbsent(
149                 action.getSubcategory() != null ? action.getSubcategory() : "",
150                 k -> new ArrayList<>())
151             .add(action);
152       }
153       for (var entry : actionsByCategory.entrySet())
154       {
155         var category = entry.getKey();
156         var actions = entry.getValue();
157         // create submenu named {subcategory} with {service} or use root menu
158         var atMenu = category.isEmpty() ? menu : new JMenu(String.format("%s with %s", category, service.getName()));
159         if (atMenu != menu)
160           menu.add(atMenu); // add only if submenu
161         // sort actions by name pulling nulls to the front
162         actions.sort(Comparator.comparing(
163             ActionI::getName, Comparator.nullsFirst(Comparator.naturalOrder())));
164         for (int i = 0; i < actions.size(); i++)
165         {
166           addEntriesForAction(actions.get(i), atMenu, atMenu == menu);
167         }
168       }
169     }
170   }
171
172   private void addEntriesForAction(ActionI<?> action, JMenu menu, boolean isTopLevel)
173   {
174     var service = action.getWebService();
175     String itemName;
176     if (isTopLevel)
177     {
178       itemName = service.getName();
179       if (action.getName() != null && !action.getName().isEmpty())
180         itemName += " " + action.getName();
181     }
182     else
183     {
184       if (action.getName() == null || action.getName().isEmpty())
185         itemName = "Run";
186       else
187         itemName = action.getName();
188     }
189     var datastore = service.getParamDatastore();
190     {
191       String text = itemName;
192       if (datastore.hasParameters() || datastore.hasPresets())
193         text += " with defaults";
194       JMenuItem item = new JMenuItem(text);
195       item.addActionListener(e -> {
196         runAction(action, frame.getCurrentView(), Collections.emptyList(),
197             Credentials.empty());
198       });
199       menu.add(item);
200     }
201     if (datastore.hasParameters())
202     {
203       JMenuItem item = new JMenuItem("Edit settings and run...");
204       item.addActionListener(e -> {
205         openEditParamsDialog(datastore, null, null).thenAccept(args -> {
206           if (args != null)
207             runAction(action, frame.getCurrentView(), args, Credentials.empty());
208         });
209       });
210       menu.add(item);
211     }
212     var presets = datastore.getPresets();
213     if (presets != null && presets.size() > 0)
214     {
215       final var presetsMenu = new JMenu(MessageManager.formatMessage(
216           "label.run_with_preset_params", service.getName()));
217       final int dismissDelay = ToolTipManager.sharedInstance()
218           .getDismissDelay();
219       final int QUICK_TOOLTIP = 1500;
220       for (var preset : presets)
221       {
222         var item = new JMenuItem(preset.getName());
223         item.addMouseListener(new MouseAdapter()
224         {
225           @Override
226           public void mouseEntered(MouseEvent evt)
227           {
228             ToolTipManager.sharedInstance().setDismissDelay(QUICK_TOOLTIP);
229           }
230
231           @Override
232           public void mouseExited(MouseEvent evt)
233           {
234             ToolTipManager.sharedInstance().setDismissDelay(dismissDelay);
235           }
236         });
237         String tooltipTitle = MessageManager.getString(
238             preset.isModifiable() ? "label.user_preset" : "label.service_preset");
239         String tooltip = String.format("<strong>%s</strong><br/>%s",
240             tooltipTitle, preset.getDescription());
241         tooltip = JvSwingUtils.wrapTooltip(true, tooltip);
242         item.setToolTipText(tooltip);
243         item.addActionListener(event -> {
244           runAction(action, frame.getCurrentView(), preset.getArguments(),
245               Credentials.empty());
246         });
247         presetsMenu.add(item);
248       }
249       menu.add(presetsMenu);
250     }
251   }
252
253   private void addInteractiveEntries(List<WebService<?>> services, JMenu menu)
254   {
255     Map<String, List<WebService<?>>> byServiceName = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
256     for (var service : services)
257     {
258       byServiceName.computeIfAbsent(service.getName(), k -> new ArrayList<>())
259           .add(service);
260     }
261     for (var entry : byServiceName.entrySet())
262     {
263       var group = new InteractiveServiceEntryGroup(entry.getKey(), entry.getValue());
264       group.appendTo(menu);
265     }
266   }
267
268   private class InteractiveServiceEntryGroup
269   {
270     JLabel serviceLabel;
271
272     JMenuItem urlItem = new JMenuItem();
273
274     JCheckBoxMenuItem serviceItem = new JCheckBoxMenuItem();
275
276     JMenuItem editParamsItem = new JMenuItem("Edit parameters...");
277
278     JMenu presetsMenu = new JMenu("Change preset");
279
280     JMenu alternativesMenu = new JMenu("Choose action");
281     {
282       urlItem.setForeground(Color.BLUE);
283       urlItem.setVisible(false);
284       serviceItem.setVisible(false);
285       editParamsItem.setVisible(false);
286       presetsMenu.setVisible(false);
287     }
288
289     InteractiveServiceEntryGroup(String name, List<WebService<?>> services)
290     {
291       serviceLabel = new JLabel(name);
292       serviceLabel.setBorder(new EmptyBorder(0, 6, 0, 6));
293       buildAlternativesMenu(services);
294     }
295
296     private void buildAlternativesMenu(List<WebService<?>> services)
297     {
298       var menu = alternativesMenu;
299       services.sort((ws1, ws2) -> {
300         var res = ws1.getUrl().toString().compareTo(ws2.getUrl().toString());
301         if (res == 0)
302           res = ws1.getName().compareTo(ws2.getName());
303         return res;
304       });
305       URL lastHost = null;
306       for (var service : services)
307       {
308         // Adding url "separator" before each group
309         URL host = service.getUrl();
310         if (!host.equals(lastHost))
311         {
312           if (lastHost != null)
313             menu.addSeparator();
314           var item = new JMenuItem(host.toString());
315           item.setForeground(Color.BLUE);
316           item.addActionListener(e -> Desktop.showUrl(host.toString()));
317           menu.add(item);
318           lastHost = host;
319         }
320         menu.addSeparator();
321         var actionsByCategory = new TreeMap<String, List<ActionI<?>>>();
322         for (ActionI<?> action : service.getActions())
323         {
324           actionsByCategory
325               .computeIfAbsent(
326                   action.getSubcategory() != null ? action.getSubcategory() : "",
327                   k -> new ArrayList<>())
328               .add(action);
329         }
330         actionsByCategory.forEach((key, actions) -> {
331           var atMenu = key.isEmpty() ? menu : new JMenu(key + " with " + service.getName());
332           boolean topLevel = atMenu == menu;
333           if (!topLevel)
334             menu.add(atMenu);
335           actions.sort(Comparator.comparing(
336               a -> a.getName(),
337               Comparator.nullsFirst(Comparator.naturalOrder())));
338           for (ActionI<?> action : actions)
339           {
340             var item = new JMenuItem(action.getFullName());
341             item.addActionListener(e -> setAlternative(action));
342             atMenu.add(item);
343           }
344         });
345       }
346     }
347
348     private void setAlternative(ActionI<?> action)
349     {
350       final var arguments = new ArrayList<ArgumentI>();
351       final WsParamSetI[] lastPreset = { null };
352
353       // update selected url menu item
354       String url = action.getWebService().getUrl().toString();
355       urlItem.setText(url);
356       urlItem.setVisible(true);
357       for (var l : urlItem.getActionListeners())
358         urlItem.removeActionListener(l);
359       urlItem.addActionListener(e -> Desktop.showUrl(url));
360
361       // update selected service menu item
362       serviceItem.setText(action.getFullName());
363       serviceItem.setVisible(true);
364       for (var l : serviceItem.getActionListeners())
365         serviceItem.removeActionListener(l);
366       WebService<?> service = action.getWebService();
367       serviceItem.addActionListener(e -> {
368         runAction(action, frame.getCurrentView(), arguments,
369             Credentials.empty());
370       });
371       serviceItem.setSelected(true);
372
373       // update edit parameters menu item
374       var datastore = service.getParamDatastore();
375       editParamsItem.setVisible(datastore.hasParameters());
376       for (var l : editParamsItem.getActionListeners())
377         editParamsItem.removeActionListener(l);
378       if (datastore.hasParameters())
379       {
380         editParamsItem.addActionListener(e -> {
381           openEditParamsDialog(service.getParamDatastore(), lastPreset[0], arguments)
382               .thenAccept(args -> {
383                 if (args != null)
384                 {
385                   lastPreset[0] = null;
386                   arguments.clear();
387                   arguments.addAll(args);
388                   runAction(action, frame.getCurrentView(),
389                       arguments, Credentials.empty());
390                 }
391               });
392         });
393       }
394
395       // update presets menu
396       presetsMenu.removeAll();
397       presetsMenu.setEnabled(datastore.hasPresets());
398       if (datastore.hasPresets())
399       {
400         for (WsParamSetI preset : datastore.getPresets())
401         {
402           var item = new JMenuItem(preset.getName());
403           item.addActionListener(e -> {
404             lastPreset[0] = preset;
405             runAction(action, frame.getCurrentView(),
406                 preset.getArguments(), Credentials.empty());
407           });
408           presetsMenu.add(item);
409         }
410       }
411
412       runAction(action, frame.getCurrentView(), arguments,
413           Credentials.empty());
414     }
415
416     void appendTo(JMenu menu)
417     {
418       menu.add(serviceLabel);
419       menu.add(urlItem);
420       menu.add(serviceItem);
421       menu.add(editParamsItem);
422       menu.add(presetsMenu);
423       menu.add(alternativesMenu);
424     }
425   }
426
427
428   private void runAction(ActionI<?> action, AlignmentViewport viewport,
429       List<ArgumentI> args, Credentials credentials)
430   {
431     // casting and instance checks can be avoided with some effort,
432     // let them be for now.
433     if (action instanceof AlignmentAction)
434     {
435       // TODO: test if selection contains enough sequences
436       var _action = (AlignmentAction) action;
437       var handler = new AlignmentServiceGuiHandler(_action, frame);
438       BaseTask<?, AlignmentResult> task = _action.createTask(viewport, args, credentials);
439       var executor = PollingTaskExecutor.fromPool(viewport.getServiceExecutor());
440       task.addTaskEventListener(handler);
441       var future = executor.submit(task);
442       task.setCancelAction(() -> { future.cancel(true); });
443       return;
444     }
445     if (action instanceof AnnotationAction)
446     {
447       var calcManager = viewport.getCalcManager();
448
449       var _action = (AnnotationAction) action;
450       var worker = new AlignCalcWorkerAdapter(viewport, frame.alignPanel,
451           _action, args, credentials);
452       var handler = new AnnotationServiceGuiHandler(_action, frame);
453       worker.setWorkerListener(handler);
454       for (var w : calcManager.getWorkers())
455       {
456         if (worker.getCalcName() != null && worker.getCalcName().equals(w.getCalcName()))
457         {
458           calcManager.cancelWorker(w);
459           calcManager.removeWorker(w);
460         }
461       }
462       if (action.getWebService().isInteractive())
463         calcManager.registerWorker(worker);
464       else
465         calcManager.startWorker(worker);
466       return;
467     }
468     throw new IllegalArgumentException(
469         String.format("Illegal action type %s", action.getClass().getName()));
470   }
471
472   private static CompletionStage<List<ArgumentI>> openEditParamsDialog(
473       ParamDatastoreI paramStore, WsParamSetI preset, List<ArgumentI> arguments)
474   {
475     final WsJobParameters jobParams;
476     if (preset == null && arguments != null && arguments.size() > 0)
477       jobParams = new WsJobParameters(paramStore, null, arguments);
478     else
479       jobParams = new WsJobParameters(paramStore, preset, null);
480     if (preset != null)
481       jobParams.setName(MessageManager.getString(
482           "label.adjusting_parameters_for_calculation"));
483     var stage = jobParams.showRunDialog();
484     return stage.thenApply(startJob -> {
485       if (!startJob)
486         return null; // null if cancelled
487       if (jobParams.getPreset() != null)
488         return jobParams.getPreset().getArguments();
489       if (jobParams.isServiceDefaults())
490         return Collections.emptyList();
491       else
492         return jobParams.getJobParams();
493     });
494   }
495 }