From a55eeb73e20cd76d77c70d0a91657fd475974f46 Mon Sep 17 00:00:00 2001 From: weisj Date: Sat, 26 Oct 2019 14:59:01 +0200 Subject: [PATCH] Made some fields available to subclasses of NumberedTextComponent. Fixed background painting for ScrollBar overlay. Changed selection painting to accommodate styled documents. Added general index listener to NumberingPane. Signed-off-by: weisj --- .../components/text/IndexListener.java | 32 ++ .../components/text/LineHighlighter.java | 3 +- .../text/NumberedTextComponent.java | 11 +- .../components/text/NumberingPane.java | 20 + .../ui/numberingpane/DarkNumberingPaneUI.java | 4 + .../ui/scrollpane/DarkScrollBarUI.java | 20 +- .../weisj/darklaf/ui/text/DarkCaret.java | 11 +- .../darklaf/ui/text/DarkHighlighter.java | 29 ++ .../darklaf/ui/text/DarkTextFieldUI.java | 2 +- .../weisj/darklaf/ui/text/DarkTextUI.java | 6 +- .../DarkHighlightPainter.java | 375 ++++++++---------- src/test/java/ScrollPaneDemo.java | 8 +- 12 files changed, 289 insertions(+), 232 deletions(-) create mode 100644 src/main/java/com/github/weisj/darklaf/components/text/IndexListener.java create mode 100644 src/main/java/com/github/weisj/darklaf/ui/text/DarkHighlighter.java diff --git a/src/main/java/com/github/weisj/darklaf/components/text/IndexListener.java b/src/main/java/com/github/weisj/darklaf/components/text/IndexListener.java new file mode 100644 index 00000000..9d694bee --- /dev/null +++ b/src/main/java/com/github/weisj/darklaf/components/text/IndexListener.java @@ -0,0 +1,32 @@ +/* + * MIT License + * + * Copyright (c) 2019 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 com.github.weisj.darklaf.components.text; + +import java.awt.event.MouseEvent; +import java.util.EventListener; + +public interface IndexListener extends EventListener { + + void indexClicked(final int index, final int offset, final MouseEvent e); +} diff --git a/src/main/java/com/github/weisj/darklaf/components/text/LineHighlighter.java b/src/main/java/com/github/weisj/darklaf/components/text/LineHighlighter.java index 6d416037..94fd8e68 100644 --- a/src/main/java/com/github/weisj/darklaf/components/text/LineHighlighter.java +++ b/src/main/java/com/github/weisj/darklaf/components/text/LineHighlighter.java @@ -46,6 +46,7 @@ public class LineHighlighter implements Highlighter.HighlightPainter, ChangeList */ public LineHighlighter(final JTextComponent component, final Color color) { this.component = component; + lastView = new Rectangle(0, 0, 0, 0); setColor(color); } @@ -93,7 +94,7 @@ public class LineHighlighter implements Highlighter.HighlightPainter, ChangeList Rectangle currentView = component.modelToView2D(offset).getBounds(); // Remove the highlighting from the previously highlighted line - if (lastView.y != currentView.y) { + if (lastView != null && lastView.y != currentView.y) { component.repaint(0, lastView.y, component.getWidth(), lastView.height); lastView = currentView; } diff --git a/src/main/java/com/github/weisj/darklaf/components/text/NumberedTextComponent.java b/src/main/java/com/github/weisj/darklaf/components/text/NumberedTextComponent.java index 328b70b0..b506282d 100644 --- a/src/main/java/com/github/weisj/darklaf/components/text/NumberedTextComponent.java +++ b/src/main/java/com/github/weisj/darklaf/components/text/NumberedTextComponent.java @@ -31,11 +31,14 @@ import java.awt.*; public class NumberedTextComponent extends JPanel { - private NumberingPane numberingPane; + protected final OverlayScrollPane overlayScrollPane; + protected final NumberingPane numberingPane; + protected final JTextComponent textComponent; public NumberedTextComponent(final JTextComponent textComponent) { super(new BorderLayout()); - OverlayScrollPane overlayScrollPane = new OverlayScrollPane(textComponent); + this.textComponent = textComponent; + overlayScrollPane = new OverlayScrollPane(textComponent); numberingPane = new NumberingPane(); numberingPane.setTextComponent(textComponent); overlayScrollPane.getVerticalScrollBar().setBlockIncrement(textComponent.getFont().getSize()); @@ -46,4 +49,8 @@ public class NumberedTextComponent extends JPanel { public NumberingPane getNumberingPane() { return numberingPane; } + + public JTextComponent getTextComponent() { + return textComponent; + } } diff --git a/src/main/java/com/github/weisj/darklaf/components/text/NumberingPane.java b/src/main/java/com/github/weisj/darklaf/components/text/NumberingPane.java index 6b15fc1c..a77dc23b 100644 --- a/src/main/java/com/github/weisj/darklaf/components/text/NumberingPane.java +++ b/src/main/java/com/github/weisj/darklaf/components/text/NumberingPane.java @@ -157,12 +157,32 @@ public class NumberingPane extends JComponent { return pos; } + public void addIndexListener(final IndexListener listener) { + listenerList.add(IndexListener.class, listener); + } + + public void removeIndexListener(final IndexListener listener) { + listenerList.remove(IndexListener.class, listener); + } + public Collection getIcons() { return iconMap.values(); } + public Icon getIcon(final Position position) { + return iconMap.get(position); + } + public void removeIconAt(final Position position) { var icon = iconMap.remove(position); firePropertyChange("icons", icon, icon); } + + public List getIconListeners() { + return listenerMap.values().stream().flatMap(List::stream).collect(Collectors.toList()); + } + + public IndexListener[] getIndexListeners() { + return listenerList.getListeners(IndexListener.class); + } } diff --git a/src/main/java/com/github/weisj/darklaf/ui/numberingpane/DarkNumberingPaneUI.java b/src/main/java/com/github/weisj/darklaf/ui/numberingpane/DarkNumberingPaneUI.java index e5133ef3..5e5c1c17 100644 --- a/src/main/java/com/github/weisj/darklaf/ui/numberingpane/DarkNumberingPaneUI.java +++ b/src/main/java/com/github/weisj/darklaf/ui/numberingpane/DarkNumberingPaneUI.java @@ -266,6 +266,10 @@ public class DarkNumberingPaneUI extends ComponentUI { } } catch (BadLocationException ignored) { } } + var list = numberingPane.getIndexListeners(); + for (var listener : list) { + listener.indexClicked(start, offset, e); + } } } diff --git a/src/main/java/com/github/weisj/darklaf/ui/scrollpane/DarkScrollBarUI.java b/src/main/java/com/github/weisj/darklaf/ui/scrollpane/DarkScrollBarUI.java index 6d69998b..c68daa3f 100644 --- a/src/main/java/com/github/weisj/darklaf/ui/scrollpane/DarkScrollBarUI.java +++ b/src/main/java/com/github/weisj/darklaf/ui/scrollpane/DarkScrollBarUI.java @@ -440,10 +440,6 @@ public class DarkScrollBarUI extends BasicScrollBarUI { if (c.isOpaque()) { g.setColor(scrollbar.getBackground()); g.fillRect(bounds.x, bounds.y, bounds.width, bounds.height); - } else { - if (c.getClientProperty("scrollBar.updateAction") != null) { - ((Runnable) c.getClientProperty("scrollBar.updateAction")).run(); - } } Graphics2D g2 = (Graphics2D) g.create(); g2.setColor(getTrackColor()); @@ -530,7 +526,7 @@ public class DarkScrollBarUI extends BasicScrollBarUI { trackAlpha *= (float) (1 - (double) frame / totalFrames); } if (scrollbar != null) { - scrollbar.repaint(); + scrollbar.getParent().repaint(); } } @@ -538,7 +534,7 @@ public class DarkScrollBarUI extends BasicScrollBarUI { protected void paintCycleEnd() { trackAlpha = 0; if (scrollbar != null) { - scrollbar.repaint(); + scrollbar.getParent().repaint(); } } }; @@ -556,7 +552,7 @@ public class DarkScrollBarUI extends BasicScrollBarUI { thumbAlpha *= (float) (1 - (double) frame / totalFrames); } if (scrollbar != null) { - scrollbar.repaint(getThumbBounds()); + scrollbar.getParent().repaint(); } } @@ -564,7 +560,7 @@ public class DarkScrollBarUI extends BasicScrollBarUI { protected void paintCycleEnd() { thumbAlpha = 0; if (scrollbar != null) { - scrollbar.repaint(); + scrollbar.getParent().repaint(); } } }; @@ -578,7 +574,7 @@ public class DarkScrollBarUI extends BasicScrollBarUI { public void paintNow(final int frame, final int totalFrames, final int cycle) { trackAlpha = ((float) frame * MAX_TRACK_ALPHA) / totalFrames; if (scrollbar != null) { - scrollbar.repaint(); + scrollbar.getParent().repaint(); } } @@ -586,7 +582,7 @@ public class DarkScrollBarUI extends BasicScrollBarUI { protected void paintCycleEnd() { trackAlpha = MAX_TRACK_ALPHA; if (scrollbar != null) { - scrollbar.repaint(); + scrollbar.getParent().repaint(); } } }; @@ -601,7 +597,7 @@ public class DarkScrollBarUI extends BasicScrollBarUI { public void paintNow(final int frame, final int totalFrames, final int cycle) { thumbAlpha = ((float) frame * MAX_THUMB_ALPHA) / totalFrames; if (scrollbar != null) { - scrollbar.repaint(getThumbBounds()); + scrollbar.getParent().repaint(); } } @@ -609,7 +605,7 @@ public class DarkScrollBarUI extends BasicScrollBarUI { protected void paintCycleEnd() { thumbAlpha = MAX_THUMB_ALPHA; if (scrollbar != null) { - scrollbar.repaint(); + scrollbar.getParent().repaint(); } var p = MouseInfo.getPointerInfo().getLocation(); SwingUtilities.convertPointFromScreen(p, scrollbar); diff --git a/src/main/java/com/github/weisj/darklaf/ui/text/DarkCaret.java b/src/main/java/com/github/weisj/darklaf/ui/text/DarkCaret.java index 03a7b920..7328cc73 100644 --- a/src/main/java/com/github/weisj/darklaf/ui/text/DarkCaret.java +++ b/src/main/java/com/github/weisj/darklaf/ui/text/DarkCaret.java @@ -261,8 +261,7 @@ public class DarkCaret extends DefaultCaret implements UIResource { } validateWidth(r); - if (width > 0 && height > 0 && - !contains(r.x, r.y, r.width, r.height)) { + if (width > 0 && height > 0 && !contains(r.x, r.y, r.width, r.height)) { Rectangle clip = g.getClipBounds(); if (clip != null && !clip.contains(this)) { // Clip doesn't contain the old location, force it @@ -295,15 +294,13 @@ public class DarkCaret extends DefaultCaret implements UIResource { } g.setXORMode(textAreaBg); int y = r.y + r.height; - g.drawLine(r.x, y, r.x + r.width - 1, y); + g.fillRect(r.x, y - 1, r.width, 1); break; case THICK_VERTICAL_LINE_STYLE: - g.drawLine(r.x, r.y, r.x, r.y + r.height); - r.x++; - g.drawLine(r.x, r.y, r.x, r.y + r.height); + g.fillRect(r.x, r.y, 2, r.height); break; case VERTICAL_LINE_STYLE: - g.drawLine(r.x, r.y, r.x, r.y + r.height); + g.fillRect(r.x, r.y, 1, r.height); break; } } diff --git a/src/main/java/com/github/weisj/darklaf/ui/text/DarkHighlighter.java b/src/main/java/com/github/weisj/darklaf/ui/text/DarkHighlighter.java new file mode 100644 index 00000000..ecbf16cd --- /dev/null +++ b/src/main/java/com/github/weisj/darklaf/ui/text/DarkHighlighter.java @@ -0,0 +1,29 @@ +/* + * MIT License + * + * Copyright (c) 2019 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 com.github.weisj.darklaf.ui.text; + +import javax.swing.plaf.basic.BasicTextUI; + +public class DarkHighlighter extends BasicTextUI.BasicHighlighter { +} diff --git a/src/main/java/com/github/weisj/darklaf/ui/text/DarkTextFieldUI.java b/src/main/java/com/github/weisj/darklaf/ui/text/DarkTextFieldUI.java index 264a89ca..725b1a50 100644 --- a/src/main/java/com/github/weisj/darklaf/ui/text/DarkTextFieldUI.java +++ b/src/main/java/com/github/weisj/darklaf/ui/text/DarkTextFieldUI.java @@ -256,7 +256,7 @@ public class DarkTextFieldUI extends DarkTextFieldUIBridge implements PropertyCh @Override protected DarkCaret.CaretStyle getDefaultCaretStyle() { - return DarkCaret.CaretStyle.THICK_VERTICAL_LINE_STYLE; + return DarkCaret.CaretStyle.VERTICAL_LINE_STYLE; } @Override diff --git a/src/main/java/com/github/weisj/darklaf/ui/text/DarkTextUI.java b/src/main/java/com/github/weisj/darklaf/ui/text/DarkTextUI.java index 4ce2d6b9..b4342f56 100644 --- a/src/main/java/com/github/weisj/darklaf/ui/text/DarkTextUI.java +++ b/src/main/java/com/github/weisj/darklaf/ui/text/DarkTextUI.java @@ -36,6 +36,7 @@ import javax.swing.plaf.basic.BasicTextUI; import javax.swing.text.Caret; import javax.swing.text.DefaultEditorKit; import javax.swing.text.EditorKit; +import javax.swing.text.Highlighter; import javax.swing.text.JTextComponent; import javax.swing.text.TextAction; import java.awt.*; @@ -106,7 +107,10 @@ public abstract class DarkTextUI extends BasicTextUI { super.installUI(c); } - + @Override + protected Highlighter createHighlighter() { + return new DarkHighlighter(); + } /* * Implementation of BasicTextUI. diff --git a/src/main/java/javax/swing/text/DefaultHighlighterDark/DarkHighlightPainter.java b/src/main/java/javax/swing/text/DefaultHighlighterDark/DarkHighlightPainter.java index 7860eb49..4927e895 100644 --- a/src/main/java/javax/swing/text/DefaultHighlighterDark/DarkHighlightPainter.java +++ b/src/main/java/javax/swing/text/DefaultHighlighterDark/DarkHighlightPainter.java @@ -37,7 +37,6 @@ import javax.swing.text.Highlighter; import javax.swing.text.JTextComponent; import javax.swing.text.Position; import javax.swing.text.View; -import javax.swing.text.html.InlineView; import java.awt.*; import java.awt.geom.Arc2D; import java.awt.geom.Area; @@ -52,7 +51,7 @@ import java.awt.geom.Rectangle2D; */ public class DarkHighlightPainter extends DefaultHighlighter.DefaultHighlightPainter { - private static final boolean DEBUG_COLOR = false; + private static final boolean DEBUG_COLOR = true; private Paint paint; private boolean roundedEdges; private AlphaComposite alphaComposite; @@ -184,11 +183,7 @@ public class DarkHighlightPainter extends DefaultHighlighter.DefaultHighlightPai g2d.setComposite(getAlphaComposite()); } try { - if (view instanceof InlineView) { - dirtyShape = paintLayerImplInline(g2d, offs0, offs1, bounds, c, view); - } else { - dirtyShape = paintLayerImpl(g2d, offs0, offs1, bounds, c, view); - } + dirtyShape = paintLayerImpl(g2d, offs0, offs1, c); } catch (BadLocationException ignored) { } finally { context.restore(); @@ -217,58 +212,43 @@ public class DarkHighlightPainter extends DefaultHighlighter.DefaultHighlightPai } protected Shape paintLayerImpl(final Graphics2D g2d, final int offs0, final int offs1, - final Shape bounds, @NotNull final JTextComponent c, - final View view) throws BadLocationException { + @NotNull final JTextComponent c) throws BadLocationException { Shape dirtyShape; Rectangle posStart = c.modelToView2D(c.getSelectionStart()).getBounds(); Rectangle posEnd = c.modelToView2D(c.getSelectionEnd()).getBounds(); + Rectangle posEndPrev = c.modelToView2D(Math.max(0, c.getSelectionEnd() - 1)).getBounds(); Rectangle posOffs0 = c.modelToView2D(offs0).getBounds(); + Rectangle posOffs0Prev = c.modelToView2D(Math.max(0, offs0 - 1)).getBounds(); Rectangle posOffs1 = c.getUI().modelToView2D(c, offs1, Position.Bias.Backward).getBounds(); - Paint paint = getPaint(); - if (paint == null) { - g2d.setColor(c.getSelectionColor()); - } else { - g2d.setPaint(paint); - } + Rectangle posOffs1Forward = c.modelToView2D(offs1).getBounds(); + Rectangle posOffs1Next = c.modelToView2D(Math.min(c.getDocument().getLength(), offs1 - 1)).getBounds(); + boolean selectionStart = c.getSelectionStart() >= offs0; + boolean selectionEnd = c.getSelectionEnd() <= offs1; - if (offs0 == offs1) { - /* - * No selection. We should still paint something. - */ - if (DEBUG_COLOR) g2d.setColor(Color.YELLOW); - Shape s = view.modelToView(offs0, bounds, Position.Bias.Forward); - Rectangle r = s.getBounds(); - dirtyShape = paintSelectionLine(g2d, r, c, posStart, posEnd); - } else if (posOffs0.y != posStart.y && posOffs1.y != posEnd.y - && posOffs1.y >= posOffs0.y && posOffs1.y <= posOffs0.y + posOffs0.height) { - if (DEBUG_COLOR) g2d.setColor(Color.ORANGE); - // Contained in view, can just use bounds. - //Full lines of selection - var alloc = new Rectangle(posOffs0.x, posOffs0.y, posOffs1.x + posOffs1.width - posOffs0.x, - posOffs1.y + posOffs1.height - posOffs0.y); - dirtyShape = paintSelectionLine(g2d, alloc, c, posStart, posEnd); + var margin = c.getMargin(); + + boolean isToEndOfLine = posOffs1.y < posEnd.y && (posOffs1Forward.y != posOffs1Next.y); + 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 { - // Should only render part of View. - //Start/End parts of selection - Shape shape = view.modelToView(offs0, Position.Bias.Forward, offs1, Position.Bias.Backward, bounds); - Rectangle r = shape.getBounds(); - if (posEnd.y == posStart.y) { - dirtyShape = paintSelection(g2d, c, r); - } else if (posOffs0.y == posStart.y) { - dirtyShape = paintSelectionStart(g2d, r, c, posStart, posEnd); - } else { - dirtyShape = paintSelectionEnd(g2d, r, c, posStart, posEnd); - } + alloc = new Rectangle(posOffs0.x, posOffs0.y, posOffs1.x + posOffs1.width - posOffs0.x, + posOffs1.y + posOffs1.height - posOffs0.y); } - return dirtyShape; - } - protected Shape paintLayerImplInline(final Graphics2D g2d, final int offs0, final int offs1, - final Shape bounds, @NotNull final JTextComponent c, - final View view) throws BadLocationException { - Shape dirtyShape; - Rectangle posStart = c.modelToView2D(c.getSelectionStart()).getBounds(); - Rectangle posEnd = c.modelToView2D(c.getSelectionEnd()).getBounds(); + 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)); + + alloc.width = Math.max(2 * arcSize, alloc.width); + alloc.x = Math.max(margin.left, Math.min(c.getWidth() - margin.right - alloc.width, alloc.x)); Paint paint = getPaint(); if (paint == null) { @@ -277,161 +257,81 @@ public class DarkHighlightPainter extends DefaultHighlighter.DefaultHighlightPai g2d.setPaint(paint); } - if (offs0 == offs1) { - if (DEBUG_COLOR) g2d.setColor(Color.YELLOW); - Shape s = view.modelToView(offs0, bounds, Position.Bias.Forward); - Rectangle r = s.getBounds(); - g2d.drawLine(r.x, r.y, r.x, r.y + r.height); - dirtyShape = r; - } else if (offs0 == view.getStartOffset() && offs1 == view.getEndOffset() && posEnd.y != posStart.y) { - // Contained in view, can just use bounds. - //Full lines of selection + 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); + } else if (!selectionStart && !selectionEnd) { if (DEBUG_COLOR) g2d.setColor(Color.ORANGE); - dirtyShape = paintSelectionLine(g2d, bounds.getBounds(), c, posStart, posEnd); + dirtyShape = paintMiddleSelection(g2d, alloc, c, + isToEndOfLine, isToStartOfLine); } else { // Should only render part of View. //Start/End parts of selection - Shape shape = view.modelToView(offs0, Position.Bias.Forward, offs1, Position.Bias.Backward, bounds); - Rectangle r = shape.getBounds(); - if ((offs0 != view.getStartOffset() && offs1 != view.getEndOffset()) || posEnd.y == posStart.y) { - dirtyShape = paintSelection(g2d, c, r); - } else if (offs1 == view.getEndOffset()) { - dirtyShape = paintSelectionStart(g2d, r, c, posStart, posEnd); + if (posEnd.y == posStart.y) { + dirtyShape = paintSelection(g2d, c, alloc, selectionStart, selectionEnd); + } else if (selectionStart) { + dirtyShape = paintSelectionStart(g2d, alloc, c, posStart, posOffs0, + endBeforeStart); } else { - dirtyShape = paintSelectionEnd(g2d, r, c, posStart, posEnd); + 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; } @NotNull - @Contract("_, _, _ -> param3") - private Shape paintSelection(final Graphics2D g2d, final JTextComponent c, final Rectangle r) { - /* - * Selection is contained to one line. - */ - if (DEBUG_COLOR) g2d.setColor(Color.BLUE); - if (isRounded(c)) { - g2d.fillRoundRect(r.x, r.y, r.width, r.height, arcSize, arcSize); - } else { - g2d.fillRect(r.x, r.y, r.width, r.height); + private Shape paintMiddleSelection(final Graphics2D g, final Rectangle r, final JTextComponent c, + final boolean toEndOfLine, final boolean toStartOfLine) { + if (toStartOfLine) { + r.width -= arcSize; + r.x += arcSize; + } + if (toEndOfLine) { + r.width -= arcSize; } + g.fillRect(r.x, r.y, r.width, r.height); return r; } - @Contract("_, _, _, _, _ -> param2") + /* + * Selection is contained to one line. + */ + @Contract("_, _, _, _, _ -> param3") @NotNull - private Shape paintSelectionLine(@NotNull final Graphics2D g2d, - @NotNull final Rectangle alloc, - @NotNull final JTextComponent c, - @NotNull final Rectangle posStart, - @NotNull final Rectangle posEnd) throws BadLocationException { - var margin = c.getMargin(); - boolean leftToRight = c.getComponentOrientation().isLeftToRight(); - boolean isFirstLine = alloc.y == posStart.y; - boolean isSecondLine = posStart.y + posStart.height == alloc.y; - boolean isSecondLastLine = alloc.y + alloc.height == posEnd.y; - - if (alloc.width == 0 && isFirstLine && !leftToRight) { - return alloc; - } - if (alloc.y == posStart.y && ((leftToRight || posStart.x - c.getWidth() - margin.right - posStart.x == 1))) { - alloc.width = c.getWidth() - margin.right - alloc.x; - } else { - alloc.width = c.getWidth() - margin.left - margin.right; - alloc.x = margin.left; - } - if (!leftToRight && alloc.y == posStart.y) { - alloc.width -= posStart.x - margin.left; - alloc.x = posStart.x; - } + 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)) { - boolean roundRightBottom; - boolean roundLeftTop; - boolean roundLeftBottom; - boolean roundRightTop; - if (leftToRight) { - boolean isLastLine = isSecondLastLine && posEnd.x == margin.left; - roundRightBottom = isSecondLastLine; - roundLeftTop = isFirstLine || isSecondLine && posStart.x != margin.left; - roundLeftBottom = isLastLine; - roundRightTop = isFirstLine; - } else { - boolean isLastLine = isSecondLastLine - && (posEnd.x == c.getWidth() - margin.right - 1 || posEnd.x == margin.left); - if (c.modelToView2D(c.getSelectionEnd() - 1).getBounds().y == posEnd.y) { - isLastLine = false; - } - roundRightBottom = isLastLine || (isSecondLastLine - && posEnd.x < (c.getWidth() - margin.right - arcSize / 2)); - roundLeftBottom = isLastLine; - roundLeftTop = isFirstLine || (isSecondLine && posStart.x > margin.left + arcSize / 2); - roundRightTop = isFirstLine || isSecondLine && c.getWidth() - margin.right - posStart.x == 1; - } - - if (!(roundLeftBottom || roundRightBottom || roundRightTop || roundLeftTop)) { - g2d.fillRect(alloc.x, alloc.y, alloc.width, alloc.height); - return alloc; - } - // At least one round corner now. - g2d.fillRoundRect(alloc.x, alloc.y, alloc.width, alloc.height, arcSize, arcSize); - if (!roundRightBottom) { - g2d.fillRect(alloc.x + alloc.width - arcSize, alloc.y + alloc.height - arcSize, - arcSize, arcSize); - } - if (!roundLeftBottom) { - g2d.fillRect(alloc.x, alloc.y + alloc.height - arcSize, arcSize, arcSize); - } - if (!roundRightTop) { - g2d.fillRect(alloc.x + alloc.width - arcSize, alloc.y, arcSize, arcSize); - } - if (!roundLeftTop) { - g2d.fillRect(alloc.x, alloc.y, arcSize, arcSize); - } - - boolean drawArc = isFirstLine && !roundLeftBottom; - drawArc = drawArc && alloc.x >= margin.left + arcSize; - if (drawArc) { - paintStartArc(g2d, alloc); - alloc.x -= arcSize; - alloc.width += arcSize; - } + paintRoundedLeftRight(g2d, selectionStart, selectionEnd, r); } else { - g2d.fillRect(alloc.x, alloc.y, alloc.width, alloc.height); + g2d.fillRect(r.x, r.y, r.width, r.height); } - return alloc; - } - - private boolean isRounded(final JTextComponent c) { - return roundedEdges || Boolean.TRUE.equals(c.getClientProperty("JTextComponent.roundedSelection")); + return r; } + @Contract("_, _, _, _, _, _ -> param2") @NotNull - @Contract("_, _, _, _, _ -> param2") private Shape paintSelectionStart(@NotNull final Graphics2D g2d, @NotNull final Rectangle r, @NotNull final JTextComponent c, - @NotNull final Rectangle posStart, final Rectangle posEnd) { + @NotNull final Rectangle posStart, + final Rectangle posOffs0, + final boolean endBeforeStart) { if (DEBUG_COLOR) g2d.setColor(Color.RED); - - r.width = c.getWidth() - posStart.x - c.getMargin().right; var margin = c.getMargin(); if (isRounded(c)) { - boolean roundLeftBottom = posEnd.x < (posStart.x + 2) && posEnd.y == posStart.y + posStart.height; - boolean roundRightBottom = posEnd.y == posStart.y + posStart.height; - if (!c.getComponentOrientation().isLeftToRight()) { - roundLeftBottom = roundLeftBottom || c.getWidth() - c.getMargin().right - posEnd.x == 1; - } - - g2d.fillRoundRect(r.x, r.y, r.width, r.height, arcSize, arcSize); - if (!roundLeftBottom && !roundRightBottom) { - //Optimize draw calls - g2d.fillRect(r.x, r.y + r.height - arcSize, r.width, arcSize); - } else if (!roundLeftBottom) { - g2d.fillRect(r.x, r.y + r.height - arcSize, arcSize, arcSize); - } else if (!roundRightBottom) { - g2d.fillRect(r.x + r.width - arcSize, r.y + r.height - arcSize, arcSize, arcSize); - } - boolean drawCorner = !roundLeftBottom && r.x >= margin.left + arcSize; + paintRoundRect(g2d, r, arcSize, true, false, endBeforeStart, false); + boolean drawCorner = posOffs0.equals(posStart) && !endBeforeStart && r.x >= margin.left + arcSize; if (drawCorner) { paintStartArc(g2d, r); r.x -= arcSize; @@ -443,40 +343,29 @@ public class DarkHighlightPainter extends DefaultHighlighter.DefaultHighlightPai return r; } - @Contract("_, _, _, _, _ -> param2") + private boolean isRounded(final JTextComponent c) { + return roundedEdges || Boolean.TRUE.equals(c.getClientProperty("JTextComponent.roundedSelection")); + } + + @Contract("_, _, _, _, _, _, _, _, _ -> param2") @NotNull private Shape paintSelectionEnd(@NotNull final Graphics2D g2d, @NotNull final Rectangle r, @NotNull final JTextComponent c, final Rectangle posStart, - final Rectangle posEnd) { + final boolean isFirstLine, final boolean isSecondLine, + final boolean extendToStart, final boolean extendToEnd, + final boolean endBeforeStart) { if (DEBUG_COLOR) g2d.setColor(Color.GREEN); - var margin = c.getMargin(); - r.width += r.x - margin.left; - r.x = margin.left; - if (!c.getComponentOrientation().isLeftToRight() && r.y == posStart.y) { - r.width -= posStart.x - margin.left; - r.x = posStart.x; - } - if (!c.getComponentOrientation().isLeftToRight()) { - if (posEnd.x >= (c.getWidth() - margin.right - arcSize / 2)) { - //Fill to right edge even if corners are not rounded. - r.width = c.getWidth() - margin.right - margin.left; - } + if (r.x + r.width >= c.getWidth() - margin.right - arcSize / 2.0) { + int end = c.getWidth() - margin.right; + r.width = end - r.x; } if (isRounded(c)) { - boolean roundRightTop = posEnd.x < (posStart.x + 2) && posEnd.y == posStart.y + posStart.height; - boolean roundLeftTop = posEnd.y == posStart.y + posStart.height && posStart.x != margin.left; - - g2d.fillRoundRect(r.x, r.y, r.width, r.height, arcSize, arcSize); - if (!roundLeftTop && !roundRightTop) { - //Optimize draw calls - g2d.fillRect(r.x, r.y, r.width, arcSize); - } else if (!roundLeftTop) { - g2d.fillRect(r.x, r.y, arcSize, arcSize); - } else if (!roundRightTop) { - g2d.fillRect(r.x + r.width - arcSize, r.y, arcSize, arcSize); - } - boolean drawCorner = !roundRightTop && r.x + r.width <= c.getWidth() - margin.right - arcSize; + 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; @@ -487,6 +376,82 @@ public class DarkHighlightPainter extends DefaultHighlighter.DefaultHighlightPai return r; } + private Shape paintExtension(final Graphics2D g2d, @NotNull 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) { + var margin = c.getMargin(); + boolean rounded = isRounded(c); + if (isToEndOfLine) { + if (DEBUG_COLOR) g2d.setColor(Color.CYAN); + int start = r.x + r.width; + int w = c.getWidth() - start - margin.right; + w = Math.max(2 * arcSize, w); + start = Math.min(start, c.getWidth() - margin.right - w); + if (rounded) { + boolean roundTop = isFirstLine || selectionStart; + boolean roundBottom = isLastLine || (isSecondLastLine && posEnd.x + posEnd.width <= start + w - arcSize); + boolean roundLeftTop = isFirstLine && start == margin.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 = margin.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() - margin.right; + paintRoundRect(g2d, new Rectangle(start, r.y, w, r.height), arcSize, + roundTop, false, roundBottom, roundRightBottom); + } else { + g2d.fillRect(r.x, r.y, end - r.x, r.height); + } + r.width += w; + r.x = start; + } + return r; + } + + private void paintRoundRect(@NotNull final Graphics g, @NotNull 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 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); + 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 paintStartArc(@NotNull final Graphics2D g2d, @NotNull final Rectangle r) { if (DEBUG_COLOR) g2d.setColor(Color.PINK); diff --git a/src/test/java/ScrollPaneDemo.java b/src/test/java/ScrollPaneDemo.java index 722b2115..75973557 100644 --- a/src/test/java/ScrollPaneDemo.java +++ b/src/test/java/ScrollPaneDemo.java @@ -15,12 +15,14 @@ public final class ScrollPaneDemo extends MultiSplitLayout { LafManager.install(); final var frame = new JFrame(); frame.setSize(500, 500); - var overlayScroll = new JScrollPane(new JEditorPane() {{ -// setEditorKit(new HTMLEditorKit()); + var overlayScroll = new JTextPane() {{ setText(TestResources.LOREM_IPSUM.repeat(10)); setFont(Font.getFont(Font.MONOSPACED)); // setComponentOrientation(ComponentOrientation.RIGHT_TO_LEFT); - }}); +// SimpleAttributeSet attribs = new SimpleAttributeSet(); +// StyleConstants.setAlignment(attribs, StyleConstants.ALIGN_RIGHT); +// setParagraphAttributes(attribs, true); + }}; frame.setContentPane(new JPanel(new BorderLayout()) {{ add(overlayScroll, BorderLayout.CENTER); }});