/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package org.netbeans.modules.ant.freeform;

import java.awt.event.ActionEvent;
import java.io.File;
import java.io.IOException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.JComponent;
import javax.swing.JMenuItem;
import javax.swing.JSeparator;
import org.apache.tools.ant.module.api.support.ActionUtils;
import org.netbeans.api.project.ProjectManager;
import org.netbeans.modules.ant.freeform.ui.TargetMappingPanel;
import org.netbeans.modules.ant.freeform.ui.UnboundTargetAlert;
import org.netbeans.spi.project.ActionProgress;
import org.netbeans.spi.project.ActionProvider;
import org.netbeans.spi.project.SingleMethod;
import org.netbeans.spi.project.support.ant.AntProjectHelper;
import org.netbeans.spi.project.ui.support.DefaultProjectOperations;
import org.netbeans.spi.project.ui.support.ProjectSensitiveActions;
import org.openide.DialogDisplayer;
import org.openide.ErrorManager;
import org.openide.NotifyDescriptor;
import org.openide.awt.ActionID;
import org.openide.awt.ActionReference;
import org.openide.awt.ActionRegistration;
import org.openide.awt.DynamicMenuContent;
import org.openide.awt.StatusDisplayer;
import org.openide.execution.ExecutorTask;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileUtil;
import org.openide.loaders.DataObject;
import org.openide.util.ContextAwareAction;
import org.openide.util.Lookup;
import org.openide.util.Mutex;
import org.openide.util.NbBundle;
import org.openide.util.Task;
import org.openide.util.TaskListener;
import org.openide.util.Union2;
import org.openide.util.actions.Presenter;
import org.openide.xml.XMLUtil;
import org.w3c.dom.Element;

/**
 * Action bindings for a freeform project.
 * @author Jesse Glick
 */
public final class Actions implements ActionProvider {

    private static final Logger LOG = Logger.getLogger(Actions.class.getName());

    /**
     * Some routine global actions for which we can supply a display name.
     * These are IDE-specific.
     */
    private static final Set<String> COMMON_IDE_GLOBAL_ACTIONS = new HashSet<String>(Arrays.asList(
        ActionProvider.COMMAND_DEBUG,
        ActionProvider.COMMAND_PROFILE,
        ActionProvider.COMMAND_DELETE,
        ActionProvider.COMMAND_COPY,
        ActionProvider.COMMAND_MOVE,
        ActionProvider.COMMAND_RENAME));
    /**
     * Similar to {@link #COMMON_IDE_GLOBAL_ACTIONS}, but these are not IDE-specific.
     * We also mark all of these as bound in the project; if the user
     * does not really have a binding, they are prompted for one when
     * the action is "run".
     */
    private static final Set<String> COMMON_NON_IDE_GLOBAL_ACTIONS = new HashSet<String>(Arrays.asList(
        ActionProvider.COMMAND_BUILD,
        ActionProvider.COMMAND_CLEAN,
        ActionProvider.COMMAND_REBUILD,
        ActionProvider.COMMAND_RUN,
        ActionProvider.COMMAND_TEST,
        // XXX JavaProjectConstants.COMMAND_JAVADOC
        "javadoc", // NOI18N
        // XXX WebProjectConstants.COMMAND_REDEPLOY
        // XXX should this really be here? perhaps not, once web part of #46886 is implemented...
        "redeploy",
        // XXX deploy action of EJB freeform project
        "deploy")); // NOI18N
    
    private final FreeformProject project;
    
    /**
     * Create a new action provider.
     * @param project the associated project
     */
    public Actions(FreeformProject project) {
        this.project = project;
    }
    
    public String[] getSupportedActions() {
        final Element genldata = project.getPrimaryConfigurationData();
        final Element actionsEl = XMLUtil.findElement(genldata, "ide-actions", FreeformProjectType.NS_GENERAL); // NOI18N
        // Use a set, not a list, since when using context you can define one action several times:
        final Set<String> names = new LinkedHashSet<String>();
        if (actionsEl != null) {                            
            for (Element actionEl : XMLUtil.findSubElements(actionsEl)) {
                names.add(actionEl.getAttribute("name")); // NOI18N
            }
            // #46886: also always enable all common global actions, in case they should be selected:
            names.addAll(COMMON_NON_IDE_GLOBAL_ACTIONS);
        }
        names.add(COMMAND_RENAME);
        names.add(COMMAND_MOVE);
        names.add(COMMAND_COPY);
        names.add(COMMAND_DELETE);
        return names.toArray(new String[names.size()]);
    }
    
