JAL-3878 Implement prot. seq. an. discovery for slivka.
[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.Collection;
9 import java.util.Collections;
10 import java.util.Comparator;
11 import java.util.HashMap;
12 import java.util.HashSet;
13 import java.util.List;
14 import java.util.Map;
15 import java.util.Objects;
16 import java.util.TreeMap;
17 import java.util.concurrent.CompletionStage;
18
19 import javax.swing.JMenu;
20 import javax.swing.JMenuItem;
21 import javax.swing.ToolTipManager;
22
23 import jalview.gui.AlignFrame;
24 import jalview.gui.Desktop;
25 import jalview.gui.JvSwingUtils;
26 import jalview.gui.WsJobParameters;
27 import jalview.util.MessageManager;
28 import jalview.viewmodel.AlignmentViewport;
29 import jalview.ws.params.ArgumentI;
30 import jalview.ws.params.ParamDatastoreI;
31 import jalview.ws.params.WsParamSetI;
32 import jalview.ws2.actions.alignment.AlignmentAction;
33 import jalview.ws2.actions.annotation.AnnotationAction;
34 import jalview.ws2.actions.api.ActionI;
35 import jalview.ws2.actions.api.TaskI;
36 import jalview.ws2.api.Credentials;
37 import jalview.ws2.api.WebService;
38 import jalview.ws2.client.api.WebServiceProviderI;
39
40 public class WebServicesMenuManager
41 {
42   private final JMenu menu;
43
44   private final AlignFrame frame;
45
46   private JMenuItem inProgressItem = new JMenuItem("Service discovery in progress");
47
48   private JMenuItem noServicesItem = new JMenuItem("No services available");
49   {
50     inProgressItem.setEnabled(false);
51     inProgressItem.setVisible(false);
52     noServicesItem.setEnabled(false);
53   }
54
55   public WebServicesMenuManager(String name, AlignFrame frame)
56   {
57     this.frame = frame;
58     menu = new JMenu(name);
59     menu.add(inProgressItem);
60     menu.add(noServicesItem);
61   }
62
63   public JMenu getMenu()
64   {
65     return menu;
66   }
67
68   public void setNoServices(boolean noServices)
69   {
70     noServicesItem.setVisible(noServices);
71   }
72
73   public void setInProgress(boolean inProgress)
74   {
75     inProgressItem.setVisible(inProgress);
76   }
77
78   public void setServices(WebServiceProviderI services)
79   {
80     menu.removeAll();
81     // services grouped by their category
82     Map<String, List<WebService<?>>> oneshotServices = new HashMap<>();
83     Map<String, List<WebService<?>>> interactiveServices = new HashMap<>();
84     for (WebService<?> service : services.getServices())
85     {
86       var map = service.isInteractive() ? interactiveServices : oneshotServices;
87       map.computeIfAbsent(service.getCategory(), k -> new ArrayList<>())
88           .add(service);
89     }
90     var allKeysSet = new HashSet<>(oneshotServices.keySet());
91     allKeysSet.addAll(interactiveServices.keySet());
92     var allKeys = new ArrayList<>(allKeysSet);
93     allKeys.sort(Comparator.naturalOrder());
94     for (String category : allKeys)
95     {
96       var categoryMenu = new JMenu(category);
97       var oneshot = oneshotServices.get(category);
98       if (oneshot != null)
99         addOneshotEntries(oneshot, categoryMenu);
100       var interactive = interactiveServices.get(category);
101       if (interactive != null)
102       {
103         if (oneshot != null)
104           categoryMenu.addSeparator();
105         // addInteractiveEntries(interactive, categoryMenu);
106       }
107       menu.add(categoryMenu);
108     }
109     menu.add(inProgressItem);
110     menu.add(noServicesItem);
111   }
112
113   private void addOneshotEntries(List<WebService<?>> services, JMenu menu)
114   {
115     services.sort(Comparator
116         .<WebService<?>, String> comparing(s -> s.getUrl().toString())
117         .thenComparing(WebService::getName));
118     URL lastHost = null;
119     for (WebService<?> service : services)
120     {
121       // if new host differs from the last one, add entry separating them
122       URL host = service.getUrl();
123       if (!host.equals(lastHost))
124       {
125         if (lastHost != null)
126           menu.addSeparator();
127         var item = new JMenuItem(host.toString());
128         item.setForeground(Color.BLUE);
129         item.addActionListener(e -> Desktop.showUrl(host.toString()));
130         menu.add(item);
131         lastHost = host;
132       }
133       menu.addSeparator();
134       // group actions by their subcategory, sorted
135       var actionsByCategory = new TreeMap<String, List<ActionI<?>>>();
136       for (ActionI<?> action : service.getActions())
137       {
138         actionsByCategory
139             .computeIfAbsent(
140                 Objects.requireNonNullElse(action.getSubcategory(), ""),
141                 k -> new ArrayList<>())
142             .add(action);
143       }
144       actionsByCategory.forEach((k, v) -> {
145         // create submenu named {subcategory} with {service} or use root menu
146         var atMenu = k.isEmpty() ? menu : new JMenu(String.format("%s with %s", k, service.getName()));
147         if (atMenu != menu)
148           menu.add(atMenu); // add only if submenu
149         // sort actions by name pulling nulls to the front
150         v.sort(Comparator.comparing(
151             ActionI::getName, Comparator.nullsFirst(Comparator.naturalOrder())));
152         for (ActionI<?> action : v)
153         {
154           addEntriesForAction(action, atMenu, atMenu == menu);
155         }
156       });
157     }
158   }
159
160   private void addEntriesForAction(ActionI<?> action, JMenu menu, boolean isTopLevel)
161   {
162     var service = action.getWebService();
163     String itemName;
164     if (isTopLevel)
165     {
166       itemName = service.getName();
167       if (action.getName() != null && !action.getName().isEmpty())
168         itemName += " " + action.getName();
169     }
170     else
171     {
172       if (action.getName() == null || action.getName().isEmpty())
173         itemName = "Run";
174       else
175         itemName = action.getName();
176     }
177     var datastore = service.getParamDatastore();
178     {
179       String text = itemName;
180       if (datastore.hasParameters() || datastore.hasPresets())
181         text += "with defaults";
182       JMenuItem item = new JMenuItem(text);
183       item.addActionListener(e -> {
184         runAction(action, frame.getCurrentView(), Collections.emptyList(),
185             Credentials.empty());
186       });
187       menu.add(item);
188     }
189     if (datastore.hasParameters())
190     {
191       JMenuItem item = new JMenuItem("Edit settings and run...");
192       item.addActionListener(e -> {
193         openEditParamsDialog(datastore, null, null).thenAccept(args -> {
194           if (args != null)
195             runAction(action, frame.getCurrentView(), args, Credentials.empty());
196         });
197       });
198       menu.add(item);
199     }
200     var presets = datastore.getPresets();
201     if (presets != null && presets.size() > 0)
202     {
203       final var presetsMenu = new JMenu(MessageManager.formatMessage(
204           "label.run_with_preset_params", service.getName()));
205       final int dismissDelay = ToolTipManager.sharedInstance()
206           .getDismissDelay();
207       final int QUICK_TOOLTIP = 1500;
208       for (var preset : presets)
209       {
210         var item = new JMenuItem(preset.getName());
211         item.addMouseListener(new MouseAdapter()
212         {
213           @Override
214           public void mouseEntered(MouseEvent evt)
215           {
216             ToolTipManager.sharedInstance().setDismissDelay(QUICK_TOOLTIP);
217           }
218
219           @Override
220           public void mouseExited(MouseEvent evt)
221           {
222             ToolTipManager.sharedInstance().setDismissDelay(dismissDelay);
223           }
224         });
225         String tooltipTitle = MessageManager.getString(
226             preset.isModifiable() ? "label.user_preset" : "label.service_preset");
227         String tooltip = String.format("<strong>%s</strong><br/>%s",
228             tooltipTitle, preset.getDescription());
229         tooltip = JvSwingUtils.wrapTooltip(true, tooltip);
230         item.setToolTipText(tooltip);
231         item.addActionListener(event -> {
232           runAction(action, frame.getCurrentView(), preset.getArguments(),
233               Credentials.empty());
234         });
235         presetsMenu.add(item);
236       }
237       menu.add(presetsMenu);
238     }
239   }
240
241   private TaskI<?> runAction(ActionI<?> action, AlignmentViewport viewport,
242       List<ArgumentI> args, Credentials credentials)
243   {
244     // casting and instance checks can be avoided with some effort,
245     // let them be for now.
246     if (action instanceof AlignmentAction)
247     {
248       // TODO: test if selection contains enough sequences
249       var _action = (AlignmentAction) action;
250       var handler = new AlignmentServiceGuiHandler(_action, frame);
251       return _action.perform(viewport, args, credentials, handler);
252     }
253     if (action instanceof AnnotationAction)
254     {
255       var _action = (AnnotationAction) action;
256       var handler = new AnnotationServiceGuiHandler(_action, frame);
257       return _action.perform(viewport, args, credentials, handler);
258     }
259     throw new IllegalArgumentException(
260         String.format("Illegal action type %s", action.getClass().getName()));
261   }
262
263   private static CompletionStage<List<ArgumentI>> openEditParamsDialog(
264       ParamDatastoreI paramStore, WsParamSetI preset, List<ArgumentI> arguments)
265   {
266     final WsJobParameters jobParams;
267     if (preset == null && arguments != null && arguments.size() > 0)
268       jobParams = new WsJobParameters(paramStore, preset, arguments);
269     else
270       jobParams = new WsJobParameters(paramStore, preset, null);
271     if (preset != null)
272       jobParams.setName(MessageManager.getString(
273           "label.adjusting_parameters_for_calculation"));
274     var stage = jobParams.showRunDialog();
275     return stage.thenApply(startJob -> {
276       if (!startJob)
277         return null; // null if cancelled
278       if (jobParams.getPreset() != null)
279         return jobParams.getPreset().getArguments();
280       if (jobParams.isServiceDefaults())
281         return Collections.emptyList();
282       else
283         return jobParams.getJobParams();
284     });
285   }
286 }