# HG changeset patch # Parent f5132a3ee0abffae7a21f970eb333f6fe85c51aa # User Jesse Glick #64991: permit project command actions to be enabled on multiselections. diff --git a/java.api.common/src/org/netbeans/modules/java/api/common/project/BaseActionProvider.java b/java.api.common/src/org/netbeans/modules/java/api/common/project/BaseActionProvider.java --- a/java.api.common/src/org/netbeans/modules/java/api/common/project/BaseActionProvider.java +++ b/java.api.common/src/org/netbeans/modules/java/api/common/project/BaseActionProvider.java @@ -45,6 +45,7 @@ package org.netbeans.modules.java.api.common.project; import java.awt.Dialog; +import java.awt.EventQueue; import java.awt.event.MouseEvent; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; @@ -384,6 +385,7 @@ @Override public void invokeAction( final String command, final Lookup context ) throws IllegalArgumentException { + assert EventQueue.isDispatchThread(); if (COMMAND_DELETE.equals(command)) { DefaultProjectOperations.performDefaultDeleteOperation(project); return ; diff --git a/projectui/nbproject/project.xml b/projectui/nbproject/project.xml --- a/projectui/nbproject/project.xml +++ b/projectui/nbproject/project.xml @@ -108,7 +108,7 @@ 1 - 1.33 + 1.43 diff --git a/projectui/src/org/netbeans/modules/project/ui/actions/ActionsUtil.java b/projectui/src/org/netbeans/modules/project/ui/actions/ActionsUtil.java --- a/projectui/src/org/netbeans/modules/project/ui/actions/ActionsUtil.java +++ b/projectui/src/org/netbeans/modules/project/ui/actions/ActionsUtil.java @@ -48,6 +48,7 @@ import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -90,7 +91,7 @@ */ // #74161: do not cache // First find out whether there is a project directly in the Lookup - Set result = new HashSet(); + Set result = new LinkedHashSet(); // XXX or use OpenProjectList.projectByDisplayName? for (Project p : lookup.lookupAll(Project.class)) { result.add(p); } diff --git a/projectui/src/org/netbeans/modules/project/ui/actions/FileAction.java b/projectui/src/org/netbeans/modules/project/ui/actions/FileAction.java --- a/projectui/src/org/netbeans/modules/project/ui/actions/FileAction.java +++ b/projectui/src/org/netbeans/modules/project/ui/actions/FileAction.java @@ -113,6 +113,7 @@ r[0] = new Runnable() { @Override public void run() { Project[] projects = ActionsUtil.getProjectsFromLookup( context, command ); + // XXX #64991: handle >1 project (tricky since must pass subset of selection to each) if ( projects.length != 1 ) { if (projects.length == 0 && globalProvider(context) != null) { enable[0] = true; diff --git a/projectui/src/org/netbeans/modules/project/ui/actions/MainProjectAction.java b/projectui/src/org/netbeans/modules/project/ui/actions/MainProjectAction.java --- a/projectui/src/org/netbeans/modules/project/ui/actions/MainProjectAction.java +++ b/projectui/src/org/netbeans/modules/project/ui/actions/MainProjectAction.java @@ -44,18 +44,16 @@ package org.netbeans.modules.project.ui.actions; -import java.awt.Toolkit; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.text.MessageFormat; import java.util.Arrays; +import java.util.LinkedList; import javax.swing.Icon; import org.netbeans.api.project.Project; -import org.netbeans.api.project.ProjectUtils; import org.netbeans.api.project.ui.OpenProjects; import org.netbeans.modules.project.ui.OpenProjectList; import static org.netbeans.modules.project.ui.actions.Bundle.*; -import org.netbeans.spi.project.ActionProvider; import org.netbeans.spi.project.ui.support.ProjectActionPerformer; import org.openide.DialogDisplayer; import org.openide.NotifyDescriptor; @@ -66,9 +64,11 @@ import org.openide.util.NbBundle.Messages; import org.openide.util.WeakListeners; -/** Invokes command on the main project. - * - * @author Pet Hrebejk +/** + * Similar to {@link ProjectAction} but has a different selection model. + * First uses the main project, if set. + * Else uses the selected projects, if any. + * Finally, if just one project is open, uses that. */ public class MainProjectAction extends LookupSensitiveAction implements PropertyChangeListener { @@ -85,7 +85,7 @@ } @SuppressWarnings("LeakingThisInConstructor") - public MainProjectAction(String command, ProjectActionPerformer performer, String name, Icon icon) { + private MainProjectAction(String command, ProjectActionPerformer performer, String name, Icon icon) { super(icon, null, new Class[] {Project.class, DataObject.class}); this.command = command; @@ -117,54 +117,23 @@ @Messages("MainProjectAction.no_main=Set a main project, or select one project or project file, or keep just one project open.") public @Override void actionPerformed(Lookup context) { - // first try to find main project - Project p = OpenProjectList.getDefault().getMainProject(); - - // then try to find some selected project - if (p == null) { - Project[] projects = ActionsUtil.getProjectsFromLookup(context, command); - if (projects.length == 1) { - p = projects[0]; - } - } - - // then if there is only one project opened in IDE - use it - if (p == null) { - Project[] projects = OpenProjects.getDefault().getOpenProjects(); - if (projects.length == 1) { - p = projects[0]; - } - } + Project mainProject = OpenProjectList.getDefault().getMainProject(); + Project[] projects = selection(mainProject, context); // if no main project or no selected or more than one project opened, // then show warning - if (p == null) { + if (projects.length == 0) { DialogDisplayer.getDefault().notify(new NotifyDescriptor.Message(MainProjectAction_no_main(), NotifyDescriptor.WARNING_MESSAGE)); return; } - if ( command != null ) { - ActionProvider ap = p.getLookup().lookup(ActionProvider.class); - if (ap != null) { - if (Arrays.asList(ap.getSupportedActions()).contains(command)) { - ap.invokeAction(command, Lookup.EMPTY); - } else { - // #47160: was a supported command (e.g. on a freeform project) but was then removed. - Toolkit.getDefaultToolkit().beep(); - refreshView(null, false); - } - } - } - else { - performer.perform( p ); + if (command != null && projects.length > 0) { + ProjectAction.runSequentially(new LinkedList(Arrays.asList(projects)), this, command); + } else if (performer != null && projects.length == 1) { + performer.perform(projects[0]); } } - - // Private methods --------------------------------------------------------- - - // Implementation of PropertyChangeListener -------------------------------- - public @Override void propertyChange( PropertyChangeEvent evt ) { if (OpenProjectList.PROPERTY_MAIN_PROJECT.equals(evt.getPropertyName()) || OpenProjectList.PROPERTY_OPEN_PROJECTS.equals(evt.getPropertyName())) { @@ -172,50 +141,51 @@ } } + private Project[] selection(Project mainProject, Lookup context) { + if (mainProject != null) { + return new Project[] {mainProject}; + } + Lookup theContext = context; + if (theContext == null) { + theContext = LastActivatedWindowLookup.INSTANCE; + } + if (theContext != null) { + Project[] projects = ActionsUtil.getProjectsFromLookup(theContext, command); + if (projects.length > 0) { + return projects; + } + } + Project[] projects = OpenProjects.getDefault().getOpenProjects(); + if (projects.length == 1) { + return projects; + } + return new Project[0]; + } + private void refreshView(final Lookup context, boolean immediate) { Runnable r= new Runnable() { public @Override void run() { - Project p = OpenProjectList.getDefault().getMainProject(); - Lookup theContext = context; + Project mainProject = OpenProjectList.getDefault().getMainProject(); + Project[] projects = selection(mainProject, context); - if (p == null) { - if (theContext == null) { - theContext = LastActivatedWindowLookup.INSTANCE; - } - if (theContext != null) { - Project[] projects = ActionsUtil.getProjectsFromLookup(theContext, command); - if (projects.length == 1) { - p = projects[0]; - } - } - } - - if (p == null) { - Project[] projects = OpenProjects.getDefault().getOpenProjects(); - if (projects.length == 1) { - p = projects[0]; - } - } - - Project mainProject = OpenProjectList.getDefault().getMainProject(); - - final String presenterName = getPresenterName(name, mainProject, p); + final String presenterName = getPresenterName(name, mainProject, projects); final boolean enabled; if ( command == null ) { - enabled = performer.enable(p); + enabled = projects.length == 1 && performer.enable(projects[0]); } - else { - if ( p == null ) { - enabled = false; + else if (projects.length == 0) { + enabled = false; + } else { + boolean e = true; + for (Project p : projects) { + if (!ActionsUtil.commandSupported(p, command, Lookup.EMPTY)) { + e = false; + break; + } } - else if ( ActionsUtil.commandSupported ( p, command, Lookup.EMPTY ) ) { - enabled = true; - } - else { - enabled = false; - } + enabled = e; } Mutex.EVENT.writeAccess(new Runnable() { @@ -234,22 +204,14 @@ } } - private String getPresenterName(String name, Project mPrj, Project cPrj) { - String toReturn = ""; - Object[] formatterArgs; - if (mPrj == null) { - if (cPrj == null) { - formatterArgs = new Object[] { 0 }; - } else { - formatterArgs = new Object[] { 1, ProjectUtils.getInformation(cPrj).getDisplayName() }; - } + private String getPresenterName(String name, Project mPrj, Project[] cPrj) { + if (name == null) { + return ""; + } else if (mPrj == null) { + return ActionsUtil.formatProjectSensitiveName(name, cPrj); } else { - formatterArgs = new Object[] { -1 }; + return MessageFormat.format(name, -1); } - if (name != null) { - toReturn = MessageFormat.format(name, formatterArgs); - } - return toReturn; } @Override diff --git a/projectui/src/org/netbeans/modules/project/ui/actions/ProjectAction.java b/projectui/src/org/netbeans/modules/project/ui/actions/ProjectAction.java --- a/projectui/src/org/netbeans/modules/project/ui/actions/ProjectAction.java +++ b/projectui/src/org/netbeans/modules/project/ui/actions/ProjectAction.java @@ -44,11 +44,17 @@ package org.netbeans.modules.project.ui.actions; +import java.awt.Toolkit; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.Queue; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.Level; import java.util.logging.LogRecord; import javax.swing.Action; import javax.swing.Icon; import org.netbeans.api.project.Project; +import org.netbeans.spi.project.ActionProgress; import org.netbeans.spi.project.ActionProvider; import org.netbeans.spi.project.ui.support.ProjectActionPerformer; import org.openide.awt.Actions; @@ -58,6 +64,7 @@ import org.openide.util.Lookup; import org.openide.util.Mutex; import org.openide.util.NbBundle; +import org.openide.util.lookup.Lookups; /** Action sensitive to current project * @@ -118,26 +125,56 @@ @Override protected void actionPerformed( Lookup context ) { Project[] projects = ActionsUtil.getProjectsFromLookup( context, command ); - - if ( projects.length == 1 ) { - if ( command != null ) { - ActionProvider ap = projects[0].getLookup().lookup(ActionProvider.class); - LogRecord r = new LogRecord(Level.FINE, "PROJECT_ACTION"); // NOI18N - r.setResourceBundle(NbBundle.getBundle(ProjectAction.class)); - r.setParameters(new Object[] { - getClass().getName(), - projects[0].getClass().getName(), - getValue(NAME) - }); - r.setLoggerName(UILOG.getName()); - UILOG.log(r); - ap.invokeAction( command, Lookup.EMPTY ); + if (command != null && projects.length > 0) { + runSequentially(new LinkedList(Arrays.asList(projects)), this, command); + } else if (performer != null && projects.length == 1) { + performer.perform(projects[0]); + } + } + static void runSequentially(final Queue queue, final LookupSensitiveAction a, final String command) { + Project p = queue.remove(); + final ActionProvider ap = p.getLookup().lookup(ActionProvider.class); + if (ap == null) { + return; + } + if (!Arrays.asList(ap.getSupportedActions()).contains(command)) { + // #47160: was a supported command (e.g. on a freeform project) but was then removed. + Toolkit.getDefaultToolkit().beep(); + a.refresh(a.getLookup(), false); + return; + } + LogRecord r = new LogRecord(Level.FINE, "PROJECT_ACTION"); // NOI18N + r.setResourceBundle(NbBundle.getBundle(ProjectAction.class)); + r.setParameters(new Object[] { + a.getClass().getName(), + p.getClass().getName(), + a.getValue(NAME) + }); + r.setLoggerName(UILOG.getName()); + UILOG.log(r); + Mutex.EVENT.writeAccess(new Runnable() { + @Override public void run() { + if (queue.isEmpty()) { + ap.invokeAction(command, Lookup.EMPTY); + } else { + final AtomicBoolean started = new AtomicBoolean(); + ap.invokeAction(command, Lookups.singleton(new ActionProgress() { + @Override protected void started() { + started.set(true); + } + @Override public void finished(boolean success) { + if (success) { // OK, next... + runSequentially(queue, a, command); + } // else build failed, so skip others + } + })); + if (!started.get()) { + // Did not run action for some reason; try others? + runSequentially(queue, a, command); + } + } } - else if ( performer != null ) { - performer.perform( projects[0] ); - } - } - + }); } @Override @@ -147,7 +184,7 @@ Project[] projects = ActionsUtil.getProjectsFromLookup( context, command ); final boolean enable; if ( command != null ) { - enable = projects.length == 1; + enable = projects.length > 0; } else if ( performer != null && projects.length == 1 ) { enable = performer.enable(projects[0]); } else { diff --git a/projectui/test/unit/src/org/netbeans/modules/project/ui/actions/MainProjectActionTest.java b/projectui/test/unit/src/org/netbeans/modules/project/ui/actions/MainProjectActionTest.java new file mode 100644 --- /dev/null +++ b/projectui/test/unit/src/org/netbeans/modules/project/ui/actions/MainProjectActionTest.java @@ -0,0 +1,120 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2012 Oracle and/or its affiliates. All rights reserved. + * + * Oracle and Java are registered trademarks of Oracle and/or its affiliates. + * Other names may be trademarks of their respective owners. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common + * Development and Distribution License("CDDL") (collectively, the + * "License"). You may not use this file except in compliance with the + * License. You can obtain a copy of the License at + * http://www.netbeans.org/cddl-gplv2.html + * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the + * specific language governing permissions and limitations under the + * License. When distributing the software, include this License Header + * Notice in each file and include the License file at + * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the GPL Version 2 section of the License file that + * accompanied this code. If applicable, add the following below the + * License Header, with the fields enclosed by brackets [] replaced by + * your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * + * If you wish your version of this file to be governed by only the CDDL + * or only the GPL Version 2, indicate your decision by adding + * "[Contributor] elects to include this software in this distribution + * under the [CDDL or GPL Version 2] license." If you do not indicate a + * single choice of license, a recipient has the option to distribute + * your version of this file under either the CDDL, the GPL Version 2 or + * to extend the choice of license to its licensees as provided above. + * However, if you add GPL Version 2 code and therefore, elected the GPL + * Version 2 license, then the option applies only if the new code is + * made subject to such option by the copyright holder. + * + * Contributor(s): + * + * Portions Copyrighted 2012 Sun Microsystems, Inc. + */ + +package org.netbeans.modules.project.ui.actions; + +import java.util.ArrayList; +import java.util.List; +import org.netbeans.api.annotations.common.SuppressWarnings; +import org.netbeans.api.project.ProjectManager; +import org.netbeans.junit.NbTestCase; +import org.netbeans.spi.project.ActionProgress; +import org.netbeans.spi.project.ActionProvider; +import org.openide.filesystems.FileObject; +import org.openide.filesystems.FileUtil; +import org.openide.util.Lookup; +import org.openide.util.lookup.Lookups; +import org.openide.util.test.MockLookup; + +public class MainProjectActionTest extends NbTestCase { + + public MainProjectActionTest(String name) { + super(name); + } + + @Override protected boolean runInEQ() { + return true; + } + + private FileObject p1, p2; + private TestSupport.TestProject prj1, prj2; + + @Override protected void setUp() throws Exception { + MockLookup.setInstances(new TestSupport.TestProjectFactory()); + FileObject r = FileUtil.createMemoryFileSystem().getRoot(); + p1 = TestSupport.createTestProject(r, "p1"); + prj1 = (TestSupport.TestProject) ProjectManager.getDefault().findProject(p1); + p2 = TestSupport.createTestProject(r, "p2"); + prj2 = (TestSupport.TestProject) ProjectManager.getDefault().findProject(p2); + } + + @SuppressWarnings({"UWF_FIELD_NOT_INITIALIZED_IN_CONSTRUCTOR", "SIC_INNER_SHOULD_BE_STATIC_ANON"}) + public void testSeqRun() throws Exception { + final String CMD = "cmd"; + final List invocations = new ArrayList(); + class BlockingRun implements ActionProvider { + final int which; + final boolean success; + BlockingRun(int which, boolean success) { + this.which = which; + this.success = success; + } + @Override public String[] getSupportedActions() { + return new String[] {CMD}; + } + @Override public boolean isActionEnabled(String command, Lookup context) { + return true; + } + @Override public void invokeAction(String command, Lookup context) { + ActionProgress listener = ActionProgress.start(context); + invocations.add(which); + listener.finished(success); + } + } + BlockingRun ap1 = new BlockingRun(1, true); + prj1.setLookup(Lookups.singleton(ap1)); + BlockingRun ap2 = new BlockingRun(2, true); + prj2.setLookup(Lookups.singleton(ap2)); + LookupSensitiveAction a = new MainProjectAction(CMD, "a", null); + a.actionPerformed(Lookups.fixed(prj1, prj2)); + assertEquals("[1, 2]", invocations.toString()); + ap2 = new BlockingRun(2, false); + prj2.setLookup(Lookups.singleton(ap2)); + a.actionPerformed(Lookups.fixed(prj1, prj2)); + assertEquals("[1, 2, 1, 2]", invocations.toString()); + ap1 = new BlockingRun(1, false); + prj1.setLookup(Lookups.singleton(ap1)); + a.actionPerformed(Lookups.fixed(prj1, prj2)); + assertEquals("[1, 2, 1, 2, 1]", invocations.toString()); + } + +} diff --git a/projectui/test/unit/src/org/netbeans/modules/project/ui/actions/ProjectActionTest.java b/projectui/test/unit/src/org/netbeans/modules/project/ui/actions/ProjectActionTest.java --- a/projectui/test/unit/src/org/netbeans/modules/project/ui/actions/ProjectActionTest.java +++ b/projectui/test/unit/src/org/netbeans/modules/project/ui/actions/ProjectActionTest.java @@ -47,11 +47,11 @@ import java.util.ArrayList; import java.util.List; import javax.swing.Action; -import javax.swing.KeyStroke; import org.netbeans.api.project.Project; import org.netbeans.api.project.ProjectManager; import org.netbeans.junit.MockServices; import org.netbeans.junit.NbTestCase; +import org.netbeans.spi.project.ActionProgress; import org.netbeans.spi.project.ActionProvider; import org.netbeans.spi.project.ui.support.ProjectActionPerformer; import org.openide.filesystems.FileObject; @@ -77,6 +77,7 @@ private DataObject d2_1; private DataObject d2_2; private TestSupport.TestProject project1; + private TestActionProvider tap1; private TestSupport.TestProject project2; @Override protected boolean runInEQ() { @@ -98,7 +99,8 @@ d1_2 = DataObject.find(f1_2); project1 = (TestSupport.TestProject)ProjectManager.getDefault().findProject( p1 ); - project1.setLookup( Lookups.fixed( new Object[] { new TestActionProvider() } ) ); + tap1 = new TestActionProvider(); + project1.setLookup(Lookups.singleton(tap1)); p2 = TestSupport.createTestProject( workDir, "project2" ); f2_1 = p2.createData("f2_1.java"); @@ -125,6 +127,24 @@ assertEnablement(action, false); lookup.change(d1_1, d2_1); assertEnablement(action, false); + TestActionProvider tap2 = new TestActionProvider(); + project2.setLookup(Lookups.singleton(tap2)); + lookup.change(d2_1); + assertEnablement(action, true); + lookup.change(d1_1, d2_1); + assertEnablement(action, true); + action.actionPerformed(null); + assertEquals("[COMMAND]", tap1.invocations.toString()); + assertEquals("[COMMAND]", tap2.invocations.toString()); + tap1.listenerSuccess = true; + tap2.listenerSuccess = true; + action.actionPerformed(null); + assertEquals("[COMMAND, COMMAND]", tap1.invocations.toString()); + assertEquals("[COMMAND, COMMAND]", tap2.invocations.toString()); + tap1.listenerSuccess = false; + action.actionPerformed(null); + assertEquals("[COMMAND, COMMAND, COMMAND]", tap1.invocations.toString()); + assertEquals("[COMMAND, COMMAND]", tap2.invocations.toString()); } public void testProviderEnablement() throws Exception { @@ -177,6 +197,7 @@ private String[] ACTIONS = new String[] { COMMAND }; private List invocations = new ArrayList(); + Boolean listenerSuccess; public String[] getSupportedActions() { return ACTIONS; @@ -186,6 +207,9 @@ if ( COMMAND.equals( command ) ) { invocations.add( command ); + if (listenerSuccess != null) { + ActionProgress.start(context).finished(listenerSuccess); + } } else { throw new IllegalArgumentException();