    public boolean isActionEnabled(String command, Lookup context) throws IllegalArgumentException {
        if (COMMAND_DELETE.equals(command)) {
            return true;
        }
        if (COMMAND_COPY.equals(command)) {
            return true;
        }
        if (COMMAND_RENAME.equals(command)) {
            return true;
        }
        if (COMMAND_MOVE.equals(command)) {
            return true;
        }
        
        Element genldata = project.getPrimaryConfigurationData();
        Element actionsEl = XMLUtil.findElement(genldata, "ide-actions", FreeformProjectType.NS_GENERAL); // NOI18N
        if (actionsEl == null) {
            throw new IllegalArgumentException("No commands supported"); // NOI18N
        }
        boolean foundAction = false;
        for (Element actionEl : XMLUtil.findSubElements(actionsEl)) {
            if (actionEl.getAttribute("name").equals(command)) { // NOI18N
                foundAction = true;
                // XXX perhaps check also existence of script
                Element contextEl = XMLUtil.findElement(actionEl, "context", FreeformProjectType.NS_GENERAL); // NOI18N
                if (contextEl != null) {
                    // Check whether the context contains files all in this folder,
                    // matching the pattern if any, and matching the arity (single/multiple).
                    Map<String,FileObject> selection = findSelection(contextEl, context, project,
                            command.equals(SingleMethod.COMMAND_RUN_SINGLE_METHOD) || command.equals(SingleMethod.COMMAND_DEBUG_SINGLE_METHOD) ? new AtomicReference<String>() : null);
                    LOG.log(Level.FINE, "detected selection {0} for command {1} in {2}", new Object[] {selection, command, project});
                    if (selection.size() == 1) {
                        // Definitely enabled.
                        return true;
                    } else if (!selection.isEmpty()) {
                        // Multiple selection; check arity.
                        Element arityEl = XMLUtil.findElement(contextEl, "arity", FreeformProjectType.NS_GENERAL); // NOI18N
                        assert arityEl != null : "No <arity> in <context> for " + command;
                        if (XMLUtil.findElement(arityEl, "separated-files", FreeformProjectType.NS_GENERAL) != null) { // NOI18N
                            // Supports multiple selection, take it.
                            return true;
                        }
                    }
                } else {
                    // Not context-sensitive.
                    return true;
                }
            }
        }
        if (COMMON_NON_IDE_GLOBAL_ACTIONS.contains(command)) {
            // #46886: these are always enabled if they are not specifically bound.
            return true;
        }
        if (foundAction) {
            // Was at least one context-aware variant but did not match.
            return false;
        } else {
            throw new IllegalArgumentException("Unrecognized command: " + command); // NOI18N
        }
    }
    
    public void invokeAction(String command, Lookup context) throws IllegalArgumentException {
        if (COMMAND_DELETE.equals(command)) {
            DefaultProjectOperations.performDefaultDeleteOperation(project);
            return ;
        }
        if (COMMAND_COPY.equals(command)) {
            DefaultProjectOperations.performDefaultCopyOperation(project);
            return ;
        }
        if (COMMAND_RENAME.equals(command)) {
            DefaultProjectOperations.performDefaultRenameOperation(project, null);
            return ;
        }
        if (COMMAND_MOVE.equals(command)) {
            DefaultProjectOperations.performDefaultMoveOperation(project);
            return ;
        }
        
        Element genldata = project.getPrimaryConfigurationData();
        Element actionsEl = XMLUtil.findElement(genldata, "ide-actions", FreeformProjectType.NS_GENERAL); // NOI18N
        if (actionsEl == null) {
            throw new IllegalArgumentException("No commands supported"); // NOI18N
        }
        boolean foundAction = false;
        for (Element actionEl : XMLUtil.findSubElements(actionsEl)) {
            if (actionEl.getAttribute("name").equals(command)) { // NOI18N
                foundAction = true;
                runConfiguredAction(command, project, actionEl, context);
            }
        }
        if (!foundAction) {
            if (COMMON_NON_IDE_GLOBAL_ACTIONS.contains(command)) {
                // #46886: try to bind it.
                if (addGlobalBinding(command)) {
                    // If bound, run it immediately.
                    invokeAction(command, context);
                }
            } else {
                throw new IllegalArgumentException("Unrecognized command: " + command); // NOI18N
            }
        }
    }
    
