diff --git a/options.api/apichanges.xml b/options.api/apichanges.xml --- a/options.api/apichanges.xml +++ b/options.api/apichanges.xml @@ -75,6 +75,20 @@ + + + Annotation to register keywords for some panel in the Options dialog + + + + + + Introduced an annotation inside OptionsPanelController + to register keywords for some panel in the Options dialog declaratively. + + + + Annotations to register dialog panels diff --git a/options.api/manifest.mf b/options.api/manifest.mf --- a/options.api/manifest.mf +++ b/options.api/manifest.mf @@ -2,6 +2,6 @@ OpenIDE-Module: org.netbeans.modules.options.api/1 OpenIDE-Module-Localizing-Bundle: org/netbeans/modules/options/Bundle.properties OpenIDE-Module-Layer: org/netbeans/modules/options/resources/mf-layer.xml -OpenIDE-Module-Specification-Version: 1.27 +OpenIDE-Module-Specification-Version: 1.28 AutoUpdate-Show-In-Client: false AutoUpdate-Essential-Module: true diff --git a/options.api/nbproject/project.xml b/options.api/nbproject/project.xml --- a/options.api/nbproject/project.xml +++ b/options.api/nbproject/project.xml @@ -200,6 +200,26 @@ + + qa-functional + + org.netbeans.libs.junit4 + + + + org.netbeans.modules.jellytools.platform + + + + org.netbeans.modules.jemmy + + + + org.netbeans.modules.nbjunit + + + + org.netbeans.api.options diff --git a/options.api/src/org/netbeans/api/options/OptionsDisplayer.java b/options.api/src/org/netbeans/api/options/OptionsDisplayer.java --- a/options.api/src/org/netbeans/api/options/OptionsDisplayer.java +++ b/options.api/src/org/netbeans/api/options/OptionsDisplayer.java @@ -74,6 +74,22 @@ * @since 1.8 */ public static final String ADVANCED = "Advanced"; // NOI18N + /** Registration name of Keymaps category (aka Keymap). + * @since 1.28 + */ + public static final String KEYMAPS = "Keymaps"; // NOI18N + /** Registration name of FontsAndColors category (aka Fonts & Colors). + * @since 1.28 + */ + public static final String FONTSANDCOLORS = "FontsAndColors"; // NOI18N + /** Registration name of Editor category. + * @since 1.28 + */ + public static final String EDITOR = "Editor"; // NOI18N + /** Registration name of General category. + * @since 1.28 + */ + public static final String GENERAL = "General"; // NOI18N private String currentCategoryID = null; private AtomicBoolean operationCancelled; diff --git a/options.api/src/org/netbeans/modules/options/CategoryModel.java b/options.api/src/org/netbeans/modules/options/CategoryModel.java --- a/options.api/src/org/netbeans/modules/options/CategoryModel.java +++ b/options.api/src/org/netbeans/modules/options/CategoryModel.java @@ -81,6 +81,7 @@ Collections.synchronizedMap(new LinkedHashMap()); private MasterLookup masterLookup; static final String OD_LAYER_FOLDER_NAME = "OptionsDialog"; // NOI18N + static final String OD_LAYER_KEYWORDS_FOLDER_NAME = OD_LAYER_FOLDER_NAME.concat("/Keywords"); // NOI18N private Result result; Set> getCategories() { diff --git a/options.api/src/org/netbeans/modules/options/OptionsPanelControllerProcessor.java b/options.api/src/org/netbeans/modules/options/OptionsPanelControllerProcessor.java --- a/options.api/src/org/netbeans/modules/options/OptionsPanelControllerProcessor.java +++ b/options.api/src/org/netbeans/modules/options/OptionsPanelControllerProcessor.java @@ -42,37 +42,56 @@ package org.netbeans.modules.options; +import java.io.IOException; +import java.io.InputStream; import java.lang.annotation.Annotation; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; import java.util.HashSet; +import java.util.Properties; import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import javax.annotation.processing.Processor; import javax.annotation.processing.RoundEnvironment; import javax.annotation.processing.SupportedSourceVersion; import javax.lang.model.SourceVersion; import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.PackageElement; import javax.lang.model.element.TypeElement; +import javax.swing.JPanel; +import org.netbeans.api.options.OptionsDisplayer; import org.netbeans.spi.options.AdvancedOption; import org.netbeans.spi.options.OptionsCategory; import org.netbeans.spi.options.OptionsPanelController; import org.netbeans.spi.options.OptionsPanelController.ContainerRegistration; +import org.netbeans.spi.options.OptionsPanelController.Keywords; +import org.netbeans.spi.options.OptionsPanelController.KeywordsRegistration; import org.netbeans.spi.options.OptionsPanelController.SubRegistration; import org.netbeans.spi.options.OptionsPanelController.TopLevelRegistration; import org.openide.filesystems.annotations.LayerBuilder; import org.openide.filesystems.annotations.LayerBuilder.File; import org.openide.filesystems.annotations.LayerGeneratingProcessor; import org.openide.filesystems.annotations.LayerGenerationException; +import org.openide.util.NbBundle; import org.openide.util.lookup.ServiceProvider; @ServiceProvider(service=Processor.class) @SupportedSourceVersion(SourceVersion.RELEASE_6) public class OptionsPanelControllerProcessor extends LayerGeneratingProcessor { + private Element originatingElement; + public @Override Set getSupportedAnnotationTypes() { return new HashSet(Arrays.asList( TopLevelRegistration.class.getCanonicalName(), ContainerRegistration.class.getCanonicalName(), - SubRegistration.class.getCanonicalName() + SubRegistration.class.getCanonicalName(), + KeywordsRegistration.class.getCanonicalName(), + Keywords.class.getCanonicalName() )); } @@ -108,6 +127,40 @@ keywords(e, r.keywords(), r.keywordsCategory(), r, file); file.write(); } + + ArrayList advanced = new ArrayList(); + for (Element e : roundEnv.getElementsAnnotatedWith(Keywords.class)) { + Keywords annotation = e.getAnnotation(Keywords.class); + if (annotation != null) { + originatingElement = e; + String location = annotation.location(); + if(location.equals(OptionsDisplayer.ADVANCED)) { + if(getBundleValue(annotation.tabTitle(), annotation, "tabTitle").trim().isEmpty()) { + throw new LayerGenerationException("Must specify both location and tabTitle", e, processingEnv, annotation, "tabTitle"); + } + if(annotation.index() != -1) { + throw new LayerGenerationException("No need to specify index, panels in Miscellaneous category are sorted alphabetically", e, processingEnv, annotation, "index"); + } + advanced.add(annotation); + } else if(!location.equals(OptionsDisplayer.GENERAL) && !location.equals(OptionsDisplayer.KEYMAPS)) { + if(annotation.index() < 0) { + throw new LayerGenerationException("You need to specify a non-negative value for index", e, processingEnv, annotation, "index"); + } + } + } + } + Collections.sort(advanced, new AdvancedComparable()); + + for (Element e : roundEnv.getElementsAnnotatedWith(Keywords.class)) { + handleElement(e, e.getAnnotation(Keywords.class), advanced, ""); + } + for (Element e : roundEnv.getElementsAnnotatedWith(KeywordsRegistration.class)) { + KeywordsRegistration r = e.getAnnotation(KeywordsRegistration.class); + Keywords[] panels = r.value(); + for (int i = 0; i < panels.length; i++) { + handleElement(e, panels[i], advanced, Integer.toString(-(i + 1))); + } + } for (Element e : roundEnv.getElementsAnnotatedWith(ContainerRegistration.class)) { ContainerRegistration r = e.getAnnotation(ContainerRegistration.class); LayerBuilder builder = layer(e); @@ -124,6 +177,90 @@ return true; } + private void handleElement(Element e, Keywords annotation, ArrayList advanced, String name) throws LayerGenerationException { + originatingElement = e; + int index = annotation.index(); + if (annotation.location().equals(OptionsDisplayer.ADVANCED)) { + index = advanced.indexOf(annotation); + } + File file = layer(e). + file("OptionsDialog/Keywords/".concat(e.asType().toString()).concat(name)). + stringvalue("instanceOf", JPanel.class.getName()). + stringvalue("location", annotation.location()). + stringvalue("tabTitle", getBundleValue(annotation.tabTitle(), annotation, "tabTitle")). + intvalue("index", index); + StringBuilder keywordsSB = new StringBuilder(); + String[] keywords = annotation.keywords(); + keywordsSB.append(getBundleValue(keywords[0], annotation, "keywords").toUpperCase()); + for (int j = 1; j < keywords.length; j++) { + keywordsSB.append(",").append(getBundleValue(keywords[j], annotation, "keywords").toUpperCase()); + } + file.stringvalue("keywords", keywordsSB.toString()); + file.write(); + } + + private class AdvancedComparable implements Comparator { + + @Override + public int compare(Keywords r1, Keywords r2) { + return r1.tabTitle().compareTo(r2.tabTitle()); + } + } + + private String getBundleValue(String label, Annotation annotation, String annotationMethod) throws LayerGenerationException { + String javaIdentifier = "(?:\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*)"; + Matcher m = Pattern.compile("((?:" + javaIdentifier + "\\.)+[^\\s.#]+)?#(\\S*)").matcher(label); + if (m.matches()) { + String bundle = m.group(1); + String key = m.group(2); + if (bundle == null) { + Element referenceElement = originatingElement; + while (referenceElement != null && referenceElement.getKind() != ElementKind.PACKAGE) { + referenceElement = referenceElement.getEnclosingElement(); + } + if (referenceElement == null) { + throw new LayerGenerationException("No reference element to determine package in '" + label + "'", originatingElement); + } + bundle = ((PackageElement) referenceElement).getQualifiedName() + ".Bundle"; + } + return verifyBundleValue(bundle, key, m.group(1) == null, annotation, annotationMethod); + } + return label; + } + + private String verifyBundleValue(String bundle, String key, boolean samePackage, Annotation annotation, String annotationMethod) throws LayerGenerationException { + if (processingEnv == null) { + return ""; + } + if (samePackage) { + for (Element e = originatingElement; e != null; e = e.getEnclosingElement()) { + NbBundle.Messages m = e.getAnnotation(NbBundle.Messages.class); + if (m != null) { + for (String kv : m.value()) { + if (kv.startsWith(key + "=")) { + return kv.substring(kv.indexOf("=") + 1); + } + } + } + } + } + try { + InputStream is = layer(originatingElement).validateResource(bundle.replace('.', '/') + ".properties", originatingElement, null, null, false).openInputStream(); + try { + Properties p = new Properties(); + p.load(is); + if (p.getProperty(key) == null) { + throw new LayerGenerationException("No key '" + key + "' found in " + bundle, originatingElement, processingEnv, annotation, annotationMethod); + } + return p.getProperty(key); + } finally { + is.close(); + } + } catch (IOException x) { + throw new LayerGenerationException("Could not open " + bundle + ": " + x, originatingElement, processingEnv, annotation, annotationMethod); + } + } + private void iconBase(Element e, String iconBase, Annotation r, File file, LayerBuilder builder) throws LayerGenerationException { builder.validateResource(iconBase, e, r, "iconBase", true); file.stringvalue("iconBase", iconBase); diff --git a/options.api/src/org/netbeans/spi/options/OptionsPanelController.java b/options.api/src/org/netbeans/spi/options/OptionsPanelController.java --- a/options.api/src/org/netbeans/spi/options/OptionsPanelController.java +++ b/options.api/src/org/netbeans/spi/options/OptionsPanelController.java @@ -316,4 +316,63 @@ int position() default Integer.MAX_VALUE; } + /** + * Similar to {@link Keywords} but permits multiple registrations of + * one class. + * + * @since org.netbeans.modules.options.api/1 1.28 + */ + @Target({ElementType.TYPE}) + @Retention(RetentionPolicy.SOURCE) + public @interface KeywordsRegistration { + + /** + * List of Keywords registrations. + */ + Keywords[] value(); + } + + /** + * Registers keywords for some panel in the Options dialog. Should be placed + * on a {@link JPanel} instance. + * + * @since org.netbeans.modules.options.api/1 1.28 + */ + @Target({ElementType.TYPE}) + @Retention(RetentionPolicy.SOURCE) + public @interface Keywords { + + /** + * Keywords for use with search inside the Options dialog. You may use + * {@code #key} syntax. + */ + String[] keywords(); + + /** + * Keyword category for use with search inside the Options dialog. + * + * Location of this panel inside some top-level panel matching + * {@link ContainerRegistration#id} or {@link SubRegistration#location}. + * Typically this should be a reference to a compile-time constant also + * used for the container's ID. + * + * If the panel is in the Miscellaneous category you must also specify {@link #tabTitle}). + */ + String location(); + + /** + * Optional title that must be used if the panel is in the Miscellaneous category. + * + * You may use {@code #key} syntax. + */ + String tabTitle() default ""; + + /** + * Position relative to sibling subpanels matching the tab index in a tabbed pane. + * If the panel is in the Miscellaneous panel there is no need to specify the index + * as tabs in Miscellaneous category are always sorted alphabetically. + */ + int index() default -1; + } + } diff --git a/options.api/test/qa-functional/src/org/netbeans/modules/options/SearchInOptionsTest.java b/options.api/test/qa-functional/src/org/netbeans/modules/options/SearchInOptionsTest.java new file mode 100644 --- /dev/null +++ b/options.api/test/qa-functional/src/org/netbeans/modules/options/SearchInOptionsTest.java @@ -0,0 +1,261 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2012 Oracle and/or its affiliates. All rights reserved. + * + * Oracle and Java are registered trademarks of Oracle and/or its affiliates. + * Other names may be trademarks of their respective owners. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common + * Development and Distribution License("CDDL") (collectively, the + * "License"). You may not use this file except in compliance with the + * License. You can obtain a copy of the License at + * http://www.netbeans.org/cddl-gplv2.html + * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the + * specific language governing permissions and limitations under the + * License. When distributing the software, include this License Header + * Notice in each file and include the License file at + * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the GPL Version 2 section of the License file that + * accompanied this code. If applicable, add the following below the + * License Header, with the fields enclosed by brackets [] replaced by + * your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * + * If you wish your version of this file to be governed by only the CDDL + * or only the GPL Version 2, indicate your decision by adding + * "[Contributor] elects to include this software in this distribution + * under the [CDDL or GPL Version 2] license." If you do not indicate a + * single choice of license, a recipient has the option to distribute + * your version of this file under either the CDDL, the GPL Version 2 or + * to extend the choice of license to its licensees as provided above. + * However, if you add GPL Version 2 code and therefore, elected the GPL + * Version 2 license, then the option applies only if the new code is + * made subject to such option by the copyright holder. + * + * Contributor(s): + * + * Portions Copyrighted 2012 Sun Microsystems, Inc. + */ +package org.netbeans.modules.options; + +import java.awt.Component; +import java.awt.event.KeyEvent; +import java.util.ArrayList; +import java.util.Arrays; +import javax.swing.JLabel; +import junit.framework.Test; +import org.netbeans.jellytools.JellyTestCase; +import org.netbeans.jellytools.OptionsOperator; +import org.netbeans.jemmy.ComponentChooser; +import org.netbeans.jemmy.EventTool; +import org.netbeans.jemmy.operators.JLabelOperator; +import org.netbeans.jemmy.operators.JTabbedPaneOperator; +import org.netbeans.jemmy.operators.JTextFieldOperator; +import org.netbeans.jemmy.operators.Operator; +import org.netbeans.jemmy.operators.Operator.StringComparator; +import org.netbeans.junit.NbModuleSuite; + +/** + * + * @author theofanis + */ +public class SearchInOptionsTest extends JellyTestCase { + + private StringComparator stringComparator; + private OptionsOperator optionsOperator; + private JTextFieldOperator jTextFieldOperator; + private JTabbedPaneOperator jTabbedPaneOperator; + private String[] categoryIDs; + + /** + * Constructor required by JUnit + */ + public SearchInOptionsTest(String testName) { + super(testName); + } + + /** + * Creates suite from particular test cases. + */ + public static Test suite() { + return NbModuleSuite.createConfiguration(SearchInOptionsTest.class).addTest( + "testSearchInOptionsWindow").clusters(".*").enableModules(".*").gui(true).suite(); + } + + public void testSearchInOptionsWindow() { + OptionsOperator.invoke(); + new EventTool().waitNoEvent(1000); + log("Option dialog was opened"); + + optionsOperator = new OptionsOperator(); + new EventTool().waitNoEvent(1000); + + jTextFieldOperator = new JTextFieldOperator(optionsOperator); + stringComparator = Operator.getDefaultStringComparator(); + categoryIDs = CategoryModel.getInstance().getCategoryIDs(); + + int[] tabIndexes = {0}; + String[] selectedCategories = {"Editor"}; + ArrayList enabledCategories = new ArrayList(); + enabledCategories.add("Editor"); + searchFor("general editor", tabIndexes, selectedCategories, enabledCategories); + + tabIndexes[0] = 6; + searchFor("macros", tabIndexes, selectedCategories, enabledCategories); + + tabIndexes[0] = 2; + searchFor("completion", tabIndexes, selectedCategories, enabledCategories); + + tabIndexes[0] = 3; + searchFor("templates", tabIndexes, selectedCategories, enabledCategories); + + tabIndexes[0] = 8; + searchFor("dictionary", tabIndexes, selectedCategories, enabledCategories); + + tabIndexes[0] = 7; + searchFor("on save", tabIndexes, selectedCategories, enabledCategories); + + enabledCategories.clear(); + enabledCategories.add("General"); + tabIndexes[0] = -1; + selectedCategories[0] = "General"; + searchFor("proxy", tabIndexes, selectedCategories, enabledCategories); + + enabledCategories.clear(); + enabledCategories.add("Fonts & Colors"); + tabIndexes[0] = 0; + selectedCategories[0] = "FontsAndColors"; + searchFor("syntax", tabIndexes, selectedCategories, enabledCategories); + + tabIndexes[0] = 1; + searchFor("highlighting", tabIndexes, selectedCategories, enabledCategories); + + tabIndexes[0] = 4; + searchFor("versioning", tabIndexes, selectedCategories, enabledCategories); + + enabledCategories.clear(); + enabledCategories.add("Keymap"); + tabIndexes[0] = -1; + selectedCategories[0] = "Keymaps"; + searchFor("keymap", tabIndexes, selectedCategories, enabledCategories); + + enabledCategories.clear(); + enabledCategories.add("Java"); + tabIndexes[0] = 5; + selectedCategories[0] = "Java"; + searchFor("maven", tabIndexes, selectedCategories, enabledCategories); + + tabIndexes[0] = 0; + selectedCategories[0] = "Java"; + searchFor("ant", tabIndexes, selectedCategories, enabledCategories); + + enabledCategories.add("Miscellaneous"); + tabIndexes = new int[2]; + selectedCategories = new String[2]; + tabIndexes[0] = 4; + tabIndexes[1] = 2; + selectedCategories[0] = "Java"; + selectedCategories[1] = "Miscellaneous"; + searchFor("me", tabIndexes, selectedCategories, enabledCategories); + + enabledCategories.clear(); + enabledCategories.add("Java"); + tabIndexes = new int[1]; + selectedCategories = new String[1]; + tabIndexes[0] = 7; + selectedCategories[0] = "Java"; + searchFor("fx", tabIndexes, selectedCategories, enabledCategories); + + enabledCategories.clear(); + enabledCategories.add("Miscellaneous"); + tabIndexes[0] = 3; + selectedCategories[0] = "Miscellaneous"; + searchFor("groovy", tabIndexes, selectedCategories, enabledCategories); + + tabIndexes[0] = 5; + selectedCategories[0] = "Miscellaneous"; + searchFor("javascript", tabIndexes, selectedCategories, enabledCategories); + + tabIndexes[0] = 7; + selectedCategories[0] = "Miscellaneous"; + searchFor("svg", tabIndexes, selectedCategories, enabledCategories); + + tabIndexes[0] = 8; + selectedCategories[0] = "Miscellaneous"; + searchFor("terminal", tabIndexes, selectedCategories, enabledCategories); + + enabledCategories.add("Fonts & Colors"); + tabIndexes = new int[2]; + selectedCategories = new String[2]; + tabIndexes[0] = 3; + tabIndexes[1] = 1; + selectedCategories[0] = "FontsAndColors"; + selectedCategories[1] = "Miscellaneous"; + searchFor("diff", tabIndexes, selectedCategories, enabledCategories); + + enabledCategories.clear(); + enabledCategories.addAll(Arrays.asList("General", "Editor", "Fonts & Colors", "Keymap", "Java", "PHP", "C/C++", "Miscellaneous")); + tabIndexes = new int[1]; + selectedCategories = new String[1]; + tabIndexes[0] = 1; + selectedCategories[0] = "Miscellaneous"; + searchFor("", tabIndexes, selectedCategories, enabledCategories); + } + + private void searchFor(String searchTxt, int[] selectedTabIndexes, String[] selectedCategories, ArrayList enabledCategories) { + jTextFieldOperator.setText(searchTxt); + new EventTool().waitNoEvent(500); + jTextFieldOperator.pushKey(KeyEvent.VK_ENTER); + new EventTool().waitNoEvent(1000); + for (int i = 0; i < selectedCategories.length; i++) { + String selectedCategory = selectedCategories[i]; + if (selectedCategory.equals("General")) { + optionsOperator.selectGeneral(); + } else if (selectedCategory.equals("Editor")) { + optionsOperator.selectEditor(); + } else if (selectedCategory.equals("FontsAndColors")) { + optionsOperator.selectFontAndColors(); + } else if (selectedCategory.equals("Keymaps")) { + optionsOperator.selectKeymap(); + } else if (selectedCategory.equals("Java")) { + optionsOperator.selectJava(); + } else if (selectedCategory.equals("Miscellaneous")) { + optionsOperator.selectMiscellaneous(); + } + new EventTool().waitNoEvent(1000); + int selectedTabIndex = selectedTabIndexes[i]; + if (selectedTabIndex != -1) { + jTabbedPaneOperator = new JTabbedPaneOperator(optionsOperator); + assertEquals(selectedTabIndex, jTabbedPaneOperator.getSelectedIndex()); + } + } + for(String category : categoryIDs) { + if(enabledCategories.contains(category)) { + assertTrue(getJLabelOperator(category).isEnabled()); + } + } + } + + private JLabelOperator getJLabelOperator(final String category) { + return new JLabelOperator(optionsOperator, new ComponentChooser() { + @Override + public boolean checkComponent(Component comp) { + if (comp.getClass().getName().equals("org.netbeans.modules.options.OptionsPanel$CategoryButton") ||// NOI18N + comp.getClass().getName().equals("org.netbeans.modules.options.OptionsPanel$NimbusCategoryButton")) { // NOI18N + if (((JLabel) comp).getText() != null) { + return stringComparator.equals(((JLabel) comp).getText(), category); + } + } + return false; + } + + @Override + public String getDescription() { + return "OptionsPanel$CategoryButton with text " + category; // NOI18N + } + }); + } +}