Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -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<Project>() {
Expand Down Expand Up @@ -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)) {
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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
Expand Down
16 changes: 16 additions & 0 deletions ide/projectapi/src/org/netbeans/api/project/ProjectManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}


/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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();
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ protected void findProjects(Set<Project> projects, ProgressHandle h, int start,
}
if (fo != null && fo.isFolder()) {
try {
Project p = ProjectManager.getDefault().findProject(fo);

@jtulach jtulach Sep 23, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • This change makes sure the "top most folder" of each DirectoryGroup is visible
    • e.g. the folder selected in Open Folder as Workspace is always visible
    • thanks to Show nested projects co-located and indented #9602 the "top most folder" is the first project in the list
    • ... as it has the shortest path and all other projects are nested
  • if the root folder is recognized as a regular NetBeans project...
    • ...then the behavior is the same as it was by now
  • if the root folder isn't real project, a GenericPrj is created for it
  • if there is no project beneath the root folder, then one gets just a simple "files view" of the root project:
Files View of a Folder

@jtulach jtulach Sep 23, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Open root of NetBeans folder
  • btw. the list of projects is pretty long as it includes all the testing projects found in the NetBeans codebase
  • but that's how it always have been ...
  • ... no change in that
    • just nobody really tried to use "Open Folder as Group" yet ...

Project p = ProjectManager.getDefault().findProjectOrFallback(fo);
if (p != null) {
projects.add(p);
if (h != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -160,19 +161,9 @@ public void actionPerformed(java.awt.event.ActionEvent evt) {
}// </editor-fold>//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();

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This DirectoryGroupEditPanel already provides support for the Open Folder (as Workspace) functionality. It is just this complex:

Project Groups...

Selecting File / Project Groups... menu item opens a dialog

New group...

The New group... button opens another dialog. One needs to choose Folder of Projects and Browse for it (that opens another dialog):

Browse for a folder

Only then one can click "Create Group" button. Which scans for all the projects in the given folder and opens them in Projects view (after closing all previous ones).

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

Expand Down
Original file line number Diff line number Diff line change
@@ -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);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This new Open Folder as Workspace... action provides the same function as is currently hidden in the Projects Group... dialog, but it is way more straightforward:

Open Folder (as Workspace)...

Just choose the new action, then select a folder in the file chooser and voilá! No change in NetBeans abilities, ... but now they are exposed to the world the way the (current development) world wants to see them

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;
}

}
Loading