    /**
     * Find a file selection in a lookup context based on a project.xml <context> declaration.
     * If all DataObject's (or FileObject's) in the lookup match the folder named in the declaration,
     * and match any optional pattern declaration, then they are returned as a map from relative
     * path to actual file object. Otherwise an empty map is returned.
     * @param methodName if not null, look for {@link SingleMethod} rather than {@link DataObject}, and set the method name here
     */
    private static Map<String,FileObject> findSelection(Element contextEl, Lookup context, FreeformProject project, AtomicReference<String> methodName) {
        Collection<? extends FileObject> files;
        if (methodName == null) {
            Collection<? extends DataObject> filesDO = context.lookupAll(DataObject.class);
            if (filesDO.isEmpty()) {
                 return Collections.emptyMap();
            }
            Collection<FileObject> _files = new ArrayList<FileObject>(filesDO.size());
            for (DataObject d : filesDO) {
                _files.add(d.getPrimaryFile());
            }
            files = _files;
        } else {
            SingleMethod meth = context.lookup(SingleMethod.class);
            if (meth == null) {
                return Collections.emptyMap();
            }
            methodName.set(meth.getMethodName());
            files = Collections.singleton(meth.getFile());
        }
        Element folderEl = XMLUtil.findElement(contextEl, "folder", FreeformProjectType.NS_GENERAL); // NOI18N
        assert folderEl != null : "Must have <folder> in <context>";
        String rawtext = XMLUtil.findText(folderEl);
        assert rawtext != null : "Must have text contents in <folder>";
        String evaltext = project.evaluator().evaluate(rawtext);
        if (evaltext == null) {
            return Collections.emptyMap();
        }
        FileObject folder = project.helper().resolveFileObject(evaltext);
        if (folder == null) {
            return Collections.emptyMap();
        }
        Pattern pattern = null;
        Element patternEl = XMLUtil.findElement(contextEl, "pattern", FreeformProjectType.NS_GENERAL); // NOI18N
        if (patternEl != null) {
            String text = XMLUtil.findText(patternEl);
            assert text != null : "Must have text contents in <pattern>";
            try {
                pattern = Pattern.compile(text);
            } catch (PatternSyntaxException e) {
                org.netbeans.modules.ant.freeform.Util.err.annotate(e, ErrorManager.UNKNOWN, "From <pattern> in " + FileUtil.getFileDisplayName(project.getProjectDirectory().getFileObject(AntProjectHelper.PROJECT_XML_PATH)), null, null, null); // NOI18N
                org.netbeans.modules.ant.freeform.Util.err.notify(e);
                return Collections.emptyMap();
            }
        }
        Map<String,FileObject> result = new HashMap<String,FileObject>();
        for (FileObject file : files) {
            String path = FileUtil.getRelativePath(folder, file);
            if (path == null) {
                return Collections.emptyMap();
            }
            if (pattern != null && !pattern.matcher(path).find()) {
                return Collections.emptyMap();
            }
            result.put(path, file);
        }
        return result;
    }
    
