/* * Copyright 2003-2011 JetBrains s.r.o. * * 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 jetbrains.mps.nodeEditor.cells; import jetbrains.mps.editor.runtime.style.Measure; import jetbrains.mps.editor.runtime.style.Padding; import jetbrains.mps.editor.runtime.style.StyleAttributes; import jetbrains.mps.editor.runtime.style.StyleImpl; import jetbrains.mps.nodeEditor.EditorSettings; import jetbrains.mps.openapi.editor.style.Style; import jetbrains.mps.openapi.editor.style.StyleAttribute; import jetbrains.mps.openapi.editor.style.StyleChangeEvent; import jetbrains.mps.openapi.editor.style.StyleListener; import jetbrains.mps.openapi.editor.style.StyleRegistry; import org.jetbrains.annotations.NotNull; import java.awt.Color; import java.awt.Component; import java.awt.Font; import java.awt.FontMetrics; import java.awt.Graphics; import java.util.Set; public class TextLine { // COLORS: Remove hardcoded color private static final Color ERROR_COLOR = StyleRegistry.getInstance() != null && StyleRegistry.getInstance().isDarkTheme() ? StyleRegistry.getInstance().getEditorBackground() : new Color(255, 220, 220); private String myText; private int myDescent = 0; private Font myFont = EditorSettings.getInstance().getDefaultEditorFont(); private FontMetrics myFontMetrics; private int myCaretPosition = 0; private int myCaretX = -1; private int mySelectionStartX = -1; private int mySelectionEndX = -1; private int myTextEndX = -1; private int myMinWidth = -1; private int myStartTextSelectionPosition = 0; private int myEndTextSelectionPosition = 0; private int myWidth = 0; private int myHeight = 0; private int myTextHeight = 0; private boolean myCaretEnabled = true; private int myMinimalLength = 0; private double myLineSpacing = EditorSettings.getInstance().getLineSpacing(); private Color mySelectedTextColor = EditorSettings.getInstance().getSelectionForegroundColor(); private Color myTextSelectedTextColor = EditorSettings.getInstance().getSelectionForegroundColor(); private Color myTextSelectedBackgroundColor = EditorSettings.getInstance().getSelectionBackgroundColor(); private Color myErrorColor = Color.red; private boolean myShowsErrorColor = false; private boolean myNull; private Style myStyle; private int myPaddingLeft; private int myPaddingRight; private int myPaddingTop; private int myPaddingBottom; private boolean myControlOvered; private boolean myStrikeOut; private boolean myUnderlined; private Color myTextColor; private Color myNullTextColor; private Color myTextBackground; private Color myNullTextBackground; private Color mySelectedTextBackground; private Color myNulLSelectedTextBackground; private boolean myShowCaret; private boolean mySelected; private boolean myInitialized; private int myFontCorrectionRightGap; private int myFontCorrectionTextShift; public TextLine(String text) { this(text, new StyleImpl(), false); } public TextLine(String text, @NotNull Style style, boolean isNull) { setText(text); myNull = isNull; myStyle = style; showTextColor(); } public String getText() { return myText; } public void setText(String text) { if (text == null) { text = ""; } if (text.equals(myText)) { return; } doSetText(text); doSetCaretPosition(Math.min(myText.length(), getCaretPosition())); setStartTextSelectionPosition(getCaretPosition()); setEndTextSelectionPosition(getCaretPosition()); } private void doSetText(String text) { myText = text; myCaretX = -1; mySelectionStartX = -1; mySelectionEndX = -1; myTextEndX = -1; } public String getTextBeforeCaret() { return myText.substring(0, getCaretPosition()); } public String getTextAfterCaret() { return myText.substring(getCaretPosition(), myText.length()); } public int getWidth() { return myWidth; } public int getHeight() { return myHeight; } public int getStartTextSelectionPosition() { return myStartTextSelectionPosition; } public int getEndTextSelectionPosition() { return myEndTextSelectionPosition; } /** * @param length Minimal size of the text edit field in chars. */ public void setMinimalLength(int length) { myMinimalLength = length; myMinWidth = -1; } private void updateStyle(Set<StyleAttribute> attributes) { if (attributes == null || attributes.contains(StyleAttributes.FONT_SIZE) || attributes.contains(StyleAttributes.FONT_STYLE)) { //this is the most expensive calculation EditorSettings settings = EditorSettings.getInstance(); Integer styleFontSize = myStyle.get(StyleAttributes.FONT_SIZE); String family = settings.getFontFamily(); Integer style = myStyle.get(StyleAttributes.FONT_STYLE); int fontSize = styleFontSize != null ? styleFontSize : settings.getFontSize(); myFont = FontRegistry.getInstance().getFont(family, style, fontSize); myFontMetrics = null; myFontCorrectionRightGap = FontRegistry.getInstance().isFakeItalic(family, style) ? 1 : 0; myFontCorrectionTextShift = (style & Font.ITALIC) > 0 ? -1 : 0; } myPaddingLeft = getHorizontalInternalInset(myStyle.get(StyleAttributes.PADDING_LEFT)); myPaddingRight = getHorizontalInternalInset(myStyle.get(StyleAttributes.PADDING_RIGHT)); myPaddingTop = getVerticalInternalInset(myStyle.get(StyleAttributes.PADDING_TOP)); myPaddingBottom = getVerticalInternalInset(myStyle.get(StyleAttributes.PADDING_BOTTOM)); myControlOvered = myStyle.get(StyleAttributes.CONTROL_OVERED_REFERENCE); myStrikeOut = myStyle.get(StyleAttributes.STRIKE_OUT); myUnderlined = myStyle.get(StyleAttributes.UNDERLINED); myTextColor = myStyle.get(StyleAttributes.TEXT_COLOR); myNullTextColor = myStyle.get(StyleAttributes.NULL_TEXT_COLOR); myTextBackground = myStyle.get(StyleAttributes.TEXT_BACKGROUND_COLOR); myNullTextBackground = myStyle.get(StyleAttributes.NULL_TEXT_BACKGROUND_COLOR); mySelectedTextBackground = myStyle.get(StyleAttributes.SELECTED_TEXT_BACKGROUND_COLOR); myNulLSelectedTextBackground = myStyle.get(StyleAttributes.NULL_SELECTED_TEXT_BACKGROUND_COLOR); } private void init() { if (myInitialized) { return; } myInitialized = true; updateStyle(null); myStyle.addListener(e -> { Set<StyleAttribute> changedAttributes = e.getChangedAttributes(); updateStyle(changedAttributes); }); } public void relayout() { FontMetrics metrics = getFontMetrics(); myHeight = (int) (metrics.getHeight() * myLineSpacing + getPaddingTop() + getPaddingBottom()); myTextHeight = (int) (metrics.getHeight() * myLineSpacing); int minWidth = calculateMinWidth(); int width = metrics.charsWidth(myText.toCharArray(), 0, myText.length()) + myFontCorrectionRightGap + getPaddingLeft() + getPaddingRight(); myWidth = Math.max(minWidth, width); myDescent = metrics.getDescent(); } int getEffectiveWidth() { int minWidth = calculateMinWidth(); int effectiveWidth = myWidth - getPaddingLeft() - getPaddingRight(); return Math.max(minWidth, effectiveWidth); } private int calculateMinWidth() { if (myMinWidth == -1) { myMinWidth = Math.max(myMinimalLength * getFontMetrics().charWidth('w'), 2); } return myMinWidth; } private int getHorizontalInternalInset(Padding p) { double value = p.getValue(); Measure type = p.getType(); if (type == null) { type = Measure.SPACES; } if (type.equals(Measure.SPACES)) { return (int) (charWidth() * value); } if (type.equals(Measure.PIXELS)) { return (int) value; } return 0; } private int getVerticalInternalInset(Padding p) { double value = p.getValue(); Measure type = p.getType(); if (type == null) { type = Measure.SPACES; } if (type.equals(Measure.SPACES)) { return (int) (charHeight() * value); } if (type.equals(Measure.PIXELS)) { return (int) value; } return 0; } public int getPaddingLeft() { init(); return myPaddingLeft; } public int getPaddingRight() { init(); return myPaddingRight; } public int getPaddingTop() { init(); return myPaddingTop; } public int getPaddingBottom() { init(); return myPaddingBottom; } public int charWidth() { FontMetrics metrics = getFontMetrics(); return metrics.charWidth('w'); } public int charHeight() { return getFontMetrics().getHeight(); } public boolean isCaretEnabled() { return myCaretEnabled; } public void setCaretEnabled(boolean caretEnabled) { myCaretEnabled = caretEnabled; } public void home() { setCaretPosition(0); } public void end() { setCaretPosition(getText().length()); } public void showErrorColor() { myShowsErrorColor = true; } public void showTextColor() { myShowsErrorColor = false; } public Color getBackgroundColor() { if (myShowsErrorColor) { return ERROR_COLOR; } return null; } public Color getTextColor() { init(); if (myControlOvered) { return EditorSettings.getInstance().getHyperlinkColor(); } if (!myNull && myTextColor != null) { return myTextColor; } else { return myNullTextColor; } } public Color getEffectiveTextColor() { if (myShowsErrorColor) { return myErrorColor; } else { return getTextColor(); } } public Color getEffectiveSelectedTextColor() { if (myShowsErrorColor) { return ERROR_COLOR; } else { return mySelectedTextColor != null ? mySelectedTextColor : getTextColor(); } } public Color getTextBackgroundColor() { init(); if (myShowsErrorColor) { return ERROR_COLOR; } else { if (!myNull) { return myTextBackground; } else { return myNullTextBackground; } } } public void setSelectedTextColor(Color selectedTextColor) { mySelectedTextColor = selectedTextColor; } public Color getSelectedTextBackgroundColor() { init(); if (!myNull) { return mySelectedTextBackground; } else { return myNulLSelectedTextBackground; } } public Font getFont() { init(); return myFont; } public boolean isSelected() { return mySelected; } public void setSelected(boolean isSelected) { mySelected = isSelected; } public boolean isShowCaret() { return myShowCaret; } public void setShowCaret(boolean showCaret) { myShowCaret = showCaret; } public void paint(Graphics g, int shiftX, int shiftY) { paint(g, shiftX, shiftY, null); } public void paint(Graphics g, int shiftX, int shiftY, Color forcedTextColor) { Color backgroundColor; Color textColor; Color textBackgroundColor; backgroundColor = getBackgroundColor(); if (forcedTextColor != null) { textColor = forcedTextColor; textBackgroundColor = null; } else { if (mySelected) { textColor = getEffectiveSelectedTextColor(); textBackgroundColor = getSelectedTextBackgroundColor(); } else { textColor = getEffectiveTextColor(); textBackgroundColor = getTextBackgroundColor(); } } if (backgroundColor != null && !g.getColor().equals(backgroundColor) && !mySelected) { g.setColor(backgroundColor); g.fillRect(shiftX + getPaddingLeft(), shiftY + getPaddingTop(), myWidth, myTextHeight); } if (textBackgroundColor != null) { g.setColor(textBackgroundColor); g.fillRect(shiftX + getPaddingLeft(), shiftY + getPaddingTop(), myWidth, myTextHeight); } g.setFont(getFont()); if (!g.getColor().equals(textColor)) { g.setColor(textColor); } int selectionStartX = shiftX + getPaddingLeft() + getSelectionStartX(); int selectionEndX = shiftX + getPaddingLeft() + getSelectionEndX(); int endLineX = shiftX + getPaddingLeft() + getTextEndX(); int baselineY = shiftY + myHeight - myDescent - getPaddingBottom() - getPaddingTop(); int centerLineY = shiftY + (myHeight - getPaddingBottom() + getPaddingTop()) / 2; if (getStartTextSelectionPosition() > 0) { g.drawString(myText.substring(0, getStartTextSelectionPosition()), shiftX + getPaddingLeft() + myFontCorrectionTextShift, baselineY); if (isUnderlined()) { g.drawLine(shiftX + getPaddingLeft(), baselineY + 1, selectionStartX, baselineY + 1); } if (isStrikeOut()) { drawStrikeOutLine(g, shiftX + getPaddingLeft(), selectionStartX, centerLineY); } } if (getEndTextSelectionPosition() <= myText.length()) { g.drawString(myText.substring(getEndTextSelectionPosition()), selectionEndX + myFontCorrectionTextShift, baselineY); if (isUnderlined()) { g.drawLine(selectionEndX, baselineY + 1, endLineX, baselineY + 1); } if (isStrikeOut()) { drawStrikeOutLine(g, selectionEndX, endLineX, centerLineY); } } if (getStartTextSelectionPosition() < getEndTextSelectionPosition()) { //drawing textual selection String selectedText = getTextuallySelectedText(); g.setColor(myTextSelectedBackgroundColor); // Filling smaller rectangle to not cover frames created by other messages if (selectionEndX - selectionStartX - 2 + myFontCorrectionRightGap > 0) { g.fillRect(selectionStartX + 1, shiftY + getPaddingTop() + 1, selectionEndX - selectionStartX - 2 + myFontCorrectionRightGap, myTextHeight - 2); } g.setColor(myTextSelectedTextColor != null ? myTextSelectedTextColor : getTextColor()); g.drawString(selectedText, selectionStartX + myFontCorrectionTextShift, baselineY); if (isUnderlined()) { g.drawLine(selectionStartX, baselineY + 1, selectionEndX, baselineY + 1); } if (isStrikeOut()) { drawStrikeOutLine(g, selectionStartX, selectionEndX, centerLineY); } g.setColor(textColor); } if (myShowCaret) { drawCaret(g, shiftX, shiftY); } } private void drawStrikeOutLine(Graphics g, int beginX, int endX, int constY) { g.drawLine(beginX, constY + 1, endX, constY + 1); } private void drawCaret(Graphics g, int shiftX, int shiftY) { if (!myCaretEnabled) { return; } int x = getCaretX(shiftX); if (getCaretPosition() != 0) { x--; } g.setColor(EditorSettings.getInstance().getCaretColor()); g.fillRect(x, shiftY, 2, myTextHeight); } public void repaintCaret(Component component, int shiftX, int shiftY) { int x = getCaretX(shiftX); component.repaint(x - 1, shiftY - 1, x + 2, myTextHeight + 2); } public int getCaretX(int shiftX) { if (myCaretX == -1) { myCaretX = getTextWidth(getCaretPosition()); } return shiftX + getPaddingLeft() + myCaretX; } private int getSelectionStartX() { if (mySelectionStartX == -1) { mySelectionStartX = getTextWidth(getStartTextSelectionPosition()); } return mySelectionStartX; } private int getSelectionEndX() { if (mySelectionEndX == -1) { mySelectionEndX = getTextWidth(getEndTextSelectionPosition()); } return mySelectionEndX; } private int getTextEndX() { if (myTextEndX == -1) { myTextEndX = getTextWidth(getText().length()); } return myTextEndX; } private int getTextWidth(int caretPosition) { FontMetrics metrics = getFontMetrics(); return metrics.charsWidth(myText.toCharArray(), 0, caretPosition); } public FontMetrics getFontMetrics() { if (myFontMetrics == null) { myFontMetrics = FontRegistry.getInstance().getFontMetrics(getFont()); } return myFontMetrics; } public String getTextuallySelectedText() { if (getStartTextSelectionPosition() > getEndTextSelectionPosition()) { return ""; } return myText.substring(getStartTextSelectionPosition(), getEndTextSelectionPosition()); } public void resetSelection() { setStartTextSelectionPosition(getCaretPosition()); setEndTextSelectionPosition(getCaretPosition()); } public boolean hasNonTrivialSelection() { return (getStartTextSelectionPosition() != getCaretPosition() || getEndTextSelectionPosition() != getCaretPosition()); } public void setStartTextSelectionPosition(int i) { assert i >= 0; this.myStartTextSelectionPosition = Math.min(i, myText.length()); mySelectionStartX = -1; } public void setEndTextSelectionPosition(int i) { assert i >= 0; this.myEndTextSelectionPosition = Math.min(i, myText.length()); mySelectionEndX = -1; } public void selectAll() { setStartTextSelectionPosition(0); setEndTextSelectionPosition(getText().length()); } public void deselectAll() { setStartTextSelectionPosition(getCaretPosition()); setEndTextSelectionPosition(getCaretPosition()); } public boolean isEverythingSelected() { return getStartTextSelectionPosition() == 0 && getEndTextSelectionPosition() == getText().length(); } public void setCaretByXCoord(int x) { setCaretPosition(getCaretPositionByXCoord(x)); } public int getCaretPositionByXCoord(int _x) { int x = _x - getPaddingLeft(); FontMetrics metrics = getFontMetrics(); char[] chars = getText().toCharArray(); int caretPosition = myText.length(); int len = 0; for (int i = 0; i < myText.length(); i++) { int newLen = metrics.charsWidth(chars, 0, i + 1); if (x <= (len + newLen + 1) / 2) { caretPosition = i; break; } len = newLen; } return caretPosition; } public int getCaretPosition() { return myCaretPosition; } public void setCaretPosition(int i) { setCaretPosition(i, false); } private void doSetCaretPosition(int position) { myCaretPosition = position; myCaretX = -1; } public void setCaretPosition(int position, boolean duringSelection) { assert position >= 0; if (!duringSelection) { doSetCaretPosition(Math.min(myText.length(), position)); setStartTextSelectionPosition(getCaretPosition()); setEndTextSelectionPosition(getCaretPosition()); return; } int old = getCaretPosition(); doSetCaretPosition(Math.min(myText.length(), position)); if (getEndTextSelectionPosition() == old) { setEndTextSelectionPosition(getCaretPosition()); } else { setStartTextSelectionPosition(getCaretPosition()); } if (getEndTextSelectionPosition() < getStartTextSelectionPosition()) { int temp = getEndTextSelectionPosition(); setEndTextSelectionPosition(getStartTextSelectionPosition()); setStartTextSelectionPosition(temp); } } public boolean isUnderlined() { init(); if (myControlOvered) { return true; } return myUnderlined; } public boolean isStrikeOut() { init(); return myStrikeOut; } public int getAscent() { return myTextHeight - myDescent; } public int getDescent() { return myDescent; } }