/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright 1997-2010 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]"
*
* Contributor(s):
*
* The Original Software is NetBeans. The Initial Developer of the Original
* Software is Sun Microsystems, Inc. Portions Copyright 1997-2007 Sun
* Microsystems, Inc. All Rights Reserved.
*
* 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.
*/
package org.netbeans.modules.ruby.options;
import java.awt.Component;
import java.awt.Container;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.prefs.AbstractPreferences;
import java.util.prefs.BackingStoreException;
import java.util.prefs.Preferences;
import javax.swing.ComboBoxModel;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JEditorPane;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.JViewport;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import org.netbeans.api.editor.settings.SimpleValueNames;
import org.netbeans.api.ruby.platform.RubyInstallation;
import org.netbeans.editor.BaseDocument;
import org.netbeans.modules.editor.indent.api.Reformat;
import org.netbeans.modules.options.editor.spi.PreferencesCustomizer;
import org.netbeans.modules.options.editor.spi.PreviewProvider;
import org.openide.text.CloneableEditorSupport;
import org.openide.util.HelpCtx;
import org.openide.util.NbBundle;
/**
*
* @author phrebejk
*
* @todo Add an RHTML options category, such that I can see the effects of
* switching the RHTML toggles?
*/
public class FmtOptions {
public static final String expandTabToSpaces = SimpleValueNames.EXPAND_TABS;
public static final String tabSize = SimpleValueNames.TAB_SIZE;
public static final String spacesPerTab = SimpleValueNames.SPACES_PER_TAB;
public static final String indentSize = SimpleValueNames.INDENT_SHIFT_WIDTH;
public static final String rightMargin = SimpleValueNames.TEXT_LIMIT_WIDTH; //NOI18N
public static final String continuationIndentSize = "continuationIndentSize"; //NOI18N
public static final String reformatComments = "reformatComments"; //NOI18N
public static final String indentHtml = "indentHtml"; //NOI18N
public static Preferences lastValues;
static final String CODE_STYLE_PROFILE = "CodeStyle"; // NOI18N
private static final String DEFAULT_PROFILE = "default"; // NOI18N
static final String PROJECT_PROFILE = "project"; // NOI18N
static final String usedProfile = "usedProfile"; // NOI18N
private FmtOptions() {}
public static int getDefaultAsInt(String key) {
return Integer.parseInt(defaults.get(key));
}
public static boolean getDefaultAsBoolean(String key) {
return Boolean.parseBoolean(defaults.get(key));
}
public static String getDefaultAsString(String key) {
return defaults.get(key);
}
public static String getCurrentProfileId() {
return DEFAULT_PROFILE;
}
// public static boolean getGlobalExpandTabToSpaces() {
// Preferences prefs = MimeLookup.getLookup(RubyInstallation.RUBY_MIME_TYPE).lookup(Preferences.class);
// return prefs.getBoolean(SimpleValueNames.EXPAND_TABS, getDefaultAsBoolean(expandTabToSpaces));
// }
//
// public static int getGlobalTabSize() {
// Preferences prefs = MimeLookup.getLookup(RubyInstallation.RUBY_MIME_TYPE).lookup(Preferences.class);
// return prefs.getInt(SimpleValueNames.TAB_SIZE, getDefaultAsInt(tabSize));
// }
//
// public static int getGlobalSpacesPerTab() {
// Preferences prefs = MimeLookup.getLookup(RubyInstallation.RUBY_MIME_TYPE).lookup(Preferences.class);
// return prefs.getInt(SimpleValueNames.SPACES_PER_TAB, getDefaultAsInt(spacesPerTab));
// }
//
// public static int getGlobalIndentSize() {
// Preferences prefs = MimeLookup.getLookup(RubyInstallation.RUBY_MIME_TYPE).lookup(Preferences.class);
// return prefs.getInt(SimpleValueNames.INDENT_SHIFT_WIDTH, -1);
// }
//
// public static int getGlobalRightMargin() {
// Preferences prefs = MimeLookup.getLookup(RubyInstallation.RUBY_MIME_TYPE).lookup(Preferences.class);
// return prefs.getInt(SimpleValueNames.TEXT_LIMIT_WIDTH, getDefaultAsInt(rightMargin));
// }
public static boolean isInteger(String optionID) {
String value = defaults.get(optionID);
try {
Integer.parseInt(value);
return true;
} catch (NumberFormatException numberFormatException) {
return false;
}
}
// Private section ---------------------------------------------------------
private static final String TRUE = "true"; // NOI18N
private static final String FALSE = "false"; // NOI18N
private static Map<String,String> defaults;
static {
createDefaults();
}
private static void createDefaults() {
String defaultValues[][] = {
{ expandTabToSpaces, TRUE}, //NOI18N
{ tabSize, "2"}, //NOI18N
{ spacesPerTab, "2"}, //NOI18N
{ indentSize, "2"}, //NOI18N
{ continuationIndentSize, "2"}, //NOI18N
{ rightMargin, "120"}, //NOI18N
{ reformatComments, FALSE }, //NOI18N
{ indentHtml, TRUE }, //NOI18N
};
defaults = new HashMap<String,String>();
for (java.lang.String[] strings : defaultValues) {
defaults.put(strings[0], strings[1]);
}
}
// Support section ---------------------------------------------------------
public static class CategorySupport implements ActionListener, DocumentListener, PreviewProvider, PreferencesCustomizer {
public static final String OPTION_ID = "org.netbeans.modules.ruby.options.FormatingOptions.ID";
private static final int LOAD = 0;
private static final int STORE = 1;
private static final int ADD_LISTENERS = 2;
// private static final ComboItem bracePlacement[] = new ComboItem[] {
// new ComboItem( BracePlacement.SAME_LINE.name(), "LBL_bp_SAME_LINE" ), // NOI18N
// new ComboItem( BracePlacement.NEW_LINE.name(), "LBL_bp_NEW_LINE" ), // NOI18N
// new ComboItem( BracePlacement.NEW_LINE_HALF_INDENTED.name(), "LBL_bp_NEW_LINE_HALF_INDENTED" ), // NOI18N
// new ComboItem( BracePlacement.NEW_LINE_INDENTED.name(), "LBL_bp_NEW_LINE_INDENTED" ) // NOI18N
// };
// private static final ComboItem bracesGeneration[] = new ComboItem[] {
// new ComboItem( BracesGenerationStyle.GENERATE.name(), "LBL_bg_GENERATE" ), // NOI18N
// new ComboItem( BracesGenerationStyle.LEAVE_ALONE.name(), "LBL_bg_LEAVE_ALONE" ), // NOI18N
// new ComboItem( BracesGenerationStyle.ELIMINATE.name(), "LBL_bg_ELIMINATE" ) // NOI18N
// };
//
// private static final ComboItem wrap[] = new ComboItem[] {
// new ComboItem( WrapStyle.WRAP_ALWAYS.name(), "LBL_wrp_WRAP_ALWAYS" ), // NOI18N
// new ComboItem( WrapStyle.WRAP_IF_LONG.name(), "LBL_wrp_WRAP_IF_LONG" ), // NOI18N
// new ComboItem( WrapStyle.WRAP_NEVER.name(), "LBL_wrp_WRAP_NEVER" ) // NOI18N
// };
private final String previewText;
// private String forcedOptions[][];
// private boolean changed = false;
// private boolean loaded = false;
private final String id;
protected final JPanel panel;
private final List<JComponent> components = new LinkedList<JComponent>();
private JEditorPane previewPane;
private final Preferences preferences;
private final Preferences previewPrefs;
protected CategorySupport(Preferences preferences, String id, JPanel panel, String previewText, String[]... forcedOptions) {
this.preferences = preferences;
this.id = id;
this.panel = panel;
this.previewText = previewText != null ? previewText : NbBundle.getMessage(FmtOptions.class, "SAMPLE_Default"); //NOI18N
// Scan the panel for its components
scan(panel, components);
// Initialize the preview preferences
Preferences forcedPrefs = new PreviewPreferences();
for (String[] option : forcedOptions) {
forcedPrefs.put( option[0], option[1]);
}
this.previewPrefs = new ProxyPreferences(preferences, forcedPrefs);
// Load and hook up all the components
loadFrom(preferences);
addListeners();
}
protected void addListeners() {
scan(ADD_LISTENERS, null);
}
protected void loadFrom(Preferences preferences) {
// loaded = true;
scan(LOAD, preferences);
// loaded = false;
}
//
// public void applyChanges() {
// storeTo(preferences);
// }
//
protected void storeTo(Preferences p) {
scan(STORE, p);
}
protected void notifyChanged() {
// if (loaded)
// return;
storeTo(preferences);
refreshPreview();
}
// ActionListener implementation ---------------------------------------
public void actionPerformed(ActionEvent e) {
notifyChanged();
}
// DocumentListener implementation -------------------------------------
public void insertUpdate(DocumentEvent e) {
notifyChanged();
}
public void removeUpdate(DocumentEvent e) {
notifyChanged();
}
public void changedUpdate(DocumentEvent e) {
notifyChanged();
}
// PreviewProvider methods -----------------------------------------------------
public JComponent getPreviewComponent() {
if (previewPane == null) {
previewPane = new JEditorPane();
previewPane.getAccessibleContext().setAccessibleName(NbBundle.getMessage(FmtOptions.class, "AN_Preview")); //NOI18N
previewPane.getAccessibleContext().setAccessibleDescription(NbBundle.getMessage(FmtOptions.class, "AD_Preview")); //NOI18N
previewPane.putClientProperty("HighlightsLayerIncludes", "^org\\.netbeans\\.modules\\.editor\\.lib2\\.highlighting\\.SyntaxHighlighting$"); //NOI18N
previewPane.setEditorKit(CloneableEditorSupport.getEditorKit(RubyInstallation.RUBY_MIME_TYPE));
previewPane.setEditable(false);
}
return previewPane;
}
public void refreshPreview() {
JEditorPane jep = (JEditorPane) getPreviewComponent();
try {
int rm = previewPrefs.getInt(rightMargin, getDefaultAsInt(rightMargin));
jep.putClientProperty("TextLimitLine", rm); //NOI18N
}
catch( NumberFormatException e ) {
// Ignore it
}
int rm = 30;
try {
rm = previewPrefs.getInt(rightMargin, getDefaultAsInt(rightMargin));
// Estimate text line in preview pane
JComponent pc = previewPane;
if (previewPane.getParent() instanceof JViewport) {
pc = (JViewport)previewPane.getParent();
}
Font font = pc.getFont();
FontMetrics metrics = pc.getFontMetrics(font);
int cw = metrics.charWidth('x');
if (cw > 0) {
int nrm = pc.getWidth() / cw;
if (nrm > 3) {
rm = nrm-2;
}
}
//pane.putClientProperty("TextLimitLine", rm); // NOI18N
}
catch( NumberFormatException e ) {
// Ignore it
}
jep.setIgnoreRepaint(true);
jep.setText(previewText);
final Document doc = jep.getDocument();
final Reformat formatter = Reformat.get(doc);
formatter.lock();
try {
((BaseDocument) doc).runAtomic(new Runnable() {
public void run() {
try {
// The formatter is automatically going to use the previewPrefs.
formatter.reformat(0, doc.getLength());
} catch (BadLocationException ble) {
// ignore
}
}
});
} finally {
formatter.unlock();
}
jep.setIgnoreRepaint(false);
jep.scrollRectToVisible(new Rectangle(0,0,10,10) );
jep.repaint(100);
}
// PreferencesCustomizer implementation --------------------------------
public JComponent getComponent() {
return panel;
}
public String getDisplayName() {
return panel.getName();
}
public String getId() {
return id;
}
public HelpCtx getHelpCtx() {
return null;
}
// PreferencesCustomizer.Factory implementation ------------------------
public static final class Factory implements PreferencesCustomizer.Factory {
private final String id;
private final Class<? extends JPanel> panelClass;
private final String previewText;
private final String[][] forcedOptions;
public Factory(String id, Class<? extends JPanel> panelClass, String previewText, String[]... forcedOptions) {
this.id = id;
this.panelClass = panelClass;
this.previewText = previewText;
this.forcedOptions = forcedOptions;
}
public PreferencesCustomizer create(Preferences preferences) {
try {
return new CategorySupport(preferences, id, panelClass.newInstance(), previewText, forcedOptions);
} catch (Exception e) {
return null;
}
}
} // End of CategorySupport.Factory class
// Private methods -----------------------------------------------------
private void performOperation(int operation, JComponent jc, String optionID, Preferences p) {
switch(operation) {
case LOAD:
loadData(jc, optionID, p);
break;
case STORE:
storeData(jc, optionID, p);
break;
case ADD_LISTENERS:
addListener(jc);
break;
}
}
private void scan(int what, Preferences p ) {
for (JComponent jc : components) {
Object o = jc.getClientProperty(OPTION_ID);
if (o instanceof String) {
performOperation(what, jc, (String)o, p);
} else if (o instanceof String[]) {
for(String oid : (String[])o) {
performOperation(what, jc, oid, p);
}
}
}
}
private void scan(Container container, List<JComponent> components) {
for (Component c : container.getComponents()) {
if (c instanceof JComponent) {
JComponent jc = (JComponent)c;
Object o = jc.getClientProperty(OPTION_ID);
if (o instanceof String || o instanceof String[])
components.add(jc);
}
if (c instanceof Container)
scan((Container)c, components);
}
}
/** Very smart method which tries to set the values in the components correctly
*/
private void loadData( JComponent jc, String optionID, Preferences node ) {
if ( jc instanceof JTextField ) {
JTextField field = (JTextField)jc;
field.setText( node.get(optionID, getDefaultAsString(optionID)) );
}
else if ( jc instanceof JCheckBox ) {
JCheckBox checkBox = (JCheckBox)jc;
boolean df = getDefaultAsBoolean(optionID);
checkBox.setSelected( node.getBoolean(optionID, df));
}
else if ( jc instanceof JComboBox) {
JComboBox cb = (JComboBox)jc;
String value = node.get(optionID, getDefaultAsString(optionID) );
ComboBoxModel model = createModel(value);
cb.setModel(model);
ComboItem item = whichItem(value, model);
cb.setSelectedItem(item);
}
}
private void storeData( JComponent jc, String optionID, Preferences node ) {
if ( jc instanceof JTextField ) {
JTextField field = (JTextField)jc;
String text = field.getText();
// XXX test for numbers
if ( isInteger(optionID) ) {
try {
int i = Integer.parseInt(text);
} catch (NumberFormatException e) {
return;
}
}
// XXX: watch out, tabSize, spacesPerTab, indentSize and expandTabToSpaces
// fall back on getGlopalXXX() values and not getDefaultAsXXX value,
// which is why we must not remove them. Proper solution would be to
// store formatting preferences to MimeLookup and not use NbPreferences.
// The problem currently is that MimeLookup based Preferences do not support subnodes.
if (!optionID.equals(tabSize) &&
!optionID.equals(spacesPerTab) && !optionID.equals(indentSize) &&
getDefaultAsString(optionID).equals(text)
) {
node.remove(optionID);
} else {
node.put(optionID, text);
}
}
else if ( jc instanceof JCheckBox ) {
JCheckBox checkBox = (JCheckBox)jc;
if (!optionID.equals(expandTabToSpaces) && getDefaultAsBoolean(optionID) == checkBox.isSelected())
node.remove(optionID);
else
node.putBoolean(optionID, checkBox.isSelected());
}
else if ( jc instanceof JComboBox) {
JComboBox cb = (JComboBox)jc;
// Logger.global.info( cb.getSelectedItem() + " " + optionID);
String value = ((ComboItem) cb.getSelectedItem()).value;
if (getDefaultAsString(optionID).equals(value))
node.remove(optionID);
else
node.put(optionID,value);
}
}
private void addListener( JComponent jc ) {
if ( jc instanceof JTextField ) {
JTextField field = (JTextField)jc;
field.addActionListener(this);
field.getDocument().addDocumentListener(this);
}
else if ( jc instanceof JCheckBox ) {
JCheckBox checkBox = (JCheckBox)jc;
checkBox.addActionListener(this);
}
else if ( jc instanceof JComboBox) {
JComboBox cb = (JComboBox)jc;
cb.addActionListener(this);
}
}
private ComboBoxModel createModel( String value ) {
// // is it braces placement?
// for (ComboItem comboItem : bracePlacement) {
// if ( value.equals( comboItem.value) ) {
// return new DefaultComboBoxModel( bracePlacement );
// }
// }
//
// // is it braces generation?
// for (ComboItem comboItem : bracesGeneration) {
// if ( value.equals( comboItem.value) ) {
// return new DefaultComboBoxModel( bracesGeneration );
// }
// }
//
// // is it wrap
// for (ComboItem comboItem : wrap) {
// if ( value.equals( comboItem.value) ) {
// return new DefaultComboBoxModel( wrap );
// }
// }
return null;
}
private static ComboItem whichItem(String value, ComboBoxModel model) {
for (int i = 0; i < model.getSize(); i++) {
ComboItem item = (ComboItem)model.getElementAt(i);
if ( value.equals(item.value)) {
return item;
}
}
return null;
}
private static class ComboItem {
String value;
String displayName;
public ComboItem(String value, String key) {
this.value = value;
this.displayName = NbBundle.getMessage(FmtOptions.class, key);
}
@Override
public String toString() {
return displayName;
}
}
}
public static class PreviewPreferences extends AbstractPreferences {
private Map<String,Object> map = new HashMap<String, Object>();
public PreviewPreferences() {
super(null, ""); // NOI18N
}
protected void putSpi(String key, String value) {
map.put(key, value);
}
protected String getSpi(String key) {
return (String)map.get(key);
}
protected void removeSpi(String key) {
map.remove(key);
}
protected void removeNodeSpi() throws BackingStoreException {
throw new UnsupportedOperationException("Not supported yet.");
}
protected String[] keysSpi() throws BackingStoreException {
String array[] = new String[map.keySet().size()];
return map.keySet().toArray( array );
}
protected String[] childrenNamesSpi() throws BackingStoreException {
throw new UnsupportedOperationException("Not supported yet.");
}
protected AbstractPreferences childSpi(String name) {
throw new UnsupportedOperationException("Not supported yet.");
}
protected void syncSpi() throws BackingStoreException {
throw new UnsupportedOperationException("Not supported yet.");
}
protected void flushSpi() throws BackingStoreException {
throw new UnsupportedOperationException("Not supported yet.");
}
}
// read-only, no subnodes
public static final class ProxyPreferences extends AbstractPreferences {
private final Preferences[] delegates;
public ProxyPreferences(Preferences... delegates) {
super(null, ""); // NOI18N
this.delegates = delegates;
}
protected void putSpi(String key, String value) {
throw new UnsupportedOperationException("Not supported yet.");
}
protected String getSpi(String key) {
for(Preferences p : delegates) {
String value = p.get(key, null);
if (value != null) {
return value;
}
}
return null;
}
protected void removeSpi(String key) {
throw new UnsupportedOperationException("Not supported yet.");
}
protected void removeNodeSpi() throws BackingStoreException {
throw new UnsupportedOperationException("Not supported yet.");
}
protected String[] keysSpi() throws BackingStoreException {
Set<String> keys = new HashSet<String>();
for(Preferences p : delegates) {
keys.addAll(Arrays.asList(p.keys()));
}
return keys.toArray(new String[ keys.size() ]);
}
protected String[] childrenNamesSpi() throws BackingStoreException {
throw new UnsupportedOperationException("Not supported yet.");
}
protected AbstractPreferences childSpi(String name) {
throw new UnsupportedOperationException("Not supported yet.");
}
protected void syncSpi() throws BackingStoreException {
throw new UnsupportedOperationException("Not supported yet.");
}
protected void flushSpi() throws BackingStoreException {
throw new UnsupportedOperationException("Not supported yet.");
}
} // End of ProxyPreferences class
}