    /**
     * Run a project action as described by subelements <script> and <target>.
     */
    private static void runConfiguredAction(
            final String command,
            final FreeformProject project,
            final Element actionEl,
            final Lookup context) {

        final List<String> targetNames = new ArrayList<String>();
        final Properties props = new Properties();
        final Union2<FileObject,String> scriptFile = ProjectManager.mutex().readAccess(new Mutex.Action<Union2<FileObject,String>>() {
            @Override
            public Union2<FileObject,String> run() {
                Union2<FileObject,String> result;
                String script;
                Element scriptEl = XMLUtil.findElement(actionEl, "script", FreeformProjectType.NS_GENERAL); // NOI18N
                if (scriptEl != null) {
                    script = XMLUtil.findText(scriptEl);
                } else {
                    script = "build.xml"; // NOI18N
                }
                String scriptLocation = project.evaluator().evaluate(script);
                final FileObject sf = scriptLocation == null ? null : project.helper().resolveFileObject(scriptLocation);
                if (sf != null) {
                    result = Union2.<FileObject,String>createFirst(sf);
                } else {
                    return Union2.<FileObject,String>createSecond(scriptLocation);
                }
                List<Element> targets = XMLUtil.findSubElements(actionEl);
                for (Element targetEl : targets) {
                    if (!targetEl.getLocalName().equals("target")) { // NOI18N
                        continue;
                    }
                    targetNames.add(XMLUtil.findText(targetEl));
                }
                Element contextEl = XMLUtil.findElement(actionEl, "context", FreeformProjectType.NS_GENERAL); // NOI18N
                if (contextEl != null) {
                    AtomicReference<String> methodName = SingleMethod.COMMAND_RUN_SINGLE_METHOD.equals(command) || SingleMethod.COMMAND_DEBUG_SINGLE_METHOD.equals(command) ?
                        new AtomicReference<String>() : null;
                    Map<String,FileObject> selection = findSelection(contextEl, context, project, methodName);
                    if (selection.isEmpty()) {
                        return null;
                    }
                    if (methodName != null && methodName.get() != null) {
                        props.setProperty("method", methodName.get());
                    }
                    String separator = null;
                    if (selection.size() > 1) {
                        // Find the right separator.
                        Element arityEl = XMLUtil.findElement(contextEl, "arity", FreeformProjectType.NS_GENERAL); // NOI18N
                        assert arityEl != null : "No <arity> in <context> for " + actionEl.getAttribute("name");
                        Element sepFilesEl = XMLUtil.findElement(arityEl, "separated-files", FreeformProjectType.NS_GENERAL); // NOI18N
                        if (sepFilesEl == null) {
                            // Only handles single files -> skip it.
                            return null;
                        }
                        separator = XMLUtil.findText(sepFilesEl);
                        if(separator == null) {
                            // is set-up to handle multiple files but no separator is found -> skip it.
                            String message = "No separator found for " + command + " command. <separated-files>,</separated-files> could be used.";
                            LOG.log(Level.WARNING, message);
                            StatusDisplayer.getDefault().setStatusText(message);
                            return null;
                        }
                    }
                    Element formatEl = XMLUtil.findElement(contextEl, "format", FreeformProjectType.NS_GENERAL); // NOI18N
                    assert formatEl != null : "No <format> in <context> for " + actionEl.getAttribute("name");
                    String format = XMLUtil.findText(formatEl);
                    StringBuilder buf = new StringBuilder();
                    Iterator<Map.Entry<String,FileObject>> it = selection.entrySet().iterator();
                    while (it.hasNext()) {
                        Map.Entry<String,FileObject> entry = it.next();
                        if (format.equals("absolute-path")) { // NOI18N
                            File f = FileUtil.toFile(entry.getValue());
                            if (f == null) {
                                // Not a disk file??
                                return null;
                            }
                            buf.append(f.getAbsolutePath());
                        } else if (format.equals("relative-path")) { // NOI18N
                            buf.append(entry.getKey());
                        } else if (format.equals("absolute-path-noext")) { // NOI18N
                            File f = FileUtil.toFile(entry.getValue());
                            if (f == null) {
                                // Not a disk file??
                                return null;
                            }
                            String path = f.getAbsolutePath();
                            int dot = path.lastIndexOf('.');
                            if (dot > path.lastIndexOf('/')) {
                                path = path.substring(0, dot);
                            }
                            buf.append(path);
                        } else if (format.equals("relative-path-noext")) { // NOI18N
                            String path = entry.getKey();
                            int dot = path.lastIndexOf('.');
                            if (dot > path.lastIndexOf('/')) {
                                path = path.substring(0, dot);
                            }
                            buf.append(path);
                        } else {
                            assert format.equals("java-name") : format;
                            String path = entry.getKey();
                            int dot = path.lastIndexOf('.');
                            String dotless;
                            if (dot == -1 || dot < path.lastIndexOf('/')) {
                                dotless = path;
                            } else {
                                dotless = path.substring(0, dot);
                            }
                            String javaname = dotless.replace('/', '.');
                            buf.append(javaname);
                        }
                        if (it.hasNext()) {
                            assert separator != null;
                            buf.append(separator);
                        }
                    }
                    Element propEl = XMLUtil.findElement(contextEl, "property", FreeformProjectType.NS_GENERAL); // NOI18N
                    assert propEl != null : "No <property> in <context> for " + actionEl.getAttribute("name");
                    String prop = XMLUtil.findText(propEl);
                    assert prop != null : "Must have text contents in <property>";
                    props.setProperty(prop, buf.toString());
                }
                for (Element propEl : targets) {
                    if (!propEl.getLocalName().equals("property")) { // NOI18N
                        continue;
                    }
                    String rawtext = XMLUtil.findText(propEl);
                    if (rawtext == null) {
                        // Legal to have e.g. <property name="intentionally-left-blank"/>
                        rawtext = ""; // NOI18N
                    }
                    String evaltext = project.evaluator().evaluate(rawtext); // might be null
                    if (evaltext != null) {
                        props.setProperty(propEl.getAttribute("name"), evaltext); // NOI18N
                    }
                }
                return result;
            }
        });
        if (scriptFile == null) {
            return;
        } else if (scriptFile.hasFirst()) {
            final String[] targetNameArray;
            if (!targetNames.isEmpty()) {
                targetNameArray = targetNames.toArray(new String[targetNames.size()]);
            } else {
                // Run default target.
                targetNameArray = null;
            }
            TARGET_RUNNER.runTarget(scriptFile.first(), targetNameArray, props, ActionProgress.start(context));
        } else {
            assert scriptFile.hasSecond();
            //#57011: if the script does not exist, show a warning:
            final NotifyDescriptor nd = new NotifyDescriptor.Message(
                MessageFormat.format(
                    NbBundle.getMessage(Actions.class, "LBL_ScriptFileNotFoundError"),
                    new Object[] {scriptFile.second()}),
                    NotifyDescriptor.ERROR_MESSAGE);
            DialogDisplayer.getDefault().notify(nd);
        }
    }

