2
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
4
* Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
6
* The contents of this file are subject to the terms of either the GNU
7
* General Public License Version 2 only ("GPL") or the Common
8
* Development and Distribution License("CDDL") (collectively, the
9
* "License"). You may not use this file except in compliance with the
10
* License. You can obtain a copy of the License at
11
* http://www.netbeans.org/cddl-gplv2.html
12
* or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
13
* specific language governing permissions and limitations under the
14
* License. When distributing the software, include this License Header
15
* Notice in each file and include the License file at
16
* nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this
17
* particular file as subject to the "Classpath" exception as provided
18
* by Sun in the GPL Version 2 section of the License file that
19
* accompanied this code. If applicable, add the following below the
20
* License Header, with the fields enclosed by brackets [] replaced by
21
* your own identifying information:
22
* "Portions Copyrighted [year] [name of copyright owner]"
26
* The Original Software is NetBeans. The Initial Developer of the Original
27
* Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
28
* Microsystems, Inc. All Rights Reserved.
30
* If you wish your version of this file to be governed by only the CDDL
31
* or only the GPL Version 2, indicate your decision by adding
32
* "[Contributor] elects to include this software in this distribution
33
* under the [CDDL or GPL Version 2] license." If you do not indicate a
34
* single choice of license, a recipient has the option to distribute
35
* your version of this file under either the CDDL, the GPL Version 2 or
36
* to extend the choice of license to its licensees as provided above.
37
* However, if you add GPL Version 2 code and therefore, elected the GPL
38
* Version 2 license, then the option applies only if the new code is
39
* made subject to such option by the copyright holder.
42
package org.netbeans.spi.project.ui.support;
44
import java.awt.Dialog;
45
import java.awt.Image;
46
import java.awt.event.ActionListener;
47
import java.io.IOException;
48
import java.util.ArrayList;
49
import java.util.Collections;
50
import java.util.HashMap;
51
import java.util.List;
53
import java.util.logging.Level;
54
import java.util.logging.Logger;
55
import javax.swing.JComponent;
56
import javax.swing.JPanel;
58
import org.netbeans.modules.project.uiapi.CategoryModel;
59
import org.netbeans.modules.project.uiapi.CategoryView;
60
import org.netbeans.modules.project.uiapi.CategoryChangeSupport;
61
import org.netbeans.modules.project.uiapi.CustomizerDialog;
62
import org.netbeans.modules.project.uiapi.CustomizerPane;
63
import org.netbeans.modules.project.uiapi.Utilities;
64
import org.netbeans.spi.project.ui.support.ProjectCustomizer.Category;
65
import org.openide.cookies.InstanceCookie;
66
import org.openide.filesystems.FileObject;
67
import org.openide.filesystems.FileStateInvalidException;
68
import org.openide.filesystems.Repository;
69
import org.openide.loaders.DataFolder;
70
import org.openide.loaders.DataObject;
71
import org.openide.util.HelpCtx;
72
import org.openide.util.Lookup;
74
/** Support for creating dialogs which can be used as project
75
* customizers. The dialog may display multiple panels or categories.
76
* @see org.netbeans.spi.project.ui.CustomizerProvider
77
* @see ProjectCustomizer.Category
79
* @author Petr Hrebejk, Martin Krauskopf
81
public final class ProjectCustomizer {
83
/** Factory/Namespace class only. */
84
private ProjectCustomizer() {
87
/** Creates standard customizer dialog which can be used for implementation
88
* of {@link org.netbeans.spi.project.ui.CustomizerProvider}. You don't need
89
* to call <code>pack()</code> method on the dialog. The resulting dialog will
91
* Call <code>show()</code> on the dialog to make it visible. The dialog
92
* will be closed automatically after click on "OK" or "Cancel" button.
94
* @param categories array of descriptions of categories to be shown in the
95
* dialog. Note that categories have the <code>valid</code>
96
* property. If any of the given categories is not valid cusomizer's
97
* OK button will be disabled until all categories become valid
99
* @param componentProvider creator of GUI components for categories in the
101
* @param preselectedCategory name of one of the supplied categories or null.
102
* Category with given name will be selected. If <code>null</code>
103
* or if the category of given name does not exist the first category will
105
* @param okOptionListener listener which will be notified when the user presses
107
* @param helpCtx Help context for the dialog, which will be used when the
108
* panels in the customizer do not specify their own help context.
109
* @return standard project customizer dialog.
111
public static Dialog createCustomizerDialog( Category[] categories,
112
CategoryComponentProvider componentProvider,
113
String preselectedCategory,
114
ActionListener okOptionListener,
116
CustomizerPane innerPane = createCustomizerPane(categories, componentProvider, preselectedCategory);
117
Dialog dialog = CustomizerDialog.createDialog( okOptionListener, innerPane, helpCtx, categories, componentProvider);
122
* Creates standard customizer dialog that can be used for implementation of
123
* {@link org.netbeans.spi.project.ui.CustomizerProvider} based on content of a folder in Layers.
124
* Use this method when you want to allow composition and 3rd party additions to your customizer UI.
125
* You don't need to call <code>pack()</code> method on the dialog. The resulting dialog will
127
* Call <code>show()</code> on the dialog to make it visible. The dialog
128
* will be closed automatically after click on "OK" or "Cancel" button.
130
* @since org.netbeans.modules.projectuiapi/1 1.15
131
* @param folderPath the path in the System Filesystem that is used as root for panel composition.
132
* The content of the folder is assummed to be {@link org.netbeans.spi.project.ui.support.ProjectCustomizer.CompositeCategoryProvider} instances
133
* @param context the context for the panels, up to the project type what the context shall be, for example org.netbeans.api.project.Project instance
134
* @param preselectedCategory name of one of the supplied categories or null.
135
* Category with given name will be selected. If <code>null</code>
136
* or if the category of given name does not exist the first category will
138
* @param okOptionListener listener which will be notified when the user presses
140
* @param helpCtx Help context for the dialog, which will be used when the
141
* panels in the customizer do not specify their own help context.
142
* @return standard project customizer dialog.
144
public static Dialog createCustomizerDialog( String folderPath,
146
String preselectedCategory,
147
ActionListener okOptionListener,
149
FileObject root = Repository.getDefault().getDefaultFileSystem().findResource(folderPath);
151
throw new IllegalArgumentException("The designated path " + folderPath + " doesn't exist. Cannot create customizer.");
153
DataFolder def = DataFolder.findFolder(root);
154
assert def != null : "Cannot find DataFolder for " + folderPath;
155
DelegateCategoryProvider prov = new DelegateCategoryProvider(def, context);
156
return createCustomizerDialog(prov.getSubCategories(),
158
preselectedCategory, okOptionListener, helpCtx);
162
/** Creates standard innerPane for customizer dialog.
164
private static CustomizerPane createCustomizerPane( Category[] categories,
165
CategoryComponentProvider componentProvider,
166
String preselectedCategory ) {
168
CategoryChangeSupport changeSupport = new CategoryChangeSupport();
169
registerCategoryChangeSupport(changeSupport, categories);
171
CategoryModel categoryModel = new CategoryModel( categories );
172
JPanel categoryView = new CategoryView( categoryModel );
173
CustomizerPane customizerPane = new CustomizerPane( categoryView, categoryModel, componentProvider );
175
if ( preselectedCategory == null ) {
176
preselectedCategory = categories[0].getName();
179
Category c = categoryModel.getCategory( preselectedCategory );
181
categoryModel.setCurrentCategory( c );
184
return customizerPane;
187
private static void registerCategoryChangeSupport(final CategoryChangeSupport changeSupport,
188
final Category[] categories) {
189
for (int i = 0; i < categories.length; i++) {
190
Utilities.putCategoryChangeSupport(categories[i], changeSupport);
191
Category[] subCategories = categories[i].getSubcategories();
192
if (subCategories != null) {
193
registerCategoryChangeSupport(changeSupport, subCategories);
199
/** Provides components for categories.
201
public static interface CategoryComponentProvider {
203
/** Creates component which has to be shown for given category.
204
* @param category The Category
205
* @return UI component for category customization
207
JComponent create( Category category );
212
* Interface for creation of Customizer categories and their respective UI panels.
213
* Implementations are to be registered in System FileSystem via module layers. Used by the
214
* {@link org.netbeans.spi.project.ui.support.ProjectCustomizer#createCustomizerDialog(String,Lookup,String,ActionListener,HelpCtx)}
215
* The panel/category created by the provider can get notified that the customizer got
216
* closed by setting an <code>ActionListener</code> to
217
* {@link org.netbeans.spi.project.ui.support.ProjectCustomizer.Category#setOkButtonListener} .
218
* UI Component can be defined for category folder that is represented as node with subnodes in the category
219
* tree of project customizer. Name of the file that defines the instance class in layer for such category
220
* must be named "Self". Such CompositeCategory won't have the createCategory() method called, but will have the category created by
221
* the infrastructure based on the folder content.
222
* For details and usage see issue #91276.
223
* @since org.netbeans.modules.projectuiapi/1 1.22
225
public static interface CompositeCategoryProvider {
228
* create the Category instance for the given project customizer context.
229
* @param context Lookup instance passed from project The content is up to the project type, please consult documentation
230
* for the project type you want to integrate your panel into.
231
* @return A category instance, can be null, in which case no category and no panels are created for given context.
232
* The instance is expected to have no subcategories.
234
Category createCategory( Lookup context );
237
* create the UI component for given category and context.
238
* The panel/category created by the provider can get notified that the customizer got
239
* closed by setting an <code>ActionListener</code> to
240
* {@link org.netbeans.spi.project.ui.support.ProjectCustomizer.Category#setOkButtonListener}.
241
* @param category Category instance that was created in the createCategory method.
242
* @param context Lookup instance passed from project The content is up to the project type, please consult documentation
243
* for the project type you want to integrate your panel into.
245
JComponent createComponent (Category category, Lookup context );
248
/** Describes category of properties to be customized by given component
250
public static final class Category {
253
private String displayName;
255
private Category[] subcategories;
256
private boolean valid;
257
private String errorMessage;
258
private ActionListener okListener;
260
/** Private constructor. See the factory method.
262
private Category( String name,
265
Category[] subcategories ) {
268
this.displayName = displayName;
270
this.subcategories = subcategories;
271
this.valid = true; // default
274
/** Factory method which creates new category description.
275
* @param name Programmatic name of the category
276
* @param displayName Name to be shown to the user
277
* @param icon Icon for given category. Will use default icon if null.
278
* @param subcategories Subcategories to be shown under given category.
279
* Category won't be expandable if null or empty array.
280
* @return a new category description
282
public static Category create( String name,
285
Category... subcategories ) {
286
return new Category( name, displayName, icon, subcategories );
289
// Public methods ------------------------------------------------------
291
/** Gets programmatic name of given category.
292
* @return Programmatic name of the category
294
public String getName() {
298
/** Gets display name of given category.
299
* @return Display name of the category
301
public String getDisplayName() {
302
return this.displayName;
305
/** Gets icon of given category.
306
* @return Icon name of the category or null
308
public Image getIcon() {
312
/** Gets subcategories of given category.
313
* @return Subcategories of the category or null
315
public Category[] getSubcategories() {
316
return this.subcategories;
320
* Returns an error message for this category.
321
* @return the error message (could be null)
323
public String getErrorMessage() {
328
* Returns whether this category is valid or not. See {@link
329
* ProjectCustomizer#createCustomizerDialog} for more details.
330
* @return whether this category is valid or not (true by default)
332
public boolean isValid() {
337
* Set a validity of this category. See {@link
338
* ProjectCustomizer#createCustomizerDialog} for more details.
339
* @param valid set whether this category is valid or not
341
public void setValid(boolean valid) {
342
if (this.valid != valid) {
344
Utilities.getCategoryChangeSupport(this).firePropertyChange(
345
CategoryChangeSupport.VALID_PROPERTY, !valid, valid);
350
* Set an errror message for this category which than may be shown in a
351
* project customizer.
353
* @param message message for this category. To <em>reset</em> a
354
* message usually <code>null</code> or an empty string is
355
* passed. (similar to behaviour of {@link
356
* javax.swing.text.JTextComponent#setText(String)})
358
public void setErrorMessage(String message) {
359
if (message == null) {
362
if (!message.equals(this.errorMessage)) {
363
String oldMessage = this.errorMessage;
364
this.errorMessage = message;
365
Utilities.getCategoryChangeSupport(this).firePropertyChange(
366
CategoryChangeSupport.ERROR_MESSAGE_PROPERTY, oldMessage, message);
371
* Set the action listener that will get notified when the changes in the customizer
373
* @param okButtonListener ActionListener to notify
374
* @since org.netbeans.modules.projectuiapi/1 1.20
376
public void setOkButtonListener(ActionListener okButtonListener) {
377
okListener = okButtonListener;
381
* Returns the action listener associated with this category that gets notified
382
* when OK button is pressed on the customizer.
383
* @return instance of ActionListener or null if not set.
384
* @since org.netbeans.modules.projectuiapi/1 1.20
386
public ActionListener getOkButtonListener() {
392
/*private*/ static class DelegateCategoryProvider implements CategoryComponentProvider, CompositeCategoryProvider, Lookup.Provider {
394
private final Lookup context;
395
private final Map<ProjectCustomizer.Category,CompositeCategoryProvider> category2provider;
396
private final DataFolder folder;
397
private final CompositeCategoryProvider selfProvider;
399
public DelegateCategoryProvider(DataFolder folder, Lookup context) {
400
this(folder, context, new HashMap<ProjectCustomizer.Category,CompositeCategoryProvider>());
403
private DelegateCategoryProvider(DataFolder folder, Lookup context, Map<ProjectCustomizer.Category,CompositeCategoryProvider> cat2Provider) {
404
this(folder, context, cat2Provider, null);
407
private DelegateCategoryProvider(DataFolder folder, Lookup context, Map<ProjectCustomizer.Category,CompositeCategoryProvider> cat2Provider, CompositeCategoryProvider sProv) {
408
this.context = context;
409
this.folder = folder;
410
category2provider = cat2Provider;
411
selfProvider = sProv;
414
public JComponent create(ProjectCustomizer.Category category) {
415
CompositeCategoryProvider prov = category2provider.get(category);
416
assert prov != null : "Category doesn't have a provider associated.";
417
return prov.createComponent(category, context);
420
public ProjectCustomizer.Category[] getSubCategories() {
422
return readCategories(folder);
423
} catch (IOException exc) {
424
Logger.getAnonymousLogger().log(Level.WARNING, "Cannot construct Project UI panels", exc);
425
return new ProjectCustomizer.Category[0];
426
} catch (ClassNotFoundException ex) {
427
Logger.getAnonymousLogger().log(Level.WARNING, "Cannot construct Project UI panels", ex);
428
return new ProjectCustomizer.Category[0];
433
/*private*/ ProjectCustomizer.Category[] readCategories(DataFolder folder) throws IOException, ClassNotFoundException {
434
List<ProjectCustomizer.Category> toRet = new ArrayList<ProjectCustomizer.Category>();
435
for (DataObject dob : folder.getChildren()) {
436
if (dob instanceof DataFolder) {
437
CompositeCategoryProvider sProvider = null;
438
DataObject subDobs[] = ((DataFolder) dob).getChildren();
439
for (DataObject subDob : subDobs) {
440
if (subDob.getName().equals("Self")) { // NOI18N
441
InstanceCookie cookie = subDob.getCookie(InstanceCookie.class);
442
if (cookie != null && CompositeCategoryProvider.class.isAssignableFrom(cookie.instanceClass())) {
443
sProvider = (CompositeCategoryProvider) cookie.instanceCreate();
447
CompositeCategoryProvider prov = null;
448
if (sProvider != null) {
449
prov = new DelegateCategoryProvider((DataFolder) dob, context, category2provider, sProvider);
451
prov = new DelegateCategoryProvider((DataFolder) dob, context, category2provider);
453
ProjectCustomizer.Category cat = prov.createCategory(context);
455
category2provider.put(cat, prov);
457
if (!dob.getName().equals("Self")) { // NOI18N
458
InstanceCookie cook = dob.getCookie(InstanceCookie.class);
459
if (cook != null && CompositeCategoryProvider.class.isAssignableFrom(cook.instanceClass())) {
460
CompositeCategoryProvider provider = (CompositeCategoryProvider)cook.instanceCreate();
461
ProjectCustomizer.Category cat = provider.createCategory(context);
464
category2provider.put(cat, provider);
465
includeSubcats(cat.getSubcategories(), provider);
470
return toRet.toArray(new ProjectCustomizer.Category[toRet.size()]);
473
private void includeSubcats(ProjectCustomizer.Category[] cats, ProjectCustomizer.CompositeCategoryProvider provider) {
475
for (ProjectCustomizer.Category cat : cats) {
476
category2provider.put(cat, provider);
477
includeSubcats(cat.getSubcategories(), provider);
483
* provides category for folder..
485
public ProjectCustomizer.Category createCategory(Lookup context) {
486
FileObject fo = folder.getPrimaryFile();
487
String dn = fo.getNameExt();
489
dn = fo.getFileSystem().getStatus().annotateName(fo.getNameExt(), Collections.singleton(fo));
490
} catch (FileStateInvalidException ex) {
491
Logger.getAnonymousLogger().log(Level.WARNING, "Cannot retrieve display name for folder " + fo.getPath(), ex);
493
return ProjectCustomizer.Category.create(folder.getName(), dn, null, getSubCategories());
497
* provides component for folder category
499
public JComponent createComponent(ProjectCustomizer.Category category, Lookup context) {
500
if (selfProvider != null) {
501
return selfProvider.createComponent(category, context);
506
public Lookup getLookup() {