Skip to content

Commit

Permalink
macOS themes: make spinner look like macOS stepper (issue #497; PR #533)
Browse files Browse the repository at this point in the history
  • Loading branch information
DevCharly committed Nov 15, 2022
1 parent 2be2dae commit 62f0ef1
Show file tree
Hide file tree
Showing 6 changed files with 150 additions and 36 deletions.
Expand Up @@ -17,7 +17,10 @@
package com.formdev.flatlaf.ui;

import java.awt.Component;
import java.awt.Graphics;
import javax.swing.JSpinner;
import javax.swing.UIManager;
import javax.swing.plaf.SpinnerUI;
import com.formdev.flatlaf.ui.FlatStylingSupport.Styleable;

/**
Expand All @@ -35,6 +38,19 @@ public class FlatRoundBorder
// only used via styling (not in UI defaults, but has likewise client properties)
/** @since 2 */ @Styleable protected Boolean roundRect;

@Override
public void paintBorder( Component c, Graphics g, int x, int y, int width, int height ) {
// make mac style spinner border smaller (border does not surround arrow buttons)
if( isMacStyleSpinner( c ) ) {
int macStyleButtonsWidth = ((FlatSpinnerUI)((JSpinner)c).getUI()).getMacStyleButtonsWidth();
width -= macStyleButtonsWidth;
if( !c.getComponentOrientation().isLeftToRight() )
x += macStyleButtonsWidth;
}

super.paintBorder( c, g, x, y, width, height );
}

@Override
protected int getArc( Component c ) {
if( isCellEditor( c ) )
Expand All @@ -43,6 +59,17 @@ protected int getArc( Component c ) {
Boolean roundRect = FlatUIUtils.isRoundRect( c );
if( roundRect == null )
roundRect = this.roundRect;
return roundRect != null ? (roundRect ? Short.MAX_VALUE : 0) : arc;
return roundRect != null
? (roundRect ? Short.MAX_VALUE : 0)
: (isMacStyleSpinner( c ) ? 0 : arc);
}

private boolean isMacStyleSpinner( Component c ) {
if( c instanceof JSpinner ) {
SpinnerUI ui = ((JSpinner)c).getUI();
if( ui instanceof FlatSpinnerUI )
return ((FlatSpinnerUI)ui).isMacStyle();
}
return false;
}
}
Expand Up @@ -340,20 +340,31 @@ protected Component createPreviousButton() {

private Component createArrowButton( int direction, String name ) {
FlatArrowButton button = new FlatArrowButton( direction, arrowType, buttonArrowColor,
buttonDisabledArrowColor, buttonHoverArrowColor, null, buttonPressedArrowColor, null );
buttonDisabledArrowColor, buttonHoverArrowColor, null, buttonPressedArrowColor, null )
{
@Override
public int getArrowWidth() {
return isMacStyle() ? 7 : super.getArrowWidth();
}
@Override
public float getArrowThickness() {
return isMacStyle() ? 1.5f : super.getArrowThickness();
}
@Override
public float getYOffset() {
return isMacStyle() ? 0 : super.getYOffset();
}
@Override
public boolean isRoundBorderAutoXOffset() {
return isMacStyle() ? false : super.isRoundBorderAutoXOffset();
}
};
button.setName( name );
button.setYOffset( (direction == SwingConstants.NORTH) ? 1.25f : -1.25f );
if( direction == SwingConstants.NORTH )
installNextButtonListeners( button );
else
installPreviousButtonListeners( button );

if( "mac".equals( buttonStyle ) ) {
button.setArrowWidth( 7 );
button.setArrowThickness( 1.5f );
button.setYOffset( (direction == SwingConstants.NORTH) ? 0.75f : -0.75f );
button.setRoundBorderAutoXOffset( false );
}
return button;
}

Expand Down Expand Up @@ -381,10 +392,13 @@ public void update( Graphics g, JComponent c ) {
int width = c.getWidth();
int height = c.getHeight();
boolean enabled = spinner.isEnabled();
boolean ltr = spinner.getComponentOrientation().isLeftToRight();
boolean isMacStyle = isMacStyle();
int macStyleButtonsWidth = isMacStyle ? getMacStyleButtonsWidth() : 0;

// paint background
g2.setColor( getBackground( enabled ) );
FlatUIUtils.paintComponentBackground( g2, 0, 0, width, height, focusWidth, arc );
FlatUIUtils.paintComponentBackground( g2, ltr ? 0 : macStyleButtonsWidth, 0, width - macStyleButtonsWidth, height, focusWidth, arc );

// paint button background and separator
boolean paintButton = !"none".equals( buttonStyle );
Expand All @@ -393,22 +407,20 @@ public void update( Graphics g, JComponent c ) {
Component button = (handler.nextButton != null) ? handler.nextButton : handler.previousButton;
int arrowX = button.getX();
int arrowWidth = button.getWidth();
boolean isLeftToRight = spinner.getComponentOrientation().isLeftToRight();
Color separatorColor = enabled ? buttonSeparatorColor : buttonDisabledSeparatorColor;

if( "mac".equals( buttonStyle ) ) {
if( isMacStyle ) {
Insets insets = spinner.getInsets();
int gapX = scale( 3 );
int gapY = scale( 1 );
int bx = arrowX + gapX;
int by = insets.top + gapY;
int bw = arrowWidth - (gapX * 2);
int bh = height - insets.top - insets.bottom - (gapY * 2);
int lineWidth = Math.round( FlatUIUtils.getBorderLineWidth( spinner ) );
int bx = arrowX;
int by = insets.top - lineWidth;
int bw = arrowWidth;
int bh = height - insets.top - insets.bottom + (lineWidth * 2);
float lw = scale( buttonSeparatorWidth );

// buttons border
FlatUIUtils.paintOutlinedComponent( g2, bx, by, bw, bh,
0, 0, 0, lw, arc - focusWidth,
0, 0, 0, lw, scale( 12 ),
null, separatorColor, buttonBackground );

// separator between buttons
Expand All @@ -423,7 +435,7 @@ public void update( Graphics g, JComponent c ) {
if( enabled && buttonBackground != null ) {
g2.setColor( buttonBackground );
Shape oldClip = g2.getClip();
if( isLeftToRight )
if( ltr )
g2.clipRect( arrowX, 0, width - arrowX, height );
else
g2.clipRect( 0, 0, arrowX + arrowWidth, height );
Expand All @@ -435,7 +447,7 @@ public void update( Graphics g, JComponent c ) {
if( separatorColor != null && buttonSeparatorWidth > 0 ) {
g2.setColor( separatorColor );
float lw = scale( buttonSeparatorWidth );
float lx = isLeftToRight ? arrowX : arrowX + arrowWidth - lw;
float lx = ltr ? arrowX : arrowX + arrowWidth - lw;
g2.fill( new Rectangle2D.Float( lx, focusWidth, lw, height - 1 - (focusWidth * 2) ) );
}
}
Expand All @@ -446,6 +458,19 @@ public void update( Graphics g, JComponent c ) {
FlatUIUtils.resetRenderingHints( g, oldRenderingHints );
}

boolean isMacStyle() {
return "mac".equals( buttonStyle );
}

int getMacStyleButtonsWidth() {
return (handler.nextButton != null || handler.previousButton != null)
? scale( MAC_STEPPER_GAP ) + scale( MAC_STEPPER_WIDTH )
: 0;
}

private static final int MAC_STEPPER_WIDTH = 15;
private static final int MAC_STEPPER_GAP = 3;

//---- class Handler ------------------------------------------------------

private class Handler
Expand Down Expand Up @@ -502,6 +527,7 @@ public void layoutContainer( Container parent ) {
Insets insets = parent.getInsets();
Rectangle r = FlatUIUtils.subtractInsets( new Rectangle( size ), insets );

// editor gets all space if there are no buttons
if( nextButton == null && previousButton == null ) {
if( editor != null )
editor.setBounds( r );
Expand All @@ -517,20 +543,36 @@ public void layoutContainer( Container parent ) {
int minButtonWidth = (maxButtonWidth * 3) / 4;

// make button area square (except if width is limited)
int buttonsWidth = Math.min( Math.max( buttonsRect.height, minButtonWidth ), maxButtonWidth );
buttonsRect.width = buttonsWidth;
boolean isMacStyle = isMacStyle();
int buttonsGap = isMacStyle ? scale( MAC_STEPPER_GAP ) : 0;
int prefButtonWidth = isMacStyle ? scale( MAC_STEPPER_WIDTH ) : buttonsRect.height;
int buttonsWidth = Math.min( Math.max( prefButtonWidth, minButtonWidth ), maxButtonWidth );

if( parent.getComponentOrientation().isLeftToRight() ) {
editorRect.width -= buttonsWidth;
buttonsRect.x += editorRect.width;
} else {
editorRect.x += buttonsWidth;
editorRect.width -= buttonsWidth;
// update editor and buttons bounds
buttonsRect.width = buttonsWidth;
editorRect.width -= buttonsWidth + buttonsGap;
boolean ltr = parent.getComponentOrientation().isLeftToRight();
if( ltr )
buttonsRect.x += editorRect.width + buttonsGap;
else
editorRect.x += buttonsWidth + buttonsGap;

// in mac button style increase buttons height and move to the right
// for exact alignment with border
if( isMacStyle ) {
int lineWidth = Math.round( FlatUIUtils.getBorderLineWidth( spinner ) );
if( lineWidth > 0 ) {
buttonsRect.x += ltr ? lineWidth : -lineWidth;
buttonsRect.y -= lineWidth;
buttonsRect.height += lineWidth * 2;
}
}

// set editor bounds
if( editor != null )
editor.setBounds( editorRect );

// set buttons bounds
int nextHeight = (buttonsRect.height / 2) + (buttonsRect.height % 2); // round up
if( nextButton != null )
nextButton.setBounds( buttonsRect.x, buttonsRect.y, buttonsRect.width, nextHeight );
Expand Down
Expand Up @@ -253,6 +253,7 @@ Slider.focusedColor = $Component.focusColor
Spinner.buttonStyle = mac
Spinner.disabledBackground = @disabledComponentBackground
Spinner.buttonBackground = @buttonBackground
Spinner.buttonArrowColor = @foreground
Spinner.buttonSeparatorWidth = 0


Expand Down
6 changes: 3 additions & 3 deletions flatlaf-testing/dumps/uidefaults/FlatMacDarkLaf_1.8.0.txt
Expand Up @@ -982,12 +982,12 @@ SliderUI com.formdev.flatlaf.ui.FlatSliderUI
Spinner.arrowButtonSize 16,5 java.awt.Dimension
Spinner.background #282828 HSL 0 0 16 javax.swing.plaf.ColorUIResource [UI]
Spinner.border [lazy] 3,3,3,3 false com.formdev.flatlaf.ui.FlatRoundBorder [UI]
Spinner.buttonArrowColor #b7b7b7 HSL 0 0 72 javax.swing.plaf.ColorUIResource [UI]
Spinner.buttonArrowColor #dddddd HSL 0 0 87 javax.swing.plaf.ColorUIResource [UI]
Spinner.buttonBackground #565656 HSL 0 0 34 javax.swing.plaf.ColorUIResource [UI]
Spinner.buttonDisabledArrowColor #777777 HSL 0 0 47 javax.swing.plaf.ColorUIResource [UI]
Spinner.buttonDisabledSeparatorColor #ffffff0c 5% HSLA 0 0 100 5 javax.swing.plaf.ColorUIResource [UI]
Spinner.buttonHoverArrowColor #d1d1d1 HSL 0 0 82 com.formdev.flatlaf.util.DerivedColor [UI] lighten(10%)
Spinner.buttonPressedArrowColor #eaeaea HSL 0 0 92 com.formdev.flatlaf.util.DerivedColor [UI] lighten(20%)
Spinner.buttonHoverArrowColor #f7f7f7 HSL 0 0 97 / #d1d1d1 HSL 0 0 82 com.formdev.flatlaf.util.DerivedColor [UI] lighten(10%)
Spinner.buttonPressedArrowColor #ffffff HSL 0 0 100 / #eaeaea HSL 0 0 92 com.formdev.flatlaf.util.DerivedColor [UI] lighten(20%)
Spinner.buttonSeparatorColor #ffffff19 10% HSLA 0 0 100 10 javax.swing.plaf.ColorUIResource [UI]
Spinner.buttonSeparatorWidth 0
Spinner.buttonStyle mac
Expand Down
Expand Up @@ -45,6 +45,27 @@ public static void main( String[] args ) {

FlatTextComponentsTest() {
initComponents();
updatePreferredSizes();
}

@Override
public void updateUI() {
super.updateUI();

if( comboBox5 != null )
updatePreferredSizes();
}

private void updatePreferredSizes() {
Dimension size40 = UIScale.scale( new Dimension( 60, 40 ) );
comboBox5.setPreferredSize( size40 );
spinner4.setPreferredSize( size40 );

Dimension size14 = UIScale.scale( new Dimension( 60, 14 ) );
comboBox6.setPreferredSize( size14 );
comboBox6.setMinimumSize( size14 );
spinner5.setPreferredSize( size14 );
spinner5.setMinimumSize( size14 );
}

private void editableChanged() {
Expand Down Expand Up @@ -216,18 +237,19 @@ private void initComponents() {
JComboBox<String> comboBox3 = new JComboBox<>();
JLabel spinnerLabel = new JLabel();
JSpinner spinner1 = new JSpinner();
JSpinner spinner6 = new JSpinner();
JLabel label2 = new JLabel();
JComboBox<String> comboBox2 = new JComboBox<>();
JSpinner spinner2 = new JSpinner();
JLabel label1 = new JLabel();
JComboBox<String> comboBox5 = new JComboBox<>();
JSpinner spinner4 = new JSpinner();
comboBox5 = new JComboBox<>();
spinner4 = new JSpinner();
JLabel label3 = new JLabel();
JComboBox<String> comboBox4 = new JComboBox<>();
JSpinner spinner3 = new JSpinner();
JLabel label4 = new JLabel();
JComboBox<String> comboBox6 = new JComboBox<>();
JSpinner spinner5 = new JSpinner();
comboBox6 = new JComboBox<>();
spinner5 = new JSpinner();
JLabel label5 = new JLabel();
textField = new JTextField();
dragEnabledCheckBox = new JCheckBox();
Expand Down Expand Up @@ -563,6 +585,10 @@ private void initComponents() {
spinner1.setComponentPopupMenu(popupMenu1);
add(spinner1, "cell 1 7,growx");

//---- spinner6 ----
spinner6.setBorder(BorderFactory.createEmptyBorder());
add(spinner6, "cell 2 7,growx");

//---- label2 ----
label2.setText("<html>Large row height:<br>(default pref height)</html>");
add(label2, "cell 0 8,aligny top,growy 0");
Expand Down Expand Up @@ -690,6 +716,10 @@ private void initComponents() {
private JCheckBox trailingComponentVisibleCheckBox;
private JCheckBox showClearButtonCheckBox;
private JCheckBox showRevealButtonCheckBox;
private JComboBox<String> comboBox5;
private JSpinner spinner4;
private JComboBox<String> comboBox6;
private JSpinner spinner5;
private JTextField textField;
private JCheckBox dragEnabledCheckBox;
private JTextArea textArea;
Expand Down
Expand Up @@ -403,6 +403,12 @@ new FormModel {
}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
"value": "cell 1 7,growx"
} )
add( new FormComponent( "javax.swing.JSpinner" ) {
name: "spinner6"
"border": new javax.swing.border.EmptyBorder( 0, 0, 0, 0 )
}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
"value": "cell 2 7,growx"
} )
add( new FormComponent( "javax.swing.JLabel" ) {
name: "label2"
"text": "<html>Large row height:<br>(default pref height)</html>"
Expand Down Expand Up @@ -435,13 +441,17 @@ new FormModel {
"editable": true
auxiliary() {
"JavaCodeGenerator.typeParameters": "String"
"JavaCodeGenerator.variableLocal": false
}
}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
"value": "cell 1 10,growx"
} )
add( new FormComponent( "javax.swing.JSpinner" ) {
name: "spinner4"
"preferredSize": new java.awt.Dimension( 60, 40 )
auxiliary() {
"JavaCodeGenerator.variableLocal": false
}
}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
"value": "cell 1 11,growx"
} )
Expand Down Expand Up @@ -478,6 +488,7 @@ new FormModel {
"minimumSize": new java.awt.Dimension( 60, 14 )
auxiliary() {
"JavaCodeGenerator.typeParameters": "String"
"JavaCodeGenerator.variableLocal": false
}
}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
"value": "cell 1 14,growx"
Expand All @@ -486,6 +497,9 @@ new FormModel {
name: "spinner5"
"minimumSize": new java.awt.Dimension( 60, 14 )
"preferredSize": new java.awt.Dimension( 60, 14 )
auxiliary() {
"JavaCodeGenerator.variableLocal": false
}
}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
"value": "cell 1 15,growx,hmax 14"
} )
Expand Down

0 comments on commit 62f0ef1

Please sign in to comment.