    @ActionID(id = "org.netbeans.modules.ant.freeform.Actions$Custom", category = "Project")
    @ActionRegistration(displayName = "Custom Freeform Actions", lazy=false) // should not be displayed in UI anyway
    @ActionReference(position = 300, path = "Projects/org-netbeans-modules-ant-freeform/Actions")
    public static final class Custom extends AbstractAction implements ContextAwareAction {
        public Custom() {
            setEnabled(false);
            putValue(DynamicMenuContent.HIDE_WHEN_DISABLED, true);
        }
        public @Override void actionPerformed(ActionEvent e) {
            assert false;
        }
        public @Override Action createContextAwareInstance(Lookup actionContext) {
            Collection<? extends FreeformProject> projects = actionContext.lookupAll(FreeformProject.class);
            if (projects.size() != 1) {
                return this;
            }
            final FreeformProject p = projects.iterator().next();
            class A extends AbstractAction implements Presenter.Popup {
                public @Override void actionPerformed(ActionEvent e) {
                    assert false;
                }
                public @Override JMenuItem getPopupPresenter() {
                    class M extends JMenuItem implements DynamicMenuContent {
                        public @Override JComponent[] getMenuPresenters() {
                            Action[] actions = contextMenuCustomActions(p);
                            JComponent[] comps = new JComponent[actions.length];
                            for (int i = 0; i < actions.length; i++) {
                                if (actions[i] != null) {
                                    JMenuItem item = new JMenuItem();
                                    org.openide.awt.Actions.connect(item, actions[i], true);
                                    comps[i] = item;
                                } else {
                                    comps[i] = new JSeparator();
                                }
                            }
                            return comps;
                        }
                        public @Override JComponent[] synchMenuPresenters(JComponent[] items) {
                            return getMenuPresenters();
                        }
                    }
                    return new M();
                }
            }
            return new A();
        }
    }
    
