blob: 390b6caeb505d93a8acdb378a481acd3760f7360 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.netbeans.modules.xml.schema.completion;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.event.KeyEvent;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.text.*;
import javax.swing.Icon;
import javax.xml.XMLConstants;
import org.netbeans.editor.BaseDocument;
import org.netbeans.api.editor.completion.Completion;
import org.netbeans.api.lexer.Token;
import org.netbeans.api.lexer.TokenHierarchy;
import org.netbeans.api.lexer.TokenSequence;
import org.netbeans.api.xml.lexer.XMLTokenId;
import org.netbeans.spi.editor.completion.CompletionItem;
import org.netbeans.spi.editor.completion.CompletionTask;
import org.netbeans.modules.xml.axi.AXIComponent;
import org.netbeans.modules.xml.schema.completion.spi.CompletionContext;
import org.netbeans.modules.xml.schema.completion.util.CompletionContextImpl;
import org.netbeans.modules.xml.schema.completion.util.CompletionUtil;
import org.netbeans.spi.editor.completion.support.AsyncCompletionTask;
import org.netbeans.swing.plaf.LFCustoms;
/**
*
* @author Samaresh (Samaresh.Panda@Sun.Com)
*/
public abstract class CompletionResultItem implements CompletionItem {
private static final Logger _logger = Logger.getLogger(CompletionResultItem.class.getName());
private static final Color COLOR = LFCustoms.shiftColor(new Color(64, 64, 255));
public static final String
ICON_ELEMENT = "element.png", //NOI18N
ICON_ATTRIBUTE = "attribute.png", //NOI18N
ICON_VALUE = "value.png", //NOI18N
ICON_LOCATION = "/org/netbeans/modules/xml/schema/completion/resources/"; //NOI18N
protected boolean shift = false;
protected String typedChars;
protected String itemText;
protected javax.swing.Icon icon;
protected CompletionPaintComponent component;
protected AXIComponent axiComponent;
protected int extraPaintGap = CompletionPaintComponent.DEFAULT_ICON_WIDTH;
protected TokenSequence tokenSequence;
protected final CompletionContextImpl context;
/**
* Creates a new instance of CompletionUtil
*/
public CompletionResultItem(AXIComponent component, CompletionContext context) {
this(component, context, null);
}
public CompletionResultItem(AXIComponent component, CompletionContext context,
TokenSequence tokenSequence) {
this.context = (CompletionContextImpl) context;
this.axiComponent = component;
setTokenSequence(tokenSequence);
if (context != null) {
this.typedChars = context.getTypedChars();
}
}
Icon getIcon(){
return icon;
}
public AXIComponent getAXIComponent() {
return axiComponent;
}
/**
* The completion item's name.
*/
public String getItemText() {
return itemText;
}
/**
* The text user sees in the CC list. Normally some additional info
* such as cardinality etc. are added to the item's name.
*
*/
public abstract String getDisplayText();
/**
* Replacement text is the one that gets inserted into the document when
* user selects this item from the CC list.
*/
public abstract String getReplacementText();
/**
* Returns the relative caret position.
* The caller must call this w.r.t. the offset
* e.g. component.setCaretPosition(offset + getCaretPosition())
*/
public abstract int getCaretPosition();
@Override
public String toString() {
return getItemText();
}
Color getPaintColor() {
return LFCustoms.shiftColor(COLOR);
}
public int getExtraPaintGap() {
return extraPaintGap;
}
public void setExtraPaintGap(int extraPaintGap) {
this.extraPaintGap = extraPaintGap;
}
public TokenSequence getTokenSequence() {
return tokenSequence;
}
public void setTokenSequence(TokenSequence tokenSequence) {
this.tokenSequence = tokenSequence;
}
protected int removeTextLength(JTextComponent component, int offset, int removeLength) {
return removeLength;
}
protected int caretOffset() {
return -1;
}
/**
* Prepares values in the item, by inspecting the Component, token sequence etc.
* The method is called before the text is changed, and is called under document write lock.
*
* @param component the editing component
* @param proposedText the proposed text
* @param offset the offset where the completion occurs
*/
protected void prepare(JTextComponent component, String proposedText, int offset) {
}
/**
* Actually replaces a piece of document by passes text.
* @param component a document source
* @param text a string to be inserted
* @param offset the target offset
* @param len a length that should be removed before inserting text
*/
protected void replaceText(final JTextComponent component, final String text,
final int offset, final int len) {
final BaseDocument doc = (BaseDocument) component.getDocument();
doc.runAtomic(new Runnable() {
@Override
public void run() {
try {
int caretPos = component.getCaretPosition();
if ((context != null) && (context.canReplace(text))) {
prepare(component, text, offset);
int l2 = removeTextLength(component, offset, len);
String insertingText = getInsertingText(component, offset, text, l2);
if (l2 > 0) doc.remove(offset, l2);
doc.insertString(offset, insertingText, null);
// fix for issue #186007
caretPos = offset + getCaretPosition();
caretPos -= text.length() - insertingText.length();
} else {
caretPos = offset + getCaretPosition(); // change the caret position
}
int docLength = doc.getLength();
if (docLength == 0) {
caretPos = 0;
} else if (caretPos > doc.getLength()) {
caretPos = doc.getLength();
}
component.setCaretPosition(caretPos);
String prefix = CompletionUtil.getPrefixFromTag(text);
if (prefix == null) {
return;
}
//insert namespace declaration for the new prefix
if ((context != null) && (! context.isSpecialCompletion()) &&
(! context.isPrefixBeingUsed(prefix))) {
String tns = context.getTargetNamespaceByPrefix(prefix);
// CC has made a suggestion, so materialize it:
if (tns == null) {
tns = context.getSuggestedNamespace().get(prefix);
}
if (tns != null) {
doc.insertString(CompletionUtil.getNamespaceInsertionOffset(doc), " " +
XMLConstants.XMLNS_ATTRIBUTE + ":" + prefix + "=\"" +
tns + "\"", null);
}
}
} catch (Exception e) {
_logger.log(Level.SEVERE,
e.getMessage() == null ? e.getClass().getName() : e.getMessage(), e);
}
}
});
}
protected final TokenSequence createTokenSequence(JTextComponent component) {
if (tokenSequence == null) {
TokenHierarchy tokenHierarchy = TokenHierarchy.get(component.getDocument());
this.tokenSequence = tokenHierarchy.tokenSequence();
}
return tokenSequence;
}
private String stripCommonPrefix(String prefix, String replacement, String original) {
if (replacement.startsWith(prefix) && original.startsWith(prefix)) {
return replacement.substring(prefix.length());
} else {
return replacement;
}
}
private void resetTokenSequence() {
tokenSequence = null;
}
protected String getInsertingText(JTextComponent component, int textPos, String primaryText, int removeLen) {
if ((primaryText == null) || (primaryText.length() < 1)) {
return primaryText;
}
createTokenSequence(component);
if (tokenSequence.move(textPos) == 0) {
tokenSequence.movePrevious();
} else {
tokenSequence.moveNext();
}
Token token = tokenSequence.token();
boolean isTextTag = CompletionUtil.isTextTag(token);
if (! (isTextTag || CompletionUtil.isEndTagPrefix(token) ||
CompletionUtil.isTagFirstChar(token))) {
return primaryText;
}
int tokenOffset = tokenSequence.offset();
if (isTextTag) {
String tokenText = token.text().toString();
boolean isCaretAfterTag =
(tokenText.startsWith(CompletionUtil.END_TAG_PREFIX) &&
(textPos == tokenOffset + CompletionUtil.END_TAG_PREFIX.length()))
||
(tokenText.startsWith(CompletionUtil.TAG_FIRST_CHAR) &&
(textPos == tokenOffset + CompletionUtil.TAG_FIRST_CHAR.length()));
if (! isCaretAfterTag) {
return primaryText;
}
}
String tokenText = token.text().toString();
if (removeLen > 0) {
// in the middle of the tag; must return text without starting / end tag
primaryText = stripCommonPrefix(CompletionUtil.END_TAG_PREFIX, primaryText, tokenText);
primaryText = stripCommonPrefix(CompletionUtil.TAG_FIRST_CHAR, primaryText, tokenText);
}
if (primaryText.endsWith(CompletionUtil.TAG_LAST_CHAR)) {
boolean endPresent = false;
STOP: while (!endPresent && tokenSequence.moveNext()) {
Token t = tokenSequence.token();
switch ((XMLTokenId)t.id()) {
case WS:
case ARGUMENT:
case VALUE:
case OPERATOR:
break;
case TAG: {
String tt = t.text().toString();
if (tt.equals(CompletionUtil.TAG_LAST_CHAR) || tt.equals("/>")) {
endPresent = true;
break;
}
}
default:
break STOP;
}
}
if (endPresent) {
primaryText = primaryText.substring(0, primaryText.length() -1);
}
}
if ((tokenOffset > -1) && (tokenOffset < textPos)) {
textPos = tokenOffset;
}
boolean isDifferentTextFound = false;
int i = 0;
for (; i < primaryText.length(); ++i, ++textPos) {
try {
String strDoc = component.getText(textPos, 1),
strText = primaryText.substring(i, i + 1);
isDifferentTextFound = (! strDoc.equals(strText));
if (isDifferentTextFound) break;
} catch(BadLocationException e) {
_logger.log(Level.WARNING,
e.getMessage() == null ? e.getClass().getName() : e.getMessage(), e);
isDifferentTextFound = true;
}
}
String text = isDifferentTextFound ? primaryText.substring(Math.max(0, i - removeLen)) : "";
return text;
}
////////////////////////////////////////////////////////////////////////////////
///////////////////methods from CompletionItem interface////////////////////////
////////////////////////////////////////////////////////////////////////////////
@Override
public CompletionTask createDocumentationTask() {
return new AsyncCompletionTask(new DocumentationQuery(this));
}
@Override
public CompletionTask createToolTipTask() {
return new AsyncCompletionTask(new ToolTipQuery(this));
}
@Override
public void defaultAction(JTextComponent component) {
String selectedText = component.getSelectedText();
int charsToRemove = selectedText != null ? selectedText.length() :
(typedChars == null ? 0 : typedChars.length()),
substOffset = selectedText != null ? component.getSelectionStart() :
component.getCaretPosition() - charsToRemove;
if(!shift) Completion.get().hideAll();
if(getReplacementText().equals(typedChars))
return;
replaceText(component, getReplacementText(), substOffset, charsToRemove);
}
@Override
public CharSequence getInsertPrefix() {
return getItemText();
}
public abstract CompletionPaintComponent getPaintComponent();
@Override
public int getPreferredWidth(Graphics g, Font defaultFont) {
CompletionPaintComponent renderComponent = getPaintComponent();
return renderComponent.getPreferredSize().width;
//return getPaintComponent().getWidth(getItemText(), defaultFont);
}
@Override
public int getSortPriority() {
return 0;
}
@Override
public CharSequence getSortText() {
return getItemText();
}
@Override
public boolean instantSubstitution(JTextComponent component) {
defaultAction(component);
return true;
}
@Override
public void processKeyEvent(KeyEvent e) {
shift = (e.getKeyCode() == KeyEvent.VK_ENTER &&
e.getID() == KeyEvent.KEY_PRESSED && e.isShiftDown());
}
@Override
public void render(Graphics g, Font defaultFont,
Color defaultColor, Color backgroundColor,
int width, int height, boolean selected) {
CompletionPaintComponent renderComponent = getPaintComponent();
renderComponent.setFont(defaultFont);
renderComponent.setForeground(defaultColor);
renderComponent.setBackground(backgroundColor);
renderComponent.setBounds(0, 0, width, height);
renderComponent.setSelected(selected);
renderComponent.paintComponent(g);
}
}