diff --git a/ide/projectapi.nb/src/org/netbeans/modules/projectapi/nb/GenericPrj.java b/ide/projectapi.nb/src/org/netbeans/modules/projectapi/nb/GenericPrj.java new file mode 100644 index 000000000000..f88a814ad5fc --- /dev/null +++ b/ide/projectapi.nb/src/org/netbeans/modules/projectapi/nb/GenericPrj.java @@ -0,0 +1,45 @@ +/* + * 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.projectapi.nb; + +import org.netbeans.api.project.Project; +import org.openide.filesystems.FileObject; +import org.openide.util.Lookup; +import org.openide.util.lookup.Lookups; + +final class GenericPrj implements Project { + private final FileObject dir; + private final Lookup lkp; + + public GenericPrj(FileObject dir) { + this.dir = dir; + this.lkp = Lookups.fixed(this); + } + + @Override + public FileObject getProjectDirectory() { + return dir; + } + + @Override + public Lookup getLookup() { + return lkp; + } + +} diff --git a/ide/projectapi.nb/src/org/netbeans/modules/projectapi/nb/NbProjectManager.java b/ide/projectapi.nb/src/org/netbeans/modules/projectapi/nb/NbProjectManager.java index 372cd5f82ee5..f27e1dcdc6d1 100644 --- a/ide/projectapi.nb/src/org/netbeans/modules/projectapi/nb/NbProjectManager.java +++ b/ide/projectapi.nb/src/org/netbeans/modules/projectapi/nb/NbProjectManager.java @@ -65,7 +65,7 @@ * @author Jesse Glick */ @ServiceProvider(service = ProjectManagerImplementation.class, position = 1000) -public final class NbProjectManager implements ProjectManagerImplementation { +public final class NbProjectManager implements ProjectManagerImplementation.WithFallback { // XXX need to figure out how to convince the system that a Project object is modified // so that Save All and the exit dialog work... could temporarily use a DataLoader @@ -90,7 +90,7 @@ public void resultChanged(LookupEvent e) { } }); } - + private static enum LoadStatus { /** * Marker for a directory which is known to not be a project. @@ -212,6 +212,17 @@ public Mutex getMutex( */ @Override public Project findProject(final FileObject projectDirectory) throws IOException, IllegalArgumentException { + return findProjectImpl(projectDirectory, false); + } + + @Override + public Project findProjectOrFallback(FileObject projectDirectory) throws IOException, IllegalArgumentException { + var found = findProjectImpl(projectDirectory, true); + assert found != null; + return found; + } + + private Project findProjectImpl(FileObject projectDirectory, boolean fallback) throws IOException, IllegalArgumentException { Parameters.notNull("projectDirectory", projectDirectory); //NOI18N try { return getMutex().readAccess(new Mutex.ExceptionAction() { @@ -250,11 +261,17 @@ public Project run() throws IOException { assert !LoadStatus.LOADING_PROJECT.is(o); wasSomeSuchProject = LoadStatus.SOME_SUCH_PROJECT.is(o); if (LoadStatus.NO_SUCH_PROJECT.is(o)) { - if (LOG.isLoggable(Level.FINE)) { - LOG.log(Level.FINE, "findProject({0}) in {1}: NO_SUCH_PROJECT", new Object[] {projectDirectory, Thread.currentThread().getName()}); + if (fallback) { + // treat a not checked project yet + o = null; + } else { + if (LOG.isLoggable(Level.FINE)) { + LOG.log(Level.FINE, "findProject({0}) in {1}: NO_SUCH_PROJECT", new Object[]{projectDirectory, Thread.currentThread().getName()}); + } + return null; } - return null; - } else if (o != null && !LoadStatus.SOME_SUCH_PROJECT.is(o)) { + } + if (o != null && !LoadStatus.SOME_SUCH_PROJECT.is(o)) { Project p = o.first().get(); if (p != null) { if (LOG.isLoggable(Level.FINE)) { @@ -285,7 +302,7 @@ public Project run() throws IOException { } boolean resetLP = false; try { - Project p = createProject(projectDirectory); + Project p = createProject(projectDirectory, fallback); //Thread.dumpStack(); synchronized (dir2Proj) { dir2Proj.notifyAll(); @@ -366,13 +383,16 @@ public Project run() throws IOException { * @return a project made from it, or null if it is not recognized * @throws IOException if there was a problem loading the project */ - private Project createProject(FileObject dir) throws IOException { + private Project createProject(FileObject dir, boolean fallback) throws IOException { assert dir != null; assert dir.isFolder(); assert getMutex().isReadAccess(); ProjectStateImpl state = new ProjectStateImpl(); for (ProjectFactory factory : factories.allInstances()) { Project p = factory.loadProject(dir, state); + if (p == null && fallback) { + p = new GenericPrj(dir); + } if (p != null) { if (TIMERS.isLoggable(Level.FINE)) { LogRecord rec = new LogRecord(Level.FINE, "Project"); // NOI18N diff --git a/ide/projectapi/src/org/netbeans/api/project/ProjectManager.java b/ide/projectapi/src/org/netbeans/api/project/ProjectManager.java index 81da1b4c7178..9b8a77e2a34b 100644 --- a/ide/projectapi/src/org/netbeans/api/project/ProjectManager.java +++ b/ide/projectapi/src/org/netbeans/api/project/ProjectManager.java @@ -141,6 +141,22 @@ public Project findProject(@NonNull final FileObject projectDirectory) throws IO } return impl.findProject(projectDirectory); } + + /** @since 1.111 */ + @NonNull + public Project findProjectOrFallback(@NonNull FileObject projectDirectory) throws IOException, IllegalArgumentException { + if (projectDirectory == null) { + throw new IllegalArgumentException("Attempted to pass a null directory to findProject"); // NOI18N + } + if (!projectDirectory.isFolder()) { + throw new IllegalArgumentException("Attempted to pass a non-directory to findProject: " + projectDirectory); // NOI18N + } + if (impl instanceof ProjectManagerImplementation.WithFallback implV2) { + return implV2.findProjectOrFallback(projectDirectory); + } else { + throw new IllegalArgumentException("Cannot create fallback project for " + projectDirectory); // NOI18N + } + } /** diff --git a/ide/projectapi/src/org/netbeans/spi/project/ProjectManagerImplementation.java b/ide/projectapi/src/org/netbeans/spi/project/ProjectManagerImplementation.java index c95437bd9204..ab33b296f18b 100644 --- a/ide/projectapi/src/org/netbeans/spi/project/ProjectManagerImplementation.java +++ b/ide/projectapi/src/org/netbeans/spi/project/ProjectManagerImplementation.java @@ -182,6 +182,12 @@ Mutex getMutex( */ void saveAllProjects() throws IOException; + /** @since 1.111 */ + interface WithFallback extends ProjectManagerImplementation { + @NonNull + Project findProjectOrFallback(@NonNull FileObject projectDirectory) throws IOException, IllegalArgumentException; + } + /** * Callback to notify the {@link ProjectManager} about changes. */ diff --git a/ide/projectapi/test/unit/src/org/netbeans/api/project/ProjectManagerTest.java b/ide/projectapi/test/unit/src/org/netbeans/api/project/ProjectManagerTest.java index eec0751279b4..dbfe16cc51a8 100644 --- a/ide/projectapi/test/unit/src/org/netbeans/api/project/ProjectManagerTest.java +++ b/ide/projectapi/test/unit/src/org/netbeans/api/project/ProjectManagerTest.java @@ -65,6 +65,7 @@ public ProjectManagerTest(String name) { private FileObject goodproject2; private FileObject badproject; private FileObject mysteryproject; + private FileObject justADir; private ProjectManager pm; protected @Override Level logLevel() { @@ -82,6 +83,7 @@ protected void setUp() throws Exception { badproject = scratch.createFolder("bad"); badproject.createFolder("testproject").createData("broken"); mysteryproject = scratch.createFolder("mystery"); + justADir = scratch.createFolder("justADir"); MockLookup.setInstances(TestUtil.testProjectFactory()); pm = ProjectManager.getDefault(); NbProjectManagerAccessor.reset(); @@ -206,6 +208,27 @@ public void testIsProject() throws Exception { assertFalse("Should not have been able to load mysteryproject", pm.isProject(mysteryproject)); } + public void testIsFallbackProject() throws Exception { + var nothing = pm.findProject(justADir); + assertNull("No project is found for just a dir", nothing); + var generic = pm.findProjectOrFallback(justADir); + assertNotNull("But one can ask for a fallback project", generic); + + var then = pm.findProject(justADir); + assertSame("since then findProject works for just a dir", generic, then); + + var ref = new WeakReference<>(generic); + generic = null; + then = null; + // give the references time to disappear + Thread.sleep(TimedWeakReference.TIMEOUT); + + assertGC("The fallback project gets GCed when no longer used", ref); + + var nothingAgain = pm.findProject(justADir); + assertNull("Since then, findProject again returns null", nothingAgain); + } + public void testIsProject2() throws Exception { ProjectManager.Result r = pm.isProject2(goodproject); assertNotNull("Should have recognized goodproject", r); diff --git a/ide/projectui/src/org/netbeans/modules/project/ui/groups/AdHocGroup.java b/ide/projectui/src/org/netbeans/modules/project/ui/groups/AdHocGroup.java index a58a5d1dfcac..f940fa4fe3c5 100644 --- a/ide/projectui/src/org/netbeans/modules/project/ui/groups/AdHocGroup.java +++ b/ide/projectui/src/org/netbeans/modules/project/ui/groups/AdHocGroup.java @@ -20,7 +20,6 @@ package org.netbeans.modules.project.ui.groups; import java.util.Arrays; -import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; diff --git a/ide/projectui/src/org/netbeans/modules/project/ui/groups/DirectoryGroup.java b/ide/projectui/src/org/netbeans/modules/project/ui/groups/DirectoryGroup.java index adacb9bb6248..c70d46568f35 100644 --- a/ide/projectui/src/org/netbeans/modules/project/ui/groups/DirectoryGroup.java +++ b/ide/projectui/src/org/netbeans/modules/project/ui/groups/DirectoryGroup.java @@ -77,7 +77,7 @@ protected void findProjects(Set projects, ProgressHandle h, int start, } if (fo != null && fo.isFolder()) { try { - Project p = ProjectManager.getDefault().findProject(fo); + Project p = ProjectManager.getDefault().findProjectOrFallback(fo); if (p != null) { projects.add(p); if (h != null) { diff --git a/ide/projectui/src/org/netbeans/modules/project/ui/groups/DirectoryGroupEditPanel.java b/ide/projectui/src/org/netbeans/modules/project/ui/groups/DirectoryGroupEditPanel.java index 039d04aa4242..4923449e7b94 100644 --- a/ide/projectui/src/org/netbeans/modules/project/ui/groups/DirectoryGroupEditPanel.java +++ b/ide/projectui/src/org/netbeans/modules/project/ui/groups/DirectoryGroupEditPanel.java @@ -19,6 +19,7 @@ package org.netbeans.modules.project.ui.groups; +import java.awt.HeadlessException; import java.io.File; import java.util.prefs.Preferences; import javax.swing.JFileChooser; @@ -160,19 +161,9 @@ public void actionPerformed(java.awt.event.ActionEvent evt) { }// //GEN-END:initComponents private void directoryButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_directoryButtonActionPerformed - JFileChooser chooser = new JFileChooser(); - chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); - chooser.setMultiSelectionEnabled(false); - File start = ProjectChooser.getProjectsFolder(); - if (folderField.getText() != null && folderField.getText().trim().length() > 0) { - start = new File(folderField.getText().trim()); - } - chooser.setCurrentDirectory(start); - if (chooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) { - File f = chooser.getSelectedFile(); - if (f != null) { - folderField.setText(f.getAbsolutePath()); - } + File f = OpenFolderAsGroupAction.showWorkspaceFolderChooser(this, folderField.getText()); + if (f != null) { + folderField.setText(f.getAbsolutePath()); } }//GEN-LAST:event_directoryButtonActionPerformed diff --git a/ide/projectui/src/org/netbeans/modules/project/ui/groups/OpenFolderAsGroupAction.java b/ide/projectui/src/org/netbeans/modules/project/ui/groups/OpenFolderAsGroupAction.java new file mode 100644 index 000000000000..c8d94cbfc9df --- /dev/null +++ b/ide/projectui/src/org/netbeans/modules/project/ui/groups/OpenFolderAsGroupAction.java @@ -0,0 +1,69 @@ +/* + * 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.project.ui.groups; + +import java.awt.HeadlessException; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.io.File; +import javax.swing.JFileChooser; +import org.netbeans.spi.project.ui.support.ProjectChooser; +import org.openide.awt.ActionID; +import org.openide.awt.ActionReference; +import org.openide.awt.ActionRegistration; +import org.openide.filesystems.FileObject; +import org.openide.filesystems.FileUtil; +import org.openide.util.NbBundle.Messages; + +@ActionID( + category = "Project", + id = "org.netbeans.modules.project.ui.groups.OpenFolderAsGroupAction" +) +@ActionRegistration( + displayName = "#CTL_OpenFolderAsGroupAction", + lazy = true, asynchronous = true +) +@ActionReference(path = "Menu/File", position = 1200, separatorAfter = 1250) +@Messages("CTL_OpenFolderAsGroupAction=Open Fol&der as Workspace...") +public final class OpenFolderAsGroupAction implements ActionListener { + @Override + public void actionPerformed(ActionEvent ev) { + File dir = showWorkspaceFolderChooser(null, null); + FileObject folder = FileUtil.toFileObject(dir); + if (folder != null) { + DirectoryGroup group = DirectoryGroup.create(folder.getNameExt(), folder); + Group.setActiveGroup(group, true); + } + } + + static File showWorkspaceFolderChooser(java.awt.Component parent, final String hintPath) throws HeadlessException { + JFileChooser chooser = new JFileChooser(); + chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); + chooser.setMultiSelectionEnabled(false); + File start = ProjectChooser.getProjectsFolder(); + if (hintPath != null && hintPath.trim().length() > 0) { + start = new File(hintPath.trim()); + } + chooser.setCurrentDirectory(start); + final int result = chooser.showOpenDialog(parent); + File f = result == JFileChooser.APPROVE_OPTION ? chooser.getSelectedFile() : null; + return f; + } + +}