    /**
     * Build the context menu for a project.
     * @param p a freeform project
     * @return a list of actions (or null for separators)
     */
    static Action[] contextMenuCustomActions(FreeformProject p) {
        List<Action> actions = new ArrayList<Action>();
        Element genldata = p.getPrimaryConfigurationData();
        Element viewEl = XMLUtil.findElement(genldata, "view", FreeformProjectType.NS_GENERAL); // NOI18N
        if (viewEl != null) {
            Element contextMenuEl = XMLUtil.findElement(viewEl, "context-menu", FreeformProjectType.NS_GENERAL); // NOI18N
            if (contextMenuEl != null) {
                actions.add(null);
                for (Element actionEl : XMLUtil.findSubElements(contextMenuEl)) {
                    if (actionEl.getLocalName().equals("ide-action")) { // NOI18N
                        String cmd = actionEl.getAttribute("name");
                        String displayName;
                        if (COMMON_IDE_GLOBAL_ACTIONS.contains(cmd) || COMMON_NON_IDE_GLOBAL_ACTIONS.contains(cmd)) {
                            displayName = NbBundle.getMessage(Actions.class, "CMD_" + cmd);
                        } else {
                            // OK, fall back to raw name.
                            displayName = cmd;
                        }
                        actions.add(ProjectSensitiveActions.projectCommandAction(cmd, displayName, null));
                    } else if (actionEl.getLocalName().equals("separator")) { // NOI18N
                        actions.add(null);
                    } else {
                        assert actionEl.getLocalName().equals("action") : actionEl;
                        actions.add(new CustomAction(p, actionEl));
                    }
                }
            }
        }
        return actions.toArray(new Action[actions.size()]);
    }
    
    private static final class CustomAction extends AbstractAction {

        private final FreeformProject p;
        private final Element actionEl;
        
        public CustomAction(FreeformProject p, Element actionEl) {
            this.p = p;
            this.actionEl = actionEl;
        }
        
        public void actionPerformed(ActionEvent e) {
            runConfiguredAction(null, p, actionEl, Lookup.EMPTY);
        }
        
        public boolean isEnabled() {
            String script;
            Element scriptEl = XMLUtil.findElement(actionEl, "script", FreeformProjectType.NS_GENERAL); // NOI18N
            if (scriptEl != null) {
                script = XMLUtil.findText(scriptEl);
            } else {
                script = "build.xml"; // NOI18N
            }
            String scriptLocation = p.evaluator().evaluate(script);
            return p.helper().resolveFileObject(scriptLocation) != null;
        }
        
        public Object getValue(String key) {
            if (key.equals(Action.NAME)) {
                Element labelEl = XMLUtil.findElement(actionEl, "label", FreeformProjectType.NS_GENERAL); // NOI18N
                return XMLUtil.findText(labelEl);
            } else {
                return super.getValue(key);
            }
        }
        
    }
    
    // Overridable for unit tests only:
    static TargetRunner TARGET_RUNNER = new TargetRunner();
    
    static class TargetRunner {
        public TargetRunner() {}
        public void runTarget(FileObject scriptFile, String[] targetNameArray, Properties props, final ActionProgress listener) {
            try {
                ActionUtils.runTarget(scriptFile, targetNameArray, props).addTaskListener(new TaskListener() {
                    @Override public void taskFinished(Task task) {
                        listener.finished(((ExecutorTask) task).result() == 0);
                    }
                });
            } catch (IOException e) {
                ErrorManager.getDefault().notify(e);
                listener.finished(false);
            }
        }
    }
    
    /**
     * Prompt the user to make a binding for a common global command.
     * Available targets are shown. If one is selected, it is bound
     * (and also added to the context menu of the project), as if the user
     * had picked it in {@link TargetMappingPanel}.
     * @param command the command name as in {@link ActionProvider}
     * @return true if a binding was successfully created, false if it was cancelled
     * @see "#46886"
     */
    private boolean addGlobalBinding(String command) {
        try {
            return new UnboundTargetAlert(project, command).accepted();
        } catch (IOException e) {
            // Problem generating bindings - so skip it.
            ErrorManager.getDefault().notify(e);
            return false;
        }
    }
    
}
