Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

List: rounded selection #547

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
201 changes: 201 additions & 0 deletions flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatListUI.java
Expand Up @@ -17,20 +17,33 @@
package com.formdev.flatlaf.ui;

import java.awt.Color;
import java.awt.Component;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Insets;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.beans.PropertyChangeListener;
import java.util.Map;
import javax.swing.DefaultListCellRenderer;
import javax.swing.JComponent;
import javax.swing.JList;
import javax.swing.ListCellRenderer;
import javax.swing.ListModel;
import javax.swing.ListSelectionModel;
import javax.swing.UIManager;
import javax.swing.event.ListSelectionListener;
import javax.swing.plaf.ComponentUI;
import javax.swing.plaf.basic.BasicListUI;
import com.formdev.flatlaf.FlatClientProperties;
import com.formdev.flatlaf.ui.FlatStylingSupport.Styleable;
import com.formdev.flatlaf.ui.FlatStylingSupport.StyleableUI;
import com.formdev.flatlaf.util.Graphics2DProxy;
import com.formdev.flatlaf.util.LoggingFacade;
import com.formdev.flatlaf.util.UIScale;

/**
* Provides the Flat LaF UI delegate for {@link javax.swing.JList}.
Expand Down Expand Up @@ -59,6 +72,8 @@
*
* @uiDefault List.selectionInactiveBackground Color
* @uiDefault List.selectionInactiveForeground Color
* @uiDefault List.selectionInsets Insets
* @uiDefault List.selectionArc int
*
* <!-- FlatListCellBorder -->
*
Expand All @@ -76,6 +91,8 @@ public class FlatListUI
@Styleable protected Color selectionForeground;
@Styleable protected Color selectionInactiveBackground;
@Styleable protected Color selectionInactiveForeground;
/** @since 3 */ @Styleable protected Insets selectionInsets;
/** @since 3 */ @Styleable protected int selectionArc;

// for FlatListCellBorder
/** @since 2 */ @Styleable protected Insets cellMargins;
Expand Down Expand Up @@ -103,6 +120,8 @@ protected void installDefaults() {
selectionForeground = UIManager.getColor( "List.selectionForeground" );
selectionInactiveBackground = UIManager.getColor( "List.selectionInactiveBackground" );
selectionInactiveForeground = UIManager.getColor( "List.selectionInactiveForeground" );
selectionInsets = UIManager.getInsets( "List.selectionInsets" );
selectionArc = UIManager.getInt( "List.selectionArc" );

toggleSelectionColors();
}
Expand Down Expand Up @@ -161,6 +180,29 @@ protected PropertyChangeListener createPropertyChangeListener() {
};
}

@Override
protected ListSelectionListener createListSelectionListener() {
ListSelectionListener superListener = super.createListSelectionListener();
return e -> {
superListener.valueChanged( e );

// for united rounded selection, repaint parts of the rows/columns that adjoin to the changed rows/columns
if( useUnitedRoundedSelection( true, true ) &&
!list.isSelectionEmpty() &&
(list.getMaxSelectionIndex() - list.getMinSelectionIndex()) >= 1 )
{
int size = list.getModel().getSize();
int firstIndex = Math.min( Math.max( e.getFirstIndex(), 0 ), size - 1 );
int lastIndex = Math.min( Math.max( e.getLastIndex(), 0 ), size - 1 );
Rectangle r = getCellBounds( list, firstIndex, lastIndex );
if( r != null ) {
int arc = (int) Math.ceil( UIScale.scale( selectionArc / 2f ) );
list.repaint( r.x - arc, r.y - arc, r.width + (arc * 2), r.height + (arc * 2) );
}
}
};
}

