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/.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/.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/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. 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/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..ffd91fe6bd8d --- /dev/null +++ b/ide/generic.project/manifest.mf @@ -0,0 +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 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..4e45be8f99bc --- /dev/null +++ b/ide/generic.project/nbproject/project.xml @@ -0,0 +1,177 @@ + + + + org.netbeans.modules.apisupport.project + + + org.netbeans.modules.generic.project + + + org.netbeans.api.annotations.common + + + + 1 + 1.48 + + + + org.netbeans.api.templates + + + + 1.28 + + + + org.netbeans.modules.projectapi + + + + 1 + 1.106 + + + + org.netbeans.modules.projectuiapi + + + + 1 + 1.95.0.8 + + + + org.netbeans.modules.projectuiapi.base + + + + 1 + 1.101 + + + + org.netbeans.modules.queries + + + + 1 + 1.27 + + + + org.openide.awt + + + + 7.88 + + + + org.openide.dialogs + + + + 7.66 + + + + 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..67bc5f084cec --- /dev/null +++ b/ide/generic.project/src/org/netbeans/modules/generic/project/GenericProject.java @@ -0,0 +1,335 @@ +/* + * 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.Objects; +import java.util.prefs.Preferences; +import org.netbeans.api.annotations.common.StaticResource; +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.support.LookupProviderSupport; +import org.openide.filesystems.FileObject; +import org.openide.util.Lookup; +import org.openide.util.lookup.Lookups; + +/** + * 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 shows the + * files contained in the project directory, subject to the IDE's + * global "ignored files" filter (see + * {@link #isGlobalFileFilterEnabled}) and the project's own + * additional pattern (see {@link #getFilesFilterPattern}). + * + *

The marker file doubles as the project's auxiliary data file + * (see {@link GpAuxiliaryProperties}): it marks the directory as a + * project and stores what {@link ProjectUtils#getPreferences} + * persists (e.g. the display name override). For local (marker-less) + * projects the same data lives in the user configuration. + */ +public final class GenericProject implements Project { + + /** + * Name of the marker file: identical to the auxiliary data file + * ({@link GpAuxiliaryProperties#dataFileName}), which thus has a + * dual role — it both marks the directory as a project and + * stores the project's auxiliary data. + */ + public static final String MARKER_FILE = GpAuxiliaryProperties.dataFileName(); + + /** + * 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 under which this project type is registered + * in the layer filesystem: + *

+ */ + 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 + + 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; + + /** + * Marks this project instance as explicitly deleted, see + * {@link #deleted}. + */ + public void markDeleted() { + deleted = true; + } + + /** + * 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", + // 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), + new GpAuxiliaryProperties(this), + this + ), "Projects/" + PROJECT_KEY + "/Lookup"); // NOI18N + } + + @Override + public FileObject getProjectDirectory() { + return projectDirectory; + } + + @Override + public Lookup getLookup() { + return lookup; + } + + static boolean isProject(FileObject projectDirectory) { + return projectDirectory != 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)); + } + + /** + * 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; + } + + /** + * 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(); + } + } + } + + /** + * 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(); + } + + /** + * Key of the global file filter toggle in the project's private + * preferences ({@link ProjectUtils#getPreferences}). + */ + static final String GLOBAL_FILE_FILTER_KEY = "useGlobalFileFilter"; // NOI18N + + /** + * Whether the project's logical view applies the IDE's global + * "ignored files" filter (Tools/Options/File Types). Enabled by + * default. + */ + public static boolean isGlobalFileFilterEnabled(GenericProject prj) { + Preferences prefs = ProjectUtils.getPreferences(prj, GenericProject.class, false); + if (prefs == null) { + return true; + } + return prefs.getBoolean(GLOBAL_FILE_FILTER_KEY, true); + } + + /** + * Stores the state of the global file filter toggle. The enabled + * state is the default, so it is stored by removing the key and a + * project with no other overrides keeps an empty data file. + */ + public static void setGlobalFileFilterEnabled(GenericProject prj, boolean enabled) { + if (isGlobalFileFilterEnabled(prj) == enabled) { + return; + } + Preferences prefs = ProjectUtils.getPreferences(prj, GenericProject.class, false); + if (prefs == null) { + return; + } + if (enabled) { + prefs.remove(GLOBAL_FILE_FILTER_KEY); + } else { + prefs.put(GLOBAL_FILE_FILTER_KEY, "false"); // NOI18N + } + } + + /** + * Key of the additional files filter pattern in the project's + * shared preferences ({@link ProjectUtils#getPreferences}) + * — the pattern is project content, like a {@code .gitignore}. + */ + static final String FILES_FILTER_PATTERN_KEY = "fileFilterPattern"; // NOI18N + + /** + * Returns the project's additional "ignored files" pattern — a + * regular expression matched with {@code find()} on the file name + * only — or {@code null} if none is stored. + */ + public static String getFilesFilterPattern(GenericProject prj) { + Preferences prefs = ProjectUtils.getPreferences(prj, GenericProject.class, true); + if (prefs == null) { + return null; + } + String pattern = prefs.get(FILES_FILTER_PATTERN_KEY, null); + return (pattern == null || pattern.isBlank()) ? null : pattern; + } + + /** + * Stores the project's additional "ignored files" pattern, or + * clears it for {@code null} or a blank string (the default, which + * keeps the data file clean). + */ + public static void setFilesFilterPattern(GenericProject prj, String pattern) { + if (pattern != null && pattern.isBlank()) { + pattern = null; + } + if (Objects.equals(pattern, getFilesFilterPattern(prj))) { + return; + } + Preferences prefs = ProjectUtils.getPreferences(prj, GenericProject.class, true); + if (prefs == null) { + return; + } + if (pattern == null) { + prefs.remove(FILES_FILTER_PATTERN_KEY); + } else { + prefs.put(FILES_FILTER_PATTERN_KEY, pattern); + } + } +} diff --git a/ide/generic.project/src/org/netbeans/modules/generic/project/GpAuxiliaryProperties.java b/ide/generic.project/src/org/netbeans/modules/generic/project/GpAuxiliaryProperties.java new file mode 100644 index 000000000000..d4e522088868 --- /dev/null +++ b/ide/generic.project/src/org/netbeans/modules/generic/project/GpAuxiliaryProperties.java @@ -0,0 +1,242 @@ +/* + * 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.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.netbeans.spi.project.AuxiliaryProperties; +import org.openide.filesystems.FileObject; +import org.openide.filesystems.FileUtil; +import org.openide.util.NbBundle.Messages; + +/** + * The {@link AuxiliaryProperties} of a generic project, stored in a + * single {@link Properties} file per project (name brandable via the + * module bundle, defaults to {@code .nbgp}). + * + *

The file's location depends on the project's nature: for a + * project with the marker file the marker + * ({@link GenericProject#MARKER_FILE}) is the data file, so + * the data is versioned along with the project; for a local + * (marker-less) project it lives in the user configuration + * (see {@link #dataFolder}), so that nothing auxiliary is written + * into the working tree. Shared and private settings are + * distinguished by a {@code shared} / {@code private} key prefix. + * + *

The location is re-evaluated on every access: if the marker + * file is created or deleted while the project is open, the data is + * moved to the new location on the next access (the file at the old + * one is removed); if it is deleted while no instance of the project + * is open, the data is lost with it. + */ +final class GpAuxiliaryProperties implements AuxiliaryProperties { + + private static final Logger LOG = Logger.getLogger(GpAuxiliaryProperties.class.getName()); + + private static final String PROJECTS_FOLDER = "Projects"; // NOI18N + private static final String SHARED_PREFIX = "shared."; // NOI18N + private static final String PRIVATE_PREFIX = "private."; // NOI18N + + private final GenericProject prj; + private final Properties data = new Properties(); + private boolean loaded; + private Boolean loadedInProject; + + GpAuxiliaryProperties(GenericProject prj) { + this.prj = prj; + } + + /** + * The path of the folder in the user configuration that holds the + * auxiliary data of a local project with the given directory. + */ + static String dataFolder(FileObject projectDirectory) { + return PROJECTS_FOLDER + "/" // NOI18N + + projectDirectory.getNameExt().replace('/', '_') + + "-" // NOI18N + + "%08x".formatted(projectDirectory.getPath().hashCode()); + } + + /** + * The name of the auxiliary data file, brandable via the module + * bundle, defaults to {@code .nbgp}: identical to the name of + * the marker file ({@link GenericProject#MARKER_FILE}). + */ + @Messages({ + "LBL_aux_data_file_name=.nbgp", + }) + static String dataFileName() { + return Bundle.LBL_aux_data_file_name(); + } + + /** + * Whether the auxiliary data is stored inside the project + * directory: this is the case when the marker file is present, + * i.e. the project is a "real", shareable project. + */ + private boolean inProject() { + return GenericProject.hasMarker(prj.getProjectDirectory()); + } + + @Override + public String get(String key, boolean shared) { + synchronized (data) { + ensureLoaded(); + return data.getProperty(keyPrefix(shared) + key); + } + } + + @Override + public void put(String key, String value, boolean shared) { + synchronized (data) { + ensureLoaded(); + String storedKey = keyPrefix(shared) + key; + if (value == null) { + data.remove(storedKey); + } else { + data.setProperty(storedKey, value); + } + save(); + } + } + + @Override + public Iterable listKeys(boolean shared) { + List keys = new ArrayList<>(); + synchronized (data) { + ensureLoaded(); + String prefix = keyPrefix(shared); + for (Object stored : data.keySet()) { + String key = (String) stored; + if (key.startsWith(prefix)) { + keys.add(key.substring(prefix.length())); + } + } + } + return keys; + } + + private static String keyPrefix(boolean shared) { + return shared ? SHARED_PREFIX : PRIVATE_PREFIX; + } + + /** + * Resolves the data file at the given location, returning + * {@code null} if it does not exist. + */ + private FileObject locateFile(boolean inProject) { + if (inProject) { + return prj.getProjectDirectory().getFileObject(dataFileName()); + } + FileObject root = FileUtil.getConfigRoot(); + FileObject dir = root != null + ? root.getFileObject(dataFolder(prj.getProjectDirectory())) + : null; + return dir != null ? dir.getFileObject(dataFileName()) : null; + } + + /** + * Loads the data if necessary. If the storage location has changed + * since the data was last loaded (the marker file was created or + * deleted), the cached data is moved to the new location and the + * file at the old location is removed. + */ + private void ensureLoaded() { + boolean here = inProject(); + if (loaded && loadedInProject == here) { + return; + } + if (!loaded) { + loaded = true; + loadedInProject = here; + FileObject file = locateFile(here); + if (file != null) { + readFile(file); + } + return; + } + // the location has changed since the last access: move the + // cached data to the new location and remove the file at the + // old one + boolean old = loadedInProject; + loadedInProject = here; + if (save()) { + FileObject oldFile = locateFile(old); + if (oldFile != null) { + try { + oldFile.delete(); + } catch (IOException ex) { + LOG.log(Level.FINE, "Cannot remove old auxiliary data file of " + prj, ex); + } + } + } + // the cache already holds what is now stored at the new + // location + } + + private void readFile(FileObject file) { + try (InputStream is = file.getInputStream()) { + data.load(is); + } catch (IOException ex) { + LOG.log(Level.WARNING, "Cannot read auxiliary data of " + prj, ex); + } + } + + /** + * Saves the cached data to the current location. + * + * @return whether the data has been written + */ + private boolean save() { + try { + FileObject dir; + if (inProject()) { + dir = prj.getProjectDirectory(); + } else { + FileObject root = FileUtil.getConfigRoot(); + if (root == null) { + LOG.log(Level.WARNING, "No user configuration available, cannot save auxiliary data of {0}", prj); + return false; + } + dir = FileUtil.createFolder(root, dataFolder(prj.getProjectDirectory())); + } + // createData throws if the file already exists, so only + // create it if necessary + FileObject file = dir.getFileObject(dataFileName()); + if (file == null) { + file = dir.createData(dataFileName()); + } + try (OutputStream os = file.getOutputStream()) { + data.store(os, "Auxiliary configuration of " + prj.getProjectDirectory().getPath()); + } + return true; + } catch (IOException ex) { + LOG.log(Level.WARNING, "Cannot save auxiliary data of " + prj, ex); + return false; + } + } + +} \ No newline at end of file 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/GpFileFilter.java b/ide/generic.project/src/org/netbeans/modules/generic/project/GpFileFilter.java new file mode 100644 index 000000000000..5aaffbb50e4f --- /dev/null +++ b/ide/generic.project/src/org/netbeans/modules/generic/project/GpFileFilter.java @@ -0,0 +1,173 @@ +/* + * 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.Objects; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.prefs.PreferenceChangeEvent; +import java.util.prefs.PreferenceChangeListener; +import java.util.prefs.Preferences; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; +import javax.swing.SwingUtilities; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; +import org.netbeans.api.project.ProjectUtils; +import org.netbeans.api.queries.VisibilityQuery; +import org.openide.filesystems.FileObject; +import org.openide.loaders.ChangeableDataFilter; +import org.openide.loaders.DataFilter; +import org.openide.loaders.DataObject; +import org.openide.util.ChangeSupport; +import org.openide.util.RequestProcessor; +import org.openide.util.WeakListeners; + +/** + * The data filter of the project's logical view: it hides the files + * matching the project's additional "ignored files" pattern (see + * {@link GenericProject#getFilesFilterPattern}) and, unless disabled + * per project (see {@link GenericProject#isGlobalFileFilterEnabled}), + * the files hidden by the IDE's global "ignored files" pattern. + * + *

Reacts to changes of the global pattern ({@link VisibilityQuery}) + * and of the project's settings (the private and shared preferences + * nodes) by notifying its listeners on the EDT, so the project node + * rescans the directory. The query listener is registered weakly; the + * {@link #privatePrefs} and {@link #sharedPrefs} fields pin the nodes, + * which the preferences provider references only weakly. + */ +final class GpFileFilter implements ChangeableDataFilter, DataFilter.FileBased, + ChangeListener, PreferenceChangeListener { + + private static final Logger LOG = Logger.getLogger(GpFileFilter.class.getName()); // NOI18N + + private final GenericProject project; + private final Preferences privatePrefs; + private final Preferences sharedPrefs; + + private volatile boolean globalFilterEnabled; + /** The additional pattern; {@code null} means no additional filter. */ + private volatile Pattern extraPattern; + /** The stored additional pattern text, for change detection. */ + private volatile String extraPatternText; + + private final ChangeSupport changeSupport = new ChangeSupport(this); + + GpFileFilter(GenericProject project) { + this.project = project; + this.privatePrefs = ProjectUtils.getPreferences(project, GenericProject.class, false); + this.sharedPrefs = ProjectUtils.getPreferences(project, GenericProject.class, true); + this.globalFilterEnabled = GenericProject.isGlobalFileFilterEnabled(project); + this.extraPatternText = GenericProject.getFilesFilterPattern(project); + this.extraPattern = compileFilesFilterPattern(GenericProject.getFilesFilterPattern(project)); + + VisibilityQuery.getDefault().addChangeListener(WeakListeners.change(this, VisibilityQuery.getDefault())); + // change events are delivered asynchronously, on the + // preferences event thread (not the EDT) + if (privatePrefs != null) { + privatePrefs.addPreferenceChangeListener(this); + } + if (sharedPrefs != null) { + sharedPrefs.addPreferenceChangeListener(this); + } + } + + @Override + public boolean acceptDataObject(DataObject obj) { + FileObject fo = obj.getPrimaryFile(); + return fo != null && acceptFileObject(fo); + } + + @Override + public boolean acceptFileObject(FileObject fo) { + if (extraPattern != null && extraPattern.matcher(fo.getNameExt()).find()) { + return false; + } + if (!globalFilterEnabled) { + return true; + } + return VisibilityQuery.getDefault().isVisible(fo); + } + + @Override + public void preferenceChange(PreferenceChangeEvent evt) { + String key = evt.getKey(); + boolean changed = false; + if (GenericProject.GLOBAL_FILE_FILTER_KEY.equals(key)) { + boolean nue = GenericProject.isGlobalFileFilterEnabled(project); + if (nue != globalFilterEnabled) { + globalFilterEnabled = nue; + changed = true; + } + } else if (GenericProject.FILES_FILTER_PATTERN_KEY.equals(key)) { + String nue = GenericProject.getFilesFilterPattern(project); + if (!Objects.equals(nue, extraPatternText)) { + extraPatternText = nue; + extraPattern = compileFilesFilterPattern(nue); + changed = true; + } + } + if (changed) { + stateChanged(new ChangeEvent(this)); + } + } + + @Override + public void stateChanged(ChangeEvent e) { + // forward on the EDT: the global pattern may change from any + // thread (e.g. the options dialog) and the preferences event + // arrives on the preferences event thread + if (SwingUtilities.isEventDispatchThread()) { + changeSupport.fireChange(); + } else { + RequestProcessor.getDefault().post(changeSupport::fireChange); + } + } + + @Override + public void addChangeListener(ChangeListener l) { + changeSupport.addChangeListener(l); + } + + @Override + public void removeChangeListener(ChangeListener l) { + changeSupport.removeChangeListener(l); + } + + /** + * Compiles the given additional files filter pattern, returning + * {@code null} for blank input or an invalid pattern (a warning is + * logged for the latter, which thus acts as no additional filter). + */ + private static Pattern compileFilesFilterPattern(String pattern) { + if (pattern == null || pattern.isBlank()) { + return null; + } + try { + return Pattern.compile(pattern); + } catch (PatternSyntaxException ex) { + LOG.log(Level.WARNING, "Invalid files filter pattern \"{0}\": {1}", + new Object[] { pattern, ex.getMessage() }); + return null; + } + } + +} \ No newline at end of file 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/GpLogicalViewProvider.java b/ide/generic.project/src/org/netbeans/modules/generic/project/GpLogicalViewProvider.java new file mode 100644 index 000000000000..2d3d3601197b --- /dev/null +++ b/ide/generic.project/src/org/netbeans/modules/generic/project/GpLogicalViewProvider.java @@ -0,0 +1,148 @@ +/* + * 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 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.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.AbstractLookup; +import org.openide.util.lookup.InstanceContent; + +/** + * The logical view of a generic project shows the files contained + * in the project directory, filtered by the IDE's global "ignored + * files" pattern unless the project has disabled it in its + * customizer (see {@link GpFileFilter}). + * + *

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(); + Children children; + try { + children = DataFolder.findFolder(dir).createNodeChildren(new GpFileFilter(project)); + } catch (Exception ex) { + children = Children.LEAF; + } + 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) { + // 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 + public Image getIcon(int type) { + return ImageUtilities.loadImage(GenericProject.ICON); + } + + @Override + public Image getOpenedIcon(int type) { + return getIcon(type); + } + }; + 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; + } + + @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/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/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 000000000000..7f9b26179abb Binary files /dev/null and b/ide/generic.project/src/org/netbeans/modules/generic/project/gp.png differ 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 000000000000..50a4fa63a698 Binary files /dev/null and b/ide/generic.project/src/org/netbeans/modules/generic/project/gp24.png differ 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/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..066515e541f8 --- /dev/null +++ b/ide/generic.project/src/org/netbeans/modules/generic/project/layer.xml @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ide/generic.project/src/org/netbeans/modules/generic/project/ui/actions/GpActionProvider.java b/ide/generic.project/src/org/netbeans/modules/generic/project/ui/actions/GpActionProvider.java new file mode 100644 index 000000000000..600a4328ea4a --- /dev/null +++ b/ide/generic.project/src/org/netbeans/modules/generic/project/ui/actions/GpActionProvider.java @@ -0,0 +1,79 @@ +/* + * 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.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; +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. + */ +public final class GpActionProvider implements ActionProvider { + + private final GenericProject prj; + + public 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/ui/actions/GpOpenAsProjectAction.java b/ide/generic.project/src/org/netbeans/modules/generic/project/ui/actions/GpOpenAsProjectAction.java new file mode 100644 index 000000000000..d058200d1b8a --- /dev/null +++ b/ide/generic.project/src/org/netbeans/modules/generic/project/ui/actions/GpOpenAsProjectAction.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.ui.actions; + +import java.awt.event.ActionEvent; +import java.io.File; +import java.io.IOException; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.swing.AbstractAction; +import javax.swing.Action; +import org.netbeans.api.project.Project; +import org.netbeans.api.project.ProjectManager; +import org.netbeans.api.project.ui.OpenProjects; +import org.netbeans.modules.generic.project.ui.wizard.GpWizardIterator; +import org.netbeans.spi.project.ui.support.ProjectChooser; +import org.openide.awt.ActionID; +import org.openide.awt.ActionReference; +import org.openide.awt.ActionRegistration; +import org.openide.awt.DynamicMenuContent; +import org.openide.filesystems.FileObject; +import org.openide.filesystems.FileUtil; +import org.openide.loaders.DataFolder; +import org.openide.util.ContextAwareAction; +import org.openide.util.Lookup; +import org.openide.util.NbBundle.Messages; +import org.openide.util.RequestProcessor; +import static org.netbeans.modules.generic.project.ui.actions.Bundle.*; + +/** + * Context action for folder nodes: turns a folder which is not a project + * yet into a local generic project — recognition by the FileObject + * attribute only, no marker file in the working tree — and opens it in the + * Projects tab. + *

+ * Registered on {@code Loaders/folder/any/Actions}, hence available on any + * folder node (e.g. in the Favorites tab or beneath an open project). + * Mirrors the inverse action {@code OpenProjectFolderAction} of the + * projectui module. + * + * @author mbien + */ +@Messages("GpOpenAsProjectAction.LBL_action=Open as Generic Project") +@ActionID(category = "Project", id = "org.netbeans.modules.generic.project.ui.actions.GpOpenAsProjectAction") +@ActionRegistration(displayName = "#GpOpenAsProjectAction.LBL_action", lazy = false) +@ActionReference(path = "Loaders/folder/any/Actions", position = 110) +public final class GpOpenAsProjectAction extends AbstractAction implements ContextAwareAction { + + private static final Logger LOG = Logger.getLogger(GpOpenAsProjectAction.class.getName()); + private static final RequestProcessor RP = new RequestProcessor(GpOpenAsProjectAction.class); + + public GpOpenAsProjectAction() { + super(GpOpenAsProjectAction_LBL_action()); + } + + @Override + public void actionPerformed(ActionEvent e) { + // Cannot be invoked without any context. + assert false; + } + + @Override + public Action createContextAwareInstance(Lookup context) { + return new ContextAction(context); + } + + private static final class ContextAction extends AbstractAction { + + private final Lookup context; + + public ContextAction(Lookup context) { + super(GpOpenAsProjectAction_LBL_action()); + this.context = context; + checkEnabled(context); + } + + private void checkEnabled(Lookup context1) { + for (DataFolder d : context1.lookupAll(DataFolder.class)) { + if (isCandidate(d.getPrimaryFile())) { + return; + } + } + putValue(DynamicMenuContent.HIDE_WHEN_DISABLED, true); + setEnabled(false); + } + + @Override + public void actionPerformed(ActionEvent e) { + // Run asynch so that the UI is not blocked; the open + // shows a progress dialog. + RP.post(() -> { + for (DataFolder d : context.lookupAll(DataFolder.class)) { + FileObject fo = d.getPrimaryFile(); + if (isCandidate(fo)) { + try { + adopt(fo); + return; + } catch (IOException x) { + LOG.log(Level.WARNING, "Cannot open folder as a generic project: " + fo, x); // NOI18N + } + } + } + }); + } + + /** A folder on the local file system which is not a project yet. */ + private static boolean isCandidate(FileObject fo) { + File f = FileUtil.toFile(fo); + return f != null && f.isDirectory() && !ProjectManager.getDefault().isProject(fo); + } + + /** + * Marks the folder as a local generic project (attribute only, no + * marker file) and opens the resulting project. + */ + private static void adopt(FileObject fo) throws IOException { + File dir = FileUtil.normalizeFile(FileUtil.toFile(fo)); + GpWizardIterator.createProject(dir, true); + // The folder may be cached as a non-project from the + // isProject() check done to enable this action. + ProjectManager.getDefault().clearNonProjectCache(); + Project p = ProjectManager.getDefault().findProject(fo); + if (p == null) { + throw new IOException("Project not found in " + dir); // NOI18N + } + OpenProjects.getDefault().open(new Project[]{p}, false, true); + File parent = dir.getParentFile(); + if (parent != null && parent.exists()) { + ProjectChooser.setProjectsFolder(parent); + } + } + + } + +} \ No newline at end of file diff --git a/ide/generic.project/src/org/netbeans/modules/generic/project/ui/actions/GpProjectOperations.java b/ide/generic.project/src/org/netbeans/modules/generic/project/ui/actions/GpProjectOperations.java new file mode 100644 index 000000000000..29c8fa067457 --- /dev/null +++ b/ide/generic.project/src/org/netbeans/modules/generic/project/ui/actions/GpProjectOperations.java @@ -0,0 +1,157 @@ +/* + * 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.actions; + +import org.netbeans.modules.generic.project.GenericProject; + +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, everything in it is data (the + * marker file, when present, being the only metadata), so move and + * copy relocate or duplicate the directory as a whole; data files + * are listed deepest-first for the delete flow. Since the copy thus + * also inherits the project-local auxiliary data, {@link #notifyCopied} + * drops the display name override from the original, just like a + * move or folder rename, after which the folder name is + * authoritative again. + * + *

A pure rename (the folder is not renamed) does not touch the + * directory; the new name is kept as a display name override. + */ +public final class GpProjectOperations implements MoveOrRenameOperationImplementation, CopyOperationImplementation, DeleteOperationImplementation { + + private final GenericProject prj; + + public 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. The copy + // duplicates the project-local auxiliary data file (when + // the marker is present) as-is, so this drops the + // display name override inherited from the original. + 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/ui/customizer/GpCompositeCategoryProvider.java b/ide/generic.project/src/org/netbeans/modules/generic/project/ui/customizer/GpCompositeCategoryProvider.java new file mode 100644 index 000000000000..53226310361b --- /dev/null +++ b/ide/generic.project/src/org/netbeans/modules/generic/project/ui/customizer/GpCompositeCategoryProvider.java @@ -0,0 +1,94 @@ +/* + * 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.io.IOException; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.swing.JComponent; +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.filesystems.FileObject; +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 and the marker file option, using + * the form-based {@link GpGeneralPanel}. + * + *

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", +}) +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; + FileObject projectDir = prj != null ? prj.getProjectDirectory() : null; + + GpGeneralPanel panel = new GpGeneralPanel(); + panel.setNameText(info != null ? info.getDisplayName() : ""); + panel.setLocationText(projectDir != null ? projectDir.getPath() : ""); + panel.setMarkerEnabled(projectDir != null && GenericProject.hasMarker(projectDir)); + if (prj != null && projectDir != null) { + // invoked on OK, off the AWT thread, in ProjectManager write access + category.setStoreListener(evt -> { + try { + GenericProject.setMarkerState(projectDir, panel.isMarkerEnabled()); + String name = panel.getNameText().strip(); + if (!name.isEmpty()) { + GenericProject.setDisplayNameOverride((GenericProject) prj, name); + } + } catch (IOException ex) { + Logger.getLogger(GpCompositeCategoryProvider.class.getName()) + .log(Level.WARNING, "Cannot update project settings", ex); + } + }); + } + 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 diff --git a/ide/generic.project/src/org/netbeans/modules/generic/project/ui/customizer/GpFilesCategoryProvider.java b/ide/generic.project/src/org/netbeans/modules/generic/project/ui/customizer/GpFilesCategoryProvider.java new file mode 100644 index 000000000000..981fc64f7375 --- /dev/null +++ b/ide/generic.project/src/org/netbeans/modules/generic/project/ui/customizer/GpFilesCategoryProvider.java @@ -0,0 +1,91 @@ +/* + * 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.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; +import javax.swing.JComponent; +import org.netbeans.api.project.Project; +import org.netbeans.modules.generic.project.GenericProject; +import org.netbeans.spi.project.ui.support.ProjectCustomizer; +import org.openide.DialogDisplayer; +import org.openide.NotifyDescriptor; +import org.openide.util.Lookup; +import org.openide.util.NbBundle.Messages; + +/** + * Provides the "Files" category of the project customizer, toggling + * whether the project view applies the IDE's global "ignored files" + * filter and defining the project's own additional pattern. + */ +@Messages({ + "LBL_Files=Files", + "MSG_InvalidPattern=The additional ignored files pattern is not a valid regular expression:", +}) +public final class GpFilesCategoryProvider implements ProjectCustomizer.CompositeCategoryProvider { + + private static final String CATEGORY_FILES = "category-files"; // NOI18N + + @ProjectCustomizer.CompositeCategoryProvider.Registration( + projectType=GenericProject.PROJECT_KEY, + position=200 + ) + public static GpFilesCategoryProvider createFilesCategory() { + return new GpFilesCategoryProvider(); + } + + private GpFilesCategoryProvider() { + } + + @Override + public ProjectCustomizer.Category createCategory(Lookup context) { + return ProjectCustomizer.Category.create(CATEGORY_FILES, Bundle.LBL_Files(), null); + } + + @Override + public JComponent createComponent(ProjectCustomizer.Category category, Lookup context) { + Project prj = context.lookup(Project.class); + GenericProject gp = prj instanceof GenericProject g ? g : null; + GpFilesPanel panel = new GpFilesPanel(); + panel.setUseGlobalFilter(gp != null && GenericProject.isGlobalFileFilterEnabled(gp)); + String pattern = gp != null ? GenericProject.getFilesFilterPattern(gp) : null; + panel.setPatternText(pattern != null ? pattern : ""); + if (gp != null) { + // invoked on OK, off the AWT thread, in ProjectManager write access + category.setStoreListener(evt -> { + String text = panel.getPatternText().strip(); + if (!text.isEmpty()) { + try { + Pattern.compile(text); + } catch (PatternSyntaxException ex) { + DialogDisplayer.getDefault().notify(new NotifyDescriptor.Message( + Bundle.MSG_InvalidPattern() + "\n" + ex.getMessage(), + NotifyDescriptor.ERROR_MESSAGE)); + return; + } + } + GenericProject.setGlobalFileFilterEnabled(gp, panel.isUseGlobalFilter()); + GenericProject.setFilesFilterPattern(gp, text.isEmpty() ? null : text); + }); + } + return panel; + } + +} \ No newline at end of file diff --git a/ide/generic.project/src/org/netbeans/modules/generic/project/ui/customizer/GpFilesPanel.form b/ide/generic.project/src/org/netbeans/modules/generic/project/ui/customizer/GpFilesPanel.form new file mode 100644 index 000000000000..09aaace65fdf --- /dev/null +++ b/ide/generic.project/src/org/netbeans/modules/generic/project/ui/customizer/GpFilesPanel.form @@ -0,0 +1,94 @@ + + + + +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/ide/generic.project/src/org/netbeans/modules/generic/project/ui/customizer/GpFilesPanel.java b/ide/generic.project/src/org/netbeans/modules/generic/project/ui/customizer/GpFilesPanel.java new file mode 100644 index 000000000000..8ada941c653d --- /dev/null +++ b/ide/generic.project/src/org/netbeans/modules/generic/project/ui/customizer/GpFilesPanel.java @@ -0,0 +1,110 @@ +/* + * 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.JCheckBox; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JTextField; +import javax.swing.LayoutStyle; +import org.openide.awt.Mnemonics; +import org.openide.util.NbBundle; + +/** + * Form panel of the "Files" project customizer category. + */ +public class GpFilesPanel extends JPanel { + + public GpFilesPanel() { + initComponents(); + } + + @NbBundle.Messages({ + "LBL_UseGlobalFilter=Apply the IDE's ignored files pattern", + "TT_UseGlobalFilter=" + + "

When checked, files matching the IDE's global ignored files pattern " + + "are hidden in the project view.


" + + "

See Tools -> Options -> Miscellaneous -> Files

", + "LBL_Pattern=Additional ignored files pattern:", + "TT_Pattern=" + + "

A regular expression matched against the file name.

" + + "

Files matching it are hidden in the project view,

" + }) + // //GEN-BEGIN:initComponents + private void initComponents() { + + useGlobalFilterCheckBox = new JCheckBox(); + patternLabel = new JLabel(); + patternField = new JTextField(); + + Mnemonics.setLocalizedText(useGlobalFilterCheckBox, NbBundle.getMessage(GpFilesPanel.class, "LBL_UseGlobalFilter")); // NOI18N + useGlobalFilterCheckBox.setToolTipText(NbBundle.getMessage(GpFilesPanel.class, "TT_UseGlobalFilter")); // NOI18N + + Mnemonics.setLocalizedText(patternLabel, NbBundle.getMessage(GpFilesPanel.class, "LBL_Pattern")); // NOI18N + + patternField.setToolTipText(NbBundle.getMessage(GpFilesPanel.class, "TT_Pattern")); // NOI18N + + GroupLayout layout = new GroupLayout(this); + this.setLayout(layout); + layout.setHorizontalGroup(layout.createParallelGroup(GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addContainerGap() + .addComponent(patternLabel) + .addPreferredGap(LayoutStyle.ComponentPlacement.UNRELATED) + .addComponent(patternField, GroupLayout.DEFAULT_SIZE, 186, Short.MAX_VALUE) + .addContainerGap()) + .addGroup(layout.createSequentialGroup() + .addComponent(useGlobalFilterCheckBox) + .addGap(0, 0, Short.MAX_VALUE)) + ); + layout.setVerticalGroup(layout.createParallelGroup(GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addContainerGap() + .addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE) + .addComponent(patternLabel) + .addComponent(patternField, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)) + .addGap(18, 18, 18) + .addComponent(useGlobalFilterCheckBox) + .addContainerGap(GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + ); + }// //GEN-END:initComponents + + public boolean isUseGlobalFilter() { + return useGlobalFilterCheckBox.isSelected(); + } + + public void setUseGlobalFilter(boolean value) { + useGlobalFilterCheckBox.setSelected(value); + } + + public String getPatternText() { + return patternField.getText(); + } + + public void setPatternText(String value) { + patternField.setText(value); + } + + // Variables declaration - do not modify//GEN-BEGIN:variables + JTextField patternField; + JLabel patternLabel; + JCheckBox useGlobalFilterCheckBox; + // End of variables declaration//GEN-END:variables +} \ No newline at end of file diff --git a/ide/generic.project/src/org/netbeans/modules/generic/project/ui/customizer/GpGeneralPanel.form b/ide/generic.project/src/org/netbeans/modules/generic/project/ui/customizer/GpGeneralPanel.form new file mode 100644 index 000000000000..a05d4d1ea643 --- /dev/null +++ b/ide/generic.project/src/org/netbeans/modules/generic/project/ui/customizer/GpGeneralPanel.form @@ -0,0 +1,117 @@ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/ide/generic.project/src/org/netbeans/modules/generic/project/ui/customizer/GpGeneralPanel.java b/ide/generic.project/src/org/netbeans/modules/generic/project/ui/customizer/GpGeneralPanel.java new file mode 100644 index 000000000000..c5ee2002fcd6 --- /dev/null +++ b/ide/generic.project/src/org/netbeans/modules/generic/project/ui/customizer/GpGeneralPanel.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.ui.customizer; + +import javax.swing.GroupLayout; +import javax.swing.JCheckBox; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JTextField; +import javax.swing.LayoutStyle; +import org.openide.awt.Mnemonics; +import org.openide.util.NbBundle; + +/** + * Form panel of the "General" project customizer category. + */ +public class GpGeneralPanel extends JPanel { + + public GpGeneralPanel() { + initComponents(); + } + + @NbBundle.Messages({ + "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=" + + "

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

" + + "

The project becomes a local (private) project.

", + }) + @SuppressWarnings("unchecked") + // //GEN-BEGIN:initComponents + private void initComponents() { + + nameLabel = new JLabel(); + nameField = new JTextField(); + locationLabel = new JLabel(); + locationField = new JTextField(); + markerBox = new JCheckBox(); + + Mnemonics.setLocalizedText(nameLabel, NbBundle.getMessage(GpGeneralPanel.class, "LBL_Name")); // NOI18N + + nameField.setToolTipText(NbBundle.getMessage(GpGeneralPanel.class, "TT_Name")); // NOI18N + + Mnemonics.setLocalizedText(locationLabel, NbBundle.getMessage(GpGeneralPanel.class, "LBL_Location")); // NOI18N + + locationField.setEditable(false); + + Mnemonics.setLocalizedText(markerBox, NbBundle.getMessage(GpGeneralPanel.class, "LBL_MarkerFile")); // NOI18N + markerBox.setToolTipText(NbBundle.getMessage(GpGeneralPanel.class, "TT_MarkerFile")); // NOI18N + + GroupLayout layout = new GroupLayout(this); + this.setLayout(layout); + layout.setHorizontalGroup(layout.createParallelGroup(GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addContainerGap() + .addGroup(layout.createParallelGroup(GroupLayout.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.UNRELATED) + .addGroup(layout.createParallelGroup(GroupLayout.Alignment.TRAILING) + .addComponent(locationField, GroupLayout.Alignment.LEADING, GroupLayout.DEFAULT_SIZE, 204, Short.MAX_VALUE) + .addComponent(nameField)) + .addContainerGap()) + .addGroup(layout.createSequentialGroup() + .addComponent(markerBox) + .addGap(0, 0, Short.MAX_VALUE)) + ); + layout.setVerticalGroup(layout.createParallelGroup(GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addContainerGap() + .addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE) + .addComponent(nameField, GroupLayout.PREFERRED_SIZE, 22, GroupLayout.PREFERRED_SIZE) + .addComponent(nameLabel)) + .addPreferredGap(LayoutStyle.ComponentPlacement.UNRELATED) + .addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE) + .addComponent(locationLabel) + .addComponent(locationField, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)) + .addGap(18, 18, 18) + .addComponent(markerBox) + .addContainerGap(GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + ); + }// //GEN-END:initComponents + + public String getNameText() { + return nameField.getText(); + } + + public void setNameText(String value) { + nameField.setText(value); + } + + public String getLocationText() { + return locationField.getText(); + } + + public void setLocationText(String value) { + locationField.setText(value); + } + + public boolean isMarkerEnabled() { + return markerBox.isSelected(); + } + + public void setMarkerEnabled(boolean value) { + markerBox.setSelected(value); + } + + // Variables declaration - do not modify//GEN-BEGIN:variables + JTextField locationField; + JLabel locationLabel; + JCheckBox markerBox; + JTextField nameField; + JLabel nameLabel; + // End of variables declaration//GEN-END:variables +} \ No newline at end of file diff --git a/ide/generic.project/src/org/netbeans/modules/generic/project/ui/wizard/GpTemplateDescription.html b/ide/generic.project/src/org/netbeans/modules/generic/project/ui/wizard/GpTemplateDescription.html new file mode 100644 index 000000000000..f5ab3415e06b --- /dev/null +++ b/ide/generic.project/src/org/netbeans/modules/generic/project/ui/wizard/GpTemplateDescription.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/ui/wizard/GpWizardIterator.java b/ide/generic.project/src/org/netbeans/modules/generic/project/ui/wizard/GpWizardIterator.java new file mode 100644 index 000000000000..686a7623460c --- /dev/null +++ b/ide/generic.project/src/org/netbeans/modules/generic/project/ui/wizard/GpWizardIterator.java @@ -0,0 +1,159 @@ +/* + * 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.wizard; + +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.modules.generic.project.GenericProject; +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 = "#GpWizard_displayName", // NOI18N + description = "GpTemplateDescription.html", // NOI18N + iconBase = GenericProject.ICON, + position = 1000 +) +@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 GpWizardPanel()); + } + + @Override + public Set instantiate() throws IOException { + File projectDir = FileUtil.normalizeFile((File) wiz.getProperty("projdir")); // NOI18N + 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); + } + return created; + } + + /** + * Creates the project directory containing the marker file, or — + * for {@code localMarker} — marks it as a local project by the + * FileObject attribute only. + */ + public static Set createProject(File projectDir, boolean localMarker) 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 + } + // local project: recognition attribute only, + // otherwise the marker file in the working tree + GenericProject.setMarkerState(dir, !localMarker); + 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.putProperty("localMarker", 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/ui/wizard/GpWizardPanel.java b/ide/generic.project/src/org/netbeans/modules/generic/project/ui/wizard/GpWizardPanel.java new file mode 100644 index 000000000000..8cf571019774 --- /dev/null +++ b/ide/generic.project/src/org/netbeans/modules/generic/project/ui/wizard/GpWizardPanel.java @@ -0,0 +1,295 @@ +/* + * 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.wizard; + +import java.awt.Component; +import java.io.File; +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; +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", + "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.", + "ERR_NotWritable=Project Folder cannot be created.", + "ERR_FolderExists=Project Folder already exists and is not empty.", +}) +final class GpWizardPanel 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 JCheckBox localMarker = new JCheckBox(); + 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(); + localMarker.setSelected(false); + updateCreatedFolder(); + } + + @Override + 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 + 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()); + 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 + // 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()) + .addGroup(layout.createSequentialGroup() + .addContainerGap() + .addComponent(localMarker)) + ); + 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(18, 18, 18) + .addComponent(localMarker) + .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()); + } + +} 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..1b0ba3c76d93 --- /dev/null +++ b/ide/generic.project/test/unit/src/org/netbeans/modules/generic/project/GenericProjectTest.java @@ -0,0 +1,666 @@ +/* + * 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.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import java.util.prefs.Preferences; +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.VisibilityQuery; +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.AuxiliaryProperties; +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; + +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 GpFactory().isProject(root)); + } + + public void testMarkerFileIsProject() throws Exception { + FileObject root = newProjectDir(); + FileUtil.createData(root, GenericProject.MARKER_FILE); + assertTrue(new GpFactory().isProject(root)); + } + + public void testAttributeOnlyIsProject() throws Exception { + FileObject root = newProjectDir(); + root.setAttribute(GenericProject.MARKER_ATTRIBUTE, Boolean.TRUE); + assertTrue(new GpFactory().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); + } + + 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()); + } + + public void testCreateProject() throws Exception { + clearWorkDir(); + File dir = new File(getWorkDir(), "proj-" + getName()); // NOI18N + Set created = GpWizardIterator.createProject(dir, false); + 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); + } + + 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 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 GpFactory().saveProject(prj); + assertNull(root.getFileObject(GenericProject.MARKER_FILE)); + 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 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 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)); + } + + public void testProjectLookupProvidesAuxiliaryProperties() throws Exception { + FileObject root = newProjectDir(); + FileUtil.createData(root, GenericProject.MARKER_FILE); + Project prj = ProjectManager.getDefault().findProject(root); + assertNotNull(prj); + assertNotNull("the project must provide its own AuxiliaryProperties", + prj.getLookup().lookup(AuxiliaryProperties.class)); + } + + public void testMarkerProjectStoresAuxDataInProjectDir() 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"); + ProjectUtils.getPreferences(prj, GenericProject.class, false).flush(); + + // the data must live in the project directory (the marker file + // is the data file)... + assertNotNull("the auxiliary data file must exist in the project directory", + root.getFileObject(GpAuxiliaryProperties.dataFileName())); + // ...and not in the user configuration + assertNull("no auxiliary data may be created in the user configuration for marker projects", + FileUtil.getConfigRoot().getFileObject( + GpAuxiliaryProperties.dataFolder(root) + "/" + GpAuxiliaryProperties.dataFileName())); + } + + public void testRepeatedWritesReuseTheDataFile() throws Exception { + FileObject root = newProjectDir(); + FileUtil.createData(root, GenericProject.MARKER_FILE); + Project prj = ProjectManager.getDefault().findProject(root); + assertNotNull(prj); + + // several consecutive writes to the existing data file must + // not fail (createData throws if the file already exists) + GenericProject.setDisplayNameOverride((GenericProject) prj, "First Name"); + ProjectUtils.getPreferences(prj, GenericProject.class, false).flush(); + GenericProject.setDisplayNameOverride((GenericProject) prj, "Second Name"); + ProjectUtils.getPreferences(prj, GenericProject.class, false).flush(); + + assertNotNull("the auxiliary data file must exist", + root.getFileObject(GpAuxiliaryProperties.dataFileName())); + // a fresh instance (fresh cache) must see the second value, + // i.e. the second write has really reached the disk + GenericProject nue = new GenericProject(root); + assertEquals("Second Name", GenericProject.getDisplayNameOverride(nue)); + } + + public void testLocalProjectStoresAuxDataInUserConfig() throws Exception { + FileObject root = newProjectDir(); + root.setAttribute(GenericProject.MARKER_ATTRIBUTE, Boolean.TRUE); + Project prj = ProjectManager.getDefault().findProject(root); + assertNotNull(prj); + + GenericProject.setDisplayNameOverride((GenericProject) prj, "My Fancy Project"); + ProjectUtils.getPreferences(prj, GenericProject.class, false).flush(); + + // the data must live in the user configuration... + assertNotNull("the auxiliary data file must be created in the user configuration", + FileUtil.getConfigRoot().getFileObject( + GpAuxiliaryProperties.dataFolder(root) + "/" + GpAuxiliaryProperties.dataFileName())); + // ...and nowhere in the project's working tree + assertNull("no auxiliary data file may appear in the project directory", + root.getFileObject(GpAuxiliaryProperties.dataFileName())); + assertNull("no auxiliary configuration attribute may be set on the project directory", + root.getAttribute("org.netbeans.spi.project.AuxiliaryConfiguration" + + ".http://www.netbeans.org/ns/auxiliary-configuration-preferences/1#preferences")); + } + + public void testMarkerToggledMovesAuxData() 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"); + ProjectUtils.getPreferences(prj, GenericProject.class, false).flush(); + assertNotNull(root.getFileObject(GpAuxiliaryProperties.dataFileName())); + + // turning the marker off moves the data to the user configuration + GenericProject.setMarkerState(root, false); + assertEquals("My Fancy Project", GenericProject.getDisplayNameOverride((GenericProject) prj)); + assertNull("the project-local data file must be removed", + root.getFileObject(GpAuxiliaryProperties.dataFileName())); + assertNotNull("the data must now live in the user configuration", + FileUtil.getConfigRoot().getFileObject( + GpAuxiliaryProperties.dataFolder(root) + "/" + GpAuxiliaryProperties.dataFileName())); + + // and back on moves it back into the project directory + GenericProject.setMarkerState(root, true); + assertEquals("My Fancy Project", GenericProject.getDisplayNameOverride((GenericProject) prj)); + assertNotNull("the data must be back in the project directory", + root.getFileObject(GpAuxiliaryProperties.dataFileName())); + assertNull("no auxiliary data may remain in the user configuration", + FileUtil.getConfigRoot().getFileObject( + GpAuxiliaryProperties.dataFolder(root) + "/" + GpAuxiliaryProperties.dataFileName())); + } + + public void testMarkerDeletedExternallyDropsAuxData() 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"); + ProjectUtils.getPreferences(prj, GenericProject.class, false).flush(); + + // the marker file is the auxiliary data file: deleting it + // outside of the IDE (while no instance is open to migrate + // the data) drops the data with it + root.getFileObject(GenericProject.MARKER_FILE).delete(); + root.setAttribute(GenericProject.MARKER_ATTRIBUTE, Boolean.TRUE); + + // a fresh instance must start with no auxiliary data and no + // file at either location + GenericProject nue = new GenericProject(root); + assertNull("the auxiliary data must be gone with the marker file", + GenericProject.getDisplayNameOverride(nue)); + assertNull(root.getFileObject(GpAuxiliaryProperties.dataFileName())); + assertNull("no auxiliary data may appear in the user configuration", + FileUtil.getConfigRoot().getFileObject( + GpAuxiliaryProperties.dataFolder(root) + "/" + GpAuxiliaryProperties.dataFileName())); + } + + public void testPreferencesSharedAndPrivateAreIsolated() throws Exception { + FileObject root = newProjectDir(); + FileUtil.createData(root, GenericProject.MARKER_FILE); + Project prj = ProjectManager.getDefault().findProject(root); + assertNotNull(prj); + + Preferences shared = ProjectUtils.getPreferences(prj, GenericProject.class, true); + Preferences priv = ProjectUtils.getPreferences(prj, GenericProject.class, false); + shared.put("key", "shared-value"); // NOI18N + priv.put("key", "private-value"); // NOI18N + shared.flush(); + + assertEquals("shared-value", shared.get("key", null)); // NOI18N + assertEquals("private-value", priv.get("key", null)); // NOI18N + assertTrue("the shared key must be listed for the shared area", + List.of(shared.keys()).contains("key")); + assertTrue("the private key must be listed for the private area", + List.of(priv.keys()).contains("key")); + } + + public void testPreferencesSurviveNewProjectInstance() 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"); + ProjectUtils.getPreferences(prj, GenericProject.class, false).flush(); + + // a fresh instance (bypassing the ProjectManager's project + // cache) must see the value, i.e. it has really been + // persisted to disk + GenericProject nue = new GenericProject(root); + assertEquals("My Fancy Project", GenericProject.getDisplayNameOverride(nue)); + } + + public void testGlobalFileFilterEnabledByDefault() throws Exception { + FileObject root = newProjectDir(); + FileUtil.createData(root, GenericProject.MARKER_FILE); + GenericProject prj = (GenericProject) ProjectManager.getDefault().findProject(root); + assertNotNull(prj); + assertTrue("the global file filter must be enabled by default", + GenericProject.isGlobalFileFilterEnabled(prj)); + } + + public void testGlobalFileFilterToggleRoundTrip() throws Exception { + FileObject root = newProjectDir(); + FileUtil.createData(root, GenericProject.MARKER_FILE); + GenericProject prj = (GenericProject) ProjectManager.getDefault().findProject(root); + assertNotNull(prj); + + GenericProject.setGlobalFileFilterEnabled(prj, false); + assertFalse(GenericProject.isGlobalFileFilterEnabled(prj)); + // the disabled state is the only one that must be stored + assertEquals("false", ProjectUtils.getPreferences(prj, GenericProject.class, false) + .get(GenericProject.GLOBAL_FILE_FILTER_KEY, null)); // NOI18N + + GenericProject.setGlobalFileFilterEnabled(prj, true); + assertTrue(GenericProject.isGlobalFileFilterEnabled(prj)); + // the enabled state is the default and keeps the data file clean + assertNull(ProjectUtils.getPreferences(prj, GenericProject.class, false) + .get(GenericProject.GLOBAL_FILE_FILTER_KEY, null)); // NOI18N + } + + public void testFilterHidesFilesTheIdeIgnores() throws Exception { + FileObject root = newProjectDir(); + FileUtil.createData(root, GenericProject.MARKER_FILE); + FileUtil.createData(root, "readme.txt"); + FileObject git = FileUtil.createData(root, ".git"); + GenericProject prj = (GenericProject) ProjectManager.getDefault().findProject(root); + assertNotNull(prj); + + // sanity: in the test environment the IDE's global filter + // (masterfs' implementation) hides ".git" + assertFalse(VisibilityQuery.getDefault().isVisible(git)); + + GpFileFilter filter = new GpFileFilter(prj); + assertFalse("the filter must hide files the IDE ignores", + filter.acceptFileObject(git)); + assertTrue("regular files must stay visible", + filter.acceptFileObject(root.getFileObject("readme.txt"))); + + CountDownLatch latch = new CountDownLatch(1); + filter.addChangeListener(evt -> latch.countDown()); + GenericProject.setGlobalFileFilterEnabled(prj, false); + assertTrue("the filter must be notified when the toggle changes", + latch.await(10, TimeUnit.SECONDS)); + assertTrue("disabling the filter must show everything", + filter.acceptFileObject(git)); + } + + public void testFilesFilterPatternRoundTrip() throws Exception { + FileObject root = newProjectDir(); + FileUtil.createData(root, GenericProject.MARKER_FILE); + GenericProject prj = (GenericProject) ProjectManager.getDefault().findProject(root); + assertNotNull(prj); + assertNull("no additional pattern by default", + GenericProject.getFilesFilterPattern(prj)); + + GenericProject.setFilesFilterPattern(prj, ".*-old.*"); // NOI18N + assertEquals(".*-old.*", GenericProject.getFilesFilterPattern(prj)); + // the pattern is project content and lives in the shared area + assertEquals(".*-old.*", ProjectUtils.getPreferences(prj, GenericProject.class, true) + .get(GenericProject.FILES_FILTER_PATTERN_KEY, null)); // NOI18N + + // a blank pattern is the same as none + GenericProject.setFilesFilterPattern(prj, " "); // NOI18N + assertNull(GenericProject.getFilesFilterPattern(prj)); + assertNull(ProjectUtils.getPreferences(prj, GenericProject.class, true) + .get(GenericProject.FILES_FILTER_PATTERN_KEY, null)); // NOI18N + } + + public void testAdditionalFilterPattern() throws Exception { + FileObject root = newProjectDir(); + FileUtil.createData(root, GenericProject.MARKER_FILE); + FileUtil.createData(root, "readme.txt"); + FileUtil.createData(root, "readme-old.txt"); + FileObject git = FileUtil.createData(root, ".git"); + GenericProject prj = (GenericProject) ProjectManager.getDefault().findProject(root); + assertNotNull(prj); + + GpFileFilter filter = new GpFileFilter(prj); + assertTrue(filter.acceptFileObject(root.getFileObject("readme.txt"))); + + // an additional pattern hides its matches on top of the global + // filter (the two are AND-combined) + CountDownLatch latch = new CountDownLatch(1); + filter.addChangeListener(evt -> latch.countDown()); + GenericProject.setFilesFilterPattern(prj, ".*-old.*"); // NOI18N + assertTrue("the filter must be notified when the pattern changes", + latch.await(10, TimeUnit.SECONDS)); + assertFalse("files matching the additional pattern must be hidden", + filter.acceptFileObject(root.getFileObject("readme-old.txt"))); + assertTrue("other files must stay visible", + filter.acceptFileObject(root.getFileObject("readme.txt"))); + assertFalse("the global filter must still apply", + filter.acceptFileObject(git)); + + // clearing the pattern shows the files again + final CountDownLatch latch2 = new CountDownLatch(1); + filter.addChangeListener(evt -> latch2.countDown()); + GenericProject.setFilesFilterPattern(prj, null); + assertTrue(latch2.await(10, TimeUnit.SECONDS)); + assertTrue(filter.acceptFileObject(root.getFileObject("readme-old.txt"))); + } + + public void testInvalidAdditionalPatternIsIgnored() throws Exception { + FileObject root = newProjectDir(); + FileUtil.createData(root, GenericProject.MARKER_FILE); + FileUtil.createData(root, "readme-old.txt"); + GenericProject prj = (GenericProject) ProjectManager.getDefault().findProject(root); + assertNotNull(prj); + + // simulate a hand-edited data file with a broken pattern: + // it must act as no additional filter (a warning is logged) + GenericProject.setFilesFilterPattern(prj, ".*-old("); // NOI18N + GpFileFilter filter = new GpFileFilter(prj); + assertTrue("an invalid pattern must not hide files", + filter.acceptFileObject(root.getFileObject("readme-old.txt"))); + } + + private FileObject newProjectDir() throws Exception { + clearWorkDir(); + File dir = new File(getWorkDir(), "proj-" + getName()); // NOI18N + assertTrue(dir.mkdirs() || dir.isDirectory()); + return FileUtil.toFileObject(dir); + } + + public interface TestMarker { + } + + @ProjectServiceProvider(service=TestMarker.class, projectType=GenericProject.PROJECT_KEY) + public static class TestMarkerProvider implements TestMarker { + } + +} 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/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,\ 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 @@ +