All Downloads are FREE. Search and download functionalities are using the official Maven repository.

javax.swing.text.DefaultHighlighterDark.DarkHighlightPainter Maven / Gradle / Ivy

There is a newer version: 3.0.2
Show newest version
/*
 * MIT License
 *
 * Copyright (c) 2020 Jannis Weis
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
 * associated documentation files (the "Software"), to deal in the Software without restriction,
 * including without limitation the rights to use, copy, modify, merge, publish, distribute,
 * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all copies or
 * substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
 * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
 * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 *
 */
package javax.swing.text.DefaultHighlighterDark;

import java.awt.*;
import java.awt.font.TextAttribute;
import java.awt.geom.Arc2D;
import java.awt.geom.Area;
import java.awt.geom.Rectangle2D;
import java.util.Objects;

import javax.swing.*;
import javax.swing.text.*;

import sun.swing.SwingUtilities2;

import com.github.weisj.darklaf.graphics.GraphicsContext;
import com.github.weisj.darklaf.graphics.GraphicsUtil;
import com.github.weisj.darklaf.graphics.PaintUtil;
import com.github.weisj.darklaf.ui.text.StyleConstantsEx;
import com.github.weisj.darklaf.util.Alignment;
import com.github.weisj.darklaf.util.ColorWrapper;

/**
 * Note this class only sits inside this weird package because of a hack used in
 * {@link SwingUtilities2#useSelectedTextColor(Highlighter.Highlight, JTextComponent)} that makes it
 * impossible for custom highlighters to use the correct text foreground specified by
 * {@link JTextComponent#getSelectedTextColor()}.
 *
 * @author Jannis Weis
 */
public class DarkHighlightPainter extends DefaultHighlighter.DefaultHighlightPainter {

    private Paint paint;
    private Color color;
    private final HighlighterColor wrapper;
    private boolean roundedEdges;
    private boolean extendLines;

    private AlphaComposite alphaComposite;
    private float alpha;
    private final int arcSize;
    private boolean enabled;

    private int repaintCount;

    private int lastSelStart = -1;
    private int lastSelEnd = -1;

    private final Shape[] arcs = new Shape[4];

    private final Point tmpPoint = new Point();

    public DarkHighlightPainter() {
        this(null);
    }

    public DarkHighlightPainter(final Paint paint) {
        this(paint, false);
    }

    public DarkHighlightPainter(final Paint paint, final boolean rounded) {
        this(paint, rounded, 1.0f);
    }

    public DarkHighlightPainter(final Paint paint, final boolean rounded, final float alpha) {
        super(null);
        wrapper = new HighlighterColor(null);
        arcSize = UIManager.getInt("Highlight.arc");
        setPaint(paint);
        setRoundedEdges(rounded);
        setAlpha(alpha);
        setEnabled(true);
    }

    public boolean getRoundedEdges() {
        return roundedEdges;
    }

    public void setRoundedEdges(final boolean rounded) {
        roundedEdges = rounded;
    }

    @Override
    public Color getColor() {
        return wrapper;
    }

    /**
     * Paints a highlight.
     *
     * @param g the graphics context
     * @param offs0 the starting model offset >= 0
     * @param offs1 the ending model offset >= offs1
     * @param bounds the bounding box for the highlight
     * @param c the editor
     */
    @Override
    public void paint(final Graphics g, final int offs0, final int offs1, final Shape bounds, final JTextComponent c) {
        if (!enabled) return;
        Graphics2D g2d = (Graphics2D) g;
        GraphicsContext context = new GraphicsContext(g2d);
        color = c.getSelectedTextColor();
        wrapper.setColor(color);
        wrapper.setCustomForeground(!Objects.equals(color, c.getForeground()));

        if (getAlpha() < 1.0f) {
            g2d.setComposite(getAlphaComposite());
        }
        super.paint(g, offs0, offs1, bounds, c);
        context.restore();
    }

    public float getAlpha() {
        return alpha;
    }