/** @since 2 */
protected void installStyle() {
try {
Expand Down Expand Up @@ -234,4 +276,163 @@ private void toggleSelectionColors() {
list.setSelectionForeground( selectionInactiveForeground );
}
}

@SuppressWarnings( "rawtypes" )
@Override
protected void paintCell( Graphics g, int row, Rectangle rowBounds, ListCellRenderer cellRenderer,
ListModel dataModel, ListSelectionModel selModel, int leadIndex )
{
boolean isSelected = selModel.isSelectedIndex( row );

// get renderer component
@SuppressWarnings( "unchecked" )
Component rendererComponent = cellRenderer.getListCellRendererComponent( list,
dataModel.getElementAt( row ), row, isSelected, list.hasFocus() && (row == leadIndex) );

//
boolean isFileList = Boolean.TRUE.equals( list.getClientProperty( "List.isFileList" ) );
int cx, cw;
if( isFileList ) {
// see BasicListUI.paintCell()
cw = Math.min( rowBounds.width, rendererComponent.getPreferredSize().width + 4 );
cx = list.getComponentOrientation().isLeftToRight()
? rowBounds.x
: rowBounds.x + (rowBounds.width - cw);
} else {
cx = rowBounds.x;
cw = rowBounds.width;
}

// rounded selection or selection insets
if( isSelected &&
!isFileList && // rounded selection is not supported for file list
rendererComponent instanceof DefaultListCellRenderer &&
(selectionArc > 0 ||
(selectionInsets != null &&
(selectionInsets.top != 0 || selectionInsets.left != 0 || selectionInsets.bottom != 0 || selectionInsets.right != 0))) )
{
// Because selection painting is done in the cell renderer, it would be
// necessary to require a FlatLaf specific renderer to implement rounded selection.
// Using a LaF specific renderer was avoided because often a custom renderer is
// already used in applications. Then either the rounded selection is not used,
// or the application has to be changed to extend a FlatLaf renderer.
//
// To solve this, a graphics proxy is used that paints rounded selection
// if row is selected and the renderer wants to fill the background.
class RoundedSelectionGraphics extends Graphics2DProxy {
// used to avoid endless loop in case that paintCellSelection() invokes
// g.fillRect() with full bounds (selectionInsets is 0,0,0,0)
private boolean inPaintSelection;

RoundedSelectionGraphics( Graphics delegate ) {
super( (Graphics2D) delegate );
}

@Override
public Graphics create() {
return new RoundedSelectionGraphics( super.create() );
}

@Override
public Graphics create( int x, int y, int width, int height ) {
return new RoundedSelectionGraphics( super.create( x, y, width, height ) );
}

@Override
public void fillRect( int x, int y, int width, int height ) {
if( !inPaintSelection &&
x == 0 && y == 0 && width == rowBounds.width && height == rowBounds.height &&
this.getColor() == rendererComponent.getBackground() )
{
inPaintSelection = true;
paintCellSelection( this, row, x, y, width, height );
inPaintSelection = false;
} else
super.fillRect( x, y, width, height );
}
}
g = new RoundedSelectionGraphics( g );
}

// paint renderer
rendererPane.paintComponent( g, rendererComponent, list, cx, rowBounds.y, cw, rowBounds.height, true );
}

/** @since 3 */
protected void paintCellSelection( Graphics g, int row, int x, int y, int width, int height ) {
float arcTopLeft, arcTopRight, arcBottomLeft, arcBottomRight;
arcTopLeft = arcTopRight = arcBottomLeft = arcBottomRight = UIScale.scale( selectionArc / 2f );

if( list.getLayoutOrientation() == JList.VERTICAL ) {
// layout orientation: VERTICAL
if( useUnitedRoundedSelection( true, false ) ) {
if( row > 0 && list.isSelectedIndex( row - 1 ) )
arcTopLeft = arcTopRight = 0;
if( row < list.getModel().getSize() - 1 && list.isSelectedIndex( row + 1 ) )
arcBottomLeft = arcBottomRight = 0;
}
} else {
// layout orientation: VERTICAL_WRAP or HORIZONTAL_WRAP
Rectangle r = null;
if( useUnitedRoundedSelection( true, false ) ) {
// vertical: check whether cells above or below are selected
r = getCellBounds( list, row, row );

int topIndex = locationToIndex( list, new Point( r.x, r.y - 1 ) );
int bottomIndex = locationToIndex( list, new Point( r.x, r.y + r.height ) );

if( topIndex >= 0 && topIndex != row && list.isSelectedIndex( topIndex ) )
arcTopLeft = arcTopRight = 0;
if( bottomIndex >= 0 && bottomIndex != row && list.isSelectedIndex( bottomIndex ) )
arcBottomLeft = arcBottomRight = 0;
}

if( useUnitedRoundedSelection( false, true ) ) {
// horizontal: check whether cells left or right are selected
if( r == null )
r = getCellBounds( list, row, row );

int leftIndex = locationToIndex( list, new Point( r.x - 1, r.y ) );
int rightIndex = locationToIndex( list, new Point( r.x + r.width, r.y ) );

// special handling for the case that last column contains less cells than the other columns
boolean ltr = list.getComponentOrientation().isLeftToRight();
if( !ltr && leftIndex >= 0 && leftIndex != row && leftIndex == locationToIndex( list, new Point( r.x - 1, r.y - 1 ) ) )
leftIndex = -1;
if( ltr && rightIndex >= 0 && rightIndex != row && rightIndex == locationToIndex( list, new Point( r.x + r.width, r.y - 1 ) ) )
rightIndex = -1;

if( leftIndex >= 0 && leftIndex != row && list.isSelectedIndex( leftIndex ) )
arcTopLeft = arcBottomLeft = 0;
if( rightIndex >= 0 && rightIndex != row && list.isSelectedIndex( rightIndex ) )
arcTopRight = arcBottomRight = 0;
}
}

FlatUIUtils.paintSelection( (Graphics2D) g, x, y, width, height,
UIScale.scale( selectionInsets ), arcTopLeft, arcTopRight, arcBottomLeft, arcBottomRight, 0 );
}

private boolean useUnitedRoundedSelection( boolean vertical, boolean horizontal ) {
return selectionArc > 0 &&
(selectionInsets == null ||
(vertical && selectionInsets.top == 0 && selectionInsets.bottom == 0) ||
(horizontal && selectionInsets.left == 0 && selectionInsets.right == 0));
}

/**
* Paints a cell selection at the given coordinates.
* The selection color must be set on the graphics context.
* <p>
* This method is intended for use in custom cell renderers.
*
* @since 3
*/
public static void paintCellSelection( JList<?> list, Graphics g, int row, int x, int y, int width, int height ) {
if( !(list.getUI() instanceof FlatListUI) )
return;

FlatListUI ui = (FlatListUI) list.getUI();
ui.paintCellSelection( g, row, x, y, width, height );
}
}
Expand Up @@ -318,12 +318,12 @@ protected TreeSelectionListener createTreeSelectionListener() {
// same is done in BasicTreeUI.Handler.valueChanged()
tree.repaint();
} else {
int arcHeight = (int) Math.ceil( UIScale.scale( (float) selectionArc ) );
int arc = (int) Math.ceil( UIScale.scale( selectionArc / 2f ) );

for( TreePath path : changedPaths ) {
Rectangle r = getPathBounds( tree, path );
if( r != null )
tree.repaint( r.x, r.y - arcHeight, r.width, r.height + (arcHeight * 2) );
tree.repaint( r.x, r.y - arc, r.width, r.height + (arc * 2) );
}
}
}
Expand Down
Expand Up @@ -390,6 +390,8 @@ InternalFrameTitlePane.border = 0,8,0,0

