From 312b0a6599ace7b7b2b59cc9a4b9ce21c08c84d2 Mon Sep 17 00:00:00 2001 From: Jaroslav Tulach Date: Wed, 2 Sep 2026 07:45:03 +0200 Subject: [PATCH 1/3] LogicalViewProvider.WithNestedProjects allows one to logically nest projects --- .../modules/project/ui/ProjectsRootNode.java | 20 +++- .../project/ui/OpenProjectListNestedTest.java | 103 ++++++++++++------ ide/projectuiapi/apichanges.xml | 17 ++- .../spi/project/ui/LogicalViewProvider.java | 11 +- 4 files changed, 113 insertions(+), 38 deletions(-) diff --git a/ide/projectui/src/org/netbeans/modules/project/ui/ProjectsRootNode.java b/ide/projectui/src/org/netbeans/modules/project/ui/ProjectsRootNode.java index 1843189d4855..eb915d0691ee 100644 --- a/ide/projectui/src/org/netbeans/modules/project/ui/ProjectsRootNode.java +++ b/ide/projectui/src/org/netbeans/modules/project/ui/ProjectsRootNode.java @@ -58,6 +58,7 @@ import org.netbeans.api.project.Sources; import org.netbeans.spi.project.ActionProvider; import org.netbeans.spi.project.FileOwnerQueryImplementation; +import org.netbeans.spi.project.SubprojectProvider; import org.netbeans.spi.project.ui.LogicalViewProvider; import org.netbeans.spi.project.ui.support.ProjectConvertors; import org.openide.filesystems.FileChangeAdapter; @@ -461,7 +462,24 @@ final void refresh(Project p) { // Own methods --------------------------------------------------------- public Collection getKeys() { - List projects = Arrays.asList( OpenProjectList.getDefault().getOpenProjects() ); + var arr = OpenProjectList.getDefault().getOpenProjects(); + var projects = new ArrayList<>(Arrays.asList(arr)); + if (type == LOGICAL_VIEW) { + var hideNestedProjects = new HashSet(); + for (var p : projects) { + var lkp = p.getLookup(); + var nestedProjects = lkp.lookup(LogicalViewProvider.WithNestedProjects.class); + if (nestedProjects == null) { + continue; + } + var subProjects = lkp.lookup(SubprojectProvider.class); + if (subProjects == null) { + continue; + } + hideNestedProjects.addAll(subProjects.getSubprojects()); + } + projects.removeAll(hideNestedProjects); + } projects.sort(OpenProjectList.projectByDisplayName()); final List dirs = new ArrayList<>(projects.size()); diff --git a/ide/projectui/test/unit/src/org/netbeans/modules/project/ui/OpenProjectListNestedTest.java b/ide/projectui/test/unit/src/org/netbeans/modules/project/ui/OpenProjectListNestedTest.java index aba1b62dfc00..07c48e72e88a 100644 --- a/ide/projectui/test/unit/src/org/netbeans/modules/project/ui/OpenProjectListNestedTest.java +++ b/ide/projectui/test/unit/src/org/netbeans/modules/project/ui/OpenProjectListNestedTest.java @@ -32,8 +32,15 @@ import org.netbeans.junit.NbTestCase; import org.netbeans.modules.project.ui.actions.TestSupport; import org.netbeans.spi.project.SubprojectProvider; +import org.netbeans.spi.project.ui.LogicalViewProvider; import org.openide.filesystems.FileObject; import org.openide.filesystems.FileUtil; +import org.openide.loaders.DataFolder; +import org.openide.nodes.AbstractNode; +import org.openide.nodes.Children; +import org.openide.nodes.Node; +import org.openide.util.lookup.AbstractLookup; +import org.openide.util.lookup.InstanceContent; import org.openide.util.lookup.Lookups; import org.openide.util.test.MockLookup; @@ -55,8 +62,17 @@ protected void setUp() throws Exception { OpenProjectList.waitProjectsFullyOpen(); } - public void testOpenNestedProjects() throws Exception { - doOpenProjects(true, () -> { + public void testNestedSubProjects() throws Exception { + doOpenProjects(true, true, () -> { + var em = OpenProjects.getDefault().createLogicalView(); + var all = em.getRootContext().getChildren().getNodes(true); + assertEquals("Only one project is visible, the other is nested", 1, all.length); + return null; + }); + } + + public void testNoNestedSubProjects() throws Exception { + doOpenProjects(true, false, () -> { var em = OpenProjects.getDefault().createLogicalView(); var all = em.getRootContext().getChildren().getNodes(true); assertEquals("Two projects are visible", 2, all.length); @@ -64,16 +80,16 @@ public void testOpenNestedProjects() throws Exception { }); } - public void testOpenNonNestedProjects() throws Exception { - doOpenProjects(false, () -> { + public void testNestedNoSubProjects() throws Exception { + doOpenProjects(false, true, () -> { var em = OpenProjects.getDefault().createLogicalView(); var all = em.getRootContext().getChildren().getNodes(true); - assertEquals("One project is visible", 1, all.length); + assertEquals("Both projects are visible", 2, all.length); return null; }); } - private void doOpenProjects(boolean withSubprojects, Callable inner) throws Exception { + private void doOpenProjects(boolean withSubprojects, boolean withNested, Callable inner) throws Exception { MockLookup.setInstances(new TestSupport.TestProjectFactory()); clearWorkDir(); FileObject workDir = FileUtil.toFileObject(getWorkDir()); @@ -83,42 +99,61 @@ private void doOpenProjects(boolean withSubprojects, Callable inner) throw final TestSupport.TestProject mainPrj = (TestSupport.TestProject) ProjectManager.getDefault().findProject(prjFo); final TestSupport.TestProject nestedPrj = (TestSupport.TestProject) ProjectManager.getDefault().findProject(nestedFo); assertNotNull("Project found", mainPrj); - var subProvider = new SubprojectProvider() { - @Override - public Set getSubprojects() { - return Set.of(nestedPrj); - } - - @Override - public void addChangeListener(ChangeListener listener) { - } - - @Override - public void removeChangeListener(ChangeListener listener) { - } - }; + var content = new InstanceContent(); if (withSubprojects) { - mainPrj.setLookup(Lookups.singleton(subProvider)); + var subProvider = new SubprojectProvider() { + @Override + public Set getSubprojects() { + return Set.of(nestedPrj); + } + + @Override + public void addChangeListener(ChangeListener listener) { + } + + @Override + public void removeChangeListener(ChangeListener listener) { + } + }; + content.add(subProvider); + } + if (withNested) { + var logical = new LogicalViewProvider.WithNestedProjects() { + @Override + public Node createLogicalView() { + var ch = new Children.Array(); + var mainNode = new AbstractNode(ch); + var nestedNode = DataFolder.findFolder(nestedPrj.getProjectDirectory()).getNodeDelegate().cloneNode(); + ch.add(nestedNode); + return mainNode; + } + + @Override + public Node findPath(Node root, Object target) { + if (target == mainPrj || target.equals(mainPrj.getProjectDirectory())) { + return root; + } + if (target == nestedPrj || target.equals(nestedPrj.getProjectDirectory())) { + return root.getChildren().getNodeAt(0); + } + return null; + } + }; + content.add(logical); } + mainPrj.setLookup(new AbstractLookup(content)); OpenProjectList.waitProjectsFullyOpen(); assertEquals("Initially empty", 0, OpenProjects.getDefault().openProjects().get().length); - OpenProjects.getDefault().open(new Project[] { mainPrj }, true); + OpenProjects.getDefault().open(new Project[] { mainPrj, nestedPrj }, false); List arr = Arrays.asList(OpenProjects.getDefault().openProjects().get()); - if (withSubprojects) { - assertEquals("Both projects open", 2, arr.size()); - assertTrue("Prj1 is there", arr.contains(mainPrj)); - assertTrue("Nested1 is there", arr.contains(nestedPrj)); - inner.call(); - OpenProjects.getDefault().close (new Project[] { nestedPrj, mainPrj }); - } else { - assertEquals("However one project instance is there", 1, arr.size()); - assertEquals("arr[0] is equal to p", arr.get(0), mainPrj); - inner.call(); - OpenProjects.getDefault().close (new Project[] { mainPrj }); - } + assertEquals("Both projects open", 2, arr.size()); + assertTrue("Prj1 is there", arr.contains(mainPrj)); + assertTrue("Nested1 is there", arr.contains(nestedPrj)); + inner.call(); + OpenProjects.getDefault().close (new Project[] { nestedPrj, mainPrj }); if (OpenProjects.getDefault().getOpenProjects().length != 0) { fail("All projects shall be closed: " + Arrays.asList(OpenProjects.getDefault().getOpenProjects())); diff --git a/ide/projectuiapi/apichanges.xml b/ide/projectuiapi/apichanges.xml index 18071495a851..0acea839a7ec 100644 --- a/ide/projectuiapi/apichanges.xml +++ b/ide/projectuiapi/apichanges.xml @@ -20,7 +20,7 @@ --> - + + + + Logical view supports nested projects. + + + + + +

+ There is a new interface + LogicalViewProvider.WithNestedProjects + that allows a parent project to nest its children projects. +

+
+
Allow pluggable implementation for ProjectProblems API. diff --git a/ide/projectuiapi/src/org/netbeans/spi/project/ui/LogicalViewProvider.java b/ide/projectuiapi/src/org/netbeans/spi/project/ui/LogicalViewProvider.java index 8c6f19e89508..d4d73ed46f69 100644 --- a/ide/projectuiapi/src/org/netbeans/spi/project/ui/LogicalViewProvider.java +++ b/ide/projectuiapi/src/org/netbeans/spi/project/ui/LogicalViewProvider.java @@ -46,6 +46,13 @@ public interface LogicalViewProvider extends PathFinder { * @return a node displaying the contents of the project in an intuitive way * @see CommonProjectActions#forType */ - Node createLogicalView(); - + Node createLogicalView(); + + /** An extension of {@link LogicalViewProvider} handling also nested projects. + * + * @since 1.124 + */ + public interface WithNestedProjects extends LogicalViewProvider { + + } } From 1aae492e827bbf9c438ac44b1eb43c4f77982ad8 Mon Sep 17 00:00:00 2001 From: Jaroslav Tulach Date: Wed, 2 Sep 2026 07:45:34 +0200 Subject: [PATCH 2/3] Allow varargs to simplify add/remove of a single node --- .../src/org/openide/nodes/Children.java | 54 +++++-------------- 1 file changed, 14 insertions(+), 40 deletions(-) diff --git a/platform/openide.nodes/src/org/openide/nodes/Children.java b/platform/openide.nodes/src/org/openide/nodes/Children.java index 886b8412cd9c..816778cac404 100644 --- a/platform/openide.nodes/src/org/openide/nodes/Children.java +++ b/platform/openide.nodes/src/org/openide/nodes/Children.java @@ -357,7 +357,7 @@ protected Object clone() throws CloneNotSupportedException { * @param nodes set of nodes to add to the list * @return true if successfully added */ - public abstract boolean add(final Node[] nodes); + public abstract boolean add(final Node... nodes); /** Remove nodes from the list. Only nodes that are present are * removed. @@ -365,7 +365,7 @@ protected Object clone() throws CloneNotSupportedException { * @param nodes nodes to be removed * @return true if the nodes could be removed */ - public abstract boolean remove(final Node[] nodes); + public abstract boolean remove(final Node... nodes); /** Get the nodes as an enumeration. * @return enumeration of nodes @@ -437,8 +437,6 @@ public final Node getNodeAt(int index) { * this point, see {@link #getNodes(boolean)} * @return array of nodes */ - - // private static String off = ""; // NOI18N public final Node[] getNodes() { checkSupport(); return entrySupport().getNodes(false); @@ -605,12 +603,14 @@ private static final class Empty extends Children { } /** @return false, does no action */ - public boolean add(Node[] nodes) { + @Override + public boolean add(Node... nodes) { return false; } /** @return false, does no action */ - public boolean remove(Node[] nodes) { + @Override + public boolean remove(Node... nodes) { return false; } } @@ -789,7 +789,7 @@ final Collection getCollection() { * @return true if changed false if not */ @Override - public boolean add(final Node[] arr) { + public boolean add(final Node... arr) { synchronized (COLLECTION_LOCK) { if (!getCollection().addAll(Arrays.asList(arr))) { // no change to the collection @@ -805,7 +805,7 @@ public boolean add(final Node[] arr) { * @return true if changed false if not */ @Override - public boolean remove(final Node[] arr) { + public boolean remove(final Node... arr) { synchronized (COLLECTION_LOCK) { final Collection collection = getCollection(); // fast check @@ -1055,7 +1055,7 @@ protected java.util.Map initMap() { * @param arr nodes to add * @return false in the default implementation */ - public boolean add(Node[] arr) { + public boolean add(Node... arr) { return false; } @@ -1064,7 +1064,7 @@ public boolean add(Node[] arr) { * @param arr nodes to remove * @return false in the default implementation */ - public boolean remove(Node[] arr) { + public boolean remove(Node... arr) { return false; } @@ -1403,7 +1403,7 @@ void switchSupport(boolean toLazy) { */ @Deprecated @Override - public boolean add(Node[] arr) { + public boolean add(Node... arr) { if (lazySupport) { fallbackToDefaultSupport(); } @@ -1415,7 +1415,7 @@ public boolean add(Node[] arr) { */ @Deprecated @Override - public boolean remove(final Node[] arr) { + public boolean remove(final Node... arr) { if (lazySupport) { return false; } @@ -1840,12 +1840,12 @@ Children getOriginal() { } @Override - public boolean add(Node[] nodes) { + public boolean add(Node... nodes) { return getOriginal().add(nodes); } @Override - public boolean remove(Node[] nodes) { + public boolean remove(Node... nodes) { return getOriginal().remove(nodes); } @@ -1871,32 +1871,6 @@ public Node findChild(String name) { } - /* - static void printNodes (Node[] n) { - for (int i = 0; i < n.length; i++) { - System.out.println (" " + i + ". " + n[i].getName () + " number: " + System.identityHashCode (n[i])); - } - } - */ - /* JST: Useful test routine ;-) * - static { - final TopComponent.Registry r = TopComponent.getRegistry (); - r.addPropertyChangeListener (new PropertyChangeListener () { - Node last = new AbstractNode (LEAF); - - public void propertyChange (PropertyChangeEvent ev) { - Node[] arr = r.getCurrentNodes (); - if (arr != null && arr.length == 1) { - last = arr[0]; - } - System.out.println ( - "Activated node: " + last + " \nparent: " + last.getParentNode () - ); - } - }); - } - */ - private static final class ProjectManagerDeadlockDetector implements Executor { private final Mutex FALLBACK = new Mutex(); private final AtomicReference> pmMutexRef = new AtomicReference>(); From 605dd74c6b42cd89ce79221bdd19220200afa64a Mon Sep 17 00:00:00 2001 From: Jaroslav Tulach Date: Fri, 4 Sep 2026 14:25:47 +0200 Subject: [PATCH 3/3] Increasing spec.version.base --- ide/projectuiapi/nbproject/project.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ide/projectuiapi/nbproject/project.properties b/ide/projectuiapi/nbproject/project.properties index d32517796272..9746fd18edd7 100644 --- a/ide/projectuiapi/nbproject/project.properties +++ b/ide/projectuiapi/nbproject/project.properties @@ -17,7 +17,7 @@ javac.compilerargs=-Xlint -Xlint:-serial javac.release=17 -spec.version.base=1.123.0 +spec.version.base=1.124.0 is.autoload=true javadoc.arch=${basedir}/arch.xml javadoc.apichanges=${basedir}/apichanges.xml