    private AlphaComposite getAlphaComposite() {
        if (alphaComposite == null) {
            alphaComposite = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha);
        }
        return alphaComposite;
    }

    public Paint getPaint(final Component c, final Shape bounds) {
        return paint;
    }

    public void setPaint(final Paint paint) {
        this.paint = paint;
    }

    public void setAlpha(final float alpha) {
        this.alpha = alpha;
        this.alpha = Math.max(alpha, 0.0f);
        this.alpha = Math.min(1.0f, alpha);
        alphaComposite = null;
    }

    @Override
    public Shape paintLayer(final Graphics g, final int offs0, final int offs1, final Shape bounds,
            final JTextComponent c, final View view) {
        if (!enabled) return bounds;
        color = (Color) view.getAttributes().getAttribute(StyleConstantsEx.SelectedForeground);
        if (color == null) {
            color = StyleConstants.getForeground(view.getAttributes());
        }
        boolean customColor = color != null;
        if (color == null) {
            color = c.getSelectedTextColor();
        }

        boolean isForeground = Objects.equals(color, c.getForeground());
        customColor = customColor || isForeground;
        wrapper.setColor(color);
        wrapper.setCustomForeground(!isForeground);

        Graphics2D g2d = (Graphics2D) g;
        GraphicsContext context = GraphicsUtil.setupAAPainting(g2d);

        if (getAlpha() < 1.0f) {
            g2d.setComposite(getAlphaComposite());
        }

        setupColor((Graphics2D) g, c, bounds);
        Shape dirtyShape;

        boolean ltr = isLeftTorRight(c);
        boolean rounded = ltr && isRounded();
        boolean lineExtension = ltr && isLineExtendingEnabled();

        if (lineExtension || rounded) {
            dirtyShape = paintRoundedLayer(g2d, c, offs0, offs1, context, rounded, lineExtension, false);

            // Swing may not recognise the painted extension. Just repaint if the selection changed to
            // guarantee
            // everything is visible.
            if (lineExtension) {
                if ((lastSelEnd != c.getSelectionEnd() || lastSelStart != c.getSelectionStart())) {
                    lastSelEnd = c.getSelectionEnd();
                    lastSelStart = c.getSelectionStart();
                    repaintCount = 1;
                }
                if (repaintCount > 0) {
                    if (customColor) {
                        c.repaint();
                    } else {
                        Rectangle repaintBounds = dirtyShape.getBounds();
                        repaintBounds.grow(10, 10);
                        c.repaint(repaintBounds);
                    }
                    repaintCount--;
                }
            }
        } else {
            Color oldColor = wrapper.getColor();
            wrapper.setColor(g.getColor());
            dirtyShape = super.paintLayer(g, offs0, offs1, bounds, c, view);
            wrapper.setColor(oldColor);
        }

        context.restore();
        return dirtyShape;
    }

    private boolean isLeftTorRight(final JTextComponent c) {
        return !TextAttribute.RUN_DIRECTION_RTL.equals(c.getDocument().getProperty(TextAttribute.RUN_DIRECTION))
                && !Boolean.TRUE.equals(c.getDocument().getProperty("i18n"));
    }

    protected Rectangle paintRoundedLayer(final Graphics2D g, final JTextComponent c, final int offs0, final int offs1,
            final GraphicsContext context, final boolean rounded, final boolean extendLines,
            final boolean isPaintingPreceding) {
        final int selectionStart = c.getSelectionStart();
        final int selectionEnd = c.getSelectionEnd();

        Insets ins = c.getInsets();

        Rectangle posOffs0 = getPosRect(c, offs0);
        Rectangle posOffs1 = getPosRect(c, offs1, Position.Bias.Backward);

        Rectangle posStart = getPosRect(c, selectionStart);
        Rectangle posEnd = getPosRect(c, selectionEnd);

        int currentLineStart = isPaintingPreceding ? offs0 : getOffset(c, 0, posOffs0.y);
        int currentLineEnd = isPaintingPreceding ? offs1 : getOffset(c, c.getWidth(), posOffs1.y);

        int previousLineEnd = currentLineStart - 1;
        Rectangle prevEnd = getPosRect(c, previousLineEnd);
        int previousLineStart = getOffset(c, 0, prevEnd.y);
        Rectangle prevStart = getPosRect(c, previousLineStart);

        int nextLineStart = currentLineEnd + 1;
        int nextLineEnd = getOffset(c, c.getWidth(), getPosRect(c, nextLineStart).y);
        if (nextLineEnd < nextLineStart) nextLineEnd = nextLineStart;

        boolean isEndOfLine = offs1 >= c.getDocument().getLength() - 1 || getPosRect(c, offs1 + 1).y > posOffs1.y;
        boolean isStartOfLine = offs0 == 0 || getPosRect(c, offs0 - 1).y < posOffs0.y;

        Rectangle layerRect = getRect(posOffs0, posOffs1);
        Rectangle previousLineRect = getRect(prevStart, prevEnd);
        Rectangle currentLineRect = getLineRect(c, currentLineStart, currentLineEnd);
        Rectangle nextLineRect = getLineRect(c, nextLineStart, nextLineEnd);

        /*
         * Adjust adjacent line rects if they contain the start/end of the selection.
         */
        if (posStart.y == previousLineRect.y) {
            previousLineRect = getRect(posStart, getPosRect(c, previousLineEnd, Position.Bias.Backward));
        }

        if (posEnd.y == nextLineRect.y) {
            nextLineRect = getRect(getPosRect(c, nextLineStart), posEnd);
        }

        /*
         * Adjust the current line rect if it contains the start/end of the selection.
         */
        if (posStart.y == currentLineRect.y) {
            currentLineRect.width = currentLineRect.x + currentLineRect.width - posStart.x;
            currentLineRect.x = posStart.x;
        }

        if (posEnd.y == currentLineRect.y) {
            currentLineRect.width = posEnd.x + posEnd.width - currentLineRect.x;
        }

        boolean hasLineAbove = previousLineEnd >= selectionStart;
        boolean hasLineBelow = nextLineStart <= selectionEnd;

        boolean previousLineVisible = hasLineAbove && previousLineRect.width > 0 && previousLineStart < previousLineEnd;
        boolean nextLineVisible = hasLineBelow && nextLineRect.width > 0 && nextLineStart < nextLineEnd;

        boolean paintPreviousLine = false;

        if (extendLines) {
            // Adjust the line rects for the adjacent lines for correct rounded corner placement.
            extendLine(c, ins, previousLineRect, hasLineAbove && previousLineStart >= selectionStart,
                    hasLineAbove && previousLineEnd <= selectionEnd);

            boolean extendNextRight = hasLineBelow && nextLineEnd < c.getDocument().getLength()
                    && getOffset(c, getPosRect(c, nextLineEnd + 1)) <= selectionEnd;

            extendLine(c, ins, nextLineRect, hasLineBelow && nextLineStart >= selectionStart, extendNextRight);

            paintPreviousLine =
                    hasLineAbove && (previousLineStart == previousLineEnd || previousLineEnd == selectionStart);
            previousLineVisible = hasLineAbove;
            if (nextLineRect.y != posEnd.y) nextLineVisible = hasLineBelow;

            boolean extendRight = isPaintingPreceding || (hasLineBelow && (endX(layerRect) == endX(currentLineRect)));
            extendRight &= offs1 < selectionEnd;
            boolean extendLeft = isPaintingPreceding || (hasLineAbove && startX(layerRect) == startX(currentLineRect));
            extendLeft &= offs0 > selectionStart;

            extendLine(c, ins, layerRect, extendLeft, extendRight);
            extendLine(c, ins, currentLineRect, extendLeft, extendRight);

            if (isPaintingPreceding) layerRect.height = currentLineRect.height;
        }

        boolean canRoundLeft = rounded && (isStartOfLine || startX(currentLineRect) == startX(layerRect));
        boolean canRoundRight = rounded && (isEndOfLine || endX(currentLineRect) == endX(layerRect));

        boolean roundedTopLeft = !previousLineVisible || leftRoundedVisible(currentLineRect, previousLineRect);
        boolean roundedTopRight = !previousLineVisible || rightRoundedVisible(currentLineRect, previousLineRect);
        boolean roundedBottomLeft = !nextLineVisible || leftRoundedVisible(currentLineRect, nextLineRect);
        boolean roundedBottomRight = !nextLineVisible || rightRoundedVisible(currentLineRect, nextLineRect);

        boolean arcTopLeft =
                previousLineVisible && !roundedTopLeft && leftArcVisible(currentLineRect, previousLineRect);
        boolean arcTopRight =
                previousLineVisible && !roundedTopRight && rightArcVisible(currentLineRect, previousLineRect);
        boolean arcBottomLeft = nextLineVisible && !roundedBottomLeft && leftArcVisible(currentLineRect, nextLineRect);
        boolean arcBottomRight =
                nextLineVisible && !roundedBottomRight && rightArcVisible(currentLineRect, nextLineRect);

        layerRect =
                paintRoundRect(g, context, layerRect, canRoundLeft && roundedTopLeft, canRoundRight && roundedTopRight,
                        canRoundLeft && roundedBottomLeft, canRoundRight && roundedBottomRight);
        Rectangle r = paintArcs(g, context, layerRect, canRoundLeft && arcTopLeft, canRoundRight && arcTopRight,
                canRoundLeft && arcBottomLeft, canRoundRight && arcBottomRight);

        if (paintPreviousLine && !isPaintingPreceding) {
            Rectangle prev = paintRoundedLayer(g, c, Math.max(previousLineStart, selectionStart),
                    Math.min(previousLineEnd, selectionEnd), context, rounded, extendLines, true);
            convexHull(r, prev);
        }
        return r;
    }

    private void extendLine(final JTextComponent c, final Insets ins, final Rectangle rect, final boolean extendLeft,
            final boolean extendRight) {
        if (extendRight || extendLeft) {
            int x = startX(rect);
            if (extendRight) {
                rect.width = c.getWidth() - ins.left - x;
            }
            if (extendLeft) {
                rect.width = rect.x + rect.width - ins.left;
                rect.x = ins.left;
            }
        }
    }

    public boolean isLineExtendingEnabled() {
        return extendLines;
    }

    public void setLineExtendingEnabled(final boolean enabled) {
        extendLines = enabled;
    }

    private boolean leftRoundedVisible(final Rectangle current, final Rectangle other) {
        return startX(other) > startX(current) + arcSize || endX(other) < startX(current);
    }

    private boolean rightRoundedVisible(final Rectangle current, final Rectangle other) {
        return endX(other) < endX(current) - arcSize || startX(other) > endX(current);
    }

    private boolean leftArcVisible(final Rectangle current, final Rectangle other) {
        return startX(other) <= startX(current) - arcSize && endX(other) >= startX(current);
    }

    private boolean rightArcVisible(final Rectangle current, final Rectangle other) {
        return endX(other) >= endX(current) + arcSize && startX(other) <= endX(current);
    }

    private Rectangle getPosRect(final JTextComponent c, final int offset) {
        return getPosRect(c, offset, Position.Bias.Forward);
    }

    private Rectangle getPosRect(final JTextComponent c, final int offset, final Position.Bias bias) {
        try {
            return c.getUI().modelToView(c, Math.max(0, Math.min(offset, c.getDocument().getLength())), bias);
        } catch (BadLocationException ignored) {
        } catch (IllegalArgumentException e) {
            new RuntimeException("" + offset).printStackTrace();
        }
        return new Rectangle(Integer.MIN_VALUE + 100, Integer.MIN_VALUE + 100, 0, 0);
    }

    private int getOffset(final JTextComponent c, final int x, final int y) {
        tmpPoint.setLocation(x, y);
        return getOffset(c, tmpPoint);
    }

    private int getOffset(final JTextComponent c, final Rectangle r) {
        return getOffset(c, r.getLocation());
    }

    private int getOffset(final JTextComponent c, final Point p) {
        return c.viewToModel(p);
    }

    private int startX(final Rectangle r) {
        return r.x;
    }

    private int endX(final Rectangle r) {
        return r.x + r.width;
    }

    private int startY(final Rectangle r) {
        return r.y;
    }

    private int endY(final Rectangle r) {
        return r.y + r.height;
    }

    private Rectangle getLineRect(final JTextComponent c, final int startPos, final int endPos) {
        Rectangle rectStart = getPosRect(c, startPos);
        Rectangle rectEnd = endPos <= startPos ? rectStart : getPosRect(c, endPos, Position.Bias.Backward);
        return getRect(rectStart, rectEnd);
    }

    private Rectangle getRect(final Rectangle start, final Rectangle end) {
        return new Rectangle(start.x, start.y, end.x + end.width - start.x, end.y + end.height - start.y);
    }

    private void convexHull(final Rectangle start, final Rectangle end) {
        int startX = Math.min(startX(start), startX(end));
        int startY = Math.min(startY(start), startY(end));

        int endX = Math.max(endX(start), endX(end));
        int endY = Math.max(endY(start), endY(end));

        start.setRect(startX, startY, endX - startX, endY - startY);
    }

    private boolean isRounded() {
        return roundedEdges;
    }

    private Rectangle paintRoundRect(final Graphics g, final GraphicsContext context, final Rectangle shape,
            final boolean topLeft, final boolean topRight, final boolean bottomLeft, final boolean bottomRight) {
        Rectangle r = new Rectangle(shape);
        int aw = Math.min(arcSize, r.width);
        int ah = Math.min(arcSize, r.height);

        /*
         * If there is a rounded arc on one side A and none on the other (B) removing the arc B could
         * overlap arc A if the width of the allocation is too small. This happens e.g. when single
         * characters have different attribute sets.
         */
        g.clipRect(r.x, r.y, r.width, r.height);
        boolean showLeft = topLeft || bottomLeft;
        boolean showRight = topRight || bottomRight;
        boolean showTop = topRight || topLeft;
        boolean showBottom = bottomLeft || bottomRight;

        if (!showLeft) {
            r.x -= aw;
            r.width += aw;
        }
        if (!showRight) {
            r.width += aw;
        }
        if (!showBottom) {
            r.height += ah;
        }
        if (!showTop) {
            r.y -= ah;
            r.height += ah;
        }

        if (!topLeft && !topRight && !bottomRight && !bottomLeft) {
            PaintUtil.fillRect(g, r);
        } else {
            /*
             * First paint the whole rounded rectangle and remove any non visible arcs.
             */
            g.fillRoundRect(r.x, r.y, r.width, r.height, aw, ah);
            if (!topLeft) g.fillRect(r.x, r.y, aw, ah);
            if (!topRight) g.fillRect(r.x + r.width - aw, r.y, aw, ah);
            if (!bottomLeft) g.fillRect(r.x, r.y + r.height - ah, aw, ah);
            if (!bottomRight) g.fillRect(r.x + r.width - aw, r.y + r.height - ah, aw, ah);
        }
        context.restoreClip();
        return shape;
    }

    private Rectangle paintArcs(final Graphics2D g, final GraphicsContext context, final Rectangle r,
            final boolean topLeft, final boolean topRight, final boolean bottomLeft, final boolean bottomRight) {
        if (r.isEmpty()) return r;
        Rectangle alloc = new Rectangle(r);
        if (topLeft || bottomLeft) {
            alloc.x -= arcSize;
            alloc.width += arcSize;
        }
        if (topRight || bottomRight) {
            alloc.width += arcSize;
        }

        if (topLeft) {
            g.translate(r.x - arcSize, r.y);
            g.fill(getArc(Alignment.NORTH_WEST));
            context.restoreTransform();
        }
        if (topRight) {
            g.translate(r.x + r.width, r.y);
            g.fill(getArc(Alignment.NORTH_EAST));
            context.restoreTransform();
        }
        if (bottomLeft) {
            g.translate(r.x - arcSize, r.y + r.height - arcSize);
            g.fill(getArc(Alignment.SOUTH_WEST));
            context.restoreTransform();
        }
        if (bottomRight) {
            g.translate(r.x + r.width, r.y + r.height - arcSize);
            g.fill(getArc(Alignment.SOUTH_EAST));
            context.restoreTransform();
        }
        return alloc;
    }

    private void setupColor(final Graphics2D g2d, final JTextComponent c, final Shape bounds) {
        Paint paint = getPaint(c, bounds);
        if (paint == null) {
            g2d.setColor(c.getSelectionColor());
        } else {
            g2d.setPaint(paint);
        }
    }

    private Shape getArc(final Alignment a) {
        Shape arc = arcs[getIndex(a)];
        if (arc == null) {
            Area arcArea = new Area(new Rectangle2D.Double(0, 0, arcSize, arcSize));
            arcArea.subtract(new Area(getSubtractShape(a)));
            arc = arcArea;
            arcs[getIndex(a)] = arc;
        }
        return arc;
    }

    private Shape getSubtractShape(final Alignment a) {
        switch (a) {
            case NORTH_EAST:
                return new Arc2D.Double(0, 0, 2 * arcSize, 2 * arcSize, 90, 90, Arc2D.PIE);
            case NORTH_WEST:
                return new Arc2D.Double(-arcSize, 0, 2 * arcSize, 2 * arcSize, 0, 90, Arc2D.PIE);
            case SOUTH_EAST:
                return new Arc2D.Double(0, -arcSize, 2 * arcSize, 2 * arcSize, 180, 90, Arc2D.PIE);
            case SOUTH_WEST:
                return new Arc2D.Double(-arcSize, -arcSize, 2 * arcSize, 2 * arcSize, 270, 90, Arc2D.PIE);
        }
        return new Rectangle();
    }

    private int getIndex(final Alignment a) {
        switch (a) {
            case NORTH_EAST:
                return 0;
            case NORTH_WEST:
                return 1;
            case SOUTH_EAST:
                return 2;
            case SOUTH_WEST:
                return 3;
        }
        return 0;
    }

    public boolean isEnabled() {
        return enabled;
    }

    public void setEnabled(final boolean enabled) {
        wrapper.setEnabled(enabled);
        this.enabled = enabled;
    }

    private static class HighlighterColor extends ColorWrapper {

        private boolean enabled;
        private boolean customForeground;

        public void setEnabled(final boolean enabled) {
            this.enabled = enabled;
        }

        public void setCustomForeground(final boolean customForeground) {
            this.customForeground = customForeground;
        }

        public HighlighterColor(final Color color) {
            super(color);
        }

        @Override
        @SuppressWarnings("EqualsWhichDoesntCheckParameterClass")
        public boolean equals(final Object obj) {
            return obj != null && enabled && !customForeground;
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy