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 com.github.weisj.darklaf.color.ColorWrapper;
import com.github.weisj.darklaf.ui.text.StyleConstantsEx;
import com.github.weisj.darklaf.util.GraphicsContext;
import com.github.weisj.darklaf.util.GraphicsUtil;
import sun.swing.SwingUtilities2;

import javax.swing.*;
import javax.swing.plaf.TextUI;
import javax.swing.text.BadLocationException;
import javax.swing.text.DefaultHighlighter;
import javax.swing.text.Highlighter;
import javax.swing.text.JTextComponent;
import javax.swing.text.Position;
import javax.swing.text.View;
import java.awt.*;
import java.awt.geom.Arc2D;
import java.awt.geom.Area;
import java.awt.geom.Rectangle2D;

/**
 * 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 static final boolean DEBUG_COLOR = false;
    private Paint paint;
    private Color color;
    private ColorWrapper wrapper;
    private boolean roundedEdges;
    private AlphaComposite alphaComposite;
    private float alpha;
    private int selectionStart = -1;
    private int selectionEnd = -1;
    private int repaintCount = 0;
    private int arcSize;
    private boolean suppressRounded = false;


    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);
        setPaint(paint);
        setRoundedEdges(rounded);
        setAlpha(alpha);
        arcSize = UIManager.getInt("Highlight.arc");
        wrapper = new ColorWrapper(color) {
            @Override
            public boolean equals(final Object obj) {
                return obj != null;
            }
        };
    }

    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) {
        Rectangle alloc = bounds.getBounds();
        Graphics2D g2d = (Graphics2D) g;
        GraphicsContext context = new GraphicsContext(g2d);
        color = c.getSelectedTextColor();
        wrapper.setColor(color);

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

        try {
            TextUI mapper = c.getUI();
            Rectangle p0 = mapper.modelToView(c, offs0, Position.Bias.Forward);
            Rectangle p1 = mapper.modelToView(c, offs1, Position.Bias.Forward);
            setupColor(g2d, c);

            if (p0.y == p1.y) {
                // Entire highlight is on one line.
                p1.width = 0;
                Rectangle r = p0.union(p1);
                g2d.fillRect(r.x, r.y, r.width, r.height);
            } else {
                // Highlight spans lines.
                int p0ToMarginWidth = alloc.x + alloc.width - p0.x;
                g2d.fillRect(p0.x, p0.y, p0ToMarginWidth, p0.height);
                if ((p0.y + p0.height) != p1.y) {
                    g2d.fillRect(alloc.x, p0.y + p0.height, alloc.width,
                                 p1.y - (p0.y + p0.height));
                }
                g2d.fillRect(alloc.x, p1.y, (p1.x - alloc.x), p1.height);
            }

        } catch (BadLocationException ignored) {
        } finally {
            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() {
        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) {
        color = (Color) view.getAttributes().getAttribute(StyleConstantsEx.SelectedForeground);
        if (color == null) {
            color = c.getSelectedTextColor();
        }
        wrapper.setColor(color);
        Shape dirtyShape = null;
        Graphics2D g2d = (Graphics2D) g;
        GraphicsContext context = GraphicsUtil.setupAAPainting(g2d);
        Insets ins = c.getInsets();
        g2d.setClip(ins.left, ins.top, c.getWidth() - ins.left - ins.right,
                    c.getHeight() - ins.top - ins.bottom);
        if (getAlpha() < 1.0f) {
            g2d.setComposite(getAlphaComposite());
        }
        try {
            dirtyShape = paintLayerImpl(g2d, offs0, offs1, c);
        } catch (BadLocationException ignored) {
        } finally {
            context.restore();
        }
        /*
         * To make sure the the right part of the highlight is actually painted we manually repaint
         * after the selection has changed.
         */
        if (dirtyShape != null && (selectionEnd != c.getSelectionEnd()
                || selectionStart != c.getSelectionStart()
                || repaintCount < 2)) {
            selectionStart = c.getSelectionStart();
            selectionEnd = c.getSelectionEnd();
            c.repaint(dirtyShape.getBounds());
            /*
             * Sometimes one repaint process isn't enough to fully update the selection painting if in
             * Right to Left mode. This forces a second repaint.
             */
            if (!c.getComponentOrientation().isLeftToRight()) {
                repaintCount = repaintCount >= 1 ? 0 : 1;
            } else {
                repaintCount = 2;
            }
        }
        context.restore();
        return dirtyShape;
    }

    protected Shape paintLayerImpl(final Graphics2D g2d, final int offs0, final int offs1,
                                   final JTextComponent c) throws BadLocationException {
        Shape dirtyShape;
        Rectangle posStart = c.modelToView(c.getSelectionStart());
        Rectangle posStartPrev = c.modelToView(Math.max(0, c.getSelectionStart() - 1));
        Rectangle posEnd = c.modelToView(c.getSelectionEnd());
        Rectangle posEndPrev = c.modelToView(Math.max(0, c.getSelectionEnd() - 1));
        Rectangle posOffs0 = c.modelToView(offs0);
        Rectangle posOffs0Prev = c.modelToView(Math.max(0, offs0 - 1)).getBounds();
        Rectangle posOffs1 = c.getUI().modelToView(c, offs1, Position.Bias.Backward);
        Rectangle posOffs1Forward = c.modelToView(offs1);
        Rectangle posOffs1Next = c.modelToView(Math.min(c.getDocument().getLength(), offs1 - 1));
        boolean selectionStart = c.getSelectionStart() >= offs0;
        boolean selectionEnd = c.getSelectionEnd() <= offs1;

        Insets margin = c.getInsets();

        boolean firstLineNotPainted = posStartPrev.y + posStartPrev.height == posStart.y;
        boolean lastLineNotPainted = posOffs1Next.y == posEnd.y && posOffs1.y == posOffs1Forward.y;
        boolean isToEndOfLine = posOffs1.y < posEnd.y && !lastLineNotPainted;
        boolean isToStartOfLine = !selectionEnd && posOffs0.y > posStart.y && (posOffs0.y != posOffs0Prev.y);

        Rectangle alloc;
        if (offs0 == offs1 && posEnd.y != posStart.y) {
            alloc = new Rectangle(margin.left, posOffs0.y,
                                  c.getWidth() - margin.left - margin.right, posOffs0.height);
        } else {
            alloc = new Rectangle(posOffs0.x, posOffs0.y, posOffs1.x + posOffs1.width - posOffs0.x,
                                  posOffs1.y + posOffs1.height - posOffs0.y);
        }

        boolean isFirstLine = alloc.y == posStart.y;
        boolean isSecondLine = posStart.y + posStart.height == alloc.y;
        boolean isSecondLastLine = alloc.y + alloc.height == posEnd.y;
        boolean isLastLine = alloc.y == posEnd.y || isSecondLastLine && posEnd.y != posEndPrev.y;
        boolean endBeforeStart = posEnd.x < (posStart.x + arcSize / 2.0)
                && (posEnd.y == posStart.y + posStart.height
                || (posEnd.y <= posStart.y + 2 * posStart.height && posEnd.x == margin.left));

        int originalWidth = alloc.width;
        int originalX = alloc.x;
        alloc.width = Math.max(2 * arcSize, alloc.width);
        alloc.x = Math.max(margin.left, Math.min(c.getWidth() - margin.right - alloc.width, alloc.x));

        setupColor(g2d, c);

        if (offs0 == offs1 && posEnd.y != posStart.y) {
            if (DEBUG_COLOR) g2d.setColor(Color.YELLOW.darker());
            isToEndOfLine = false;
            isToStartOfLine = false;
            dirtyShape = paintMiddleSelection(g2d, alloc, c,
                                              false, false,
                                              isFirstLine, isLastLine, isSecondLastLine, isSecondLine,
                                              firstLineNotPainted, lastLineNotPainted);
        } else if (!selectionStart && !selectionEnd) {
            if (DEBUG_COLOR) g2d.setColor(Color.ORANGE);
            dirtyShape = paintMiddleSelection(g2d, alloc, c,
                                              isToEndOfLine, isToStartOfLine, isFirstLine, isLastLine,
                                              isSecondLastLine, isSecondLine, firstLineNotPainted, lastLineNotPainted);
        } else {
            // Should only render part of View.
            //Start/End parts of selection
            if (posEnd.y == posStart.y) {
                Rectangle rect = alloc;
                if (originalWidth < 2 * arcSize && isRounded(c)) {
                    suppressRounded = true;
                    rect = new Rectangle(originalX, alloc.y, originalWidth, alloc.height);
                }
                paintSelection(g2d, c, rect, selectionStart, selectionEnd);
                dirtyShape = alloc;
                suppressRounded = false;
            } else if (selectionStart) {
                dirtyShape = paintSelectionStart(g2d, alloc, c, posStart, posOffs0, endBeforeStart, isSecondLastLine,
                                                 isToEndOfLine);
            } else {
                dirtyShape = paintSelectionEnd(g2d, alloc, c, posStart,
                                               isFirstLine, isSecondLine, isToStartOfLine, isToEndOfLine,
                                               endBeforeStart);
            }
        }
        dirtyShape = paintExtension(g2d, c,
                                    isToEndOfLine, isToStartOfLine,
                                    isFirstLine, isLastLine,
                                    isSecondLine, isSecondLastLine,
                                    selectionStart, selectionEnd,
                                    posStart, posEnd, dirtyShape.getBounds());
        return dirtyShape;
    }

    private Shape paintMiddleSelection(final Graphics2D g, final Rectangle r, final JTextComponent c,
                                       final boolean toEndOfLine, final boolean toStartOfLine,
                                       final boolean isFirstLine, final boolean isLastLine,
                                       final boolean isSecondLastLine, final boolean isSecondLine,
                                       final boolean firstLineNotPainted, final boolean lastLineNotPainted) {
        if (toStartOfLine) {
            r.width -= arcSize;
            r.x += arcSize;
        }
        if (toEndOfLine) {
            r.width -= arcSize;
        }
        if (!isRounded(c)) {
            g.fillRect(r.x, r.y, r.width, r.height);
        } else {
            boolean firstVisual = isFirstLine || (isSecondLine && firstLineNotPainted);
            boolean lastVisual = isLastLine || (isSecondLine && lastLineNotPainted);
            paintRoundRect(g, r, arcSize, firstVisual, firstVisual, isLastLine, lastVisual);
        }
        return r;
    }

    private Shape paintSelectionStart(final Graphics2D g2d, final Rectangle r,
                                      final JTextComponent c,
                                      final Rectangle posStart,
                                      final Rectangle posOffs0,
                                      final boolean endBeforeStart, final boolean isSecondLastLine,
                                      final boolean extendToEnd) {
        if (DEBUG_COLOR) g2d.setColor(Color.RED);
        Insets margin = c.getInsets();
        boolean rounded = isRounded(c);
        if (rounded && extendToEnd) r.width -= arcSize;
        if (rounded) {
            boolean roundLeftBottom = endBeforeStart && isSecondLastLine;
            if (r.width < 2 * arcSize) r.width = 2 * arcSize;
            paintRoundRect(g2d, r, arcSize, true, false, roundLeftBottom, false);
            boolean drawCorner = posOffs0.equals(posStart) && !roundLeftBottom && r.x >= margin.left + arcSize;
            if (drawCorner) {
                paintStartArc(g2d, r);
                r.x -= arcSize;
                r.width += arcSize;
            }
        } else {
            g2d.fillRect(r.x, r.y, r.width, r.height);
        }
        return r;
    }

    /*
     * Selection is contained to one line.
     */


    private Shape paintSelection(final Graphics2D g2d, final JTextComponent c, final Rectangle r,
                                 final boolean selectionStart, final boolean selectionEnd) {
        if (DEBUG_COLOR) g2d.setColor(Color.BLUE);
        if (isRounded(c)) {
            paintRoundedLeftRight(g2d, selectionStart, selectionEnd, r);
        } else {
            g2d.fillRect(r.x, r.y, r.width, r.height);
        }
        return r;
    }

    private Shape paintSelectionEnd(final Graphics2D g2d, final Rectangle r,
                                    final JTextComponent c, final Rectangle posStart,
                                    final boolean isFirstLine, final boolean isSecondLine,
                                    final boolean extendToStart, final boolean extendToEnd,
                                    final boolean endBeforeStart) {
        if (DEBUG_COLOR) g2d.setColor(Color.GREEN);
        boolean rounded = isRounded(c);
        Insets margin = c.getInsets();
        if (r.x + r.width >= c.getWidth() - margin.right - arcSize / 2.0) {
            int end = c.getWidth() - margin.right;
            r.width = end - r.x;
            if (rounded && extendToEnd) r.width -= arcSize;
        }
        if (rounded) {
            boolean roundRightTop = endBeforeStart && !extendToEnd;
            boolean roundLeftBottom = !isFirstLine && !extendToStart;
            boolean roundLeftTop = isSecondLine && !extendToStart && posStart.x >= r.x + arcSize;
            paintRoundRect(g2d, r, arcSize, roundLeftTop, roundRightTop, roundLeftBottom, !extendToEnd);
            boolean drawCorner = !extendToEnd && !roundRightTop && r.x + r.width <= c.getWidth() - margin.right - arcSize;
            if (drawCorner) {
                paintEndArc(g2d, r);
                r.width += arcSize;
            }
        } else {
            g2d.fillRect(r.x, r.y, r.width, r.height);
        }
        return r;
    }

    private boolean isRounded(final JTextComponent c) {
        return !suppressRounded
                && (roundedEdges || Boolean.TRUE.equals(c.getClientProperty("JTextComponent.roundedSelection")));
    }

    private Shape paintExtension(final Graphics2D g2d, final JTextComponent c,
                                 final boolean isToEndOfLine, final boolean isToStartOfLine,
                                 final boolean isFirstLine, final boolean isLastLine,
                                 final boolean isSecondLine, final boolean isSecondLastLine,
                                 final boolean selectionStart, final boolean selectionEnd,
                                 final Rectangle posStart, final Rectangle posEnd,
                                 final Rectangle r) {
        Insets ins = c.getInsets();
        boolean rounded = isRounded(c);
        if (isToEndOfLine) {
            if (DEBUG_COLOR) g2d.setColor(Color.CYAN);
            int start = r.x + r.width;
            int w = c.getWidth() - start - ins.right;
            w = Math.max(2 * arcSize, w);
            start = Math.min(start, c.getWidth() - ins.right - w);
            if (rounded) {
                boolean roundTop = isFirstLine || selectionStart;
                boolean roundBottom = isLastLine || (isSecondLastLine && posEnd.x + posEnd.width <= start + w - arcSize);
                boolean roundLeftTop = isFirstLine && start == ins.left;
                paintRoundRect(g2d, new Rectangle(start, r.y, w, r.height), arcSize,
                               roundLeftTop, roundTop, false, roundBottom);
            } else {
                g2d.fillRect(start, r.y, w, r.height);
            }
            r.x = Math.min(r.x, start);
            r.width += w;
        }
        if (isToStartOfLine) {
            if (DEBUG_COLOR) g2d.setColor(Color.CYAN.darker());
            int start = ins.left;
            int end = r.x;
            int w = end - start;
            w = Math.max(2 * arcSize, w);
            end = Math.max(end, start + w);
            if (rounded) {
                boolean roundTop = isFirstLine || (isSecondLine && posStart.x >= start + arcSize);
                boolean roundBottom = isLastLine || selectionEnd;
                boolean roundRightBottom = isLastLine && end == c.getWidth() - ins.right;
                paintRoundRect(g2d, new Rectangle(start, r.y, w, r.height), arcSize,
                               roundTop, false, roundBottom, roundRightBottom);
            } else {
                g2d.fillRect(start, r.y, w, r.height);
            }
            r.width += w;
            r.x = start;
        }
        return r;
    }

    private void paintRoundRect(final Graphics g, final Rectangle r, final int arcSize,
                                final boolean leftTop, final boolean rightTop,
                                final boolean leftBottom, final boolean rightBottom) {
        int aw = Math.min(arcSize, r.width);
        int ah = Math.min(arcSize, r.height);
        g.fillRoundRect(r.x, r.y, r.width, r.height, aw, ah);
        if (!leftTop) g.fillRect(r.x, r.y, aw, ah);
        if (!leftBottom) g.fillRect(r.x, r.y + r.height - ah, aw, ah);
        if (!rightTop) g.fillRect(r.x + r.width - aw, r.y, aw, ah);
        if (!rightBottom) g.fillRect(r.x + r.width - aw, r.y + r.height - ah, aw, ah);
    }

    private void paintStartArc(final Graphics2D g2d, final Rectangle r) {
        if (DEBUG_COLOR) g2d.setColor(Color.PINK);
        Area arc = new Area(new Rectangle2D.Double(
                r.x - arcSize + 0.25, r.y + r.height - arcSize + 0.25, arcSize, arcSize));
        arc.subtract(new Area(new Arc2D.Double(
                r.x - 2 * arcSize + 0.25,
                r.y + r.height - 2 * arcSize + 0.25, 2 * arcSize, 2 * arcSize,
                0, -90, Arc2D.Double.PIE)));
        g2d.fill(arc);
        r.x -= arcSize;
        r.width += arcSize;
    }

    private void paintRoundedLeftRight(final Graphics g, final boolean left, final boolean right, final Rectangle r) {
        if (right || left) {
            g.fillRoundRect(r.x, r.y, r.width, r.height, arcSize, arcSize);
            if (DEBUG_COLOR) g.setColor(Color.PINK);
            if (!left) {
                g.fillRect(r.x, r.y, arcSize, r.height);
            }
            if (!right) {
                g.fillRect(r.x + r.width - arcSize, r.y, arcSize, r.height);
            }
        } else {
            g.fillRect(r.x, r.y, r.width, r.height);
        }
    }

    private void paintEndArc(final Graphics2D g2d, final Rectangle r) {
        if (DEBUG_COLOR) g2d.setColor(Color.PINK);
        Area arc = new Area(new Rectangle2D.Double(
                r.x + r.width - 0.25, r.y - 0.25, arcSize, arcSize));
        arc.subtract(new Area(new Arc2D.Double(
                r.x + r.width - 0.25,
                r.y - 0.25, 2 * arcSize, 2 * arcSize, 90, 90, Arc2D.Double.PIE)));
        g2d.fill(arc);
    }

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




© 2015 - 2024 Weber Informatics LLC | Privacy Policy