List.border = 0,0,0,0
List.cellMargins = 1,6,1,6
List.selectionInsets = 0,0,0,0
List.selectionArc = 0
List.cellFocusColor = @cellFocusColor
List.cellNoFocusBorder = com.formdev.flatlaf.ui.FlatListCellBorder$Default
List.focusCellHighlightBorder = com.formdev.flatlaf.ui.FlatListCellBorder$Focused
Expand Down Expand Up @@ -904,7 +906,6 @@ Tree.icon.closedColor = @icon
Tree.icon.openColor = @icon



#---- Styles ------------------------------------------------------------------

#---- inTextField ----
Expand Down
Expand Up @@ -254,6 +254,8 @@ void list() {
"selectionForeground", Color.class,
"selectionInactiveBackground", Color.class,
"selectionInactiveForeground", Color.class,
"selectionInsets", Insets.class,
"selectionArc", int.class,

// FlatListCellBorder
"cellMargins", Insets.class,
Expand Down
Expand Up @@ -401,6 +401,8 @@ void list() {
ui.applyStyle( "selectionForeground: #fff" );
ui.applyStyle( "selectionInactiveBackground: #fff" );
ui.applyStyle( "selectionInactiveForeground: #fff" );
ui.applyStyle( "selectionInsets: 1,2,3,4" );
ui.applyStyle( "selectionArc: 8" );

// FlatListCellBorder
ui.applyStyle( "cellMargins: 1,2,3,4" );
Expand Down
Expand Up @@ -19,6 +19,7 @@
import java.awt.Component;
import java.awt.Desktop;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Rectangle;
import java.awt.Window;
import java.awt.event.WindowAdapter;
Expand All @@ -40,6 +41,7 @@
import java.util.Objects;
import java.util.function.Predicate;
import javax.swing.*;
import javax.swing.border.Border;
import javax.swing.border.CompoundBorder;
import javax.swing.event.*;
import com.formdev.flatlaf.FlatDarculaLaf;
Expand All @@ -52,6 +54,7 @@
import com.formdev.flatlaf.demo.DemoPrefs;
import com.formdev.flatlaf.extras.FlatAnimatedLafChange;
import com.formdev.flatlaf.extras.FlatSVGIcon;
import com.formdev.flatlaf.ui.FlatListUI;
import com.formdev.flatlaf.util.LoggingFacade;
import com.formdev.flatlaf.util.StringUtils;
import net.miginfocom.swing.*;
Expand Down Expand Up @@ -89,10 +92,18 @@ public IJThemesPanel() {

// create renderer
themesList.setCellRenderer( new DefaultListCellRenderer() {
private int index;
private boolean isSelected;
private int titleHeight;

@Override
public Component getListCellRendererComponent( JList<?> list, Object value,
int index, boolean isSelected, boolean cellHasFocus )
{
this.index = index;
this.isSelected = isSelected;
this.titleHeight = 0;

String title = categories.get( index );
String name = ((IJThemeInfo)value).name;
int sep = name.indexOf( '/' );
Expand All @@ -101,11 +112,33 @@ public Component getListCellRendererComponent( JList<?> list, Object value,

JComponent c = (JComponent) super.getListCellRendererComponent( list, name, index, isSelected, cellHasFocus );
c.setToolTipText( buildToolTip( (IJThemeInfo) value ) );
if( title != null )
c.setBorder( new CompoundBorder( new ListCellTitledBorder( themesList, title ), c.getBorder() ) );
if( title != null ) {
Border titledBorder = new ListCellTitledBorder( themesList, title );
c.setBorder( new CompoundBorder( titledBorder, c.getBorder() ) );
titleHeight = titledBorder.getBorderInsets( c ).top;
}
return c;
}

@Override
public boolean isOpaque() {
return !isSelectedTitle();
}

@Override
protected void paintComponent( Graphics g ) {
if( isSelectedTitle() ) {
g.setColor( getBackground() );
FlatListUI.paintCellSelection( themesList, g, index, 0, titleHeight, getWidth(), getHeight() - titleHeight );
}

super.paintComponent( g );
}

private boolean isSelectedTitle() {
return titleHeight > 0 && isSelected && UIManager.getLookAndFeel() instanceof FlatLaf;
}

private String buildToolTip( IJThemeInfo ti ) {
if( ti.themeFile != null )
return ti.themeFile.getPath();
Expand Down
2 changes: 2 additions & 0 deletions flatlaf-testing/dumps/uidefaults/FlatDarkLaf_1.8.0.txt
Expand Up @@ -545,10 +545,12 @@ List.focusSelectedCellHighlightBorder [lazy] 1,6,1,6 false com.formdev.flatl
List.font [active] $defaultFont [UI]
List.foreground #bbbbbb HSL 0 0 73 javax.swing.plaf.ColorUIResource [UI]
List.noFocusBorder 1,1,1,1 false javax.swing.plaf.BorderUIResource$EmptyBorderUIResource [UI]
List.selectionArc 0
List.selectionBackground #4b6eaf HSL 219 40 49 javax.swing.plaf.ColorUIResource [UI]
List.selectionForeground #bbbbbb HSL 0 0 73 javax.swing.plaf.ColorUIResource [UI]
List.selectionInactiveBackground #0f2a3d HSL 205 61 15 javax.swing.plaf.ColorUIResource [UI]
List.selectionInactiveForeground #bbbbbb HSL 0 0 73 javax.swing.plaf.ColorUIResource [UI]
List.selectionInsets 0,0,0,0 javax.swing.plaf.InsetsUIResource [UI]
List.showCellFocusIndicator false
List.timeFactor 1000
ListUI com.formdev.flatlaf.ui.FlatListUI
Expand Down