/* * Copyright (c) 2012 Sam Harwell, Tunnel Vision Laboratories LLC * All rights reserved. * * The source code of this document is proprietary work, and is not licensed for * distribution. For information about licensing, contact Sam Harwell at: * sam@tunnelvisionlabs.com */ package org.antlr.netbeans.editor.formatting; import java.awt.Component; import java.awt.Container; import java.awt.Rectangle; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.util.EnumSet; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; import java.util.prefs.Preferences; import javax.swing.ComboBoxModel; import javax.swing.DefaultComboBoxModel; import javax.swing.JComboBox; import javax.swing.JComponent; import javax.swing.JEditorPane; import javax.swing.JPanel; import javax.swing.JTextField; import javax.swing.JToggleButton; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; import org.netbeans.api.annotations.common.NonNull; import org.netbeans.api.editor.settings.SimpleValueNames; 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 Sam Harwell */ @NbBundle.Messages({ "SAMPLE_Default=", "AN_Preview=", "AD_Preview=", }) public class CategorySupport implements ActionListener, DocumentListener, PreviewProvider, PreferencesCustomizer { // -J-Dorg.antlr.netbeans.editor.formatting.CategorySupport.level=FINE private static final Logger LOGGER = Logger.getLogger(CategorySupport.class.getName()); public static final String OPTION_ID = "org.antlr.netbeans.editor.formatting.FormatOptions.ID"; private static final int LOAD = 0; private static final int STORE = 1; private static final int ADD_LISTENERS = 2; private static final Map<Class<?>, ComboItem[]> COMBO_BOX_ITEMS = new HashMap<>(); private final String mimeType; private final String previewText; private final PreviewFormatter formatter; private final String id; protected final JPanel panel; private final Preferences preferences; private final List<JComponent> components = new LinkedList<>(); private JEditorPane previewPane; protected CategorySupport(String mimeType, Preferences preferences, String id, JPanel panel, String previewText, PreviewFormatter formatter) { this.mimeType = mimeType; this.preferences = preferences; this.id = id; this.panel = panel; this.previewText = previewText != null ? previewText : Bundle.SAMPLE_Default(); this.formatter = formatter; // Scan the panel for its components scan(panel, components); // Load and hook up all the components loadFrom(preferences); addListeners(); } protected void addListeners() { scan(ADD_LISTENERS, null); } protected void loadFrom(Preferences preferences) { scan(LOAD, preferences); } protected void storeTo(Preferences p) { scan(STORE, p); } protected void notifyChanged() { storeTo(preferences); refreshPreview(); } // ActionListener implementation --------------------------------------- @Override public void actionPerformed(ActionEvent e) { notifyChanged(); } // DocumentListener implementation ------------------------------------- @Override public void insertUpdate(DocumentEvent e) { notifyChanged(); } @Override public void removeUpdate(DocumentEvent e) { notifyChanged(); } @Override public void changedUpdate(DocumentEvent e) { notifyChanged(); } // PreviewProvider methods ----------------------------------------------------- @Override public JComponent getPreviewComponent() { if (previewPane == null) { previewPane = new JEditorPane(); previewPane.getAccessibleContext().setAccessibleName(Bundle.AN_Preview()); previewPane.getAccessibleContext().setAccessibleDescription(Bundle.AD_Preview()); previewPane.putClientProperty("HighlightsLayerIncludes", "^org\\.netbeans\\.modules\\.editor\\.lib2\\.highlighting\\.SyntaxHighlighting$"); //NOI18N previewPane.setEditorKit(CloneableEditorSupport.getEditorKit(mimeType)); previewPane.setEditable(false); } return previewPane; } @Override public void refreshPreview() { JEditorPane jep = (JEditorPane) getPreviewComponent(); try { int rm = FormatOptions.TEXT_LIMIT_WIDTH.getValue(preferences); jep.putClientProperty("TextLimitLine", rm); //NOI18N jep.getDocument().putProperty(SimpleValueNames.TEXT_LINE_WRAP, ""); //NOI18N jep.getDocument().putProperty(SimpleValueNames.TAB_SIZE, ""); //NOI18N jep.getDocument().putProperty(SimpleValueNames.TEXT_LIMIT_WIDTH, ""); //NOI18N } catch( NumberFormatException e ) { // Ignore it } try { // make sure the CodeStyle class is initialized Class.forName(CodeStyle.class.getName(), true, CodeStyle.class.getClassLoader()); } catch (ClassNotFoundException cnfe) { // ignore } jep.setIgnoreRepaint(true); String formattedPreviewText = formatter != null ? formatter.reformat(previewText, preferences) : previewText; jep.setText(formattedPreviewText); jep.setIgnoreRepaint(false); jep.scrollRectToVisible(new Rectangle(0,0,10,10) ); jep.repaint(100); } // PreferencesCustomizer implementation -------------------------------- @Override public JComponent getComponent() { return panel; } @Override public String getDisplayName() { return panel.getName(); } @Override public String getId() { return id; } @Override public HelpCtx getHelpCtx() { return null; } // PreferencesCustomizer.Factory implementation ------------------------ public static final class Factory implements PreferencesCustomizer.Factory { private final String mimeType; private final String id; private final Class<? extends JPanel> panelClass; private final String previewText; private final PreviewFormatter formatter; public Factory(String mimeType, String id, Class<? extends JPanel> panelClass, String previewText, PreviewFormatter formatter) { this.mimeType = mimeType; this.id = id; this.panelClass = panelClass; this.previewText = previewText; this.formatter = formatter; } @Override public PreferencesCustomizer create(Preferences preferences) { try { CategorySupport categorySupport = new CategorySupport(mimeType, preferences, id, panelClass.newInstance(), previewText, formatter); return categorySupport; } catch (InstantiationException | IllegalAccessException ex) { LOGGER.log(Level.WARNING, "An exception occurred attempting to customize preferences.", ex); return null; } } } // Private methods ----------------------------------------------------- private void performOperation(int operation, JComponent jc, AbstractFormatOption option, Preferences p) { switch(operation) { case LOAD: loadData(jc, option, p); break; case STORE: storeData(jc, option, 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 AbstractFormatOption) { performOperation(what, jc, (AbstractFormatOption)o, p); } else if (o instanceof AbstractFormatOption[]) { for(AbstractFormatOption oid : (AbstractFormatOption[])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[] || o instanceof AbstractFormatOption || o instanceof AbstractFormatOption[]) { components.add(jc); } } if (c instanceof Container) { scan((Container)c, components); } } } /** Very smart method which tries to set the values in the components correctly */ @SuppressWarnings("unchecked") private void loadData( JComponent jc, AbstractFormatOption optionID, Preferences node ) { if ( jc instanceof JTextField ) { JTextField field = (JTextField)jc; field.setText( optionID.getValueAsString(node) ); } else if ( jc instanceof JToggleButton ) { JToggleButton toggle = (JToggleButton)jc; toggle.setSelected( ((BooleanFormatOption)optionID).getValue(node) ); } else if ( jc instanceof JComboBox ) { @SuppressWarnings("rawtypes") JComboBox cb = (JComboBox)jc; Enum<?> value = ((EnumFormatOption<?>)optionID).getValue(node); @SuppressWarnings("rawtypes") ComboBoxModel model = createModel(value); cb.setModel(model); ComboItem item = whichItem(value, model); cb.setSelectedItem(item); } } private void storeData( JComponent jc, @NonNull AbstractFormatOption option, Preferences node ) { if ( jc instanceof JTextField ) { JTextField field = (JTextField)jc; String text = field.getText(); // XXX test for numbers if ( option instanceof IntFormatOption ) { 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 (!option.equals(FormatOptions.TAB_SIZE) && !option.equals(FormatOptions.SPACES_PER_TAB) && !option.equals(FormatOptions.INDENT_SHIFT_WIDTH) && option.getDefaultValueAsString().equals(text) ) { node.remove(option.getName()); } else { node.put(option.getName(), text); } } else if ( jc instanceof JToggleButton ) { JToggleButton toggle = (JToggleButton)jc; if (!option.equals(FormatOptions.EXPAND_TABS) && ((BooleanFormatOption)option).getDefaultValue() == toggle.isSelected()) node.remove(option.getName()); else node.putBoolean(option.getName(), toggle.isSelected()); } else if ( jc instanceof JComboBox ) { @SuppressWarnings("rawtypes") JComboBox cb = (JComboBox)jc; // Logger.global.info( cb.getSelectedItem() + " " + optionID); Enum<?> value = ((ComboItem) cb.getSelectedItem()).value; if (((EnumFormatOption<?>)option).getDefaultValue().equals(value)) node.remove(option.getName()); else node.put(option.getName(), value.name()); } } private void addListener( JComponent jc ) { if ( jc instanceof JTextField ) { JTextField field = (JTextField)jc; field.addActionListener(this); field.getDocument().addDocumentListener(this); } else if ( jc instanceof JToggleButton ) { JToggleButton toggle = (JToggleButton)jc; toggle.addActionListener(this); } else if ( jc instanceof JComboBox) { @SuppressWarnings("rawtypes") JComboBox cb = (JComboBox)jc; cb.addActionListener(this); } } @SuppressWarnings({"rawtypes", "unchecked"}) private static synchronized ComboBoxModel createModel( Enum<?> value ) { ComboItem[] items = COMBO_BOX_ITEMS.get(value.getClass()); if (items == null) { EnumSet<?> enumSet = EnumSet.allOf(value.getClass()); items = new ComboItem[enumSet.size()]; int i = 0; for (Object item : enumSet) { items[i++] = new ComboItem((Enum)item); } COMBO_BOX_ITEMS.put(value.getClass(), items); } return new DefaultComboBoxModel(items); } private static ComboItem whichItem(Enum<?> value, @SuppressWarnings("rawtypes") 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 { Enum<?> value; String displayName; public ComboItem(Enum<?> value) { this.value = value; this.displayName = NbBundle.getMessage(value.getClass(), String.format("LBL_%s_%s", value.getClass().getSimpleName(), value.name())); } @Override public String toString() { return displayName; } } public interface PreviewFormatter { String reformat(String text, Preferences preferences); } }