From 12f4ffaea88e0acccfadebee829a4b675b7f9e34 Mon Sep 17 00:00:00 2001 From: Michael Bien Date: Mon, 19 Oct 2026 00:11:16 +0200 Subject: [PATCH 01/13] Seed NB repo with generic-project marker files - each cluster becomes a generic project so that it can be opened - .github receives one too --- .github/.nbgp | 1 + .nbgp | 1 + apisupport/.nbgp | 1 + contrib/.nbgp | 1 + cpplite/.nbgp | 1 + enterprise/.nbgp | 1 + ergonomics/.nbgp | 1 + extide/.nbgp | 1 + extra/.nbgp | 1 + groovy/.nbgp | 1 + harness/.nbgp | 1 + ide/.nbgp | 1 + java/.nbgp | 1 + javafx/.nbgp | 1 + nb/.nbgp | 1 + php/.nbgp | 1 + platform/.nbgp | 1 + profiler/.nbgp | 1 + rust/.nbgp | 1 + webcommon/.nbgp | 1 + websvccommon/.nbgp | 1 + 21 files changed, 21 insertions(+) create mode 100644 .github/.nbgp create mode 100644 .nbgp create mode 100644 apisupport/.nbgp create mode 100644 contrib/.nbgp create mode 100644 cpplite/.nbgp create mode 100644 enterprise/.nbgp create mode 100644 ergonomics/.nbgp create mode 100644 extide/.nbgp create mode 100644 extra/.nbgp create mode 100644 groovy/.nbgp create mode 100644 harness/.nbgp create mode 100644 ide/.nbgp create mode 100644 java/.nbgp create mode 100644 javafx/.nbgp create mode 100644 nb/.nbgp create mode 100644 php/.nbgp create mode 100644 platform/.nbgp create mode 100644 profiler/.nbgp create mode 100644 rust/.nbgp create mode 100644 webcommon/.nbgp create mode 100644 websvccommon/.nbgp diff --git a/.github/.nbgp b/.github/.nbgp new file mode 100644 index 000000000000..8b137891791f --- /dev/null +++ b/.github/.nbgp @@ -0,0 +1 @@ + diff --git a/.nbgp b/.nbgp new file mode 100644 index 000000000000..8b137891791f --- /dev/null +++ b/.nbgp @@ -0,0 +1 @@ + diff --git a/apisupport/.nbgp b/apisupport/.nbgp new file mode 100644 index 000000000000..8b137891791f --- /dev/null +++ b/apisupport/.nbgp @@ -0,0 +1 @@ + diff --git a/contrib/.nbgp b/contrib/.nbgp new file mode 100644 index 000000000000..8b137891791f --- /dev/null +++ b/contrib/.nbgp @@ -0,0 +1 @@ + diff --git a/cpplite/.nbgp b/cpplite/.nbgp new file mode 100644 index 000000000000..8b137891791f --- /dev/null +++ b/cpplite/.nbgp @@ -0,0 +1 @@ + diff --git a/enterprise/.nbgp b/enterprise/.nbgp new file mode 100644 index 000000000000..8b137891791f --- /dev/null +++ b/enterprise/.nbgp @@ -0,0 +1 @@ + diff --git a/ergonomics/.nbgp b/ergonomics/.nbgp new file mode 100644 index 000000000000..8b137891791f --- /dev/null +++ b/ergonomics/.nbgp @@ -0,0 +1 @@ + diff --git a/extide/.nbgp b/extide/.nbgp new file mode 100644 index 000000000000..8b137891791f --- /dev/null +++ b/extide/.nbgp @@ -0,0 +1 @@ + diff --git a/extra/.nbgp b/extra/.nbgp new file mode 100644 index 000000000000..8b137891791f --- /dev/null +++ b/extra/.nbgp @@ -0,0 +1 @@ + diff --git a/groovy/.nbgp b/groovy/.nbgp new file mode 100644 index 000000000000..8b137891791f --- /dev/null +++ b/groovy/.nbgp @@ -0,0 +1 @@ + diff --git a/harness/.nbgp b/harness/.nbgp new file mode 100644 index 000000000000..8b137891791f --- /dev/null +++ b/harness/.nbgp @@ -0,0 +1 @@ + diff --git a/ide/.nbgp b/ide/.nbgp new file mode 100644 index 000000000000..8b137891791f --- /dev/null +++ b/ide/.nbgp @@ -0,0 +1 @@ + diff --git a/java/.nbgp b/java/.nbgp new file mode 100644 index 000000000000..8b137891791f --- /dev/null +++ b/java/.nbgp @@ -0,0 +1 @@ + diff --git a/javafx/.nbgp b/javafx/.nbgp new file mode 100644 index 000000000000..8b137891791f --- /dev/null +++ b/javafx/.nbgp @@ -0,0 +1 @@ + diff --git a/nb/.nbgp b/nb/.nbgp new file mode 100644 index 000000000000..8b137891791f --- /dev/null +++ b/nb/.nbgp @@ -0,0 +1 @@ + diff --git a/php/.nbgp b/php/.nbgp new file mode 100644 index 000000000000..8b137891791f --- /dev/null +++ b/php/.nbgp @@ -0,0 +1 @@ + diff --git a/platform/.nbgp b/platform/.nbgp new file mode 100644 index 000000000000..8b137891791f --- /dev/null +++ b/platform/.nbgp @@ -0,0 +1 @@ + diff --git a/profiler/.nbgp b/profiler/.nbgp new file mode 100644 index 000000000000..8b137891791f --- /dev/null +++ b/profiler/.nbgp @@ -0,0 +1 @@ + diff --git a/rust/.nbgp b/rust/.nbgp new file mode 100644 index 000000000000..8b137891791f --- /dev/null +++ b/rust/.nbgp @@ -0,0 +1 @@ + diff --git a/webcommon/.nbgp b/webcommon/.nbgp new file mode 100644 index 000000000000..8b137891791f --- /dev/null +++ b/webcommon/.nbgp @@ -0,0 +1 @@ + diff --git a/websvccommon/.nbgp b/websvccommon/.nbgp new file mode 100644 index 000000000000..8b137891791f --- /dev/null +++ b/websvccommon/.nbgp @@ -0,0 +1 @@ + From e6b4f10be72acee141079f043f74170f23535d5d Mon Sep 17 00:00:00 2001 From: Michael Bien Date: Mon, 19 Oct 2026 00:05:59 +0200 Subject: [PATCH 02/13] initial gp impl --- ide/generic.project/build.xml | 25 ++++ ide/generic.project/manifest.mf | 4 + .../nbproject/project.properties | 19 +++ ide/generic.project/nbproject/project.xml | 135 ++++++++++++++++++ .../modules/generic/project/Bundle.properties | 19 +++ .../generic/project/GenericProject.java | 135 ++++++++++++++++++ .../GenericProjectLogicalViewProvider.java | 77 ++++++++++ .../netbeans/modules/generic/project/gp.png | Bin 0 -> 694 bytes .../netbeans/modules/generic/project/gp.svg | 59 ++++++++ .../netbeans/modules/generic/project/gp24.png | Bin 0 -> 911 bytes .../netbeans/modules/generic/project/gp24.svg | 68 +++++++++ .../generic/project/GenericProjectTest.java | 77 ++++++++++ nbbuild/cluster.properties | 1 + 13 files changed, 619 insertions(+) create mode 100644 ide/generic.project/build.xml create mode 100644 ide/generic.project/manifest.mf create mode 100644 ide/generic.project/nbproject/project.properties create mode 100644 ide/generic.project/nbproject/project.xml create mode 100644 ide/generic.project/src/org/netbeans/modules/generic/project/Bundle.properties create mode 100644 ide/generic.project/src/org/netbeans/modules/generic/project/GenericProject.java create mode 100644 ide/generic.project/src/org/netbeans/modules/generic/project/GenericProjectLogicalViewProvider.java create mode 100644 ide/generic.project/src/org/netbeans/modules/generic/project/gp.png create mode 100644 ide/generic.project/src/org/netbeans/modules/generic/project/gp.svg create mode 100644 ide/generic.project/src/org/netbeans/modules/generic/project/gp24.png create mode 100644 ide/generic.project/src/org/netbeans/modules/generic/project/gp24.svg create mode 100644 ide/generic.project/test/unit/src/org/netbeans/modules/generic/project/GenericProjectTest.java diff --git a/ide/generic.project/build.xml b/ide/generic.project/build.xml new file mode 100644 index 000000000000..24ae57c89b81 --- /dev/null +++ b/ide/generic.project/build.xml @@ -0,0 +1,25 @@ + + + + + + diff --git a/ide/generic.project/manifest.mf b/ide/generic.project/manifest.mf new file mode 100644 index 000000000000..177f6e972319 --- /dev/null +++ b/ide/generic.project/manifest.mf @@ -0,0 +1,4 @@ +Manifest-Version: 1.0 +OpenIDE-Module: org.netbeans.modules.generic.project +OpenIDE-Module-Localizing-Bundle: org/netbeans/modules/generic/project/Bundle.properties +OpenIDE-Module-Specification-Version: 1.0 \ No newline at end of file diff --git a/ide/generic.project/nbproject/project.properties b/ide/generic.project/nbproject/project.properties new file mode 100644 index 000000000000..4112e4ee792e --- /dev/null +++ b/ide/generic.project/nbproject/project.properties @@ -0,0 +1,19 @@ +# 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. + +javac.compilerargs=-Xlint +javac.release=21 diff --git a/ide/generic.project/nbproject/project.xml b/ide/generic.project/nbproject/project.xml new file mode 100644 index 000000000000..ea3d52ec0d57 --- /dev/null +++ b/ide/generic.project/nbproject/project.xml @@ -0,0 +1,135 @@ + + + + org.netbeans.modules.apisupport.project + + + org.netbeans.modules.generic.project + + + org.netbeans.api.annotations.common + + + + 1 + 1.48 + + + + org.netbeans.modules.projectapi + + + + 1 + 1.106 + + + + org.netbeans.modules.projectuiapi + + + + 1 + 1.95.0.8 + + + + org.openide.filesystems + + + + 9.17 + + + + org.openide.loaders + + + + 7.74 + + + + org.openide.nodes + + + + 7.52 + + + + org.openide.util + + + + 9.14 + + + + org.openide.util.ui + + + + 9.15 + + + + org.openide.util.lookup + + + + 8.40 + + + + + + unit + + org.netbeans.libs.junit4 + + + + org.netbeans.modules.masterfs + + + + + org.netbeans.modules.nbjunit + + + + + org.netbeans.modules.projectapi.nb + + + + org.openide.util.lookup + + + + + + + + + diff --git a/ide/generic.project/src/org/netbeans/modules/generic/project/Bundle.properties b/ide/generic.project/src/org/netbeans/modules/generic/project/Bundle.properties new file mode 100644 index 000000000000..25ec550017b6 --- /dev/null +++ b/ide/generic.project/src/org/netbeans/modules/generic/project/Bundle.properties @@ -0,0 +1,19 @@ +# 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. + +OpenIDE-Module-Name=Generic Project +OpenIDE-Module-Display-Category=Base IDE diff --git a/ide/generic.project/src/org/netbeans/modules/generic/project/GenericProject.java b/ide/generic.project/src/org/netbeans/modules/generic/project/GenericProject.java new file mode 100644 index 000000000000..d2abdc298157 --- /dev/null +++ b/ide/generic.project/src/org/netbeans/modules/generic/project/GenericProject.java @@ -0,0 +1,135 @@ +/* + * 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.generic.project; + +import java.beans.PropertyChangeListener; +import java.io.IOException; +import javax.swing.Icon; +import javax.swing.UIManager; +import org.netbeans.api.annotations.common.StaticResource; +import org.netbeans.api.project.Project; +import org.netbeans.api.project.ProjectInformation; +import org.netbeans.spi.project.ProjectFactory; +import org.netbeans.spi.project.ProjectState; +import org.openide.filesystems.FileObject; +import org.openide.util.ImageUtilities; +import org.openide.util.Lookup; +import org.openide.util.lookup.Lookups; +import org.openide.util.lookup.ServiceProvider; + +/** + * A generic project. Any directory containing the {@link #MARKER_FILE} is + * recognized as a generic project. The logical view of the project simply shows + * all files contained in the project directory. + */ +public final class GenericProject implements Project { + + public static final String MARKER_FILE = ".nbgp"; // NOI18N + + @StaticResource + public static final String ICON = "org/netbeans/modules/generic/project/gp.svg"; // NOI18N + + private final FileObject projectDirectory; + private final Lookup lookup; + + private GenericProject(FileObject projectDirectory) { + this.projectDirectory = projectDirectory; + this.lookup = Lookups.fixed( + new GenericProjectLogicalViewProvider(this), + new GenericProjectInfo(this), + new SubprojectProviderImpl(this), + this + ); + } + + @Override + public FileObject getProjectDirectory() { + return projectDirectory; + } + + @Override + public Lookup getLookup() { + return lookup; + } + + static boolean isProject(FileObject projectDirectory) { + return projectDirectory != null + && projectDirectory.getFileObject(MARKER_FILE) != null; + } + + @ServiceProvider(service=ProjectFactory.class) + public static final class GenericProjectFactory implements ProjectFactory { + + @Override + public boolean isProject(FileObject projectDirectory) { + return GenericProject.isProject(projectDirectory); + } + + @Override + public Project loadProject(FileObject projectDirectory, ProjectState state) throws IOException { + if (GenericProject.isProject(projectDirectory)) { + return new GenericProject(projectDirectory); + } + return null; + } + + @Override + public void saveProject(Project project) throws IOException, ClassCastException { + FileObject dir = project.getProjectDirectory(); + if (dir.getFileObject(MARKER_FILE) == null) { + FileObject marker = dir.createData(MARKER_FILE); + marker.setAttribute("displayName", dir.getNameExt()); + } + } + + } + + private record GenericProjectInfo(GenericProject prj) implements ProjectInformation { + + @Override + public String getName() { + return prj.getProjectDirectory().getNameExt(); + } + + @Override + public String getDisplayName() { + return prj.getProjectDirectory().getNameExt(); + } + + @Override + public Icon getIcon() { + Icon icon = ImageUtilities.loadIcon(ICON); + return icon != null ? icon : UIManager.getIcon("Nb.Explorer.Folder.icon"); // NOI18N + } + + @Override + public Project getProject() { + return prj; + } + + @Override + public void addPropertyChangeListener(PropertyChangeListener listener) {} + + @Override + public void removePropertyChangeListener(PropertyChangeListener listener) {} + + } + +} diff --git a/ide/generic.project/src/org/netbeans/modules/generic/project/GenericProjectLogicalViewProvider.java b/ide/generic.project/src/org/netbeans/modules/generic/project/GenericProjectLogicalViewProvider.java new file mode 100644 index 000000000000..5f3b66decf41 --- /dev/null +++ b/ide/generic.project/src/org/netbeans/modules/generic/project/GenericProjectLogicalViewProvider.java @@ -0,0 +1,77 @@ +/* + * 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.generic.project; + +import java.awt.Image; +import javax.swing.Action; +import org.netbeans.spi.project.ui.LogicalViewProvider; +import org.netbeans.spi.project.ui.support.CommonProjectActions; +import org.openide.filesystems.FileObject; +import org.openide.loaders.DataFilter; +import org.openide.loaders.DataFolder; +import org.openide.nodes.AbstractNode; +import org.openide.nodes.Children; +import org.openide.nodes.Node; +import org.openide.util.ImageUtilities; +import org.openide.util.lookup.Lookups; + +/** + * The logical view of a generic project simply shows all files contained + * in the project directory. + */ +public record GenericProjectLogicalViewProvider(GenericProject project) implements LogicalViewProvider { + + @Override + public Node createLogicalView() { + FileObject dir = project.getProjectDirectory(); + Children children; + try { + children = DataFolder.findFolder(dir).createNodeChildren(DataFilter.ALL); + } catch (Exception ex) { + children = Children.LEAF; + } + AbstractNode base = new AbstractNode(children, Lookups.fixed(project, dir)) { + @Override + public Action[] getActions(boolean context) { + return new Action[] { + CommonProjectActions.closeProjectAction(), + }; + } + + @Override + public Image getIcon(int type) { + return ImageUtilities.loadImage(GenericProject.ICON); + } + + @Override + public Image getOpenedIcon(int type) { + return getIcon(type); + } + }; + base.setDisplayName(dir.getNameExt()); + return base; + } + + @Override + public Node findPath(Node root, Object target) { + return target instanceof FileObject fo && fo.equals(project.getProjectDirectory()) ? root : null; + } + +} diff --git a/ide/generic.project/src/org/netbeans/modules/generic/project/gp.png b/ide/generic.project/src/org/netbeans/modules/generic/project/gp.png new file mode 100644 index 0000000000000000000000000000000000000000..7f9b26179abb6084ea831b304373247167ed560f GIT binary patch literal 694 zcmV;n0!jUeP)Szdpb6|JUnlV0`1~>i-~dfB<62u53R8QY9=d z#qjFGR|XjS-@kthK)L@}6>SVZo-ATudv%QA@vGwuOiwS}*VfHp;AHxRq2|ZW-%J1j#PavwUj`;7Mh1M~$IrhE%v?g4;y?Z{00a=r z-#>&6`1$KM0}HnZrugqa3;+Sd^7ro_24*HEyaxORDy(SiWdK?89|##3fh=GEgD8Li z0vo_Wzy7A&K z0Rjl@f|oCzGkpB`k>SmUFAN_(e`6p77+IJZKD~PfG~mZpfB*ufgMSQfU%v*%={JUd zKpXx+ZNUeafnIq2>@i5sR)7Fv`TP6#tM6aGhyhJw;Ns+9`1hZH?Z60QXJcV__T&lB z_kWKA1Q4UGvriM3fMD6Jdr#OIhy*}0{xJMwm~!{RzCwTi0wn-Y5|f4I4`RV{V3a-s cF#rM#0Q=1h(tk=a)c^nh07*qoM6N<$f}mA0hyVZp literal 0 HcmV?d00001 diff --git a/ide/generic.project/src/org/netbeans/modules/generic/project/gp.svg b/ide/generic.project/src/org/netbeans/modules/generic/project/gp.svg new file mode 100644 index 000000000000..5f7295022b67 --- /dev/null +++ b/ide/generic.project/src/org/netbeans/modules/generic/project/gp.svg @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ide/generic.project/src/org/netbeans/modules/generic/project/gp24.png b/ide/generic.project/src/org/netbeans/modules/generic/project/gp24.png new file mode 100644 index 0000000000000000000000000000000000000000..50a4fa63a6986e19b3c3f31cead702c600c55c7d GIT binary patch literal 911 zcmV;A191F_P)&UBGYFJL2Q5kVu1+)4R6fPNMhvU<6}ft{OQvthKV!gH3FHF;*+y} zHN|WDg{3tx@QNv7HT?6d`wXjQHTZ!%3*rL=5ZHpW;s$#O2?>Fp46F>VKYd5j!pZ=W z1E~S}_F!Y2mYtHi9?+kEv05N3pv&;@-#k@I?1S`#NCu!jhIe1SW7S}+r^8@rtOs%#!<6hL z44h0q@mlcv55ji<0R*<-<@;|8OiWBTeenLv4@89j{zEY3AgcigAg~2Yj1Wu=`2G7Y z10xd)K{YS{5I|s+jEoRW4EP1~J`)EYK{YS{5I|517{Qnr@CT@wg4>Mk`*ZY|Ns9nEMK{XVg1rsCdlFop)^1M5$%J&fB!OU z+`fz9*r~G&808}~7lT(^!5Jv;AJE!0Ksp@60SF+l1#jQJX4rG+1jEU*7jTy^KrAFC z#$aym#K6LgHx)4*+_gOj#sLT*u*zRQe}Y|R@8${24|se6bouw6zZgFM_=-~rD=Q1b zzyJT?E&vE1FyqUoj|{(m{bcy^1S6=g<2v zF@OMK`S#^A!?$l=faS+;a0dDPn|uqHnVA_rzJCie>(^zN7(f8AJb(J=*|#t1QolhK z0F&Y$3ViVIA1ly;cfk1keHJDL5I`&s@7y@@<+DZDe_&Dk7l_$8*vYm4r2p0PCk%gn z|2hs60|+2SK0%>i78cfAE^$?RN?Q>B|NZ&*?e(+m?_NFhfr$YG5Gc{}0I@g_3s9kz l0!riWfmQ2cm;gY40RSRqI5E!2>AU~{002ovPDHLkV1i7)nq>e0 literal 0 HcmV?d00001 diff --git a/ide/generic.project/src/org/netbeans/modules/generic/project/gp24.svg b/ide/generic.project/src/org/netbeans/modules/generic/project/gp24.svg new file mode 100644 index 000000000000..c10101837e94 --- /dev/null +++ b/ide/generic.project/src/org/netbeans/modules/generic/project/gp24.svg @@ -0,0 +1,68 @@ + + + + + + + + Apache NetBeans Logo + + + + + + + + + + + + + + + + + + + + + diff --git a/ide/generic.project/test/unit/src/org/netbeans/modules/generic/project/GenericProjectTest.java b/ide/generic.project/test/unit/src/org/netbeans/modules/generic/project/GenericProjectTest.java new file mode 100644 index 000000000000..f5e15ce5bcad --- /dev/null +++ b/ide/generic.project/test/unit/src/org/netbeans/modules/generic/project/GenericProjectTest.java @@ -0,0 +1,77 @@ +/* + * 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.generic.project; + +import java.io.File; +import org.netbeans.api.project.Project; +import org.netbeans.api.project.ProjectManager; +import org.netbeans.junit.NbTestCase; +import org.netbeans.spi.project.ui.LogicalViewProvider; +import org.openide.filesystems.FileObject; +import org.openide.filesystems.FileUtil; +import org.openide.nodes.Node; +import org.openide.util.test.MockLookup; + +public class GenericProjectTest extends NbTestCase { + + static { + // ensures the default lookup includes the meta-inf services, + // e.g. MasterURLMapper and the ProjectManagerImplementation + MockLookup.init(); + } + + public GenericProjectTest(String name) { + super(name); + } + + public void testNoMarkerNotProject() throws Exception { + FileObject root = newProjectDir(); + assertFalse(new GenericProject.GenericProjectFactory().isProject(root)); + } + + public void testMarkerFileIsProject() throws Exception { + FileObject root = newProjectDir(); + FileUtil.createData(root, GenericProject.MARKER_FILE); + assertTrue(new GenericProject.GenericProjectFactory().isProject(root)); + } + + public void testLogicalViewShowsAllFiles() throws Exception { + FileObject root = newProjectDir(); + FileUtil.createData(root, GenericProject.MARKER_FILE); + FileUtil.createData(root, "readme.txt"); + Project prj = ProjectManager.getDefault().findProject(root); + assertNotNull(prj); + assertTrue(prj instanceof GenericProject); + LogicalViewProvider provider = prj.getLookup().lookup(LogicalViewProvider.class); + assertNotNull(provider); + Node node = provider.createLogicalView(); + assertNotNull(node); + Node[] children = node.getChildren().getNodes(true); + assertEquals(2, children.length); + } + + private FileObject newProjectDir() throws Exception { + clearWorkDir(); + File dir = new File(getWorkDir(), "proj-" + getName()); // NOI18N + assertTrue(dir.mkdirs() || dir.isDirectory()); + return FileUtil.toFileObject(dir); + } + +} diff --git a/nbbuild/cluster.properties b/nbbuild/cluster.properties index a668f4e128e3..3634081dd8ae 100644 --- a/nbbuild/cluster.properties +++ b/nbbuild/cluster.properties @@ -344,6 +344,7 @@ nb.cluster.ide=\ extexecution.base,\ extexecution.impl,\ extexecution.process,\ + generic.project,\ git,\ go.lang,\ gototest,\ From 7b7345005ce4dcbc7007c8a9ed91320fdc38afb6 Mon Sep 17 00:00:00 2001 From: Michael Bien Date: Mon, 19 Oct 2026 10:47:22 +0200 Subject: [PATCH 03/13] project wizard --- ide/generic.project/nbproject/project.xml | 24 ++ .../generic/project/GenericProject.java | 19 +- .../GenericProjectTemplateDescription.html | 29 ++ .../project/GenericProjectWizardIterator.java | 152 ++++++++++ .../project/GenericProjectWizardPanel.java | 280 ++++++++++++++++++ 5 files changed, 499 insertions(+), 5 deletions(-) create mode 100644 ide/generic.project/src/org/netbeans/modules/generic/project/GenericProjectTemplateDescription.html create mode 100644 ide/generic.project/src/org/netbeans/modules/generic/project/GenericProjectWizardIterator.java create mode 100644 ide/generic.project/src/org/netbeans/modules/generic/project/GenericProjectWizardPanel.java diff --git a/ide/generic.project/nbproject/project.xml b/ide/generic.project/nbproject/project.xml index ea3d52ec0d57..d465bcf47ae2 100644 --- a/ide/generic.project/nbproject/project.xml +++ b/ide/generic.project/nbproject/project.xml @@ -34,6 +34,14 @@ 1.48 + + org.netbeans.api.templates + + + + 1.28 + + org.netbeans.modules.projectapi @@ -52,6 +60,22 @@ 1.95.0.8 + + org.openide.awt + + + + 7.88 + + + + org.openide.dialogs + + + + 7.66 + + org.openide.filesystems diff --git a/ide/generic.project/src/org/netbeans/modules/generic/project/GenericProject.java b/ide/generic.project/src/org/netbeans/modules/generic/project/GenericProject.java index d2abdc298157..19a89baff325 100644 --- a/ide/generic.project/src/org/netbeans/modules/generic/project/GenericProject.java +++ b/ide/generic.project/src/org/netbeans/modules/generic/project/GenericProject.java @@ -74,6 +74,19 @@ static boolean isProject(FileObject projectDirectory) { && projectDirectory.getFileObject(MARKER_FILE) != null; } + /** + * Creates the marker file in the given directory if it does not + * exist yet, returning the marker. + */ + static FileObject createMarker(FileObject dir) throws IOException { + FileObject marker = dir.getFileObject(MARKER_FILE); + if (marker == null) { + marker = dir.createData(MARKER_FILE); + marker.setAttribute("displayName", dir.getNameExt()); // NOI18N + } + return marker; + } + @ServiceProvider(service=ProjectFactory.class) public static final class GenericProjectFactory implements ProjectFactory { @@ -92,11 +105,7 @@ public Project loadProject(FileObject projectDirectory, ProjectState state) thro @Override public void saveProject(Project project) throws IOException, ClassCastException { - FileObject dir = project.getProjectDirectory(); - if (dir.getFileObject(MARKER_FILE) == null) { - FileObject marker = dir.createData(MARKER_FILE); - marker.setAttribute("displayName", dir.getNameExt()); - } + createMarker(project.getProjectDirectory()); } } diff --git a/ide/generic.project/src/org/netbeans/modules/generic/project/GenericProjectTemplateDescription.html b/ide/generic.project/src/org/netbeans/modules/generic/project/GenericProjectTemplateDescription.html new file mode 100644 index 000000000000..f5ab3415e06b --- /dev/null +++ b/ide/generic.project/src/org/netbeans/modules/generic/project/GenericProjectTemplateDescription.html @@ -0,0 +1,29 @@ + + + + + + + Creates a new generic project. the project's logical view + shows all files (including sub projects) contained in the folder. + + diff --git a/ide/generic.project/src/org/netbeans/modules/generic/project/GenericProjectWizardIterator.java b/ide/generic.project/src/org/netbeans/modules/generic/project/GenericProjectWizardIterator.java new file mode 100644 index 000000000000..28397f92729a --- /dev/null +++ b/ide/generic.project/src/org/netbeans/modules/generic/project/GenericProjectWizardIterator.java @@ -0,0 +1,152 @@ +/* + * 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.generic.project; + +import java.io.File; +import java.io.IOException; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Set; +import javax.swing.JComponent; +import javax.swing.event.ChangeListener; +import org.netbeans.api.templates.TemplateRegistration; +import org.netbeans.spi.project.ui.support.ProjectChooser; +import org.openide.WizardDescriptor; +import org.openide.filesystems.FileObject; +import org.openide.filesystems.FileUtil; +import org.openide.util.NbBundle.Messages; + +/** + * The "New Generic Project" wizard. Instantiating it creates the project + * directory with the {@link GenericProject#MARKER_FILE} — or, if "keep + * repository clean" is selected, as a local project in NB's user config. + */ +@TemplateRegistration( + folder = "Project/Generic", // NOI18N + displayName = "#GenericProjectWizard_displayName", // NOI18N + description = "GenericProjectTemplateDescription.html", // NOI18N + iconBase = GenericProject.ICON, + position = 1000 +) +@Messages("GenericProjectWizard_displayName=New Generic Project") +public final class GenericProjectWizardIterator implements WizardDescriptor.InstantiatingIterator { + + private WizardDescriptor wiz; + private List> panels; + private int index; + + private List> createPanels() { + return List.of(new GenericProjectWizardPanel()); + } + + @Override + public Set instantiate() throws IOException { + File projectDir = FileUtil.normalizeFile((File) wiz.getProperty("projdir")); // NOI18N + Set created = createProject(projectDir); + File parent = projectDir.getParentFile(); + if (parent != null && parent.exists()) { + ProjectChooser.setProjectsFolder(parent); + } + return created; + } + + /** + * Creates the project directory containing the marker file. + */ + static Set createProject(File projectDir) throws IOException { + if (!projectDir.exists() && !projectDir.mkdirs()) { + throw new IOException("Cannot create folder: " + projectDir); // NOI18N + } + FileObject dir = FileUtil.toFileObject(projectDir); + if (dir == null) { + throw new IOException("Cannot map folder to the file system: " + projectDir); // NOI18N + } + GenericProject.createMarker(dir); + return Set.of(dir); + } + + @Override + public void initialize(WizardDescriptor wiz) { + this.wiz = wiz; + index = 0; + panels = createPanels(); + for (int i = 0; i < panels.size(); i++) { + if (panels.get(i).getComponent() instanceof JComponent jc) { + // Step #. + jc.putClientProperty("WizardPanel_contentSelectedIndex", i); // NOI18N + // The step names are the component names of the panels. + jc.putClientProperty("WizardPanel_contentData", jc.getName()); // NOI18N + } + } + } + + @Override + public void uninitialize(WizardDescriptor wiz) { + this.wiz.putProperty("projdir", null); // NOI18N + this.wiz.putProperty("name", null); // NOI18N + this.wiz = null; + panels = null; + } + + @Override + public String name() { + return "%d of %d".formatted(index + 1, panels.size()); // NOI18N + } + + @Override + public boolean hasNext() { + return index < panels.size() - 1; + } + + @Override + public boolean hasPrevious() { + return index > 0; + } + + @Override + public void nextPanel() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + index++; + } + + @Override + public void previousPanel() { + if (!hasPrevious()) { + throw new NoSuchElementException(); + } + index--; + } + + @Override + public WizardDescriptor.Panel current() { + return panels.get(index); + } + + @Override + public final void addChangeListener(ChangeListener l) { + } + + @Override + public final void removeChangeListener(ChangeListener l) { + } + +} diff --git a/ide/generic.project/src/org/netbeans/modules/generic/project/GenericProjectWizardPanel.java b/ide/generic.project/src/org/netbeans/modules/generic/project/GenericProjectWizardPanel.java new file mode 100644 index 000000000000..38344297ec8d --- /dev/null +++ b/ide/generic.project/src/org/netbeans/modules/generic/project/GenericProjectWizardPanel.java @@ -0,0 +1,280 @@ +/* + * 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.generic.project; + +import java.awt.Component; +import java.io.File; +import javax.swing.GroupLayout; +import javax.swing.GroupLayout.Alignment; +import javax.swing.JButton; +import javax.swing.JComponent; +import javax.swing.JFileChooser; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.LayoutStyle; +import javax.swing.JTextField; +import javax.swing.event.ChangeListener; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; +import javax.swing.text.Document; +import org.netbeans.spi.project.ui.support.ProjectChooser; +import org.openide.WizardDescriptor; +import org.openide.WizardValidationException; +import org.openide.awt.Mnemonics; +import org.openide.filesystems.FileUtil; +import org.openide.util.ChangeSupport; +import org.openide.util.NbBundle.Messages; + +/** + * The single wizard panel asking for the project name and location. + */ +@Messages({ + "LBL_CreateProjectStep=Name and Location", + "LBL_ProjectName=Project &Name:", + "LBL_ProjectLocation=Project &Location:", + "LBL_CreatedFolder=Project &Folder:", + "LBL_Browse=Br&owse...", + "LBL_BrowseDialogTitle=Select Project Location", + "MSG_DefaultProjectName=MyGenericProject", + "ERR_InvalidName=Project Name is not a valid folder name.", + "ERR_InvalidPath=Project Folder is not a valid path.", + "ERR_NotWritable=Project Folder cannot be created.", + "ERR_FolderExists=Project Folder already exists and is not empty.", +}) +final class GenericProjectWizardPanel implements WizardDescriptor.Panel, + WizardDescriptor.ValidatingPanel, + WizardDescriptor.FinishablePanel, + DocumentListener { + + private WizardDescriptor wiz; + private JComponent component; + + private final JTextField projectName = new JTextField(); + private final JTextField projectLocation = new JTextField(); + private final JTextField createdFolder = new JTextField(); + private final JButton browseButton = new JButton(); + private final ChangeSupport changeSupport = new ChangeSupport(this); + + @Override + public Component getComponent() { + if (component == null) { + component = createComponent(); + } + return component; + } + + @Override + public org.openide.util.HelpCtx getHelp() { + return null; + } + + @Override + public boolean isValid() { + getComponent(); + return valid(wiz); + } + + @Override + public final void addChangeListener(ChangeListener l) { + changeSupport.addChangeListener(l); + } + + @Override + public final void removeChangeListener(ChangeListener l) { + changeSupport.removeChangeListener(l); + } + + private void fireChangeEvent() { + changeSupport.fireChange(); + } + + @Override + public void readSettings(WizardDescriptor settings) { + wiz = settings; + File location = (File) wiz.getProperty("projdir"); // NOI18N + if (location == null || location.getParentFile() == null || !location.getParentFile().isDirectory()) { + location = ProjectChooser.getProjectsFolder(); + } else { + location = location.getParentFile(); + } + projectLocation.setText(location.getAbsolutePath()); + + String name = (String) wiz.getProperty("name"); // NOI18N + projectName.setText(name == null ? Bundle.MSG_DefaultProjectName() : name); + projectName.selectAll(); + updateCreatedFolder(); + } + + @Override + public void storeSettings(WizardDescriptor settings) { + settings.putProperty("name", projectName.getText().strip()); // NOI18N + settings.putProperty("projdir", new File(createdFolder.getText().strip())); // NOI18N + } + + @Override + public boolean isFinishPanel() { + return true; + } + + @Override + public void validate() throws WizardValidationException { + // nothing to validate + } + + private boolean valid(WizardDescriptor d) { + if (projectName.getText().isEmpty()) { + d.putProperty("WizardPanel_errorMessage", Bundle.ERR_InvalidName()); // NOI18N + return false; + } + File location = FileUtil.normalizeFile(new File(projectLocation.getText()).getAbsoluteFile()); + if (!location.isDirectory()) { + d.putProperty("WizardPanel_errorMessage", Bundle.ERR_InvalidPath()); // NOI18N + return false; + } + File destFolder = FileUtil.normalizeFile(new File(createdFolder.getText()).getAbsoluteFile()); + File parent = destFolder; + while (parent != null && !parent.exists()) { + parent = parent.getParentFile(); + } + if (parent == null || !parent.canWrite()) { + d.putProperty("WizardPanel_errorMessage", Bundle.ERR_NotWritable()); // NOI18N + return false; + } + if (FileUtil.toFileObject(parent) == null) { + d.putProperty("WizardPanel_errorMessage", Bundle.ERR_InvalidPath()); // NOI18N + return false; + } + File[] kids = destFolder.listFiles(); + if (destFolder.exists() && kids != null && kids.length > 0) { + // Folder exists and is not empty + d.putProperty("WizardPanel_errorMessage", Bundle.ERR_FolderExists()); // NOI18N + return false; + } + d.putProperty("WizardPanel_errorMessage", ""); // NOI18N + return true; + } + + private JComponent createComponent() { + JLabel nameLabel = new JLabel(); + Mnemonics.setLocalizedText(nameLabel, Bundle.LBL_ProjectName()); + JLabel locationLabel = new JLabel(); + Mnemonics.setLocalizedText(locationLabel, Bundle.LBL_ProjectLocation()); + JLabel folderLabel = new JLabel(); + Mnemonics.setLocalizedText(folderLabel, Bundle.LBL_CreatedFolder()); + createdFolder.setEditable(false); + Mnemonics.setLocalizedText(browseButton, Bundle.LBL_Browse()); + browseButton.addActionListener(e -> browse()); + + // Making all labels stretchable snaps the shorter ones to + // the widest label, so the fields start at the same x and + // stretch to the edge. + JPanel panel = new JPanel(); + GroupLayout layout = new GroupLayout(panel); + panel.setLayout(layout); + layout.setHorizontalGroup( + layout.createParallelGroup(Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addContainerGap() + .addGroup(layout.createParallelGroup(Alignment.LEADING, false) + .addComponent(nameLabel, GroupLayout.DEFAULT_SIZE, GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(locationLabel, GroupLayout.DEFAULT_SIZE, GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(folderLabel, GroupLayout.DEFAULT_SIZE, GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(Alignment.LEADING) + .addComponent(projectName, GroupLayout.DEFAULT_SIZE, 200, Short.MAX_VALUE) + .addGroup(layout.createSequentialGroup() + .addComponent(projectLocation, GroupLayout.DEFAULT_SIZE, 200, Short.MAX_VALUE) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) + .addComponent(browseButton)) + .addComponent(createdFolder, GroupLayout.DEFAULT_SIZE, 200, Short.MAX_VALUE)) + .addContainerGap()) + ); + layout.setVerticalGroup( + layout.createParallelGroup(Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addGroup(layout.createParallelGroup(Alignment.BASELINE) + .addComponent(nameLabel) + .addComponent(projectName)) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(Alignment.BASELINE) + .addComponent(locationLabel) + .addComponent(projectLocation) + .addComponent(browseButton)) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(Alignment.BASELINE) + .addComponent(folderLabel) + .addComponent(createdFolder)) + .addGap(0, 0, Short.MAX_VALUE) + ) + ); + projectName.getDocument().addDocumentListener(this); + projectLocation.getDocument().addDocumentListener(this); + + panel.setName(Bundle.LBL_CreateProjectStep()); + return panel; + } + + private void browse() { + JFileChooser chooser = new JFileChooser(); + chooser.setDialogTitle(Bundle.LBL_BrowseDialogTitle()); + chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); + String path = projectLocation.getText(); + if (!path.isEmpty()) { + File f = new File(path); + if (f.exists()) { + chooser.setSelectedFile(f); + } + } + if (JFileChooser.APPROVE_OPTION == chooser.showOpenDialog(component)) { + projectLocation.setText(FileUtil.normalizeFile(chooser.getSelectedFile()).getAbsolutePath()); + } + fireChangeEvent(); + } + + // Implementation of DocumentListener -------------------------------------- + + @Override + public void changedUpdate(DocumentEvent e) { + updateTexts(e); + } + + @Override + public void insertUpdate(DocumentEvent e) { + updateTexts(e); + } + + @Override + public void removeUpdate(DocumentEvent e) { + updateTexts(e); + } + + private void updateTexts(DocumentEvent e) { + Document doc = e.getDocument(); + if (doc == projectName.getDocument() || doc == projectLocation.getDocument()) { + updateCreatedFolder(); + } + fireChangeEvent(); + } + + private void updateCreatedFolder() { + createdFolder.setText(projectLocation.getText() + File.separator + projectName.getText()); + } + +} From da4a5e21296b9564df12b32025a2c19c79135b51 Mon Sep 17 00:00:00 2001 From: Michael Bien Date: Mon, 19 Oct 2026 09:48:51 +0200 Subject: [PATCH 04/13] [action] open required projects --- .../generic/project/GenericProject.java | 49 ++++++++++++++++++- .../GenericProjectLogicalViewProvider.java | 1 + .../generic/project/GenericProjectTest.java | 21 ++++++++ 3 files changed, 70 insertions(+), 1 deletion(-) diff --git a/ide/generic.project/src/org/netbeans/modules/generic/project/GenericProject.java b/ide/generic.project/src/org/netbeans/modules/generic/project/GenericProject.java index 19a89baff325..990695a6fd6a 100644 --- a/ide/generic.project/src/org/netbeans/modules/generic/project/GenericProject.java +++ b/ide/generic.project/src/org/netbeans/modules/generic/project/GenericProject.java @@ -21,14 +21,21 @@ import java.beans.PropertyChangeListener; import java.io.IOException; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; import javax.swing.Icon; import javax.swing.UIManager; +import javax.swing.event.ChangeListener; import org.netbeans.api.annotations.common.StaticResource; +import org.netbeans.api.project.FileOwnerQuery; import org.netbeans.api.project.Project; import org.netbeans.api.project.ProjectInformation; import org.netbeans.spi.project.ProjectFactory; import org.netbeans.spi.project.ProjectState; +import org.netbeans.spi.project.SubprojectProvider; import org.openide.filesystems.FileObject; +import org.openide.util.ChangeSupport; import org.openide.util.ImageUtilities; import org.openide.util.Lookup; import org.openide.util.lookup.Lookups; @@ -54,7 +61,7 @@ private GenericProject(FileObject projectDirectory) { this.lookup = Lookups.fixed( new GenericProjectLogicalViewProvider(this), new GenericProjectInfo(this), - new SubprojectProviderImpl(this), + new GenericSubprojectProvider(this), this ); } @@ -141,4 +148,44 @@ public void removePropertyChangeListener(PropertyChangeListener listener) {} } + /** + * Exposes the projects contained directly in the project directory + * as subprojects. Adds "Open Required Projects" and "Open Selected Project" actions. + */ + private static final class GenericSubprojectProvider implements SubprojectProvider { + + private final GenericProject prj; + private final ChangeSupport changeSupport = new ChangeSupport(this); + + GenericSubprojectProvider(GenericProject prj) { + this.prj = prj; + } + + @Override + public Set getSubprojects() { + Set subprojects = new LinkedHashSet<>(); + FileObject[] children = prj.getProjectDirectory().getChildren(); + if (children != null) { + for (FileObject child : children) { + Project owner = FileOwnerQuery.getOwner(child); + if (owner != null && !owner.equals(prj)) { + subprojects.add(owner); + } + } + } + return Collections.unmodifiableSet(subprojects); + } + + @Override + public void addChangeListener(ChangeListener listener) { + changeSupport.addChangeListener(listener); + } + + @Override + public void removeChangeListener(ChangeListener listener) { + changeSupport.removeChangeListener(listener); + } + + } + } diff --git a/ide/generic.project/src/org/netbeans/modules/generic/project/GenericProjectLogicalViewProvider.java b/ide/generic.project/src/org/netbeans/modules/generic/project/GenericProjectLogicalViewProvider.java index 5f3b66decf41..41feb6c40fa3 100644 --- a/ide/generic.project/src/org/netbeans/modules/generic/project/GenericProjectLogicalViewProvider.java +++ b/ide/generic.project/src/org/netbeans/modules/generic/project/GenericProjectLogicalViewProvider.java @@ -51,6 +51,7 @@ public Node createLogicalView() { @Override public Action[] getActions(boolean context) { return new Action[] { + CommonProjectActions.openSubprojectsAction(), CommonProjectActions.closeProjectAction(), }; } diff --git a/ide/generic.project/test/unit/src/org/netbeans/modules/generic/project/GenericProjectTest.java b/ide/generic.project/test/unit/src/org/netbeans/modules/generic/project/GenericProjectTest.java index f5e15ce5bcad..d3e187666d23 100644 --- a/ide/generic.project/test/unit/src/org/netbeans/modules/generic/project/GenericProjectTest.java +++ b/ide/generic.project/test/unit/src/org/netbeans/modules/generic/project/GenericProjectTest.java @@ -20,9 +20,11 @@ package org.netbeans.modules.generic.project; import java.io.File; +import java.util.Set; import org.netbeans.api.project.Project; import org.netbeans.api.project.ProjectManager; import org.netbeans.junit.NbTestCase; +import org.netbeans.spi.project.SubprojectProvider; import org.netbeans.spi.project.ui.LogicalViewProvider; import org.openide.filesystems.FileObject; import org.openide.filesystems.FileUtil; @@ -67,6 +69,25 @@ public void testLogicalViewShowsAllFiles() throws Exception { assertEquals(2, children.length); } + public void testSubprojectsAreFirstLevelOnly() throws Exception { + FileObject root = newProjectDir(); + FileUtil.createData(root, GenericProject.MARKER_FILE); + FileObject child = FileUtil.createFolder(root, "child"); // NOI18N + FileUtil.createData(child, GenericProject.MARKER_FILE); + FileUtil.createFolder(root, "plain"); // NOI18N + FileUtil.createData(root, "readme.txt"); // NOI18N + FileObject deep = FileUtil.createFolder(FileUtil.createFolder(child, "inner"), "deep"); // NOI18N + FileUtil.createData(deep, GenericProject.MARKER_FILE); + + Project prj = ProjectManager.getDefault().findProject(root); + assertNotNull(prj); + SubprojectProvider spp = prj.getLookup().lookup(SubprojectProvider.class); + assertNotNull(spp); + Set subs = spp.getSubprojects(); + assertEquals(1, subs.size()); + assertEquals(child, subs.iterator().next().getProjectDirectory()); + } + private FileObject newProjectDir() throws Exception { clearWorkDir(); File dir = new File(getWorkDir(), "proj-" + getName()); // NOI18N From 759198dde62a34a40f7a63bc178f1971b962fe54 Mon Sep 17 00:00:00 2001 From: Michael Bien Date: Mon, 19 Oct 2026 12:03:51 +0200 Subject: [PATCH 05/13] [action] rename action + some refactoring --- .github/workflows/main.yml | 3 ++ ide/generic.project/manifest.mf | 3 +- .../generic/project/GenericProject.java | 14 ++++----- ...ovider.java => GpLogicalViewProvider.java} | 2 +- ...iption.html => GpTemplateDescription.html} | 0 ...ardIterator.java => GpWizardIterator.java} | 10 +++--- ...ectWizardPanel.java => GpWizardPanel.java} | 2 +- .../modules/generic/project/layer.xml | 31 +++++++++++++++++++ .../generic/project/GenericProjectTest.java | 18 +++++++++-- 9 files changed, 66 insertions(+), 17 deletions(-) rename ide/generic.project/src/org/netbeans/modules/generic/project/{GenericProjectLogicalViewProvider.java => GpLogicalViewProvider.java} (96%) rename ide/generic.project/src/org/netbeans/modules/generic/project/{GenericProjectTemplateDescription.html => GpTemplateDescription.html} (100%) rename ide/generic.project/src/org/netbeans/modules/generic/project/{GenericProjectWizardIterator.java => GpWizardIterator.java} (92%) rename ide/generic.project/src/org/netbeans/modules/generic/project/{GenericProjectWizardPanel.java => GpWizardPanel.java} (99%) create mode 100644 ide/generic.project/src/org/netbeans/modules/generic/project/layer.xml diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 5e4f4debfbd6..4cc525d6f9ba 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -661,6 +661,9 @@ jobs: - name: ide/extexecution.base run: ant $OPTS -f ide/extexecution.base test + - name: ide/generic.project + run: ant $OPTS -f ide/generic.project test + - name: ide/gsf.testrunner.ui run: ant $OPTS -f ide/gsf.testrunner.ui test diff --git a/ide/generic.project/manifest.mf b/ide/generic.project/manifest.mf index 177f6e972319..ffd91fe6bd8d 100644 --- a/ide/generic.project/manifest.mf +++ b/ide/generic.project/manifest.mf @@ -1,4 +1,5 @@ Manifest-Version: 1.0 OpenIDE-Module: org.netbeans.modules.generic.project +OpenIDE-Module-Layer: org/netbeans/modules/generic/project/layer.xml OpenIDE-Module-Localizing-Bundle: org/netbeans/modules/generic/project/Bundle.properties -OpenIDE-Module-Specification-Version: 1.0 \ No newline at end of file +OpenIDE-Module-Specification-Version: 1.0 diff --git a/ide/generic.project/src/org/netbeans/modules/generic/project/GenericProject.java b/ide/generic.project/src/org/netbeans/modules/generic/project/GenericProject.java index 990695a6fd6a..2ecb17deac1d 100644 --- a/ide/generic.project/src/org/netbeans/modules/generic/project/GenericProject.java +++ b/ide/generic.project/src/org/netbeans/modules/generic/project/GenericProject.java @@ -59,9 +59,9 @@ public final class GenericProject implements Project { private GenericProject(FileObject projectDirectory) { this.projectDirectory = projectDirectory; this.lookup = Lookups.fixed( - new GenericProjectLogicalViewProvider(this), - new GenericProjectInfo(this), - new GenericSubprojectProvider(this), + new GpLogicalViewProvider(this), + new GpInfo(this), + new GpSubprojectProvider(this), this ); } @@ -95,7 +95,7 @@ static FileObject createMarker(FileObject dir) throws IOException { } @ServiceProvider(service=ProjectFactory.class) - public static final class GenericProjectFactory implements ProjectFactory { + public static final class GpFactory implements ProjectFactory { @Override public boolean isProject(FileObject projectDirectory) { @@ -117,7 +117,7 @@ public void saveProject(Project project) throws IOException, ClassCastException } - private record GenericProjectInfo(GenericProject prj) implements ProjectInformation { + private record GpInfo(GenericProject prj) implements ProjectInformation { @Override public String getName() { @@ -152,12 +152,12 @@ public void removePropertyChangeListener(PropertyChangeListener listener) {} * Exposes the projects contained directly in the project directory * as subprojects. Adds "Open Required Projects" and "Open Selected Project" actions. */ - private static final class GenericSubprojectProvider implements SubprojectProvider { + private static final class GpSubprojectProvider implements SubprojectProvider { private final GenericProject prj; private final ChangeSupport changeSupport = new ChangeSupport(this); - GenericSubprojectProvider(GenericProject prj) { + GpSubprojectProvider(GenericProject prj) { this.prj = prj; } diff --git a/ide/generic.project/src/org/netbeans/modules/generic/project/GenericProjectLogicalViewProvider.java b/ide/generic.project/src/org/netbeans/modules/generic/project/GpLogicalViewProvider.java similarity index 96% rename from ide/generic.project/src/org/netbeans/modules/generic/project/GenericProjectLogicalViewProvider.java rename to ide/generic.project/src/org/netbeans/modules/generic/project/GpLogicalViewProvider.java index 41feb6c40fa3..7846114e9bcf 100644 --- a/ide/generic.project/src/org/netbeans/modules/generic/project/GenericProjectLogicalViewProvider.java +++ b/ide/generic.project/src/org/netbeans/modules/generic/project/GpLogicalViewProvider.java @@ -36,7 +36,7 @@ * The logical view of a generic project simply shows all files contained * in the project directory. */ -public record GenericProjectLogicalViewProvider(GenericProject project) implements LogicalViewProvider { +public record GpLogicalViewProvider(GenericProject project) implements LogicalViewProvider { @Override public Node createLogicalView() { diff --git a/ide/generic.project/src/org/netbeans/modules/generic/project/GenericProjectTemplateDescription.html b/ide/generic.project/src/org/netbeans/modules/generic/project/GpTemplateDescription.html similarity index 100% rename from ide/generic.project/src/org/netbeans/modules/generic/project/GenericProjectTemplateDescription.html rename to ide/generic.project/src/org/netbeans/modules/generic/project/GpTemplateDescription.html diff --git a/ide/generic.project/src/org/netbeans/modules/generic/project/GenericProjectWizardIterator.java b/ide/generic.project/src/org/netbeans/modules/generic/project/GpWizardIterator.java similarity index 92% rename from ide/generic.project/src/org/netbeans/modules/generic/project/GenericProjectWizardIterator.java rename to ide/generic.project/src/org/netbeans/modules/generic/project/GpWizardIterator.java index 28397f92729a..6b15fdd44913 100644 --- a/ide/generic.project/src/org/netbeans/modules/generic/project/GenericProjectWizardIterator.java +++ b/ide/generic.project/src/org/netbeans/modules/generic/project/GpWizardIterator.java @@ -40,20 +40,20 @@ */ @TemplateRegistration( folder = "Project/Generic", // NOI18N - displayName = "#GenericProjectWizard_displayName", // NOI18N - description = "GenericProjectTemplateDescription.html", // NOI18N + displayName = "#GpWizard_displayName", // NOI18N + description = "GpTemplateDescription.html", // NOI18N iconBase = GenericProject.ICON, position = 1000 ) -@Messages("GenericProjectWizard_displayName=New Generic Project") -public final class GenericProjectWizardIterator implements WizardDescriptor.InstantiatingIterator { +@Messages("GpWizard_displayName=New Generic Project") +public final class GpWizardIterator implements WizardDescriptor.InstantiatingIterator { private WizardDescriptor wiz; private List> panels; private int index; private List> createPanels() { - return List.of(new GenericProjectWizardPanel()); + return List.of(new GpWizardPanel()); } @Override diff --git a/ide/generic.project/src/org/netbeans/modules/generic/project/GenericProjectWizardPanel.java b/ide/generic.project/src/org/netbeans/modules/generic/project/GpWizardPanel.java similarity index 99% rename from ide/generic.project/src/org/netbeans/modules/generic/project/GenericProjectWizardPanel.java rename to ide/generic.project/src/org/netbeans/modules/generic/project/GpWizardPanel.java index 38344297ec8d..6b165ac092c1 100644 --- a/ide/generic.project/src/org/netbeans/modules/generic/project/GenericProjectWizardPanel.java +++ b/ide/generic.project/src/org/netbeans/modules/generic/project/GpWizardPanel.java @@ -58,7 +58,7 @@ "ERR_NotWritable=Project Folder cannot be created.", "ERR_FolderExists=Project Folder already exists and is not empty.", }) -final class GenericProjectWizardPanel implements WizardDescriptor.Panel, +final class GpWizardPanel implements WizardDescriptor.Panel, WizardDescriptor.ValidatingPanel, WizardDescriptor.FinishablePanel, DocumentListener { diff --git a/ide/generic.project/src/org/netbeans/modules/generic/project/layer.xml b/ide/generic.project/src/org/netbeans/modules/generic/project/layer.xml new file mode 100644 index 000000000000..480e9e1ee993 --- /dev/null +++ b/ide/generic.project/src/org/netbeans/modules/generic/project/layer.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + diff --git a/ide/generic.project/test/unit/src/org/netbeans/modules/generic/project/GenericProjectTest.java b/ide/generic.project/test/unit/src/org/netbeans/modules/generic/project/GenericProjectTest.java index d3e187666d23..deddb60c76d3 100644 --- a/ide/generic.project/test/unit/src/org/netbeans/modules/generic/project/GenericProjectTest.java +++ b/ide/generic.project/test/unit/src/org/netbeans/modules/generic/project/GenericProjectTest.java @@ -45,13 +45,13 @@ public GenericProjectTest(String name) { public void testNoMarkerNotProject() throws Exception { FileObject root = newProjectDir(); - assertFalse(new GenericProject.GenericProjectFactory().isProject(root)); + assertFalse(new GenericProject.GpFactory().isProject(root)); } public void testMarkerFileIsProject() throws Exception { FileObject root = newProjectDir(); FileUtil.createData(root, GenericProject.MARKER_FILE); - assertTrue(new GenericProject.GenericProjectFactory().isProject(root)); + assertTrue(new GenericProject.GpFactory().isProject(root)); } public void testLogicalViewShowsAllFiles() throws Exception { @@ -88,6 +88,20 @@ public void testSubprojectsAreFirstLevelOnly() throws Exception { assertEquals(child, subs.iterator().next().getProjectDirectory()); } + public void testCreateProject() throws Exception { + clearWorkDir(); + File dir = new File(getWorkDir(), "proj-" + getName()); // NOI18N + Set created = GpWizardIterator.createProject(dir); + assertEquals(1, created.size()); + FileObject dirFo = created.iterator().next(); + FileObject marker = dirFo.getFileObject(GenericProject.MARKER_FILE); + assertNotNull(marker); + assertEquals(dir.getName(), marker.getAttribute("displayName")); // NOI18N + Project prj = ProjectManager.getDefault().findProject(dirFo); + assertNotNull(prj); + assertTrue(prj instanceof GenericProject); + } + private FileObject newProjectDir() throws Exception { clearWorkDir(); File dir = new File(getWorkDir(), "proj-" + getName()); // NOI18N From 24de229b8925fdd93ff1f146f1ca014c2412e1c0 Mon Sep 17 00:00:00 2001 From: Michael Bien Date: Mon, 19 Oct 2026 13:38:54 +0200 Subject: [PATCH 06/13] general project properties --- .../generic/project/GenericProject.java | 9 ++ .../project/GpLogicalViewProvider.java | 1 + .../GpCompositeCategoryProvider.java | 112 ++++++++++++++++++ .../ui/customizer/GpCustomizerProvider.java | 56 +++++++++ 4 files changed, 178 insertions(+) create mode 100644 ide/generic.project/src/org/netbeans/modules/generic/project/ui/customizer/GpCompositeCategoryProvider.java create mode 100644 ide/generic.project/src/org/netbeans/modules/generic/project/ui/customizer/GpCustomizerProvider.java diff --git a/ide/generic.project/src/org/netbeans/modules/generic/project/GenericProject.java b/ide/generic.project/src/org/netbeans/modules/generic/project/GenericProject.java index 2ecb17deac1d..d2dbf0f234fd 100644 --- a/ide/generic.project/src/org/netbeans/modules/generic/project/GenericProject.java +++ b/ide/generic.project/src/org/netbeans/modules/generic/project/GenericProject.java @@ -31,6 +31,7 @@ import org.netbeans.api.project.FileOwnerQuery; import org.netbeans.api.project.Project; import org.netbeans.api.project.ProjectInformation; +import org.netbeans.modules.generic.project.ui.customizer.GpCustomizerProvider; import org.netbeans.spi.project.ProjectFactory; import org.netbeans.spi.project.ProjectState; import org.netbeans.spi.project.SubprojectProvider; @@ -50,6 +51,13 @@ public final class GenericProject implements Project { public static final String MARKER_FILE = ".nbgp"; // NOI18N + /** + * The project type key used for this project type's contributions + * in the layer filesystem + * (e.g. {@code Projects/org-netbeans-modules-generic-project/Customizer}). + */ + public static final String PROJECT_KEY = "org-netbeans-modules-generic-project"; // NOI18N + @StaticResource public static final String ICON = "org/netbeans/modules/generic/project/gp.svg"; // NOI18N @@ -62,6 +70,7 @@ private GenericProject(FileObject projectDirectory) { new GpLogicalViewProvider(this), new GpInfo(this), new GpSubprojectProvider(this), + new GpCustomizerProvider(this), this ); } diff --git a/ide/generic.project/src/org/netbeans/modules/generic/project/GpLogicalViewProvider.java b/ide/generic.project/src/org/netbeans/modules/generic/project/GpLogicalViewProvider.java index 7846114e9bcf..f77708484603 100644 --- a/ide/generic.project/src/org/netbeans/modules/generic/project/GpLogicalViewProvider.java +++ b/ide/generic.project/src/org/netbeans/modules/generic/project/GpLogicalViewProvider.java @@ -53,6 +53,7 @@ public Action[] getActions(boolean context) { return new Action[] { CommonProjectActions.openSubprojectsAction(), CommonProjectActions.closeProjectAction(), + CommonProjectActions.customizeProjectAction(), }; } diff --git a/ide/generic.project/src/org/netbeans/modules/generic/project/ui/customizer/GpCompositeCategoryProvider.java b/ide/generic.project/src/org/netbeans/modules/generic/project/ui/customizer/GpCompositeCategoryProvider.java new file mode 100644 index 000000000000..f70baf14bd3d --- /dev/null +++ b/ide/generic.project/src/org/netbeans/modules/generic/project/ui/customizer/GpCompositeCategoryProvider.java @@ -0,0 +1,112 @@ +/* + * 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.generic.project.ui.customizer; + +import javax.swing.GroupLayout; +import javax.swing.GroupLayout.Alignment; +import javax.swing.JComponent; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JTextField; +import javax.swing.LayoutStyle; +import org.netbeans.api.project.Project; +import org.netbeans.api.project.ProjectInformation; +import org.netbeans.modules.generic.project.GenericProject; +import org.netbeans.spi.project.ui.support.ProjectCustomizer; +import org.openide.util.Lookup; +import org.openide.util.NbBundle.Messages; + +/** + * Provides the "General" category of the project customizer, showing + * the project name and location (both read-only). + * + *

