blob: 9935dbeb54cf1f964df03683d98760c8ba6e9993 [file] [log] [blame]
/*
* Copyright 2007 Sascha Weinreuter
*
* Licensed 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.intellij.plugins.xpathView.ui;
import com.intellij.ui.ListCellRendererWrapper;
import org.intellij.plugins.xpathView.Config;
import org.intellij.plugins.xpathView.HistoryElement;
import org.intellij.plugins.xpathView.eval.EvalExpressionDialog;
import org.intellij.plugins.xpathView.support.XPathSupport;
import org.intellij.plugins.xpathView.util.Namespace;
import org.intellij.plugins.xpathView.util.NamespaceCollector;
import org.intellij.plugins.xpathView.util.Variable;
import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer;
import com.intellij.codeInsight.intention.IntentionAction;
import com.intellij.javaee.ExternalResourceManager;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.event.DocumentAdapter;
import com.intellij.openapi.editor.event.DocumentEvent;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.ComboBox;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.util.Pair;
import com.intellij.psi.PsiDocumentManager;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiFileFactory;
import com.intellij.psi.PsiRecursiveElementVisitor;
import com.intellij.psi.PsiReference;
import com.intellij.psi.xml.XmlElement;
import com.intellij.psi.xml.XmlFile;
import com.intellij.ui.EditorTextField;
import com.intellij.uiDesigner.core.GridConstraints;
import com.intellij.util.ArrayUtil;
import com.intellij.util.IncorrectOperationException;
import com.intellij.util.LocalTimeCounter;
import com.intellij.util.containers.BidirectionalMap;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.intellij.lang.xpath.XPathFileType;
import org.intellij.lang.xpath.context.SimpleVariableContext;
import org.intellij.lang.xpath.context.ContextProvider;
import org.intellij.lang.xpath.context.ContextType;
import org.intellij.lang.xpath.context.NamespaceContext;
import org.intellij.lang.xpath.context.VariableContext;
import org.intellij.lang.xpath.psi.PrefixReference;
import org.intellij.lang.xpath.psi.QNameElement;
import org.intellij.lang.xpath.psi.XPathElement;
import javax.swing.*;
import javax.swing.event.ListDataEvent;
import javax.swing.event.ListDataListener;
import javax.swing.plaf.basic.BasicComboBoxEditor;
import javax.xml.namespace.QName;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.*;
import java.util.List;
public abstract class InputExpressionDialog<FormType extends InputForm> extends ModeSwitchableDialog {
protected final Project myProject;
protected final FormType myForm;
protected final Config mySettings;
private final HistoryModel myModel;
private final Document myDocument;
private final MultilineEditor myEditor;
private final EditorTextField myComboboxEditor;
private final ComboBox myComboBox = new ComboBox(300);
private JComponent myEditorComponent;
@Nullable private Set<Namespace> myNamespaceCache;
private InteractiveContextProvider myContextProvider;
private final PsiFile myXPathFile;
public InputExpressionDialog(final Project project, Config settings, final HistoryElement[] _history, FormType form) {
super(project, false);
myProject = project;
myForm = form;
setResizable(true);
setModal(true);
setHorizontalStretch(1.3f);
mySettings = settings;
myDocument = createXPathDocument(project, _history.length > 0 ? _history[_history.length - 1] : null);
myXPathFile = PsiDocumentManager.getInstance(myProject).getPsiFile(myDocument);
myModel = new HistoryModel(_history, myDocument);
myEditor = new MultilineEditor(myDocument, project, XPathFileType.XPATH, myModel);
myModel.addListDataListener(new ListDataListener() {
final PsiDocumentManager docMgr = PsiDocumentManager.getInstance(project);
final DaemonCodeAnalyzer analyzer = DaemonCodeAnalyzer.getInstance(project);
public void intervalAdded(ListDataEvent e) {
}
public void intervalRemoved(ListDataEvent e) {
}
public void contentsChanged(ListDataEvent e) {
final HistoryElement item = myModel.getSelectedItem();
if (item != null) {
myContextProvider.getNamespaceContext().setMap(asMap(item.namespaces));
if (myXPathFile != null) {
analyzer.restart(myXPathFile);
}
}
}
});
myComboboxEditor = new EditorTextField(myDocument, project, XPathFileType.XPATH);
myComboBox.setRenderer(new ListCellRendererWrapper<HistoryElement>() {
@Override
public void customize(JList list, HistoryElement value, int index, boolean selected, boolean hasFocus) {
setText(value != null ? value.expression : "");
}
});
myComboBox.setModel(myModel);
myComboBox.setEditor(new EditorAdapter(myComboboxEditor));
myComboBox.setEditable(true);
myDocument.addDocumentListener(new DocumentAdapter() {
public void documentChanged(DocumentEvent e) {
updateOkAction();
}
});
init();
}
protected void init() {
myForm.getIcon().setText(null);
myForm.getIcon().setIcon(Messages.getQuestionIcon());
myForm.getEditContextButton().addActionListener(new ActionListener() {
@SuppressWarnings({"unchecked"})
public void actionPerformed(ActionEvent e) {
final HistoryElement selectedItem = myModel.getSelectedItem();
final Collection<Namespace> n;
final Collection<Variable> v;
if (selectedItem != null) {
n = selectedItem.namespaces;
v = selectedItem.variables;
} else {
n = Collections.emptySet();
v = Collections.emptySet();
}
// FIXME
final Collection<Namespace> namespaces = myNamespaceCache != null ?
merge(myNamespaceCache, n, false) : n;
final Set<String> unresolvedPrefixes = findUnresolvedPrefixes();
final EditContextDialog dialog =
new EditContextDialog(myProject, unresolvedPrefixes, namespaces, v, myContextProvider);
dialog.show();
if (dialog.isOK()) {
final Pair<Collection<Namespace>, Collection<Variable>> context = dialog.getContext();
final Collection<Namespace> newNamespaces = context.getFirst();
final Collection<Variable> newVariables = context.getSecond();
updateContext(newNamespaces, newVariables);
SwingUtilities.invokeLater(new Runnable() {
public void run() {
final Editor editor = getEditor();
if (editor != null) {
editor.getContentComponent().grabFocus();
}
}
});
}
}
});
updateOkAction();
super.init();
}
void updateContext(Collection<Namespace> namespaces, Collection<Variable> variables) {
final HistoryElement selectedItem = myModel.getSelectedItem();
final HistoryElement newElement;
if (selectedItem != null) {
newElement = selectedItem.changeContext(namespaces, variables);
} else {
newElement = new HistoryElement(myDocument.getText(), variables, namespaces);
}
myModel.setSelectedItem(newElement);
// FIXME
if (myNamespaceCache == null) {
myContextProvider.getNamespaceContext().setMap(asMap(namespaces));
}
final DaemonCodeAnalyzer analyzer = DaemonCodeAnalyzer.getInstance(myProject);
analyzer.restart(myXPathFile);
}
private Set<String> findUnresolvedPrefixes() {
final Set<String> prefixes = new HashSet<String>();
myXPathFile.accept(new PsiRecursiveElementVisitor(){
public void visitElement(PsiElement element) {
if (element instanceof QNameElement) {
final PsiReference[] references = element.getReferences();
for (PsiReference reference : references) {
if (reference instanceof PrefixReference) {
final PrefixReference prefixReference = (PrefixReference)reference;
if (prefixReference.isUnresolved()) {
prefixes.add(prefixReference.getPrefix());
}
}
}
}
super.visitElement(element);
}
});
return prefixes;
}
protected FormType getForm() {
return myForm;
}
protected JComponent createCenterPanel() {
return myForm.getComponent();
}
protected void updateOkAction() {
getOKAction().setEnabled(isOkEnabled());
}
protected boolean isOkEnabled() {
return myEditor.getField().getDocument().getTextLength() > 0;
}
@Nullable
protected Editor getEditor() {
if (getMode() == Mode.ADVANCED) {
return myEditor.getField().getEditor();
} else {
return myComboboxEditor.getEditor();
}
}
protected void setModeImpl(Mode mode) {
// mySettingsPanel.setVisible(mode == Mode.ADVANCED);
myForm.getEditContextButton().setVisible(mode == Mode.ADVANCED);
if (mode == Mode.ADVANCED) {
setEditor(myEditor, GridConstraints.SIZEPOLICY_WANT_GROW);
myEditor.getField().selectAll();
} else {
setEditor(myComboBox, GridConstraints.SIZEPOLICY_FIXED);
myComboBox.setModel(myModel);
myComboBox.getEditor().selectAll();
}
SwingUtilities.invokeLater(new Runnable() {
public void run() {
final Editor editor = getEditor();
if (editor != null) {
editor.getContentComponent().grabFocus();
}
}
});
}
private void setEditor(JComponent editor, int vSizePolicy) {
if (myEditorComponent != null) {
myForm.getEditorPanel().remove(myEditorComponent);
}
final GridConstraints gridConstraints = new GridConstraints();
gridConstraints.setFill(vSizePolicy == GridConstraints.SIZEPOLICY_WANT_GROW ? GridConstraints.FILL_BOTH : GridConstraints.FILL_HORIZONTAL);
gridConstraints.setVSizePolicy(vSizePolicy);
myForm.getEditorPanel().add(myEditorComponent = editor, gridConstraints);
}
protected static Document createXPathDocument(Project project, HistoryElement historyElement) {
final String expression = historyElement != null ? historyElement.expression : "";
final PsiFile file = PsiFileFactory.getInstance(project).createFileFromText("DummyFile.xpath", XPathFileType.XPATH, expression, LocalTimeCounter.currentTime(), true);
final Document document = PsiDocumentManager.getInstance(project).getDocument(file);
// not sure why this is required...
assert document != null;
document.setReadOnly(false);
assert document.isWritable() : "WTF, document is not writable? Text = <" + expression + ">";
return document;
}
@SuppressWarnings({ "unchecked" })
public boolean show(XmlElement contextElement) {
prepareShow(contextElement);
show();
return isOK();
}
@SuppressWarnings({"unchecked"})
private void prepareShow(XmlElement contextElement) {
final NamespaceCollector.CollectedInfo collectedInfo;
if (contextElement != null) {
collectedInfo = NamespaceCollector.collectInfo((XmlFile)contextElement.getContainingFile());
myNamespaceCache = collectedInfo.namespaces;
} else {
collectedInfo = NamespaceCollector.empty();
myNamespaceCache = null;
}
myContextProvider = new InteractiveContextProvider(contextElement, collectedInfo, myModel);
myContextProvider.attachTo(myXPathFile);
final HistoryElement historyElement = myModel.getSelectedItem();
if (historyElement != null) {
myContextProvider.getNamespaceContext().setMap(asMap(historyElement.namespaces));
} else {
myContextProvider.getNamespaceContext().setMap(asMap(null));
}
updateOkAction();
}
protected static Collection<Namespace> merge(Collection<Namespace> namespaces, Collection<Namespace> cache, boolean merge) {
if (cache == null) return namespaces;
final Set<Namespace> n;
if (merge) {
n = new HashSet<Namespace>(cache);
n.removeAll(namespaces);
n.addAll(namespaces);
} else {
n = new HashSet<Namespace>(namespaces);
for (Namespace namespace : n) {
for (Namespace cached : cache) {
if (namespace.getUri().equals(cached.getUri())) {
namespace.setPrefix(cached.prefix);
}
}
}
}
return n;
}
@SuppressWarnings({"unchecked"})
protected Map<String, String> asMap(Collection<Namespace> namespaces) {
if (namespaces == null) {
if (myNamespaceCache != null) {
return Namespace.makeMap(myNamespaceCache);
} else {
return Collections.emptyMap();
}
}
if (this.myNamespaceCache != null) {
namespaces = merge(myNamespaceCache, namespaces, false);
}
return Namespace.makeMap(namespaces);
}
public JComponent getPreferredFocusedComponent() {
final Editor editor = getEditor();
if (editor != null) {
return editor.getContentComponent();
} else {
return null;
}
}
@SuppressWarnings({"unchecked"})
public Context getContext() {
final HistoryElement context = myModel.getSelectedItem();
if (context == null || context.expression == null) {
final Set<Namespace> cache = myNamespaceCache != null ? myNamespaceCache : Collections.<Namespace>emptySet();
return new Context(new HistoryElement(myDocument.getText(), Collections.<Variable>emptySet(), cache), getMode());
}
final Collection<Namespace> namespaces = myNamespaceCache != null ?
merge(myNamespaceCache, context.namespaces, false) : context.namespaces;
return new Context(new HistoryElement(context.expression, context.variables, namespaces), getMode());
}
public static class Context {
public final HistoryElement input;
public final Mode mode;
Context(HistoryElement context, Mode mode) {
this.input = context;
this.mode = mode;
}
}
protected class EditorAdapter extends BasicComboBoxEditor {
private final EditorTextField myTf;
public EditorAdapter(EditorTextField tf) {
myTf = tf;
}
public Component getEditorComponent() {
return myTf.getComponent();
}
@Nullable
public Object getItem() {
return myModel.getSelectedItem();
}
public void selectAll() {
myTf.selectAll();
}
public void setItem(Object object) {
if (object == null) {
myEditor.getField().setText("");
} else {
myEditor.getField().setText(((HistoryElement)object).expression);
}
}
}
private static class MyVariableResolver extends SimpleVariableContext {
private final HistoryModel myModel;
public MyVariableResolver(HistoryModel model) {
myModel = model;
}
@NotNull
public String[] getVariablesInScope(XPathElement element) {
final HistoryElement selectedItem = myModel.getSelectedItem();
if (selectedItem != null) {
return Variable.asSet(selectedItem.variables).toArray(new String[selectedItem.variables.size()]);
} else {
return ArrayUtil.EMPTY_STRING_ARRAY;
}
}
}
private class InteractiveContextProvider extends ContextProvider {
private final XmlElement myContextElement;
private final NamespaceCollector.CollectedInfo myCollectedInfo;
private final MyVariableResolver myVariableResolver;
private final EvalExpressionDialog.MyNamespaceContext myNamespaceContext;
public InteractiveContextProvider(XmlElement contextElement, NamespaceCollector.CollectedInfo collectedInfo, HistoryModel model) {
myContextElement = contextElement;
myCollectedInfo = collectedInfo;
myVariableResolver = new MyVariableResolver(model);
myNamespaceContext = new EvalExpressionDialog.MyNamespaceContext();
}
@NotNull
public ContextType getContextType() {
return XPathSupport.TYPE;
}
@Nullable
public XmlElement getContextElement() {
return myContextElement;
}
@NotNull
public EvalExpressionDialog.MyNamespaceContext getNamespaceContext() {
return myNamespaceContext;
}
public VariableContext getVariableContext() {
return myVariableResolver;
}
public Set<QName> getAttributes(boolean forValidation) {
return myCollectedInfo.attributes;
}
private Set<QName> filterDefaultNamespace(Set<QName> _set) {
final Set<QName> set = new HashSet<QName>(_set);
for (Iterator<QName> it = set.iterator(); it.hasNext();) {
final QName name = it.next();
final String prefix = name.getPrefix();
if (prefix == null || prefix.length() == 0) {
final String uri = name.getNamespaceURI();
if (uri != null && uri.length() > 0) {
final String assignedPrefix = myNamespaceContext.getPrefixForURI(uri, null);
if (assignedPrefix == null || assignedPrefix.length() == 0) {
it.remove();
}
}
}
}
return set;
}
public Set<QName> getElements(boolean forValidation) {
return filterDefaultNamespace(myCollectedInfo.elements);
}
}
protected class MyNamespaceContext implements NamespaceContext {
private BidirectionalMap<String, String> myMap;
@Nullable
public String getNamespaceURI(String prefix, XmlElement context) {
final String s = myMap.get(prefix);
if (s == null && prefix.length() == 0) {
return "";
}
return s;
}
@Nullable
public String getPrefixForURI(String uri, XmlElement context) {
final List<String> list = myMap.getKeysByValue(uri);
return list != null && !list.isEmpty() ? list.get(0) : null;
}
@NotNull
public Collection<String> getKnownPrefixes(XmlElement context) {
return myMap.keySet();
}
@Nullable
public PsiElement resolve(String prefix, XmlElement context) {
return null;
}
public void setMap(Map<String, String> map) {
myMap = new BidirectionalMap<String, String>();
myMap.putAll(map);
}
public IntentionAction[] getUnresolvedNamespaceFixes(PsiReference reference, String localName) {
return new IntentionAction[]{ new MyRegisterPrefixAction(reference) };
}
@Override
public String getDefaultNamespace(XmlElement context) {
return null;
}
}
private class MyRegisterPrefixAction implements IntentionAction {
private final PsiReference myReference;
public MyRegisterPrefixAction(PsiReference reference) {
myReference = reference;
}
@NotNull
public String getText() {
return "Register Namespace Prefix";
}
@NotNull
public String getFamilyName() {
return getText();
}
public boolean isAvailable(@NotNull Project project, Editor editor, PsiFile file) {
return myReference instanceof PrefixReference && myReference.getElement().isValid() && ((PrefixReference)myReference).isUnresolved();
}
public void invoke(@NotNull Project project, Editor editor, PsiFile file) throws IncorrectOperationException {
final Set<String> prefix = Collections.singleton(myReference.getCanonicalText());
final Map<String, String> myMap = myContextProvider.getNamespaceContext().myMap;
final Collection<String> list;
if (myNamespaceCache == null) {
final ExternalResourceManager erm = ExternalResourceManager.getInstance();
list = new ArrayList<String>(Arrays.asList(erm.getResourceUrls(null, true)));
for (String namespace : myMap.values()) {
list.remove(namespace);
}
Collections.sort((List<String>)list);
} else {
list = myMap.values();
}
final AddNamespaceDialog dlg = new AddNamespaceDialog(project, prefix, list, myNamespaceCache == null ?
AddNamespaceDialog.Mode.URI_EDITABLE :
AddNamespaceDialog.Mode.FIXED);
dlg.show();
if (dlg.isOK()) {
final Namespace namespace = new Namespace(dlg.getPrefix(), dlg.getURI());
final HistoryElement selectedItem = myModel.getSelectedItem();
final Collection<Namespace> n;
final Collection<Variable> v;
if (selectedItem != null) {
n = new HashSet<Namespace>(selectedItem.namespaces);
n.remove(namespace);
n.add(namespace);
v = selectedItem.variables;
} else {
n = Collections.singleton(namespace);
//noinspection unchecked
v = Collections.emptySet();
}
updateContext(n, v);
}
}
public boolean startInWriteAction() {
return false;
}
}
}