Additional categories can be added to the same dialog by registering + * further {@link ProjectCustomizer.CompositeCategoryProvider} instances + * for {@link GenericProject#PROJECT_KEY}. + */ +@Messages({ + "LBL_General=General", + "LBL_Name=Name:", + "LBL_Location=Location:", +}) +public final class GpCompositeCategoryProvider implements ProjectCustomizer.CompositeCategoryProvider { + + private static final String CATEGORY_GENERAL = "category-general"; // NOI18N + + @ProjectCustomizer.CompositeCategoryProvider.Registration( + projectType=GenericProject.PROJECT_KEY, + position=100 + ) + public static GpCompositeCategoryProvider createGeneralCategory() { + return new GpCompositeCategoryProvider(); + } + + private GpCompositeCategoryProvider() { + } + + @Override + public ProjectCustomizer.Category createCategory(Lookup context) { + return ProjectCustomizer.Category.create(CATEGORY_GENERAL, Bundle.LBL_General(), null); + } + + @Override + public JComponent createComponent(ProjectCustomizer.Category category, Lookup context) { + Project prj = context.lookup(Project.class); + ProjectInformation info = prj != null ? prj.getLookup().lookup(ProjectInformation.class) : null; + + JLabel nameLabel = new JLabel(Bundle.LBL_Name()); + JLabel locationLabel = new JLabel(Bundle.LBL_Location()); + JTextField nameField = new JTextField(info != null ? info.getName() : ""); + JTextField locationField = new JTextField(prj != null ? prj.getProjectDirectory().getPath() : ""); + nameField.setEditable(false); + locationField.setEditable(false); + + JPanel panel = new JPanel(); + GroupLayout layout = new GroupLayout(panel); + panel.setLayout(layout); + layout.setHorizontalGroup( + layout.createParallelGroup(Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addContainerGap() + .addGroup(layout.createParallelGroup(Alignment.LEADING, false) + .addComponent(nameLabel, GroupLayout.DEFAULT_SIZE, GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(locationLabel, GroupLayout.DEFAULT_SIZE, GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(Alignment.LEADING) + .addComponent(nameField, GroupLayout.DEFAULT_SIZE, 200, Short.MAX_VALUE) + .addComponent(locationField, GroupLayout.DEFAULT_SIZE, 200, Short.MAX_VALUE)) + .addContainerGap()) + ); + layout.setVerticalGroup( + layout.createParallelGroup(Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addGroup(layout.createParallelGroup(Alignment.BASELINE) + .addComponent(nameLabel) + .addComponent(nameField)) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(Alignment.BASELINE) + .addComponent(locationLabel) + .addComponent(locationField)) + .addGap(0, 0, Short.MAX_VALUE)) + ); + return panel; + } + +} \ No newline at end of file diff --git a/ide/generic.project/src/org/netbeans/modules/generic/project/ui/customizer/GpCustomizerProvider.java b/ide/generic.project/src/org/netbeans/modules/generic/project/ui/customizer/GpCustomizerProvider.java new file mode 100644 index 000000000000..32fe4b434b17 --- /dev/null +++ b/ide/generic.project/src/org/netbeans/modules/generic/project/ui/customizer/GpCustomizerProvider.java @@ -0,0 +1,56 @@ +/* + * 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.generic.project.ui.customizer; + +import java.awt.Dialog; +import org.netbeans.modules.generic.project.GenericProject; +import org.netbeans.spi.project.ui.CustomizerProvider2; +import org.netbeans.spi.project.ui.support.ProjectCustomizer; +import org.openide.util.HelpCtx; + +/** + * Shows the project properties (customizer) window for a generic project. + */ +public final class GpCustomizerProvider implements CustomizerProvider2 { + + private final GenericProject prj; + + public GpCustomizerProvider(GenericProject prj) { + this.prj = prj; + } + + @Override + public void showCustomizer() { + showCustomizer(null, null); + } + + @Override + public void showCustomizer(String preselectedCategory, String preselectedSubCategory) { + Dialog dialog = ProjectCustomizer.createCustomizerDialog( + "Projects/" + GenericProject.PROJECT_KEY + "/Customizer", // NOI18N + prj.getLookup(), + preselectedCategory, + evt -> { + }, + HelpCtx.DEFAULT_HELP); + dialog.setVisible(true); + } + +} \ No newline at end of file From 8cdb62a0fa48715c320bb955766e5972e080460b Mon Sep 17 00:00:00 2001 From: Michael Bien Date: Mon, 19 Oct 2026 15:39:06 +0200 Subject: [PATCH 07/13] marker now optional --- .../generic/project/GenericProject.java | 67 +++++++++++++++++-- .../generic/project/GpWizardIterator.java | 16 +++-- .../generic/project/GpWizardPanel.java | 15 +++++ .../GpCompositeCategoryProvider.java | 31 ++++++++- .../generic/project/GenericProjectTest.java | 48 ++++++++++++- 5 files changed, 167 insertions(+), 10 deletions(-) diff --git a/ide/generic.project/src/org/netbeans/modules/generic/project/GenericProject.java b/ide/generic.project/src/org/netbeans/modules/generic/project/GenericProject.java index d2dbf0f234fd..454e0475d8af 100644 --- a/ide/generic.project/src/org/netbeans/modules/generic/project/GenericProject.java +++ b/ide/generic.project/src/org/netbeans/modules/generic/project/GenericProject.java @@ -43,14 +43,22 @@ import org.openide.util.lookup.ServiceProvider; /** - * A generic project. Any directory containing the {@link #MARKER_FILE} is - * recognized as a generic project. The logical view of the project simply shows + * A generic project. A directory is recognized as a generic project + * if it contains the {@link #MARKER_FILE} or, for local + * (private) projects, if it carries the {@link #MARKER_ATTRIBUTE} + * FileObject attribute. The logical view of the project simply shows * all files contained in the project directory. */ public final class GenericProject implements Project { public static final String MARKER_FILE = ".nbgp"; // NOI18N + /** + * Name of the FileObject attribute that marks a project directory + * as a generic project without the marker file (local project). + */ + public static final String MARKER_ATTRIBUTE = "org.netbeans.modules.generic.project"; // NOI18N + /** * The project type key used for this project type's contributions * in the layer filesystem @@ -87,7 +95,23 @@ public Lookup getLookup() { static boolean isProject(FileObject projectDirectory) { return projectDirectory != null - && projectDirectory.getFileObject(MARKER_FILE) != null; + && (hasMarker(projectDirectory) || isAttributeProject(projectDirectory)); + } + + /** + * Whether the given directory contains the marker file. + */ + public static boolean hasMarker(FileObject dir) { + return dir != null && dir.getFileObject(MARKER_FILE) != null; + } + + /** + * Whether the given directory is marked as a generic project by + * the {@link #MARKER_ATTRIBUTE} FileObject attribute (local + * project). + */ + public static boolean isAttributeProject(FileObject dir) { + return dir != null && Boolean.TRUE.equals(dir.getAttribute(MARKER_ATTRIBUTE)); } /** @@ -103,6 +127,34 @@ static FileObject createMarker(FileObject dir) throws IOException { return marker; } + /** + * Adjusts the directory so the marker is (or isn't) a file in the + * working tree. + * + *

Turning the marker off stores the {@link #MARKER_ATTRIBUTE} + * before deleting the marker file, and turning it on + * creates the marker before clearing the attribute, so + * the directory is never in a state where it is unrecognized. + * + * @param dir the project directory + * @param markerFile whether the marker file should exist in the + * working tree afterwards + */ + public static void setMarkerState(FileObject dir, boolean markerFile) throws IOException { + if (markerFile) { + createMarker(dir); + if (isAttributeProject(dir)) { + dir.setAttribute(MARKER_ATTRIBUTE, null); + } + } else { + dir.setAttribute(MARKER_ATTRIBUTE, Boolean.TRUE); + FileObject marker = dir.getFileObject(MARKER_FILE); + if (marker != null) { + marker.delete(); + } + } + } + @ServiceProvider(service=ProjectFactory.class) public static final class GpFactory implements ProjectFactory { @@ -121,7 +173,14 @@ public Project loadProject(FileObject projectDirectory, ProjectState state) thro @Override public void saveProject(Project project) throws IOException, ClassCastException { - createMarker(project.getProjectDirectory()); + FileObject dir = project.getProjectDirectory(); + if (isAttributeProject(dir)) { + // local project: recognition lives in the FileObject + // attribute, never (re)create the marker file + dir.setAttribute(MARKER_ATTRIBUTE, Boolean.TRUE); + } else { + createMarker(dir); + } } } diff --git a/ide/generic.project/src/org/netbeans/modules/generic/project/GpWizardIterator.java b/ide/generic.project/src/org/netbeans/modules/generic/project/GpWizardIterator.java index 6b15fdd44913..7b18eadf1fa2 100644 --- a/ide/generic.project/src/org/netbeans/modules/generic/project/GpWizardIterator.java +++ b/ide/generic.project/src/org/netbeans/modules/generic/project/GpWizardIterator.java @@ -59,7 +59,8 @@ private List> createPanels() { @Override public Set instantiate() throws IOException { File projectDir = FileUtil.normalizeFile((File) wiz.getProperty("projdir")); // NOI18N - Set created = createProject(projectDir); + boolean localMarker = Boolean.TRUE.equals(wiz.getProperty("localMarker")); // NOI18N + Set created = createProject(projectDir, localMarker); File parent = projectDir.getParentFile(); if (parent != null && parent.exists()) { ProjectChooser.setProjectsFolder(parent); @@ -68,9 +69,11 @@ public Set instantiate() throws IOException { } /** - * Creates the project directory containing the marker file. + * Creates the project directory containing the marker file, or — + * for {@code localMarker} — marks it as a local project by the + * FileObject attribute only. */ - static Set createProject(File projectDir) throws IOException { + static Set createProject(File projectDir, boolean localMarker) throws IOException { if (!projectDir.exists() && !projectDir.mkdirs()) { throw new IOException("Cannot create folder: " + projectDir); // NOI18N } @@ -78,7 +81,11 @@ static Set createProject(File projectDir) throws IOException { if (dir == null) { throw new IOException("Cannot map folder to the file system: " + projectDir); // NOI18N } - GenericProject.createMarker(dir); + if (localMarker) { + GenericProject.setMarkerState(dir, false); + } else { + GenericProject.createMarker(dir); + } return Set.of(dir); } @@ -101,6 +108,7 @@ public void initialize(WizardDescriptor wiz) { public void uninitialize(WizardDescriptor wiz) { this.wiz.putProperty("projdir", null); // NOI18N this.wiz.putProperty("name", null); // NOI18N + this.wiz.putProperty("localMarker", null); // NOI18N this.wiz = null; panels = null; } diff --git a/ide/generic.project/src/org/netbeans/modules/generic/project/GpWizardPanel.java b/ide/generic.project/src/org/netbeans/modules/generic/project/GpWizardPanel.java index 6b165ac092c1..06fdac89406f 100644 --- a/ide/generic.project/src/org/netbeans/modules/generic/project/GpWizardPanel.java +++ b/ide/generic.project/src/org/netbeans/modules/generic/project/GpWizardPanel.java @@ -24,6 +24,7 @@ import javax.swing.GroupLayout; import javax.swing.GroupLayout.Alignment; import javax.swing.JButton; +import javax.swing.JCheckBox; import javax.swing.JComponent; import javax.swing.JFileChooser; import javax.swing.JLabel; @@ -52,6 +53,10 @@ "LBL_CreatedFolder=Project &Folder:", "LBL_Browse=Br&owse...", "LBL_BrowseDialogTitle=Select Project Location", + "LBL_LocalMarker=Keep repository &clean (store marker locally)", + "TT_LocalMarker=" + + "

The project marker is stored in the IDE's user directory instead of the .nbgp file in the project folder.


" + + "

The project is then only recognized by this IDE installation at this location.

", "MSG_DefaultProjectName=MyGenericProject", "ERR_InvalidName=Project Name is not a valid folder name.", "ERR_InvalidPath=Project Folder is not a valid path.", @@ -70,6 +75,7 @@ final class GpWizardPanel implements WizardDescriptor.Panel, private final JTextField projectLocation = new JTextField(); private final JTextField createdFolder = new JTextField(); private final JButton browseButton = new JButton(); + private final JCheckBox localMarker = new JCheckBox(); private final ChangeSupport changeSupport = new ChangeSupport(this); @Override @@ -119,6 +125,7 @@ public void readSettings(WizardDescriptor settings) { String name = (String) wiz.getProperty("name"); // NOI18N projectName.setText(name == null ? Bundle.MSG_DefaultProjectName() : name); projectName.selectAll(); + localMarker.setSelected(false); updateCreatedFolder(); } @@ -126,6 +133,7 @@ public void readSettings(WizardDescriptor settings) { public void storeSettings(WizardDescriptor settings) { settings.putProperty("name", projectName.getText().strip()); // NOI18N settings.putProperty("projdir", new File(createdFolder.getText().strip())); // NOI18N + settings.putProperty("localMarker", localMarker.isSelected()); // NOI18N } @Override @@ -181,6 +189,8 @@ private JComponent createComponent() { createdFolder.setEditable(false); Mnemonics.setLocalizedText(browseButton, Bundle.LBL_Browse()); browseButton.addActionListener(e -> browse()); + Mnemonics.setLocalizedText(localMarker, Bundle.LBL_LocalMarker()); + localMarker.setToolTipText(Bundle.TT_LocalMarker()); // Making all labels stretchable snaps the shorter ones to // the widest label, so the fields start at the same x and @@ -205,6 +215,9 @@ private JComponent createComponent() { .addComponent(browseButton)) .addComponent(createdFolder, GroupLayout.DEFAULT_SIZE, 200, Short.MAX_VALUE)) .addContainerGap()) + .addGroup(layout.createSequentialGroup() + .addContainerGap() + .addComponent(localMarker)) ); layout.setVerticalGroup( layout.createParallelGroup(Alignment.LEADING) @@ -221,6 +234,8 @@ private JComponent createComponent() { .addGroup(layout.createParallelGroup(Alignment.BASELINE) .addComponent(folderLabel) .addComponent(createdFolder)) + .addGap(18, 18, 18) + .addComponent(localMarker) .addGap(0, 0, Short.MAX_VALUE) ) ); diff --git a/ide/generic.project/src/org/netbeans/modules/generic/project/ui/customizer/GpCompositeCategoryProvider.java b/ide/generic.project/src/org/netbeans/modules/generic/project/ui/customizer/GpCompositeCategoryProvider.java index f70baf14bd3d..60186d231a47 100644 --- a/ide/generic.project/src/org/netbeans/modules/generic/project/ui/customizer/GpCompositeCategoryProvider.java +++ b/ide/generic.project/src/org/netbeans/modules/generic/project/ui/customizer/GpCompositeCategoryProvider.java @@ -19,8 +19,12 @@ package org.netbeans.modules.generic.project.ui.customizer; +import java.io.IOException; +import java.util.logging.Level; +import java.util.logging.Logger; import javax.swing.GroupLayout; import javax.swing.GroupLayout.Alignment; +import javax.swing.JCheckBox; import javax.swing.JComponent; import javax.swing.JLabel; import javax.swing.JPanel; @@ -30,6 +34,7 @@ import org.netbeans.api.project.ProjectInformation; import org.netbeans.modules.generic.project.GenericProject; import org.netbeans.spi.project.ui.support.ProjectCustomizer; +import org.openide.filesystems.FileObject; import org.openide.util.Lookup; import org.openide.util.NbBundle.Messages; @@ -45,6 +50,10 @@ "LBL_General=General", "LBL_Name=Name:", "LBL_Location=Location:", + "LBL_MarkerFile=Marker file in project directory", + "TT_MarkerFile=" + + "

When unchecked, the .nbgp marker file is removed from the project directory.

" + + "

The project becomes a local (private) project.

", }) public final class GpCompositeCategoryProvider implements ProjectCustomizer.CompositeCategoryProvider { @@ -70,13 +79,17 @@ public ProjectCustomizer.Category createCategory(Lookup context) { public JComponent createComponent(ProjectCustomizer.Category category, Lookup context) { Project prj = context.lookup(Project.class); ProjectInformation info = prj != null ? prj.getLookup().lookup(ProjectInformation.class) : null; + FileObject projectDir = prj != null ? prj.getProjectDirectory() : null; JLabel nameLabel = new JLabel(Bundle.LBL_Name()); JLabel locationLabel = new JLabel(Bundle.LBL_Location()); JTextField nameField = new JTextField(info != null ? info.getName() : ""); - JTextField locationField = new JTextField(prj != null ? prj.getProjectDirectory().getPath() : ""); + JTextField locationField = new JTextField(projectDir != null ? projectDir.getPath() : ""); nameField.setEditable(false); locationField.setEditable(false); + JCheckBox markerBox = new JCheckBox(Bundle.LBL_MarkerFile()); + markerBox.setToolTipText(Bundle.TT_MarkerFile()); + markerBox.setSelected(projectDir != null && GenericProject.hasMarker(projectDir)); JPanel panel = new JPanel(); GroupLayout layout = new GroupLayout(panel); @@ -93,6 +106,9 @@ public JComponent createComponent(ProjectCustomizer.Category category, Lookup co .addComponent(nameField, GroupLayout.DEFAULT_SIZE, 200, Short.MAX_VALUE) .addComponent(locationField, GroupLayout.DEFAULT_SIZE, 200, Short.MAX_VALUE)) .addContainerGap()) + .addGroup(layout.createSequentialGroup() + .addContainerGap() + .addComponent(markerBox)) ); layout.setVerticalGroup( layout.createParallelGroup(Alignment.LEADING) @@ -104,8 +120,21 @@ public JComponent createComponent(ProjectCustomizer.Category category, Lookup co .addGroup(layout.createParallelGroup(Alignment.BASELINE) .addComponent(locationLabel) .addComponent(locationField)) + .addGap(18, 18, 18) + .addComponent(markerBox) .addGap(0, 0, Short.MAX_VALUE)) ); + if (projectDir != null) { + // invoked on OK, off the AWT thread, in ProjectManager write access + category.setStoreListener(evt -> { + try { + GenericProject.setMarkerState(projectDir, markerBox.isSelected()); + } catch (IOException ex) { + Logger.getLogger(GpCompositeCategoryProvider.class.getName()) + .log(Level.WARNING, "Cannot change marker state", ex); + } + }); + } return panel; } diff --git a/ide/generic.project/test/unit/src/org/netbeans/modules/generic/project/GenericProjectTest.java b/ide/generic.project/test/unit/src/org/netbeans/modules/generic/project/GenericProjectTest.java index deddb60c76d3..c4934f9222eb 100644 --- a/ide/generic.project/test/unit/src/org/netbeans/modules/generic/project/GenericProjectTest.java +++ b/ide/generic.project/test/unit/src/org/netbeans/modules/generic/project/GenericProjectTest.java @@ -54,6 +54,12 @@ public void testMarkerFileIsProject() throws Exception { assertTrue(new GenericProject.GpFactory().isProject(root)); } + public void testAttributeOnlyIsProject() throws Exception { + FileObject root = newProjectDir(); + root.setAttribute(GenericProject.MARKER_ATTRIBUTE, Boolean.TRUE); + assertTrue(new GenericProject.GpFactory().isProject(root)); + } + public void testLogicalViewShowsAllFiles() throws Exception { FileObject root = newProjectDir(); FileUtil.createData(root, GenericProject.MARKER_FILE); @@ -91,7 +97,7 @@ public void testSubprojectsAreFirstLevelOnly() throws Exception { public void testCreateProject() throws Exception { clearWorkDir(); File dir = new File(getWorkDir(), "proj-" + getName()); // NOI18N - Set created = GpWizardIterator.createProject(dir); + Set created = GpWizardIterator.createProject(dir, false); assertEquals(1, created.size()); FileObject dirFo = created.iterator().next(); FileObject marker = dirFo.getFileObject(GenericProject.MARKER_FILE); @@ -102,6 +108,46 @@ public void testCreateProject() throws Exception { assertTrue(prj instanceof GenericProject); } + public void testCreateProjectLocal() throws Exception { + clearWorkDir(); + File dir = new File(getWorkDir(), "proj-" + getName()); // NOI18N + Set created = GpWizardIterator.createProject(dir, true); + assertEquals(1, created.size()); + FileObject dirFo = created.iterator().next(); + assertNull(dirFo.getFileObject(GenericProject.MARKER_FILE)); + assertEquals(Boolean.TRUE, dirFo.getAttribute(GenericProject.MARKER_ATTRIBUTE)); + Project prj = ProjectManager.getDefault().findProject(dirFo); + assertNotNull(prj); + assertTrue(prj instanceof GenericProject); + } + + public void testSetMarkerStateLocalAndBack() throws Exception { + FileObject root = newProjectDir(); + GenericProject.createMarker(root); + assertTrue(GenericProject.hasMarker(root)); + + GenericProject.setMarkerState(root, false); + assertFalse(GenericProject.hasMarker(root)); + assertEquals(Boolean.TRUE, root.getAttribute(GenericProject.MARKER_ATTRIBUTE)); + assertTrue(new GenericProject.GpFactory().isProject(root)); + + GenericProject.setMarkerState(root, true); + assertTrue(GenericProject.hasMarker(root)); + assertNull(root.getAttribute(GenericProject.MARKER_ATTRIBUTE)); + } + + public void testSaveProjectLocalKeepsNoMarker() throws Exception { + clearWorkDir(); + File dir = new File(getWorkDir(), "proj-" + getName()); // NOI18N + GpWizardIterator.createProject(dir, true); + FileObject root = FileUtil.toFileObject(dir); + Project prj = ProjectManager.getDefault().findProject(root); + assertNotNull(prj); + new GenericProject.GpFactory().saveProject(prj); + assertNull(root.getFileObject(GenericProject.MARKER_FILE)); + assertEquals(Boolean.TRUE, root.getAttribute(GenericProject.MARKER_ATTRIBUTE)); + } + private FileObject newProjectDir() throws Exception { clearWorkDir(); File dir = new File(getWorkDir(), "proj-" + getName()); // NOI18N From b2cac143b694c8b2b73e2018e953ef26979813d1 Mon Sep 17 00:00:00 2001 From: Michael Bien Date: Mon, 19 Oct 2026 18:40:05 +0200 Subject: [PATCH 08/13] [action] common default actions + copy, move, delete + rename --- .../generic/project/GenericProject.java | 122 +++++++++- .../generic/project/GpActionProvider.java | 78 +++++++ .../project/GpLogicalViewProvider.java | 84 ++++++- .../generic/project/GpProjectOperations.java | 162 +++++++++++++ .../modules/generic/project/layer.xml | 64 ++++++ .../GpCompositeCategoryProvider.java | 15 +- .../generic/project/GenericProjectTest.java | 215 ++++++++++++++++++ 7 files changed, 719 insertions(+), 21 deletions(-) create mode 100644 ide/generic.project/src/org/netbeans/modules/generic/project/GpActionProvider.java create mode 100644 ide/generic.project/src/org/netbeans/modules/generic/project/GpProjectOperations.java diff --git a/ide/generic.project/src/org/netbeans/modules/generic/project/GenericProject.java b/ide/generic.project/src/org/netbeans/modules/generic/project/GenericProject.java index 454e0475d8af..a0379ef7588e 100644 --- a/ide/generic.project/src/org/netbeans/modules/generic/project/GenericProject.java +++ b/ide/generic.project/src/org/netbeans/modules/generic/project/GenericProject.java @@ -20,10 +20,13 @@ package org.netbeans.modules.generic.project; import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; import java.io.IOException; import java.util.Collections; import java.util.LinkedHashSet; +import java.util.Objects; import java.util.Set; +import java.util.prefs.Preferences; import javax.swing.Icon; import javax.swing.UIManager; import javax.swing.event.ChangeListener; @@ -31,10 +34,12 @@ import org.netbeans.api.project.FileOwnerQuery; import org.netbeans.api.project.Project; import org.netbeans.api.project.ProjectInformation; +import org.netbeans.api.project.ProjectUtils; import org.netbeans.modules.generic.project.ui.customizer.GpCustomizerProvider; import org.netbeans.spi.project.ProjectFactory; import org.netbeans.spi.project.ProjectState; import org.netbeans.spi.project.SubprojectProvider; +import org.netbeans.spi.project.support.LookupProviderSupport; import org.openide.filesystems.FileObject; import org.openide.util.ChangeSupport; import org.openide.util.ImageUtilities; @@ -60,9 +65,17 @@ public final class GenericProject implements Project { public static final String MARKER_ATTRIBUTE = "org.netbeans.modules.generic.project"; // NOI18N /** - * The project type key used for this project type's contributions - * in the layer filesystem - * (e.g. {@code Projects/org-netbeans-modules-generic-project/Customizer}). + * The project type key under which this project type is registered + * in the layer filesystem: + *
    + *
  • {@code Projects//Actions} — the stock actions of the + * project node's popup menu (loaded via + * {@code CommonProjectActions.forType}),
  • + *
  • {@code Projects//Lookup} — services contributed via + * {@code @ProjectServiceProvider} / {@code LookupProvider}, + * mixed into the project's lookup (see + * {@code LookupProviderSupport}).
  • + *
*/ public static final String PROJECT_KEY = "org-netbeans-modules-generic-project"; // NOI18N @@ -72,15 +85,33 @@ public final class GenericProject implements Project { private final FileObject projectDirectory; private final Lookup lookup; + /** + * Whether this project instance has been explicitly deleted + * (stock delete operation). Set before the marker file and the + * recognition attribute are removed so that + * {@link GpFactory#saveProject} never resurrects the project + * (e.g. when a preferences flush saves it afterwards). + */ + private volatile boolean deleted; + + void markDeleted() { + deleted = true; + } + private GenericProject(FileObject projectDirectory) { this.projectDirectory = projectDirectory; - this.lookup = Lookups.fixed( + // mix the project's own services with the layer contributions + // registered for this project type ("Projects//Lookup", + // e.g. via @ProjectServiceProvider in other modules) + this.lookup = LookupProviderSupport.createCompositeLookup(Lookups.fixed( new GpLogicalViewProvider(this), new GpInfo(this), new GpSubprojectProvider(this), new GpCustomizerProvider(this), + new GpProjectOperations(this), + new GpActionProvider(this), this - ); + ), "Projects/" + PROJECT_KEY + "/Lookup"); // NOI18N } @Override @@ -155,6 +186,55 @@ public static void setMarkerState(FileObject dir, boolean markerFile) throws IOE } } + /** + * Key of the display name override in the project's private + * preferences ({@link ProjectUtils#getPreferences}). + */ + static final String DISPLAY_NAME_KEY = "displayName"; // NOI18N + + /** + * Returns the display name override of the project, or + * {@code null} if none is stored, in which case the display name + * defaults to the project folder name. + */ + public static String getDisplayNameOverride(GenericProject prj) { + Preferences prefs = ProjectUtils.getPreferences(prj, GenericProject.class, false); + if (prefs == null) { + return null; + } + String name = prefs.get(DISPLAY_NAME_KEY, null); + return (name == null || name.isBlank()) ? null : name; + } + + /** + * Stores the display name override of the project, or clears it + * for {@code null}. A name equal to the project folder name is + * treated as no override. + * + *

Fires {@link ProjectInformation#PROP_DISPLAY_NAME} on the + * project's {@link ProjectInformation} if the effective display + * name changes, so that the project view updates its label. + */ + public static void setDisplayNameOverride(GenericProject prj, String name) { + if (name != null && name.equals(prj.getProjectDirectory().getNameExt())) { + name = null; + } + if (Objects.equals(name, getDisplayNameOverride(prj))) { + return; + } + Preferences prefs = ProjectUtils.getPreferences(prj, GenericProject.class, false); + if (prefs == null) { + return; + } + if (name == null) { + prefs.remove(DISPLAY_NAME_KEY); + } else { + prefs.put(DISPLAY_NAME_KEY, name); + } + GpInfo info = (GpInfo) prj.getLookup().lookup(ProjectInformation.class); + info.fireDisplayNameChanged(); + } + @ServiceProvider(service=ProjectFactory.class) public static final class GpFactory implements ProjectFactory { @@ -173,6 +253,11 @@ public Project loadProject(FileObject projectDirectory, ProjectState state) thro @Override public void saveProject(Project project) throws IOException, ClassCastException { + if (project instanceof GenericProject gp && gp.deleted) { + // the project was explicitly deleted: never + // resurrect the marker or the attribute + return; + } FileObject dir = project.getProjectDirectory(); if (isAttributeProject(dir)) { // local project: recognition lives in the FileObject @@ -185,7 +270,14 @@ public void saveProject(Project project) throws IOException, ClassCastException } - private record GpInfo(GenericProject prj) implements ProjectInformation { + private static final class GpInfo implements ProjectInformation { + + private final GenericProject prj; + private final PropertyChangeSupport changeSupport = new PropertyChangeSupport(this); + + GpInfo(GenericProject prj) { + this.prj = prj; + } @Override public String getName() { @@ -194,7 +286,8 @@ public String getName() { @Override public String getDisplayName() { - return prj.getProjectDirectory().getNameExt(); + String override = getDisplayNameOverride(prj); + return override != null ? override : prj.getProjectDirectory().getNameExt(); } @Override @@ -208,11 +301,22 @@ public Project getProject() { return prj; } + /** + * Notifies listeners that the display name changed. + */ + void fireDisplayNameChanged() { + changeSupport.firePropertyChange(PROP_DISPLAY_NAME, null, null); + } + @Override - public void addPropertyChangeListener(PropertyChangeListener listener) {} + public void addPropertyChangeListener(PropertyChangeListener listener) { + changeSupport.addPropertyChangeListener(listener); + } @Override - public void removePropertyChangeListener(PropertyChangeListener listener) {} + public void removePropertyChangeListener(PropertyChangeListener listener) { + changeSupport.removePropertyChangeListener(listener); + } } diff --git a/ide/generic.project/src/org/netbeans/modules/generic/project/GpActionProvider.java b/ide/generic.project/src/org/netbeans/modules/generic/project/GpActionProvider.java new file mode 100644 index 000000000000..b8fc56bcef6c --- /dev/null +++ b/ide/generic.project/src/org/netbeans/modules/generic/project/GpActionProvider.java @@ -0,0 +1,78 @@ +/* + * 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.generic.project; + +import org.netbeans.spi.project.ActionProvider; +import org.netbeans.spi.project.support.ProjectOperations; +import org.netbeans.spi.project.ui.support.DefaultProjectOperations; +import org.openide.util.Lookup; + +/** + * Advertises the default project operations of generic projects so + * that the standard project actions ("Rename...", "Move...", + * "Copy...", "Delete") become available in the project's popup + * menu. + * + *

The operations are delegated to the default implementations, + * which show the standard confirmation dialogs and update the + * project through the {@link GpProjectOperations} registered in + * the lookup. + */ +final class GpActionProvider implements ActionProvider { + + private final GenericProject prj; + + GpActionProvider(GenericProject prj) { + this.prj = prj; + } + + @Override + public String[] getSupportedActions() { + return new String[] { + ActionProvider.COMMAND_RENAME, + ActionProvider.COMMAND_MOVE, + ActionProvider.COMMAND_COPY, + ActionProvider.COMMAND_DELETE, + }; + } + + @Override + public void invokeAction(String command, Lookup context) throws IllegalArgumentException { + switch (command) { + case ActionProvider.COMMAND_RENAME -> DefaultProjectOperations.performDefaultRenameOperation(prj, null); + case ActionProvider.COMMAND_MOVE -> DefaultProjectOperations.performDefaultMoveOperation(prj); + case ActionProvider.COMMAND_COPY -> DefaultProjectOperations.performDefaultCopyOperation(prj); + case ActionProvider.COMMAND_DELETE -> DefaultProjectOperations.performDefaultDeleteOperation(prj); + default -> throw new IllegalArgumentException(command); + } + } + + @Override + public boolean isActionEnabled(String command, Lookup context) throws IllegalArgumentException { + return switch (command) { + case ActionProvider.COMMAND_RENAME -> ProjectOperations.isMoveOperationSupported(prj); + case ActionProvider.COMMAND_MOVE -> ProjectOperations.isMoveOperationSupported(prj); + case ActionProvider.COMMAND_COPY -> ProjectOperations.isCopyOperationSupported(prj); + case ActionProvider.COMMAND_DELETE -> ProjectOperations.isDeleteOperationSupported(prj); + default -> throw new IllegalArgumentException(command); + }; + } + +} \ No newline at end of file diff --git a/ide/generic.project/src/org/netbeans/modules/generic/project/GpLogicalViewProvider.java b/ide/generic.project/src/org/netbeans/modules/generic/project/GpLogicalViewProvider.java index f77708484603..8fdb5d4417da 100644 --- a/ide/generic.project/src/org/netbeans/modules/generic/project/GpLogicalViewProvider.java +++ b/ide/generic.project/src/org/netbeans/modules/generic/project/GpLogicalViewProvider.java @@ -20,24 +20,41 @@ package org.netbeans.modules.generic.project; import java.awt.Image; +import java.util.logging.Level; +import java.util.logging.Logger; import javax.swing.Action; +import org.netbeans.api.project.ProjectInformation; +import org.netbeans.api.project.ProjectUtils; import org.netbeans.spi.project.ui.LogicalViewProvider; import org.netbeans.spi.project.ui.support.CommonProjectActions; import org.openide.filesystems.FileObject; import org.openide.loaders.DataFilter; +import org.openide.loaders.DataObject; +import org.openide.loaders.DataObjectNotFoundException; import org.openide.loaders.DataFolder; import org.openide.nodes.AbstractNode; import org.openide.nodes.Children; import org.openide.nodes.Node; import org.openide.util.ImageUtilities; -import org.openide.util.lookup.Lookups; +import org.openide.util.lookup.AbstractLookup; +import org.openide.util.lookup.InstanceContent; /** * The logical view of a generic project simply shows all files contained * in the project directory. + * + *

The root node exposes the project, the project directory + * {@link FileObject} and the directory's {@link DataObject} in its + * lookup, which is what the stock project actions rely on (e.g. + * "New File" uses the {@link DataObject} to determine the target + * folder). Its action set is the standard one, registered in the layer + * under {@code Projects//Actions} and loaded via + * {@link CommonProjectActions#forType}. */ public record GpLogicalViewProvider(GenericProject project) implements LogicalViewProvider { + private static final Logger LOG = Logger.getLogger(GpLogicalViewProvider.class.getName()); // NOI18N + @Override public Node createLogicalView() { FileObject dir = project.getProjectDirectory(); @@ -47,14 +64,33 @@ public Node createLogicalView() { } catch (Exception ex) { children = Children.LEAF; } - AbstractNode base = new AbstractNode(children, Lookups.fixed(project, dir)) { + ProjectInformation info = ProjectUtils.getInformation(project); + InstanceContent content = new InstanceContent(); + AbstractNode base = new AbstractNode(children, new AbstractLookup(content)) { + { + // keep the tree label in sync with the display name + // override (rename without renaming the folder) + info.addPropertyChangeListener(evt -> { + if (ProjectInformation.PROP_DISPLAY_NAME.equals(evt.getPropertyName())) { + fireDisplayNameChange(null, null); + } + }); + } + + @Override + public String getDisplayName() { + // honors the display name override (rename without + // renaming the folder) + return info.getDisplayName(); + } + @Override public Action[] getActions(boolean context) { - return new Action[] { - CommonProjectActions.openSubprojectsAction(), - CommonProjectActions.closeProjectAction(), - CommonProjectActions.customizeProjectAction(), - }; + // the stock actions registered in the layer under + // "Projects//Actions" (shadows of the + // stock project actions plus the shared + // "Projects/Actions" folder, e.g. versioning) + return CommonProjectActions.forType(GenericProject.PROJECT_KEY); } @Override @@ -67,7 +103,39 @@ public Image getOpenedIcon(int type) { return getIcon(type); } }; - base.setDisplayName(dir.getNameExt()); + content.add(project); + content.add(dir); + // also expose the DataObject of the project directory: stock + // actions (e.g. "New File") use it to determine the target + // folder + content.add(project, new InstanceContent.Convertor() { + @Override + public DataObject convert(GenericProject obj) { + try { + FileObject fo = obj.getProjectDirectory(); + return fo != null && fo.isValid() ? DataObject.find(fo) : null; + } catch (DataObjectNotFoundException ex) { + LOG.log(Level.WARNING, null, ex); + return null; + } + } + + @Override + public Class type(GenericProject obj) { + return DataObject.class; + } + + @Override + public String id(GenericProject obj) { + return dir.getPath(); + } + + @Override + public String displayName(GenericProject obj) { + return dir.getNameExt(); + } + }); + base.setName(dir.getNameExt()); return base; } diff --git a/ide/generic.project/src/org/netbeans/modules/generic/project/GpProjectOperations.java b/ide/generic.project/src/org/netbeans/modules/generic/project/GpProjectOperations.java new file mode 100644 index 000000000000..436a5223d783 --- /dev/null +++ b/ide/generic.project/src/org/netbeans/modules/generic/project/GpProjectOperations.java @@ -0,0 +1,162 @@ +/* + * 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.generic.project; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import org.netbeans.api.project.Project; +import org.netbeans.spi.project.CopyOperationImplementation; +import org.netbeans.spi.project.DeleteOperationImplementation; +import org.netbeans.spi.project.MoveOrRenameOperationImplementation; +import org.openide.filesystems.FileObject; + +/** + * Supports the default move, rename, copy and delete operations + * of generic projects. + * + *

The project is just a directory, so the marker file (when + * present) is the only metadata and everything contained in the + * directory is data. For move/rename nothing has to be updated: + * the directory — marker file and project attributes included — + * is relocated as a whole (the default move implementation does + * not use the data file list). For copy the stock flow duplicates + * the visible directory content as-is, so the new project needs + * no bookkeeping either. For delete the stock flow deletes the + * metadata, optionally the data, and finally the project folder + * if it is empty then, hence the data files are listed + * deepest-first. + * + *

A pure rename (the "Also rename project folder" option + * unchecked) does not touch the project directory; the new name + * is stored as a display name override, so the project's display + * name reports it while the programmatic name keeps the folder + * name. When the folder is renamed or moved, the (new) folder + * name is authoritative again and any override is dropped. + */ +final class GpProjectOperations implements MoveOrRenameOperationImplementation, CopyOperationImplementation, DeleteOperationImplementation { + + private final GenericProject prj; + + GpProjectOperations(GenericProject prj) { + this.prj = prj; + } + + @Override + public List getMetadataFiles() { + // The marker file (when present) is the only project + // metadata; local (attribute-based) projects keep their + // recognition in a FileObject attribute, which needs no file. + FileObject marker = prj.getProjectDirectory().getFileObject(GenericProject.MARKER_FILE); + return marker != null ? List.of(marker) : List.of(); + } + + @Override + public List getDataFiles() { + // The whole directory is the project, so all of its content + // is data. Listed deepest-first so that the stock delete + // flow (which deletes each entry and afterwards the project + // folder if it is empty) can remove everything. + List result = new ArrayList<>(); + collectDataFiles(prj.getProjectDirectory(), result); + return result; + } + + private void collectDataFiles(FileObject dir, List result) { + for (FileObject child : dir.getChildren()) { + if (child.isFolder()) { + collectDataFiles(child, result); + result.add(child); + } else if (dir.equals(prj.getProjectDirectory()) + && GenericProject.MARKER_FILE.equals(child.getName())) { + // the marker file is metadata, not data: the stock + // flow deletes it unconditionally + } else { + result.add(child); + } + } + } + + @Override + public void notifyMoving() throws IOException { + // Nothing to do: the marker file and the project attribute + // both follow the folder when it is moved. + } + + @Override + public void notifyMoved(Project original, File originalPath, String nueName) throws IOException { + if (original != null) { + // Called on the new project instance: the folder (and the + // attribute holding any display name override) has been + // renamed, so the new folder name is authoritative again. + GenericProject.setDisplayNameOverride(prj, nueName); + } + } + + @Override + public void notifyRenaming() throws IOException { + // Nothing to do. + } + + @Override + public void notifyRenamed(String nueName) throws IOException { + GenericProject.setDisplayNameOverride(prj, nueName); + } + + @Override + public void notifyCopying() throws IOException { + // Nothing to do: the stock flow copies the visible + // directory content as-is. + } + + @Override + public void notifyCopied(Project original, File originalPath, String nueName) throws IOException { + if (original != null) { + // Called on the new project instance: keep the folder + // name authoritative, like after a move. In practice + // this is a no-op — the display name override lives in + // per-project preferences keyed by location, which the + // copy does not inherit. + GenericProject.setDisplayNameOverride(prj, nueName); + } + } + + @Override + public void notifyDeleting() throws IOException { + // Remember that this project has been explicitly deleted so + // that saveProject (e.g. from a preferences flush) never + // resurrects it, and drop the recognition attribute of local + // (attribute-based) projects so the folder stops being a + // project even when it survives the delete. + prj.markDeleted(); + FileObject dir = prj.getProjectDirectory(); + if (GenericProject.isAttributeProject(dir)) { + dir.setAttribute(GenericProject.MARKER_ATTRIBUTE, null); + } + } + + @Override + public void notifyDeleted() throws IOException { + // Nothing to do: the stock flow has already deleted the + // listed files and the (now empty) project folder. + } + +} \ No newline at end of file diff --git a/ide/generic.project/src/org/netbeans/modules/generic/project/layer.xml b/ide/generic.project/src/org/netbeans/modules/generic/project/layer.xml index 480e9e1ee993..5bc99c7d4fea 100644 --- a/ide/generic.project/src/org/netbeans/modules/generic/project/layer.xml +++ b/ide/generic.project/src/org/netbeans/modules/generic/project/layer.xml @@ -28,4 +28,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ide/generic.project/src/org/netbeans/modules/generic/project/ui/customizer/GpCompositeCategoryProvider.java b/ide/generic.project/src/org/netbeans/modules/generic/project/ui/customizer/GpCompositeCategoryProvider.java index 60186d231a47..7f071a54440e 100644 --- a/ide/generic.project/src/org/netbeans/modules/generic/project/ui/customizer/GpCompositeCategoryProvider.java +++ b/ide/generic.project/src/org/netbeans/modules/generic/project/ui/customizer/GpCompositeCategoryProvider.java @@ -49,6 +49,9 @@ @Messages({ "LBL_General=General", "LBL_Name=Name:", + "TT_Name=" + + "

The name of the project as shown in the project view.

" + + "

Changing it does not rename the project folder. Enter the folder name to reset.

", "LBL_Location=Location:", "LBL_MarkerFile=Marker file in project directory", "TT_MarkerFile=" @@ -83,9 +86,9 @@ public JComponent createComponent(ProjectCustomizer.Category category, Lookup co JLabel nameLabel = new JLabel(Bundle.LBL_Name()); JLabel locationLabel = new JLabel(Bundle.LBL_Location()); - JTextField nameField = new JTextField(info != null ? info.getName() : ""); + JTextField nameField = new JTextField(info != null ? info.getDisplayName() : ""); + nameField.setToolTipText(Bundle.TT_Name()); JTextField locationField = new JTextField(projectDir != null ? projectDir.getPath() : ""); - nameField.setEditable(false); locationField.setEditable(false); JCheckBox markerBox = new JCheckBox(Bundle.LBL_MarkerFile()); markerBox.setToolTipText(Bundle.TT_MarkerFile()); @@ -124,14 +127,18 @@ public JComponent createComponent(ProjectCustomizer.Category category, Lookup co .addComponent(markerBox) .addGap(0, 0, Short.MAX_VALUE)) ); - if (projectDir != null) { + if (prj != null && projectDir != null) { // invoked on OK, off the AWT thread, in ProjectManager write access category.setStoreListener(evt -> { try { GenericProject.setMarkerState(projectDir, markerBox.isSelected()); + String name = nameField.getText().strip(); + if (!name.isEmpty()) { + GenericProject.setDisplayNameOverride((GenericProject) prj, name); + } } catch (IOException ex) { Logger.getLogger(GpCompositeCategoryProvider.class.getName()) - .log(Level.WARNING, "Cannot change marker state", ex); + .log(Level.WARNING, "Cannot update project settings", ex); } }); } diff --git a/ide/generic.project/test/unit/src/org/netbeans/modules/generic/project/GenericProjectTest.java b/ide/generic.project/test/unit/src/org/netbeans/modules/generic/project/GenericProjectTest.java index c4934f9222eb..28a5239d14b5 100644 --- a/ide/generic.project/test/unit/src/org/netbeans/modules/generic/project/GenericProjectTest.java +++ b/ide/generic.project/test/unit/src/org/netbeans/modules/generic/project/GenericProjectTest.java @@ -20,14 +20,22 @@ package org.netbeans.modules.generic.project; import java.io.File; +import java.util.HashSet; +import java.util.List; import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; import org.netbeans.api.project.Project; +import org.netbeans.api.project.ProjectInformation; import org.netbeans.api.project.ProjectManager; +import org.netbeans.api.project.ProjectUtils; import org.netbeans.junit.NbTestCase; +import org.netbeans.spi.project.ActionProvider; +import org.netbeans.spi.project.ProjectServiceProvider; 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.DataObject; import org.openide.nodes.Node; import org.openide.util.test.MockLookup; @@ -148,6 +156,206 @@ public void testSaveProjectLocalKeepsNoMarker() throws Exception { assertEquals(Boolean.TRUE, root.getAttribute(GenericProject.MARKER_ATTRIBUTE)); } + public void testDisplayNameOverrideRoundTrip() throws Exception { + FileObject root = newProjectDir(); + FileUtil.createData(root, GenericProject.MARKER_FILE); + Project prj = ProjectManager.getDefault().findProject(root); + assertNotNull(prj); + ProjectInformation info = ProjectUtils.getInformation(prj); + assertEquals(root.getNameExt(), info.getName()); + assertEquals(root.getNameExt(), info.getDisplayName()); + + GenericProject.setDisplayNameOverride((GenericProject) prj, "My Fancy Project"); + assertEquals(root.getNameExt(), info.getName()); + assertEquals("My Fancy Project", info.getDisplayName()); + + GenericProject.setDisplayNameOverride((GenericProject) prj, null); + assertEquals(root.getNameExt(), info.getDisplayName()); + } + + public void testDisplayNameOverrideFiresPropertyChange() throws Exception { + FileObject root = newProjectDir(); + FileUtil.createData(root, GenericProject.MARKER_FILE); + Project prj = ProjectManager.getDefault().findProject(root); + assertNotNull(prj); + ProjectInformation info = ProjectUtils.getInformation(prj); + + AtomicReference fired = new AtomicReference<>(); + info.addPropertyChangeListener(evt -> fired.compareAndSet(null, evt.getPropertyName())); + GenericProject.setDisplayNameOverride((GenericProject) prj, "New Name"); + assertEquals(ProjectInformation.PROP_DISPLAY_NAME, fired.get()); + } + + public void testRenameOperationStoresOverride() throws Exception { + FileObject root = newProjectDir(); + FileUtil.createData(root, GenericProject.MARKER_FILE); + Project prj = ProjectManager.getDefault().findProject(root); + assertNotNull(prj); + GpProjectOperations op = prj.getLookup().lookup(GpProjectOperations.class); + assertNotNull(op); + + op.notifyRenamed("Renamed Project"); + assertEquals("Renamed Project", ProjectUtils.getInformation(prj).getDisplayName()); + assertEquals(root.getNameExt(), ProjectUtils.getInformation(prj).getName()); + } + + public void testMovedClearsStaleOverride() throws Exception { + FileObject root = newProjectDir(); + FileUtil.createData(root, GenericProject.MARKER_FILE); + Project prj = ProjectManager.getDefault().findProject(root); + assertNotNull(prj); + GpProjectOperations op = prj.getLookup().lookup(GpProjectOperations.class); + assertNotNull(op); + + GenericProject.setDisplayNameOverride((GenericProject) prj, "Old Override"); + assertEquals("Old Override", ProjectUtils.getInformation(prj).getDisplayName()); + + // notification on the new project instance (original != null): + // the new folder name is authoritative again + op.notifyMoved(prj, new File("/tmp/does-not-matter"), root.getNameExt()); // NOI18N + assertEquals(root.getNameExt(), ProjectUtils.getInformation(prj).getDisplayName()); + } + + public void testLogicalViewNodeHonorsDisplayNameOverride() throws Exception { + FileObject root = newProjectDir(); + FileUtil.createData(root, GenericProject.MARKER_FILE); + Project prj = ProjectManager.getDefault().findProject(root); + assertNotNull(prj); + Node node = prj.getLookup().lookup(LogicalViewProvider.class).createLogicalView(); + assertEquals(root.getNameExt(), node.getDisplayName()); + + GenericProject.setDisplayNameOverride((GenericProject) prj, "Fancy Name"); + assertEquals("Fancy Name", node.getDisplayName()); + + GenericProject.setDisplayNameOverride((GenericProject) prj, null); + assertEquals(root.getNameExt(), node.getDisplayName()); + } + + public void testActionProviderSupportsRename() throws Exception { + FileObject root = newProjectDir(); + FileUtil.createData(root, GenericProject.MARKER_FILE); + Project prj = ProjectManager.getDefault().findProject(root); + assertNotNull(prj); + ActionProvider ap = prj.getLookup().lookup(ActionProvider.class); + assertNotNull(ap); + assertTrue(List.of(ap.getSupportedActions()).contains(ActionProvider.COMMAND_RENAME)); + assertTrue(ap.isActionEnabled(ActionProvider.COMMAND_RENAME, null)); + assertTrue(List.of(ap.getSupportedActions()).contains(ActionProvider.COMMAND_DELETE)); + assertTrue(ap.isActionEnabled(ActionProvider.COMMAND_DELETE, null)); + assertTrue(List.of(ap.getSupportedActions()).contains(ActionProvider.COMMAND_MOVE)); + assertTrue(ap.isActionEnabled(ActionProvider.COMMAND_MOVE, null)); + assertTrue(List.of(ap.getSupportedActions()).contains(ActionProvider.COMMAND_COPY)); + assertTrue(ap.isActionEnabled(ActionProvider.COMMAND_COPY, null)); + } + + public void testDeleteOperationFiles() throws Exception { + FileObject root = newProjectDir(); + FileUtil.createData(root, GenericProject.MARKER_FILE); + FileUtil.createData(root, "readme.txt"); + FileObject sub = FileUtil.createFolder(root, "src"); + FileUtil.createData(sub, "Main.java"); + Project prj = ProjectManager.getDefault().findProject(root); + assertNotNull(prj); + GpProjectOperations op = prj.getLookup().lookup(GpProjectOperations.class); + assertNotNull(op); + + assertEquals(List.of(root.getFileObject(GenericProject.MARKER_FILE)), op.getMetadataFiles()); + + // all of the directory's content is data, the marker file + // excluded (it is metadata), deepest-first so that the + // stock delete flow can delete every entry and the + // (now empty) project folder + FileObject readme = root.getFileObject("readme.txt"); // NOI18N + FileObject main = sub.getFileObject("Main.java"); // NOI18N + List data = op.getDataFiles(); + assertEquals(new HashSet<>(List.of(readme, main, sub)), new HashSet<>(data)); + assertTrue("children must be listed before their parent", data.indexOf(main) < data.indexOf(sub)); + } + + public void testDeleteOperationPreventsMarkerResurrection() throws Exception { + FileObject root = newProjectDir(); + FileUtil.createData(root, GenericProject.MARKER_FILE); + Project prj = ProjectManager.getDefault().findProject(root); + assertNotNull(prj); + GpProjectOperations op = prj.getLookup().lookup(GpProjectOperations.class); + assertNotNull(op); + + op.notifyDeleting(); + root.getFileObject(GenericProject.MARKER_FILE).delete(); // simulate the stock flow + + new GenericProject.GpFactory().saveProject(prj); + assertNull("a saved deleted project must not be resurrected", root.getFileObject(GenericProject.MARKER_FILE)); + } + + public void testDeleteOperationClearsAttribute() throws Exception { + FileObject root = newProjectDir(); // local (attribute-based) project + root.setAttribute(GenericProject.MARKER_ATTRIBUTE, Boolean.TRUE); + Project prj = ProjectManager.getDefault().findProject(root); + assertNotNull(prj); + GpProjectOperations op = prj.getLookup().lookup(GpProjectOperations.class); + assertNotNull(op); + + op.notifyDeleting(); + + assertNull("the recognition attribute must be dropped", root.getAttribute(GenericProject.MARKER_ATTRIBUTE)); + new GenericProject.GpFactory().saveProject(prj); + assertNull("a saved deleted local project must not gain a marker file", root.getFileObject(GenericProject.MARKER_FILE)); + } + + public void testCopyOperationKeepsFolderNameAuthoritative() throws Exception { + FileObject root = newProjectDir(); + FileUtil.createData(root, GenericProject.MARKER_FILE); + Project prj = ProjectManager.getDefault().findProject(root); + assertNotNull(prj); + GenericProject.setDisplayNameOverride((GenericProject) prj, "My Fancy Project"); + + // simulate the stock copy flow: duplicate the directory + // content, find the new project, and notify the operations + // of both projects + FileObject copy = FileUtil.createFolder(root.getParent(), "copy-" + getName()); // NOI18N + FileUtil.copyFile(root.getFileObject(GenericProject.MARKER_FILE), copy, GenericProject.MARKER_FILE, null); + Project nue = ProjectManager.getDefault().findProject(copy); + assertNotNull(nue); + GpProjectOperations opOrig = prj.getLookup().lookup(GpProjectOperations.class); + GpProjectOperations opNue = nue.getLookup().lookup(GpProjectOperations.class); + assertNotNull(opOrig); + assertNotNull(opNue); + opOrig.notifyCopied(null, FileUtil.toFile(root), copy.getNameExt()); + opNue.notifyCopied(prj, FileUtil.toFile(root), copy.getNameExt()); + + assertEquals("the folder name is authoritative on the copy", + copy.getNameExt(), ProjectUtils.getInformation(nue).getDisplayName()); + assertEquals("the original keeps its display name", + "My Fancy Project", ProjectUtils.getInformation(prj).getDisplayName()); + } + + public void testLogicalViewNodeLookupExposesProjectAndDirectory() throws Exception { + FileObject root = newProjectDir(); + FileUtil.createData(root, GenericProject.MARKER_FILE); + Project prj = ProjectManager.getDefault().findProject(root); + assertNotNull(prj); + Node node = prj.getLookup().lookup(LogicalViewProvider.class).createLogicalView(); + assertEquals(prj, node.getLookup().lookup(GenericProject.class)); + assertEquals(root, node.getLookup().lookup(FileObject.class)); + DataObject dobj = node.getLookup().lookup(DataObject.class); + assertNotNull("the project directory DataObject must be in the node lookup", dobj); + assertEquals(root, dobj.getPrimaryFile()); + } + + public void testProjectLookupContainsLayerServices() throws Exception { + FileObject root = newProjectDir(); + FileUtil.createData(root, GenericProject.MARKER_FILE); + Project prj = ProjectManager.getDefault().findProject(root); + assertNotNull(prj); + // TestMarkerProvider is registered for this project type via + // @ProjectServiceProvider, i.e. as a layer contribution under + // "Projects//Lookup": the composite project lookup must + // mix it in + assertNotNull(prj.getLookup().lookup(TestMarker.class)); + assertNotNull("the project's own services must still be present", + prj.getLookup().lookup(ProjectInformation.class)); + } + private FileObject newProjectDir() throws Exception { clearWorkDir(); File dir = new File(getWorkDir(), "proj-" + getName()); // NOI18N @@ -155,4 +363,11 @@ private FileObject newProjectDir() throws Exception { return FileUtil.toFileObject(dir); } + public interface TestMarker { + } + + @ProjectServiceProvider(service=TestMarker.class, projectType=GenericProject.PROJECT_KEY) + public static class TestMarkerProvider implements TestMarker { + } + } From 88657423c3a01e4eb5352d08cbed7080e1f8f328 Mon Sep 17 00:00:00 2001 From: Michael Bien Date: Tue, 20 Oct 2026 13:16:31 +0200 Subject: [PATCH 09/13] refactored into packages --- .../modules/generic/project/GenericProject.java | 8 +++++++- .../project/{ => ui/actions}/GpActionProvider.java | 7 ++++--- .../{ => ui/actions}/GpProjectOperations.java | 8 +++++--- .../{ => ui/wizard}/GpTemplateDescription.html | 0 .../project/{ => ui/wizard}/GpWizardIterator.java | 13 ++++++------- .../project/{ => ui/wizard}/GpWizardPanel.java | 2 +- .../modules/generic/project/GenericProjectTest.java | 2 ++ 7 files changed, 25 insertions(+), 15 deletions(-) rename ide/generic.project/src/org/netbeans/modules/generic/project/{ => ui/actions}/GpActionProvider.java (93%) rename ide/generic.project/src/org/netbeans/modules/generic/project/{ => ui/actions}/GpProjectOperations.java (95%) rename ide/generic.project/src/org/netbeans/modules/generic/project/{ => ui/wizard}/GpTemplateDescription.html (100%) rename ide/generic.project/src/org/netbeans/modules/generic/project/{ => ui/wizard}/GpWizardIterator.java (92%) rename ide/generic.project/src/org/netbeans/modules/generic/project/{ => ui/wizard}/GpWizardPanel.java (99%) diff --git a/ide/generic.project/src/org/netbeans/modules/generic/project/GenericProject.java b/ide/generic.project/src/org/netbeans/modules/generic/project/GenericProject.java index a0379ef7588e..45a4b5b88c6b 100644 --- a/ide/generic.project/src/org/netbeans/modules/generic/project/GenericProject.java +++ b/ide/generic.project/src/org/netbeans/modules/generic/project/GenericProject.java @@ -35,6 +35,8 @@ import org.netbeans.api.project.Project; import org.netbeans.api.project.ProjectInformation; import org.netbeans.api.project.ProjectUtils; +import org.netbeans.modules.generic.project.ui.actions.GpActionProvider; +import org.netbeans.modules.generic.project.ui.actions.GpProjectOperations; import org.netbeans.modules.generic.project.ui.customizer.GpCustomizerProvider; import org.netbeans.spi.project.ProjectFactory; import org.netbeans.spi.project.ProjectState; @@ -94,7 +96,11 @@ public final class GenericProject implements Project { */ private volatile boolean deleted; - void markDeleted() { + /** + * Marks this project instance as explicitly deleted, see + * {@link #deleted}. + */ + public void markDeleted() { deleted = true; } diff --git a/ide/generic.project/src/org/netbeans/modules/generic/project/GpActionProvider.java b/ide/generic.project/src/org/netbeans/modules/generic/project/ui/actions/GpActionProvider.java similarity index 93% rename from ide/generic.project/src/org/netbeans/modules/generic/project/GpActionProvider.java rename to ide/generic.project/src/org/netbeans/modules/generic/project/ui/actions/GpActionProvider.java index b8fc56bcef6c..600a4328ea4a 100644 --- a/ide/generic.project/src/org/netbeans/modules/generic/project/GpActionProvider.java +++ b/ide/generic.project/src/org/netbeans/modules/generic/project/ui/actions/GpActionProvider.java @@ -17,8 +17,9 @@ * under the License. */ -package org.netbeans.modules.generic.project; +package org.netbeans.modules.generic.project.ui.actions; +import org.netbeans.modules.generic.project.GenericProject; import org.netbeans.spi.project.ActionProvider; import org.netbeans.spi.project.support.ProjectOperations; import org.netbeans.spi.project.ui.support.DefaultProjectOperations; @@ -35,11 +36,11 @@ * project through the {@link GpProjectOperations} registered in * the lookup. */ -final class GpActionProvider implements ActionProvider { +public final class GpActionProvider implements ActionProvider { private final GenericProject prj; - GpActionProvider(GenericProject prj) { + public GpActionProvider(GenericProject prj) { this.prj = prj; } diff --git a/ide/generic.project/src/org/netbeans/modules/generic/project/GpProjectOperations.java b/ide/generic.project/src/org/netbeans/modules/generic/project/ui/actions/GpProjectOperations.java similarity index 95% rename from ide/generic.project/src/org/netbeans/modules/generic/project/GpProjectOperations.java rename to ide/generic.project/src/org/netbeans/modules/generic/project/ui/actions/GpProjectOperations.java index 436a5223d783..68b62260629c 100644 --- a/ide/generic.project/src/org/netbeans/modules/generic/project/GpProjectOperations.java +++ b/ide/generic.project/src/org/netbeans/modules/generic/project/ui/actions/GpProjectOperations.java @@ -17,7 +17,9 @@ * under the License. */ -package org.netbeans.modules.generic.project; +package org.netbeans.modules.generic.project.ui.actions; + +import org.netbeans.modules.generic.project.GenericProject; import java.io.File; import java.io.IOException; @@ -52,11 +54,11 @@ * name. When the folder is renamed or moved, the (new) folder * name is authoritative again and any override is dropped. */ -final class GpProjectOperations implements MoveOrRenameOperationImplementation, CopyOperationImplementation, DeleteOperationImplementation { +public final class GpProjectOperations implements MoveOrRenameOperationImplementation, CopyOperationImplementation, DeleteOperationImplementation { private final GenericProject prj; - GpProjectOperations(GenericProject prj) { + public GpProjectOperations(GenericProject prj) { this.prj = prj; } diff --git a/ide/generic.project/src/org/netbeans/modules/generic/project/GpTemplateDescription.html b/ide/generic.project/src/org/netbeans/modules/generic/project/ui/wizard/GpTemplateDescription.html similarity index 100% rename from ide/generic.project/src/org/netbeans/modules/generic/project/GpTemplateDescription.html rename to ide/generic.project/src/org/netbeans/modules/generic/project/ui/wizard/GpTemplateDescription.html diff --git a/ide/generic.project/src/org/netbeans/modules/generic/project/GpWizardIterator.java b/ide/generic.project/src/org/netbeans/modules/generic/project/ui/wizard/GpWizardIterator.java similarity index 92% rename from ide/generic.project/src/org/netbeans/modules/generic/project/GpWizardIterator.java rename to ide/generic.project/src/org/netbeans/modules/generic/project/ui/wizard/GpWizardIterator.java index 7b18eadf1fa2..686a7623460c 100644 --- a/ide/generic.project/src/org/netbeans/modules/generic/project/GpWizardIterator.java +++ b/ide/generic.project/src/org/netbeans/modules/generic/project/ui/wizard/GpWizardIterator.java @@ -17,7 +17,7 @@ * under the License. */ -package org.netbeans.modules.generic.project; +package org.netbeans.modules.generic.project.ui.wizard; import java.io.File; import java.io.IOException; @@ -27,6 +27,7 @@ import javax.swing.JComponent; import javax.swing.event.ChangeListener; import org.netbeans.api.templates.TemplateRegistration; +import org.netbeans.modules.generic.project.GenericProject; import org.netbeans.spi.project.ui.support.ProjectChooser; import org.openide.WizardDescriptor; import org.openide.filesystems.FileObject; @@ -73,7 +74,7 @@ public Set instantiate() throws IOException { * for {@code localMarker} — marks it as a local project by the * FileObject attribute only. */ - static Set createProject(File projectDir, boolean localMarker) throws IOException { + public static Set createProject(File projectDir, boolean localMarker) throws IOException { if (!projectDir.exists() && !projectDir.mkdirs()) { throw new IOException("Cannot create folder: " + projectDir); // NOI18N } @@ -81,11 +82,9 @@ static Set createProject(File projectDir, boolean localMarker) throw if (dir == null) { throw new IOException("Cannot map folder to the file system: " + projectDir); // NOI18N } - if (localMarker) { - GenericProject.setMarkerState(dir, false); - } else { - GenericProject.createMarker(dir); - } + // local project: recognition attribute only, + // otherwise the marker file in the working tree + GenericProject.setMarkerState(dir, !localMarker); return Set.of(dir); } diff --git a/ide/generic.project/src/org/netbeans/modules/generic/project/GpWizardPanel.java b/ide/generic.project/src/org/netbeans/modules/generic/project/ui/wizard/GpWizardPanel.java similarity index 99% rename from ide/generic.project/src/org/netbeans/modules/generic/project/GpWizardPanel.java rename to ide/generic.project/src/org/netbeans/modules/generic/project/ui/wizard/GpWizardPanel.java index 06fdac89406f..8cf571019774 100644 --- a/ide/generic.project/src/org/netbeans/modules/generic/project/GpWizardPanel.java +++ b/ide/generic.project/src/org/netbeans/modules/generic/project/ui/wizard/GpWizardPanel.java @@ -17,7 +17,7 @@ * under the License. */ -package org.netbeans.modules.generic.project; +package org.netbeans.modules.generic.project.ui.wizard; import java.awt.Component; import java.io.File; diff --git a/ide/generic.project/test/unit/src/org/netbeans/modules/generic/project/GenericProjectTest.java b/ide/generic.project/test/unit/src/org/netbeans/modules/generic/project/GenericProjectTest.java index 28a5239d14b5..3daf60fdbf1c 100644 --- a/ide/generic.project/test/unit/src/org/netbeans/modules/generic/project/GenericProjectTest.java +++ b/ide/generic.project/test/unit/src/org/netbeans/modules/generic/project/GenericProjectTest.java @@ -29,6 +29,8 @@ import org.netbeans.api.project.ProjectManager; import org.netbeans.api.project.ProjectUtils; import org.netbeans.junit.NbTestCase; +import org.netbeans.modules.generic.project.ui.actions.GpProjectOperations; +import org.netbeans.modules.generic.project.ui.wizard.GpWizardIterator; import org.netbeans.spi.project.ActionProvider; import org.netbeans.spi.project.ProjectServiceProvider; import org.netbeans.spi.project.SubprojectProvider; From 237bf6258a5430973e995c99a4ee9bccd25ab34e Mon Sep 17 00:00:00 2001 From: Michael Bien Date: Tue, 20 Oct 2026 14:05:03 +0200 Subject: [PATCH 10/13] refactored base package --- .../generic/project/GenericProject.java | 151 ++---------------- .../modules/generic/project/GpFactory.java | 66 ++++++++ .../modules/generic/project/GpInfo.java | 83 ++++++++++ .../generic/project/GpSubprojectProvider.java | 71 ++++++++ .../generic/project/GenericProjectTest.java | 14 +- 5 files changed, 236 insertions(+), 149 deletions(-) create mode 100644 ide/generic.project/src/org/netbeans/modules/generic/project/GpFactory.java create mode 100644 ide/generic.project/src/org/netbeans/modules/generic/project/GpInfo.java create mode 100644 ide/generic.project/src/org/netbeans/modules/generic/project/GpSubprojectProvider.java diff --git a/ide/generic.project/src/org/netbeans/modules/generic/project/GenericProject.java b/ide/generic.project/src/org/netbeans/modules/generic/project/GenericProject.java index 45a4b5b88c6b..388e21afc682 100644 --- a/ide/generic.project/src/org/netbeans/modules/generic/project/GenericProject.java +++ b/ide/generic.project/src/org/netbeans/modules/generic/project/GenericProject.java @@ -19,35 +19,20 @@ package org.netbeans.modules.generic.project; -import java.beans.PropertyChangeListener; -import java.beans.PropertyChangeSupport; import java.io.IOException; -import java.util.Collections; -import java.util.LinkedHashSet; import java.util.Objects; -import java.util.Set; import java.util.prefs.Preferences; -import javax.swing.Icon; -import javax.swing.UIManager; -import javax.swing.event.ChangeListener; import org.netbeans.api.annotations.common.StaticResource; -import org.netbeans.api.project.FileOwnerQuery; import org.netbeans.api.project.Project; import org.netbeans.api.project.ProjectInformation; import org.netbeans.api.project.ProjectUtils; import org.netbeans.modules.generic.project.ui.actions.GpActionProvider; import org.netbeans.modules.generic.project.ui.actions.GpProjectOperations; import org.netbeans.modules.generic.project.ui.customizer.GpCustomizerProvider; -import org.netbeans.spi.project.ProjectFactory; -import org.netbeans.spi.project.ProjectState; -import org.netbeans.spi.project.SubprojectProvider; import org.netbeans.spi.project.support.LookupProviderSupport; import org.openide.filesystems.FileObject; -import org.openide.util.ChangeSupport; -import org.openide.util.ImageUtilities; import org.openide.util.Lookup; import org.openide.util.lookup.Lookups; -import org.openide.util.lookup.ServiceProvider; /** * A generic project. A directory is recognized as a generic project @@ -104,7 +89,15 @@ public void markDeleted() { deleted = true; } - private GenericProject(FileObject projectDirectory) { + /** + * Whether this project instance has been explicitly deleted, + * see {@link #deleted}. + */ + boolean isDeleted() { + return deleted; + } + + GenericProject(FileObject projectDirectory) { this.projectDirectory = projectDirectory; // mix the project's own services with the layer contributions // registered for this project type ("Projects//Lookup", @@ -240,130 +233,4 @@ public static void setDisplayNameOverride(GenericProject prj, String name) { GpInfo info = (GpInfo) prj.getLookup().lookup(ProjectInformation.class); info.fireDisplayNameChanged(); } - - @ServiceProvider(service=ProjectFactory.class) - public static final class GpFactory implements ProjectFactory { - - @Override - public boolean isProject(FileObject projectDirectory) { - return GenericProject.isProject(projectDirectory); - } - - @Override - public Project loadProject(FileObject projectDirectory, ProjectState state) throws IOException { - if (GenericProject.isProject(projectDirectory)) { - return new GenericProject(projectDirectory); - } - return null; - } - - @Override - public void saveProject(Project project) throws IOException, ClassCastException { - if (project instanceof GenericProject gp && gp.deleted) { - // the project was explicitly deleted: never - // resurrect the marker or the attribute - return; - } - FileObject dir = project.getProjectDirectory(); - if (isAttributeProject(dir)) { - // local project: recognition lives in the FileObject - // attribute, never (re)create the marker file - dir.setAttribute(MARKER_ATTRIBUTE, Boolean.TRUE); - } else { - createMarker(dir); - } - } - - } - - private static final class GpInfo implements ProjectInformation { - - private final GenericProject prj; - private final PropertyChangeSupport changeSupport = new PropertyChangeSupport(this); - - GpInfo(GenericProject prj) { - this.prj = prj; - } - - @Override - public String getName() { - return prj.getProjectDirectory().getNameExt(); - } - - @Override - public String getDisplayName() { - String override = getDisplayNameOverride(prj); - return override != null ? override : prj.getProjectDirectory().getNameExt(); - } - - @Override - public Icon getIcon() { - Icon icon = ImageUtilities.loadIcon(ICON); - return icon != null ? icon : UIManager.getIcon("Nb.Explorer.Folder.icon"); // NOI18N - } - - @Override - public Project getProject() { - return prj; - } - - /** - * Notifies listeners that the display name changed. - */ - void fireDisplayNameChanged() { - changeSupport.firePropertyChange(PROP_DISPLAY_NAME, null, null); - } - - @Override - public void addPropertyChangeListener(PropertyChangeListener listener) { - changeSupport.addPropertyChangeListener(listener); - } - - @Override - public void removePropertyChangeListener(PropertyChangeListener listener) { - changeSupport.removePropertyChangeListener(listener); - } - - } - - /** - * Exposes the projects contained directly in the project directory - * as subprojects. Adds "Open Required Projects" and "Open Selected Project" actions. - */ - private static final class GpSubprojectProvider implements SubprojectProvider { - - private final GenericProject prj; - private final ChangeSupport changeSupport = new ChangeSupport(this); - - GpSubprojectProvider(GenericProject prj) { - this.prj = prj; - } - - @Override - public Set getSubprojects() { - Set subprojects = new LinkedHashSet<>(); - FileObject[] children = prj.getProjectDirectory().getChildren(); - if (children != null) { - for (FileObject child : children) { - Project owner = FileOwnerQuery.getOwner(child); - if (owner != null && !owner.equals(prj)) { - subprojects.add(owner); - } - } - } - return Collections.unmodifiableSet(subprojects); - } - - @Override - public void addChangeListener(ChangeListener listener) { - changeSupport.addChangeListener(listener); - } - - @Override - public void removeChangeListener(ChangeListener listener) { - changeSupport.removeChangeListener(listener); - } - - } - } diff --git a/ide/generic.project/src/org/netbeans/modules/generic/project/GpFactory.java b/ide/generic.project/src/org/netbeans/modules/generic/project/GpFactory.java new file mode 100644 index 000000000000..17e2c32fd95d --- /dev/null +++ b/ide/generic.project/src/org/netbeans/modules/generic/project/GpFactory.java @@ -0,0 +1,66 @@ +/* + * 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.generic.project; + +import java.io.IOException; +import org.netbeans.api.project.Project; +import org.netbeans.spi.project.ProjectFactory; +import org.netbeans.spi.project.ProjectState; +import org.openide.filesystems.FileObject; +import org.openide.util.lookup.ServiceProvider; + +/** + * The {@link ProjectFactory} for generic projects: recognizes a + * directory by the marker file or the local-project recognition + * attribute and instantiates {@link GenericProject}. + */ +@ServiceProvider(service=ProjectFactory.class) +public final class GpFactory implements ProjectFactory { + + @Override + public boolean isProject(FileObject projectDirectory) { + return GenericProject.isProject(projectDirectory); + } + + @Override + public Project loadProject(FileObject projectDirectory, ProjectState state) throws IOException { + if (GenericProject.isProject(projectDirectory)) { + return new GenericProject(projectDirectory); + } + return null; + } + + @Override + public void saveProject(Project project) throws IOException, ClassCastException { + if (project instanceof GenericProject gp && gp.isDeleted()) { + // the project was explicitly deleted: never + // resurrect the marker or the attribute + return; + } + FileObject dir = project.getProjectDirectory(); + if (GenericProject.isAttributeProject(dir)) { + // local project: recognition lives in the FileObject + // attribute, never (re)create the marker file + dir.setAttribute(GenericProject.MARKER_ATTRIBUTE, Boolean.TRUE); + } else { + GenericProject.createMarker(dir); + } + } +} diff --git a/ide/generic.project/src/org/netbeans/modules/generic/project/GpInfo.java b/ide/generic.project/src/org/netbeans/modules/generic/project/GpInfo.java new file mode 100644 index 000000000000..3ecc3667050c --- /dev/null +++ b/ide/generic.project/src/org/netbeans/modules/generic/project/GpInfo.java @@ -0,0 +1,83 @@ +/* + * 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.generic.project; + +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import javax.swing.Icon; +import javax.swing.UIManager; +import org.netbeans.api.project.Project; +import org.netbeans.api.project.ProjectInformation; +import org.openide.util.ImageUtilities; + +/** + * The {@link ProjectInformation} of a generic project. The display + * name is the project folder name unless a display name override is + * stored in the project's private preferences (see + * {@link GenericProject#getDisplayNameOverride}). + */ +final class GpInfo implements ProjectInformation { + + private final GenericProject prj; + private final PropertyChangeSupport changeSupport = new PropertyChangeSupport(this); + + GpInfo(GenericProject prj) { + this.prj = prj; + } + + @Override + public String getName() { + return prj.getProjectDirectory().getNameExt(); + } + + @Override + public String getDisplayName() { + String override = GenericProject.getDisplayNameOverride(prj); + return override != null ? override : prj.getProjectDirectory().getNameExt(); + } + + @Override + public Icon getIcon() { + Icon icon = ImageUtilities.loadIcon(GenericProject.ICON); + return icon != null ? icon : UIManager.getIcon("Nb.Explorer.Folder.icon"); // NOI18N + } + + @Override + public Project getProject() { + return prj; + } + + /** + * Notifies listeners that the display name changed. + */ + void fireDisplayNameChanged() { + changeSupport.firePropertyChange(PROP_DISPLAY_NAME, null, null); + } + + @Override + public void addPropertyChangeListener(PropertyChangeListener listener) { + changeSupport.addPropertyChangeListener(listener); + } + + @Override + public void removePropertyChangeListener(PropertyChangeListener listener) { + changeSupport.removePropertyChangeListener(listener); + } +} diff --git a/ide/generic.project/src/org/netbeans/modules/generic/project/GpSubprojectProvider.java b/ide/generic.project/src/org/netbeans/modules/generic/project/GpSubprojectProvider.java new file mode 100644 index 000000000000..02e228057a98 --- /dev/null +++ b/ide/generic.project/src/org/netbeans/modules/generic/project/GpSubprojectProvider.java @@ -0,0 +1,71 @@ +/* + * 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.generic.project; + +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; +import javax.swing.event.ChangeListener; +import org.netbeans.api.project.FileOwnerQuery; +import org.netbeans.api.project.Project; +import org.netbeans.spi.project.SubprojectProvider; +import org.openide.filesystems.FileObject; +import org.openide.util.ChangeSupport; + +/** + * The legacy {@link SubprojectProvider} view of the projects nested + * directly in the project directory: every child folder owned by + * another project is a subproject. + */ +final class GpSubprojectProvider implements SubprojectProvider { + + private final GenericProject prj; + private final ChangeSupport changeSupport = new ChangeSupport(this); + + GpSubprojectProvider(GenericProject prj) { + this.prj = prj; + } + + @Override + public Set getSubprojects() { + Set subprojects = new LinkedHashSet<>(); + FileObject[] children = prj.getProjectDirectory().getChildren(); + if (children != null) { + for (FileObject child : children) { + Project owner = FileOwnerQuery.getOwner(child); + if (owner != null && !owner.equals(prj)) { + subprojects.add(owner); + } + } + } + return Collections.unmodifiableSet(subprojects); + } + + @Override + public void addChangeListener(ChangeListener listener) { + changeSupport.addChangeListener(listener); + } + + @Override + public void removeChangeListener(ChangeListener listener) { + changeSupport.removeChangeListener(listener); + } + +} \ No newline at end of file diff --git a/ide/generic.project/test/unit/src/org/netbeans/modules/generic/project/GenericProjectTest.java b/ide/generic.project/test/unit/src/org/netbeans/modules/generic/project/GenericProjectTest.java index 3daf60fdbf1c..c7eb786c5a4c 100644 --- a/ide/generic.project/test/unit/src/org/netbeans/modules/generic/project/GenericProjectTest.java +++ b/ide/generic.project/test/unit/src/org/netbeans/modules/generic/project/GenericProjectTest.java @@ -55,19 +55,19 @@ public GenericProjectTest(String name) { public void testNoMarkerNotProject() throws Exception { FileObject root = newProjectDir(); - assertFalse(new GenericProject.GpFactory().isProject(root)); + assertFalse(new GpFactory().isProject(root)); } public void testMarkerFileIsProject() throws Exception { FileObject root = newProjectDir(); FileUtil.createData(root, GenericProject.MARKER_FILE); - assertTrue(new GenericProject.GpFactory().isProject(root)); + assertTrue(new GpFactory().isProject(root)); } public void testAttributeOnlyIsProject() throws Exception { FileObject root = newProjectDir(); root.setAttribute(GenericProject.MARKER_ATTRIBUTE, Boolean.TRUE); - assertTrue(new GenericProject.GpFactory().isProject(root)); + assertTrue(new GpFactory().isProject(root)); } public void testLogicalViewShowsAllFiles() throws Exception { @@ -139,7 +139,7 @@ public void testSetMarkerStateLocalAndBack() throws Exception { GenericProject.setMarkerState(root, false); assertFalse(GenericProject.hasMarker(root)); assertEquals(Boolean.TRUE, root.getAttribute(GenericProject.MARKER_ATTRIBUTE)); - assertTrue(new GenericProject.GpFactory().isProject(root)); + assertTrue(new GpFactory().isProject(root)); GenericProject.setMarkerState(root, true); assertTrue(GenericProject.hasMarker(root)); @@ -153,7 +153,7 @@ public void testSaveProjectLocalKeepsNoMarker() throws Exception { FileObject root = FileUtil.toFileObject(dir); Project prj = ProjectManager.getDefault().findProject(root); assertNotNull(prj); - new GenericProject.GpFactory().saveProject(prj); + new GpFactory().saveProject(prj); assertNull(root.getFileObject(GenericProject.MARKER_FILE)); assertEquals(Boolean.TRUE, root.getAttribute(GenericProject.MARKER_ATTRIBUTE)); } @@ -285,7 +285,7 @@ public void testDeleteOperationPreventsMarkerResurrection() throws Exception { op.notifyDeleting(); root.getFileObject(GenericProject.MARKER_FILE).delete(); // simulate the stock flow - new GenericProject.GpFactory().saveProject(prj); + new GpFactory().saveProject(prj); assertNull("a saved deleted project must not be resurrected", root.getFileObject(GenericProject.MARKER_FILE)); } @@ -300,7 +300,7 @@ public void testDeleteOperationClearsAttribute() throws Exception { op.notifyDeleting(); assertNull("the recognition attribute must be dropped", root.getAttribute(GenericProject.MARKER_ATTRIBUTE)); - new GenericProject.GpFactory().saveProject(prj); + new GpFactory().saveProject(prj); assertNull("a saved deleted local project must not gain a marker file", root.getFileObject(GenericProject.MARKER_FILE)); } From d0de93ffeb420b7f8d431315bf40d5fc04fb82a7 Mon Sep 17 00:00:00 2001 From: Michael Bien Date: Tue, 20 Oct 2026 23:18:05 +0200 Subject: [PATCH 11/13] fix FoD tests --- .../modules/ide/ergonomics/fod/FoDLayersProvider.java | 8 ++++++++ .../modules/ide/ergonomics/DynamicVerifyTest.java | 2 ++ 2 files changed, 10 insertions(+) diff --git a/ergonomics/ide.ergonomics/src/org/netbeans/modules/ide/ergonomics/fod/FoDLayersProvider.java b/ergonomics/ide.ergonomics/src/org/netbeans/modules/ide/ergonomics/fod/FoDLayersProvider.java index 9df5d459a331..52e11b94acaf 100755 --- a/ergonomics/ide.ergonomics/src/org/netbeans/modules/ide/ergonomics/fod/FoDLayersProvider.java +++ b/ergonomics/ide.ergonomics/src/org/netbeans/modules/ide/ergonomics/fod/FoDLayersProvider.java @@ -145,12 +145,20 @@ private boolean noAdditionalProjects() { if (pf.getClass().getName().contains("AntBasedProjectFactorySingleton")) { // NOI18N continue; } + // #162517 if (pf.getClass().getName().startsWith("org.netbeans.modules.ide.ergonomics")) { // NOI18N continue; } + // #248615 if (pf.getClass().getName().startsWith("org.netbeans.modules.project.ui.convertor.ProjectConvertorFactory")) { // NOI18N continue; } + // generic.project ships in the base ide cluster: its factory is + // always present and must not count as an "additional" project, + // or an empty IDE would never be detected as empty. + if (pf.getClass().getName().startsWith("org.netbeans.modules.generic.project")) { // NOI18N + continue; + } return false; } return ants.allItems().isEmpty(); diff --git a/ergonomics/ide.ergonomics/test/unit/src/org/netbeans/modules/ide/ergonomics/DynamicVerifyTest.java b/ergonomics/ide.ergonomics/test/unit/src/org/netbeans/modules/ide/ergonomics/DynamicVerifyTest.java index d17492fb3c93..893ec2a6d879 100644 --- a/ergonomics/ide.ergonomics/test/unit/src/org/netbeans/modules/ide/ergonomics/DynamicVerifyTest.java +++ b/ergonomics/ide.ergonomics/test/unit/src/org/netbeans/modules/ide/ergonomics/DynamicVerifyTest.java @@ -145,10 +145,12 @@ public void testGetAllProjectFactories() throws Exception { StringBuilder sb = new StringBuilder(); Map all = FeatureManager.projectFiles(); + // no project.file.* property in ergonomics/ide.ergonomics/ all.put("Fine", "org.netbeans.modules.project.ant.AntBasedProjectFactorySingleton"); all.put("FineToo", "org.netbeans.modules.project.ui.convertor.ProjectConvertorFactory"); all.put("MostlyOK", "org.netbeans.modules.java.openjdk.project.FilterStandardProjects"); all.put("NoProjectFileAvailable", "org.netbeans.modules.cpplite.project.CPPLiteProject$FactoryImpl"); + all.put("NoProjectFileAvailableGp", "org.netbeans.modules.generic.project.GpFactory"); /* * The "rust" cluster is not included in all cluster configurations in nbbuild/cluster.properties. From cfac659c530d54ad7ab4160334e4077b5d94ee2c Mon Sep 17 00:00:00 2001 From: Michael Bien Date: Tue, 20 Oct 2026 12:56:56 +0200 Subject: [PATCH 12/13] [2] dir project --- ide/generic.project/nbproject/project.xml | 18 +++ .../generic/project/GenericProject.java | 39 +++++- .../project/GpDirectoryProjectProvider.java | 43 ++++++ .../project/GpProjectContainerProvider.java | 112 ++++++++++++++++ .../generic/project/GpSubprojectProvider.java | 71 ---------- .../generic/project/GenericProjectTest.java | 124 +++++++++++++++++- .../project/ui/groups/NewGroupPanel.java | 24 +++- ide/projectuiapi/apichanges.xml | 24 +++- ide/projectuiapi/nbproject/project.properties | 2 +- .../ui/support/DirectoryProjectProvider.java | 66 ++++++++++ 10 files changed, 444 insertions(+), 79 deletions(-) create mode 100644 ide/generic.project/src/org/netbeans/modules/generic/project/GpDirectoryProjectProvider.java create mode 100644 ide/generic.project/src/org/netbeans/modules/generic/project/GpProjectContainerProvider.java delete mode 100644 ide/generic.project/src/org/netbeans/modules/generic/project/GpSubprojectProvider.java create mode 100644 ide/projectuiapi/src/org/netbeans/spi/project/ui/support/DirectoryProjectProvider.java diff --git a/ide/generic.project/nbproject/project.xml b/ide/generic.project/nbproject/project.xml index d465bcf47ae2..3af17479d264 100644 --- a/ide/generic.project/nbproject/project.xml +++ b/ide/generic.project/nbproject/project.xml @@ -60,6 +60,24 @@ 1.95.0.8
+ + org.netbeans.modules.projectuiapi.base + + + + 1 + 1.120 + + + + org.netbeans.modules.queries + + + + 1 + 1.27 + + org.openide.awt diff --git a/ide/generic.project/src/org/netbeans/modules/generic/project/GenericProject.java b/ide/generic.project/src/org/netbeans/modules/generic/project/GenericProject.java index 388e21afc682..1d6e280ba711 100644 --- a/ide/generic.project/src/org/netbeans/modules/generic/project/GenericProject.java +++ b/ide/generic.project/src/org/netbeans/modules/generic/project/GenericProject.java @@ -25,7 +25,9 @@ import org.netbeans.api.annotations.common.StaticResource; import org.netbeans.api.project.Project; import org.netbeans.api.project.ProjectInformation; +import org.netbeans.api.project.ProjectManager; import org.netbeans.api.project.ProjectUtils; +import org.netbeans.api.queries.SharabilityQuery; import org.netbeans.modules.generic.project.ui.actions.GpActionProvider; import org.netbeans.modules.generic.project.ui.actions.GpProjectOperations; import org.netbeans.modules.generic.project.ui.customizer.GpCustomizerProvider; @@ -105,7 +107,7 @@ boolean isDeleted() { this.lookup = LookupProviderSupport.createCompositeLookup(Lookups.fixed( new GpLogicalViewProvider(this), new GpInfo(this), - new GpSubprojectProvider(this), + new GpProjectContainerProvider(this), new GpCustomizerProvider(this), new GpProjectOperations(this), new GpActionProvider(this), @@ -185,6 +187,41 @@ public static void setMarkerState(FileObject dir, boolean markerFile) throws IOE } } + /** + * Promotes the given directory to a local (attribute-based) + * generic project and returns its canonical project instance. + * + *

If the directory already is a project (of any type), the + * existing project is returned without modification. A directory + * that the {@code SharabilityQuery} marks as {@code NOT_SHARABLE} + * is never promoted; {@code null} is returned in that case. + * + * @param dir the directory to promote (never {@code null}) + * @return the project for the directory, or {@code null} if the + * directory cannot be promoted + * @throws IOException if the recognition attribute could not be set + */ + public static Project promoteToProject(FileObject dir) throws IOException { + Project prj = ProjectManager.getDefault().findProject(dir); + if (prj != null) { + // the directory is already a project (of any type): + // idempotent, nothing to do + return prj; + } + if (SharabilityQuery.getSharability(dir) == SharabilityQuery.Sharability.NOT_SHARABLE) { + // the flip side of the NOT_SHARABLE check in the container + // provider (which is about reading projects out of a + // non-sharable directory): a per-user promotion must not + // touch a directory that is not meant to be shared + return null; + } + setMarkerState(dir, false); + // drop the negative cache entry the lookup above stored for a + // directory that was not a project yet + ProjectManager.getDefault().clearNonProjectCache(); + return ProjectManager.getDefault().findProject(dir); + } + /** * Key of the display name override in the project's private * preferences ({@link ProjectUtils#getPreferences}). diff --git a/ide/generic.project/src/org/netbeans/modules/generic/project/GpDirectoryProjectProvider.java b/ide/generic.project/src/org/netbeans/modules/generic/project/GpDirectoryProjectProvider.java new file mode 100644 index 000000000000..4023d45bb995 --- /dev/null +++ b/ide/generic.project/src/org/netbeans/modules/generic/project/GpDirectoryProjectProvider.java @@ -0,0 +1,43 @@ +/* + * 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.generic.project; + +import java.io.IOException; +import org.netbeans.api.project.Project; +import org.netbeans.spi.project.ui.support.DirectoryProjectProvider; +import org.openide.filesystems.FileObject; +import org.openide.util.lookup.ServiceProvider; + +/** + * Promotion SPI for the "New Group" (directory) flow and other usecase which + * require a project directory. + * + * The stock group machinery (projectui) asks the default lookup for this + * service when a directory group is created, and turns the + * directory into a local generic project. + */ +@ServiceProvider(service=DirectoryProjectProvider.class) +public final class GpDirectoryProjectProvider implements DirectoryProjectProvider { + + @Override + public Project createProject(FileObject dir) throws IOException { + return GenericProject.promoteToProject(dir); + } +} diff --git a/ide/generic.project/src/org/netbeans/modules/generic/project/GpProjectContainerProvider.java b/ide/generic.project/src/org/netbeans/modules/generic/project/GpProjectContainerProvider.java new file mode 100644 index 000000000000..d47f674bcc1a --- /dev/null +++ b/ide/generic.project/src/org/netbeans/modules/generic/project/GpProjectContainerProvider.java @@ -0,0 +1,112 @@ +/* + * 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.generic.project; + +import java.io.IOException; +import java.util.Enumeration; +import java.util.LinkedHashSet; +import java.util.Set; +import javax.swing.event.ChangeListener; +import org.netbeans.api.project.Project; +import org.netbeans.api.project.ProjectManager; +import org.netbeans.api.queries.SharabilityQuery; +import org.netbeans.spi.project.ProjectContainerProvider; +import org.netbeans.spi.project.SubprojectProvider; +import org.netbeans.spi.project.ui.support.ProjectConvertors; +import org.openide.filesystems.FileObject; +import org.openide.util.ChangeSupport; +import org.openide.util.Exceptions; + +/** + * Exposes the projects contained in the project directory as + * subprojects: the project is a container for the projects in its + * directory, so the stock subprojects group and "Open Required + * Projects" discover them through this project instead of a + * hard-coded directory scan. + * + *

Implements both the newer {@link ProjectContainerProvider} + * and the legacy {@link SubprojectProvider} over the same set. + */ +final class GpProjectContainerProvider implements ProjectContainerProvider, SubprojectProvider { + + private final GenericProject prj; + private final ChangeSupport changeSupport = new ChangeSupport(this); + + GpProjectContainerProvider(GenericProject prj) { + this.prj = prj; + } + + @Override + public Result getContainedProjects() { + Set projects = new LinkedHashSet<>(); + collectProjects(prj.getProjectDirectory(), projects, true); + projects.remove(prj); + return new Result(projects, false); + } + + @Override + public Set getSubprojects() { + return getContainedProjects().getProjects(); + } + + /** + * Collects the projects contained in the given folder: folders + * only, a {@code findProject} per folder, convertor projects + * skipped, and the children of a {@code NOT_SHARABLE} folder + * skipped (the check is skipped once the folder is proven + * {@code SHARABLE}). + */ + private void collectProjects(FileObject dir, Set projects, boolean checkShare) { + Enumeration e = dir.getFolders(false); + while (e.hasMoreElements()) { + if (Thread.interrupted()) { + return; + } + FileObject child = e.nextElement(); + SharabilityQuery.Sharability share = SharabilityQuery.Sharability.UNKNOWN; + if (checkShare) { + share = SharabilityQuery.getSharability(dir); + if (share == SharabilityQuery.Sharability.NOT_SHARABLE) { + continue; + } + } + Project p = null; + try { + p = ProjectManager.getDefault().findProject(child); + } catch (IOException x) { + Exceptions.printStackTrace(x); + } + if (p != null && !ProjectConvertors.isConvertorProject(p)) { + projects.add(p); + } + checkShare = share != SharabilityQuery.Sharability.SHARABLE; + } + } + + @Override + public void addChangeListener(ChangeListener listener) { + changeSupport.addChangeListener(listener); + } + + @Override + public void removeChangeListener(ChangeListener listener) { + changeSupport.removeChangeListener(listener); + } +} diff --git a/ide/generic.project/src/org/netbeans/modules/generic/project/GpSubprojectProvider.java b/ide/generic.project/src/org/netbeans/modules/generic/project/GpSubprojectProvider.java deleted file mode 100644 index 02e228057a98..000000000000 --- a/ide/generic.project/src/org/netbeans/modules/generic/project/GpSubprojectProvider.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * 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.generic.project; - -import java.util.Collections; -import java.util.LinkedHashSet; -import java.util.Set; -import javax.swing.event.ChangeListener; -import org.netbeans.api.project.FileOwnerQuery; -import org.netbeans.api.project.Project; -import org.netbeans.spi.project.SubprojectProvider; -import org.openide.filesystems.FileObject; -import org.openide.util.ChangeSupport; - -/** - * The legacy {@link SubprojectProvider} view of the projects nested - * directly in the project directory: every child folder owned by - * another project is a subproject. - */ -final class GpSubprojectProvider implements SubprojectProvider { - - private final GenericProject prj; - private final ChangeSupport changeSupport = new ChangeSupport(this); - - GpSubprojectProvider(GenericProject prj) { - this.prj = prj; - } - - @Override - public Set getSubprojects() { - Set subprojects = new LinkedHashSet<>(); - FileObject[] children = prj.getProjectDirectory().getChildren(); - if (children != null) { - for (FileObject child : children) { - Project owner = FileOwnerQuery.getOwner(child); - if (owner != null && !owner.equals(prj)) { - subprojects.add(owner); - } - } - } - return Collections.unmodifiableSet(subprojects); - } - - @Override - public void addChangeListener(ChangeListener listener) { - changeSupport.addChangeListener(listener); - } - - @Override - public void removeChangeListener(ChangeListener listener) { - changeSupport.removeChangeListener(listener); - } - -} \ No newline at end of file diff --git a/ide/generic.project/test/unit/src/org/netbeans/modules/generic/project/GenericProjectTest.java b/ide/generic.project/test/unit/src/org/netbeans/modules/generic/project/GenericProjectTest.java index c7eb786c5a4c..921f60dc43e0 100644 --- a/ide/generic.project/test/unit/src/org/netbeans/modules/generic/project/GenericProjectTest.java +++ b/ide/generic.project/test/unit/src/org/netbeans/modules/generic/project/GenericProjectTest.java @@ -20,6 +20,7 @@ package org.netbeans.modules.generic.project; import java.io.File; +import java.net.URI; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -28,19 +29,25 @@ import org.netbeans.api.project.ProjectInformation; import org.netbeans.api.project.ProjectManager; import org.netbeans.api.project.ProjectUtils; +import org.netbeans.api.queries.SharabilityQuery; import org.netbeans.junit.NbTestCase; import org.netbeans.modules.generic.project.ui.actions.GpProjectOperations; import org.netbeans.modules.generic.project.ui.wizard.GpWizardIterator; import org.netbeans.spi.project.ActionProvider; +import org.netbeans.spi.project.ProjectContainerProvider; import org.netbeans.spi.project.ProjectServiceProvider; import org.netbeans.spi.project.SubprojectProvider; import org.netbeans.spi.project.ui.LogicalViewProvider; +import org.netbeans.spi.project.ui.support.DirectoryProjectProvider; +import org.netbeans.spi.queries.SharabilityQueryImplementation2; import org.openide.filesystems.FileObject; import org.openide.filesystems.FileUtil; import org.openide.loaders.DataObject; import org.openide.nodes.Node; +import org.openide.util.Lookup; import org.openide.util.test.MockLookup; +@SuppressWarnings("null") public class GenericProjectTest extends NbTestCase { static { @@ -85,7 +92,7 @@ public void testLogicalViewShowsAllFiles() throws Exception { assertEquals(2, children.length); } - public void testSubprojectsAreFirstLevelOnly() throws Exception { + public void testSubprojectsAreContainedProjects() throws Exception { FileObject root = newProjectDir(); FileUtil.createData(root, GenericProject.MARKER_FILE); FileObject child = FileUtil.createFolder(root, "child"); // NOI18N @@ -99,9 +106,118 @@ public void testSubprojectsAreFirstLevelOnly() throws Exception { assertNotNull(prj); SubprojectProvider spp = prj.getLookup().lookup(SubprojectProvider.class); assertNotNull(spp); - Set subs = spp.getSubprojects(); - assertEquals(1, subs.size()); - assertEquals(child, subs.iterator().next().getProjectDirectory()); + assertEquals("projects nested deeper than the directory are not exposed", + Set.of(child), projectDirectories(spp.getSubprojects())); + } + + public void testContainerProviderCompleteContainedSet() throws Exception { + FileObject root = newProjectDir(); + FileUtil.createData(root, GenericProject.MARKER_FILE); + FileObject child = FileUtil.createFolder(root, "child"); // NOI18N + FileUtil.createData(child, GenericProject.MARKER_FILE); + FileObject deep = FileUtil.createFolder(FileUtil.createFolder(child, "inner"), "deep"); // NOI18N + FileUtil.createData(deep, GenericProject.MARKER_FILE); + + Project prj = ProjectManager.getDefault().findProject(root); + assertNotNull(prj); + ProjectContainerProvider pcp = prj.getLookup().lookup(ProjectContainerProvider.class); + assertNotNull(pcp); + ProjectContainerProvider.Result res = pcp.getContainedProjects(); + assertEquals("projects nested deeper than the directory are not exposed", + Set.of(child), projectDirectories(res.getProjects())); + for (Project p : res.getProjects()) { + assertNotSame("the container must not list itself", prj, p); + } + SubprojectProvider spp = prj.getLookup().lookup(SubprojectProvider.class); + assertNotNull(spp); + assertEquals("legacy and container views must agree", + projectDirectories(res.getProjects()), projectDirectories(spp.getSubprojects())); + } + + public void testContainerProviderSkipsNotSharableSubtrees() throws Exception { + FileObject root = newProjectDir(); + FileUtil.createData(root, GenericProject.MARKER_FILE); + FileObject hidden = FileUtil.createFolder(root, "hidden"); // NOI18N + FileObject inside = FileUtil.createFolder(hidden, "inside"); // NOI18N + FileUtil.createData(inside, GenericProject.MARKER_FILE); + FileObject visible = FileUtil.createFolder(root, "visible"); // NOI18N + FileUtil.createData(visible, GenericProject.MARKER_FILE); + + MockLookup.setInstances(sharabilityFor(hidden, SharabilityQuery.Sharability.NOT_SHARABLE)); + try { + Project prj = ProjectManager.getDefault().findProject(root); + assertNotNull(prj); + ProjectContainerProvider pcp = prj.getLookup().lookup(ProjectContainerProvider.class); + assertNotNull(pcp); + assertEquals(new HashSet<>(List.of(visible)), + projectDirectories(pcp.getContainedProjects().getProjects())); + } finally { + MockLookup.setInstances(); + } + } + + public void testPromotePlainDirectory() throws Exception { + FileObject dir = newProjectDir(); + Project prj = GenericProject.promoteToProject(dir); + assertNotNull(prj); + assertTrue(prj instanceof GenericProject); + assertEquals(Boolean.TRUE, dir.getAttribute(GenericProject.MARKER_ATTRIBUTE)); + assertNull("promotion must not touch the working tree", + dir.getFileObject(GenericProject.MARKER_FILE)); + assertEquals(prj, ProjectManager.getDefault().findProject(dir)); + } + + public void testPromoteIsIdempotent() throws Exception { + FileObject dir = newProjectDir(); + Project first = GenericProject.promoteToProject(dir); + Project second = GenericProject.promoteToProject(dir); + assertSame(first, second); + } + + public void testPromoteExistingProjectIsNoOp() throws Exception { + FileObject root = newProjectDir(); + FileUtil.createData(root, GenericProject.MARKER_FILE); + Project existing = ProjectManager.getDefault().findProject(root); + assertNotNull(existing); + assertSame(existing, GenericProject.promoteToProject(root)); + assertFalse("the marker project must not gain a local attribute", + GenericProject.isAttributeProject(root)); + } + + public void testPromoteNotSharableDirectory() throws Exception { + FileObject dir = newProjectDir(); + MockLookup.setInstances(sharabilityFor(dir, SharabilityQuery.Sharability.NOT_SHARABLE)); + try { + assertNull(GenericProject.promoteToProject(dir)); + assertNull(dir.getAttribute(GenericProject.MARKER_ATTRIBUTE)); + } finally { + MockLookup.setInstances(); + } + } + + public void testDirectoryProjectProviderService() throws Exception { + DirectoryProjectProvider provider = Lookup.getDefault().lookup(DirectoryProjectProvider.class); + assertNotNull("the promotion service must be registered in the default lookup", provider); + FileObject dir = newProjectDir(); + Project prj = provider.createProject(dir); + assertNotNull(prj); + assertTrue(prj instanceof GenericProject); + assertEquals(Boolean.TRUE, dir.getAttribute(GenericProject.MARKER_ATTRIBUTE)); + assertNull(dir.getFileObject(GenericProject.MARKER_FILE)); + } + + private Set projectDirectories(Set projects) { + Set dirs = new HashSet<>(); + for (Project p : projects) { + dirs.add(p.getProjectDirectory()); + } + return dirs; + } + + private static SharabilityQueryImplementation2 sharabilityFor(FileObject dir, SharabilityQuery.Sharability answer) { + return (URI uri) -> dir.toURI().equals(uri) + ? answer + : SharabilityQuery.Sharability.UNKNOWN; } public void testCreateProject() throws Exception { diff --git a/ide/projectui/src/org/netbeans/modules/project/ui/groups/NewGroupPanel.java b/ide/projectui/src/org/netbeans/modules/project/ui/groups/NewGroupPanel.java index 53fff6df79a4..154c2de71525 100644 --- a/ide/projectui/src/org/netbeans/modules/project/ui/groups/NewGroupPanel.java +++ b/ide/projectui/src/org/netbeans/modules/project/ui/groups/NewGroupPanel.java @@ -35,11 +35,14 @@ import org.netbeans.api.project.ui.OpenProjects; import static org.netbeans.modules.project.ui.groups.Bundle.*; import static org.netbeans.modules.project.ui.groups.ManageGroupsPanel.NONE_GOUP; +import org.netbeans.modules.project.ui.OpenProjectList; +import org.netbeans.spi.project.ui.support.DirectoryProjectProvider; import org.netbeans.spi.project.ui.support.ProjectChooser; import org.openide.NotificationLineSupport; import org.openide.filesystems.FileObject; import org.openide.filesystems.FileUtil; import org.openide.util.Exceptions; +import org.openide.util.Lookup; import org.openide.util.NbBundle.Messages; /** @@ -218,7 +221,26 @@ public static Group create(Type type, String name, boolean autoSync, boolean use } else { assert Type.DIR == type; FileObject f = FileUtil.toFileObject(FileUtil.normalizeFile(new File(directory))); - return DirectoryGroup.create(name, f); + // Ask a project type to promote the directory to a project + // of its own (e.g. a local generic project): the group is + // then a stock subprojects group of the promoted project, + // so nested projects are discovered through the project's + // own lookup. + DirectoryProjectProvider provider = Lookup.getDefault().lookup(DirectoryProjectProvider.class); + if (provider == null) { + throw new IllegalStateException("Cannot create a project group: no provider found"); + } + Project prj; + try { + prj = provider.createProject(f); + } catch (IOException x) { + throw new AssertionError(x); + } + if (prj == null) { + throw new IllegalStateException("Cannot create a project group for directory: " + f.getName()); + } + OpenProjectList.getDefault().open(new Project[] {prj}, false, false, true, null); + return SubprojectsGroup.create(name, prj); } } diff --git a/ide/projectuiapi/apichanges.xml b/ide/projectuiapi/apichanges.xml index 18071495a851..912ca985d1f3 100644 --- a/ide/projectuiapi/apichanges.xml +++ b/ide/projectuiapi/apichanges.xml @@ -20,7 +20,7 @@ --> - + + + +

Provider SPI for promoting a directory to a project when it becomes a group in the projects view. + + + + + +

+ New DirectoryProjectProvider SPI in + org.netbeans.spi.project.ui.support: + implementations registered in the default lookup may + turn the directory chosen in the "New Group" dialog + into a project of their type. The group machinery uses + the returned project to create a stock subprojects + group, and falls back to the stock directory group + when no implementation is registered or it returns + null. +

+
+ + Allow pluggable implementation for ProjectProblems API. diff --git a/ide/projectuiapi/nbproject/project.properties b/ide/projectuiapi/nbproject/project.properties index 9746fd18edd7..f4a7306db904 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.124.0 +spec.version.base=1.125.0 is.autoload=true javadoc.arch=${basedir}/arch.xml javadoc.apichanges=${basedir}/apichanges.xml diff --git a/ide/projectuiapi/src/org/netbeans/spi/project/ui/support/DirectoryProjectProvider.java b/ide/projectuiapi/src/org/netbeans/spi/project/ui/support/DirectoryProjectProvider.java new file mode 100644 index 000000000000..65e71e32f504 --- /dev/null +++ b/ide/projectuiapi/src/org/netbeans/spi/project/ui/support/DirectoryProjectProvider.java @@ -0,0 +1,66 @@ +/* + * 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.spi.project.ui.support; + +import java.io.IOException; +import org.netbeans.api.project.Project; +import org.openide.filesystems.FileObject; + +/** + * Provides a project for a directory which is about to become a group + * in the projects view. + * + *

When the user creates a new "directory" project group, the group + * machinery asks the default lookup for an implementation of this + * service before falling back to the stock group, which passively lists + * the projects found beneath the directory. An implementation may + * promote the directory to a project of its own type — for + * example by marking it in a per-user, non-intrusive way — and return + * the canonical project instance; the group is then shown as the stock + * subprojects group of that project, so nested projects are discovered + * through the project's own lookup instead of a hard-coded directory + * scan. + * + *

Implementations must be conservative: + *

    + *
  • a directory the implementation does not understand must be + * answered with {@code null}, never with an exception or a project + * of an unrelated type;
  • + *
  • if the directory already is a project (of any type), the existing + * canonical project instance must be returned — never a new one.
  • + *
+ * @since 1.125 + */ +public interface DirectoryProjectProvider { + + /** + * Returns the project for the given directory, creating it if this + * provider can make the directory into a project. + * @param dir a directory (never {@code null}) + * @return the project for the directory — the existing project if + * the directory already is a project, a newly promoted one + * if this provider made it so — or {@code null} if the + * provider cannot handle the directory + * @throws IOException if a project could not be determined + */ + Project createProject(FileObject dir) throws IOException; + +} + From 5fb379fd4bb9dbbbf43b31d1d9b23e4aeb0b64fc Mon Sep 17 00:00:00 2001 From: Michael Bien Date: Tue, 20 Oct 2026 20:15:47 +0200 Subject: [PATCH 13/13] [2] implement migration path on load --- .../project/ui/groups/Bundle.properties | 14 -- .../project/ui/groups/DirectoryGroup.java | 148 ------------- .../ui/groups/DirectoryGroupEditPanel.form | 147 ------------- .../ui/groups/DirectoryGroupEditPanel.java | 200 ------------------ .../modules/project/ui/groups/Group.java | 49 ++++- 5 files changed, 45 insertions(+), 513 deletions(-) delete mode 100644 ide/projectui/src/org/netbeans/modules/project/ui/groups/DirectoryGroup.java delete mode 100644 ide/projectui/src/org/netbeans/modules/project/ui/groups/DirectoryGroupEditPanel.form delete mode 100644 ide/projectui/src/org/netbeans/modules/project/ui/groups/DirectoryGroupEditPanel.java diff --git a/ide/projectui/src/org/netbeans/modules/project/ui/groups/Bundle.properties b/ide/projectui/src/org/netbeans/modules/project/ui/groups/Bundle.properties index 9d426f8d6511..46ef7d0a9863 100644 --- a/ide/projectui/src/org/netbeans/modules/project/ui/groups/Bundle.properties +++ b/ide/projectui/src/org/netbeans/modules/project/ui/groups/Bundle.properties @@ -34,8 +34,6 @@ AdHocGroupEditPanel.synchButton.text=&Save Current Project List Now SubprojectsGroupEditPanel.nameLabel.text=&Name: SubprojectsGroupEditPanel.masterProjectLabel.text=&Master Project: -DirectoryGroupEditPanel.nameLabel.text=&Name\: -DirectoryGroupEditPanel.folderLabel.text=&Folder\: SubprojectsGroupEditPanel.nameLabel.AccessibleContext.accessibleDescription=Name of the project group SubprojectsGroupEditPanel.masterProjectLabel.AccessibleContext.accessibleDescription=Group master project name SubprojectsGroupEditPanel.nameField.AccessibleContext.accessibleName=Name @@ -44,14 +42,6 @@ SubprojectsGroupEditPanel.masterProjectField.AccessibleContext.accessibleName= SubprojectsGroupEditPanel.masterProjectField.AccessibleContext.accessibleDescription= SubprojectsGroupEditPanel.AccessibleContext.accessibleName=Project group properties SubprojectsGroupEditPanel.AccessibleContext.accessibleDescription=Project group properties -DirectoryGroupEditPanel.nameLabel.AccessibleContext.accessibleDescription=Name of the project group -DirectoryGroupEditPanel.folderLabel.AccessibleContext.accessibleDescription=Project group folder -DirectoryGroupEditPanel.nameField.AccessibleContext.accessibleName=Name -DirectoryGroupEditPanel.nameField.AccessibleContext.accessibleDescription=Project group name -DirectoryGroupEditPanel.folderField.AccessibleContext.accessibleName= -DirectoryGroupEditPanel.folderField.AccessibleContext.accessibleDescription= -DirectoryGroupEditPanel.AccessibleContext.accessibleName=Project group properties -DirectoryGroupEditPanel.AccessibleContext.accessibleDescription=Project group properties AdHocGroupEditPanel.AccessibleContext.accessibleName=Project group properties AdHocGroupEditPanel.AccessibleContext.accessibleDescription=Project group properties AdHocGroupEditPanel.nameLabel.AccessibleContext.accessibleDescription=Project group name @@ -86,8 +76,4 @@ ManageGroupsPanel.propertiesButton.text=&Properties ManageGroupsPanel.removeButton.text=&Remove ManageGroupsPanel.selectionLabel.text=Select Group(s) ManageGroupsPanel.removeAllButton.text=Remove &All -DirectoryGroupEditPanel.masterProjectButton.text=&Browse... -DirectoryGroupEditPanel.directoryButton.text=Bro&wse... -DirectoryGroupEditPanel.directoryButton.text_1=Bro&wse... SubprojectsGroupEditPanel.masterProjectButton.text=&Browse... -DirectoryGroupEditPanel.directoryButton.text_2=Bro&wse... 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 deleted file mode 100644 index adacb9bb6248..000000000000 --- a/ide/projectui/src/org/netbeans/modules/project/ui/groups/DirectoryGroup.java +++ /dev/null @@ -1,148 +0,0 @@ -/* - * 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.io.IOException; -import java.net.MalformedURLException; -import java.net.URL; -import java.util.Enumeration; -import java.util.Set; -import java.util.logging.Level; -import java.util.logging.Logger; -import java.util.prefs.Preferences; -import javax.swing.SwingUtilities; -import org.netbeans.api.progress.ProgressHandle; -import org.netbeans.api.project.Project; -import org.netbeans.api.project.ProjectManager; -import org.netbeans.api.queries.SharabilityQuery; -import org.netbeans.spi.project.ui.support.ProjectConvertors; -import org.openide.filesystems.FileObject; -import org.openide.filesystems.URLMapper; -import org.openide.util.Exceptions; - -/** - * All projects which can be found beneath some directory, with an optional main project. - * @author Jesse Glick - */ -public class DirectoryGroup extends Group { - - private static final Logger LOG = Logger.getLogger(DirectoryGroup.class.getName()); - - static final String KIND = "directory"; // NOI18N - - /** - * Create a new group derived from a directory. - */ - public static DirectoryGroup create(String name, FileObject dir) { - String path = dir.toURL().toExternalForm(); - String id = sanitizeNameAndUniquifyForId(name); - LOG.log(Level.FINE, "Creating: {0}", id); - Preferences p = NODE.node(id); - p.put(KEY_NAME, name); - p.put(KEY_KIND, KIND); - p.put(KEY_PATH, path); - return new DirectoryGroup(id); - } - - DirectoryGroup(String id) { - super(id); - } - - @Override - protected void findProjects(Set projects, ProgressHandle h, int start, int end) { - assert !SwingUtilities.isEventDispatchThread(); - String dir = prefs().get(KEY_PATH, null); - FileObject fo = null; - try { - fo = URLMapper.findFileObject(new URL(dir)); - } catch (MalformedURLException x) { - LOG.log(Level.WARNING, "MalformedURLException: {0}", dir); - } - if (fo != null && fo.isFolder()) { - try { - Project p = ProjectManager.getDefault().findProject(fo); - if (p != null) { - projects.add(p); - if (h != null) { - h.progress(progressMessage(p), Math.min(++start, end)); - } - } - } catch (IOException x) { - Exceptions.printStackTrace(x); - } - Counter counter = new Counter(); - counter.start = start; - counter.end = end; - processFolderChildren(fo, projects, h, counter, true); - } - } - - public FileObject getDirectory() { - String p = prefs().get(KEY_PATH, null); - if (p != null && p.length() > 0) { - try { - return URLMapper.findFileObject(new URL(p)); - } catch (MalformedURLException x) { - LOG.log(Level.WARNING, null, x); - } - } - return null; - } - - @Override - public GroupEditPanel createPropertiesPanel() { - return new DirectoryGroupEditPanel(this); - } - - private void processFolderChildren(FileObject fo, Set projects, ProgressHandle h, Counter counter, boolean checkShare) throws IllegalArgumentException { - Enumeration e = fo.getFolders(false); - while (e.hasMoreElements()) { - try { - FileObject em = e.nextElement(); - SharabilityQuery.Sharability share = SharabilityQuery.Sharability.UNKNOWN; - if (checkShare) { - share = SharabilityQuery.getSharability(fo); - if (share == SharabilityQuery.Sharability.NOT_SHARABLE) { - continue; - } - } - Project p = ProjectManager.getDefault().findProject(em); - if (p != null && !ProjectConvertors.isConvertorProject(p)) { - projects.add(p); - if (h != null) { - h.progress(progressMessage(p), Math.min(++counter.start, counter.end)); - } - } - checkShare = share != SharabilityQuery.Sharability.SHARABLE; - // don't need to check the sharability if the current folder is marked as recursively sharable - - processFolderChildren(em, projects, h, counter, checkShare); - } catch (IOException x) { - Exceptions.printStackTrace(x); - } - } - } - - private static class Counter { - int start; - int end; - } - -} diff --git a/ide/projectui/src/org/netbeans/modules/project/ui/groups/DirectoryGroupEditPanel.form b/ide/projectui/src/org/netbeans/modules/project/ui/groups/DirectoryGroupEditPanel.form deleted file mode 100644 index d523c9f67fae..000000000000 --- a/ide/projectui/src/org/netbeans/modules/project/ui/groups/DirectoryGroupEditPanel.form +++ /dev/null @@ -1,147 +0,0 @@ - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
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 deleted file mode 100644 index 039d04aa4242..000000000000 --- a/ide/projectui/src/org/netbeans/modules/project/ui/groups/DirectoryGroupEditPanel.java +++ /dev/null @@ -1,200 +0,0 @@ -/* - * 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.io.File; -import java.util.prefs.Preferences; -import javax.swing.JFileChooser; -import javax.swing.event.DocumentEvent; -import javax.swing.event.DocumentListener; -import static org.netbeans.modules.project.ui.groups.Group.KEY_PATH; -import static org.netbeans.modules.project.ui.groups.Group.NODE; -import static org.netbeans.modules.project.ui.groups.GroupEditPanel.PROP_READY; -import org.netbeans.spi.project.ui.support.ProjectChooser; -import org.openide.filesystems.FileObject; -import org.openide.filesystems.FileUtil; -import org.openide.util.RequestProcessor; - -/** - * Panel to configure state of an existing directory-based group. - * @author Jesse Glick - */ -public class DirectoryGroupEditPanel extends GroupEditPanel { - - private final DirectoryGroup g; - - public DirectoryGroupEditPanel(DirectoryGroup g) { - this.g = g; - initComponents(); - DocumentListener l = new DocumentListener() { - @Override public void insertUpdate(DocumentEvent e) { - firePropertyChange(PROP_READY, null, null); - } - @Override public void removeUpdate(DocumentEvent e) { - firePropertyChange(PROP_READY, null, null); - } - @Override public void changedUpdate(DocumentEvent e) {} - }; - nameField.setText(g.getName()); - nameField.getDocument().addDocumentListener(l); - FileObject dir = g.getDirectory(); - if (dir != null) { - File d = FileUtil.toFile(dir); - if (d != null) { - folderField.setText(d.getAbsolutePath()); - } - } - folderField.getDocument().addDocumentListener(l); - } - - @Override - public void applyChanges() { - g.setName(nameField.getText().trim()); - updateDirectory(); - } - - private void updateDirectory() { - String s = folderField.getText(); - if (s != null && s.length() > 0) { - FileObject dir = FileUtil.toFileObject(FileUtil.normalizeFile(new File(s))); - String path = dir.toURL().toExternalForm(); - Preferences pref = NODE.node(g.id); - pref.put(KEY_PATH, path); - if(Group.getActiveGroup().equals(g)) { - RequestProcessor.getDefault().post(new Runnable() { - @Override - public void run() { - Group.open(g, null, false, null); - } - }); - } - } - } - - /** This method is called from within the constructor to - * initialize the form. - * WARNING: Do NOT modify this code. The content of this method is - * always regenerated by the Form Editor. - */ - // //GEN-BEGIN:initComponents - private void initComponents() { - - nameLabel = new javax.swing.JLabel(); - nameField = new javax.swing.JTextField(); - folderLabel = new javax.swing.JLabel(); - folderField = new javax.swing.JTextField(); - directoryButton = new javax.swing.JButton(); - - nameLabel.setLabelFor(nameField); - org.openide.awt.Mnemonics.setLocalizedText(nameLabel, org.openide.util.NbBundle.getMessage(DirectoryGroupEditPanel.class, "DirectoryGroupEditPanel.nameLabel.text")); // NOI18N - - folderLabel.setLabelFor(folderField); - org.openide.awt.Mnemonics.setLocalizedText(folderLabel, org.openide.util.NbBundle.getMessage(DirectoryGroupEditPanel.class, "DirectoryGroupEditPanel.folderLabel.text")); // NOI18N - - org.openide.awt.Mnemonics.setLocalizedText(directoryButton, org.openide.util.NbBundle.getMessage(DirectoryGroupEditPanel.class, "DirectoryGroupEditPanel.directoryButton.text_2")); // NOI18N - directoryButton.addActionListener(new java.awt.event.ActionListener() { - public void actionPerformed(java.awt.event.ActionEvent evt) { - directoryButtonActionPerformed(evt); - } - }); - - javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); - this.setLayout(layout); - layout.setHorizontalGroup( - layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(layout.createSequentialGroup() - .addGap(0, 0, 0) - .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(nameLabel) - .addComponent(folderLabel)) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(nameField, javax.swing.GroupLayout.DEFAULT_SIZE, 337, Short.MAX_VALUE) - .addGroup(layout.createSequentialGroup() - .addComponent(folderField) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(directoryButton))) - .addContainerGap()) - ); - layout.setVerticalGroup( - layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(layout.createSequentialGroup() - .addGap(0, 0, 0) - .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) - .addComponent(nameLabel) - .addComponent(nameField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) - .addComponent(folderLabel) - .addComponent(folderField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) - .addComponent(directoryButton)) - .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) - ); - - nameLabel.getAccessibleContext().setAccessibleDescription(org.openide.util.NbBundle.getMessage(DirectoryGroupEditPanel.class, "DirectoryGroupEditPanel.nameLabel.AccessibleContext.accessibleDescription")); // NOI18N - nameField.getAccessibleContext().setAccessibleName(org.openide.util.NbBundle.getMessage(DirectoryGroupEditPanel.class, "DirectoryGroupEditPanel.nameField.AccessibleContext.accessibleName")); // NOI18N - nameField.getAccessibleContext().setAccessibleDescription(org.openide.util.NbBundle.getMessage(DirectoryGroupEditPanel.class, "DirectoryGroupEditPanel.nameField.AccessibleContext.accessibleDescription")); // NOI18N - folderLabel.getAccessibleContext().setAccessibleDescription(org.openide.util.NbBundle.getMessage(DirectoryGroupEditPanel.class, "DirectoryGroupEditPanel.folderLabel.AccessibleContext.accessibleDescription")); // NOI18N - folderField.getAccessibleContext().setAccessibleName(org.openide.util.NbBundle.getMessage(DirectoryGroupEditPanel.class, "DirectoryGroupEditPanel.folderField.AccessibleContext.accessibleName")); // NOI18N - folderField.getAccessibleContext().setAccessibleDescription(org.openide.util.NbBundle.getMessage(DirectoryGroupEditPanel.class, "DirectoryGroupEditPanel.folderField.AccessibleContext.accessibleDescription")); // NOI18N - - getAccessibleContext().setAccessibleName(org.openide.util.NbBundle.getMessage(DirectoryGroupEditPanel.class, "DirectoryGroupEditPanel.AccessibleContext.accessibleName")); // NOI18N - getAccessibleContext().setAccessibleDescription(org.openide.util.NbBundle.getMessage(DirectoryGroupEditPanel.class, "DirectoryGroupEditPanel.AccessibleContext.accessibleDescription")); // NOI18N - }// //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()); - } - } - }//GEN-LAST:event_directoryButtonActionPerformed - - // Variables declaration - do not modify//GEN-BEGIN:variables - private javax.swing.JButton directoryButton; - private javax.swing.JTextField folderField; - private javax.swing.JLabel folderLabel; - private javax.swing.JTextField nameField; - private javax.swing.JLabel nameLabel; - // End of variables declaration//GEN-END:variables - - @Override - public boolean isReady() { - if(!doCheckExistingGroups(nameField, g)) { - return false; - } - String s = folderField.getText(); - if (s != null) { - return new File(s.trim()).isDirectory(); - } else { - return false; - } - } - -} diff --git a/ide/projectui/src/org/netbeans/modules/project/ui/groups/Group.java b/ide/projectui/src/org/netbeans/modules/project/ui/groups/Group.java index 484bdd425cff..29baf55686ef 100644 --- a/ide/projectui/src/org/netbeans/modules/project/ui/groups/Group.java +++ b/ide/projectui/src/org/netbeans/modules/project/ui/groups/Group.java @@ -24,6 +24,7 @@ import java.beans.PropertyChangeListener; import java.io.IOException; import java.lang.reflect.InvocationTargetException; +import java.net.URI; import java.net.URL; import java.text.Collator; import java.util.ArrayList; @@ -57,12 +58,14 @@ import org.netbeans.modules.project.ui.ProjectTab; import org.netbeans.modules.project.ui.ProjectUtilities; import static org.netbeans.modules.project.ui.groups.Bundle.*; +import org.netbeans.spi.project.ui.support.DirectoryProjectProvider; import org.openide.cookies.CloseCookie; import org.openide.filesystems.FileObject; import org.openide.filesystems.URLMapper; import org.openide.loaders.DataObject; import org.openide.loaders.DataObjectNotFoundException; import org.openide.util.Exceptions; +import org.openide.util.Lookup; import org.openide.util.NbBundle; import org.openide.util.NbBundle.Messages; import org.openide.util.NbPreferences; @@ -90,14 +93,19 @@ public abstract class Group { protected static final String KEY_NAME = "name"; // NOI18N /** Preferences key for kind of group (see constants in subclasses). */ protected static final String KEY_KIND = "kind"; // NOI18N - /** Preferences key for path (space-separated) of project URLs for AdHocGroup, or single project dir URL for SubprojectsGroup, or dir URL for DirectoryGroup. */ + /** Preferences key for path (space-separated) of project URLs for AdHocGroup, or single project dir URL for SubprojectsGroup. */ protected static final String KEY_PATH = "path"; // NOI18N - /** Preferences key for main project path URL for AdHocGroup or DirectoryGroup. */ + /** Preferences key for main project path URL for AdHocGroup. */ protected static final String KEY_MAIN = "main"; // NOI18N /** Preferences for None group. */ protected static final Preferences NONE_GROUP_NODE = NbPreferences.forModule(Group.class).node("nonegroup"); protected static final String NONE_GROUP = "(none)"; // NOI18N protected static final String noneGroupSanitizedId = "none_group"; // NOI18N + /** + * Kind of legacy directory groups, persisted before that kind was + * replaced. See {@link #migrateLegacyDirectoryGroup(String)}. + */ + private static final String LEGACY_DIRECTORY_KIND = "directory"; // NOI18N List listeners = new ArrayList(); @@ -110,14 +118,47 @@ private static Group load(String id) { return new AdHocGroup(id); } else if (SubprojectsGroup.KIND.equals(kind)) { return new SubprojectsGroup(id); - } else if (DirectoryGroup.KIND.equals(kind)) { - return new DirectoryGroup(id); + } else if (LEGACY_DIRECTORY_KIND.equals(kind)) { + migrateLegacyDirectoryGroup(id); + return new SubprojectsGroup(id); } else { LOG.log(Level.WARNING, "Cannot find project group kind for id={0}", id); return null; } } + /** + * Migrate a legacy directory group to a subprojects group. + * The stored directory is resolved to a project, promoting it if necessary. + */ + private static void migrateLegacyDirectoryGroup(String id) { + Preferences node = NODE.node(id); + try { + Project p = null; + String path = node.get(KEY_PATH, null); + if (path != null) { + FileObject dir = URLMapper.findFileObject(URI.create(path).toURL()); + if (dir != null && dir.isFolder()) { + p = ProjectManager.getDefault().findProject(dir); + if (p == null) { + DirectoryProjectProvider prov = Lookup.getDefault().lookup(DirectoryProjectProvider.class); + if (prov != null) { + p = prov.createProject(dir); + } + } + } + } + if (p == null) { + throw new IOException("no group project created"); + } + } catch (IOException | IllegalArgumentException e) { + LOG.log(Level.WARNING, "Cannot migrate legacy directory group id=" + id + " to a subprojects group, the group will be empty", e); + } finally { + // write in every case; if above fails it remains empty but is still useable + node.put(KEY_KIND, SubprojectsGroup.KIND); + } + } + /** * Find all groups. * Sorted by display name.