TextField: support "clear" (or "cancel") button to empty text field

This commit is contained in:
Karl Tauber
2021-12-13 19:16:23 +01:00
parent 8d2ed3faf6
commit a4377e81cb
17 changed files with 339 additions and 36 deletions

View File

@@ -903,6 +903,17 @@ public interface FlatClientProperties
*/
String TEXT_FIELD_TRAILING_COMPONENT = "JTextField.trailingComponent";
/**
* Specifies whether a "clear" (or "cancel") button is shown on the trailing side
* if the text field is not empty, editable and enabled. Default is {@code false}.
* <p>
* <strong>Component</strong> {@link javax.swing.JTextField} (and subclasses)<br>
* <strong>Value type</strong> {@link java.lang.Boolean}
*
* @since 2
*/
String TEXT_FIELD_SHOW_CLEAR_BUTTON = "JTextField.showClearButton";
//---- JToggleButton ------------------------------------------------------
/**

View File

@@ -45,10 +45,13 @@ import javax.swing.JToggleButton;
import javax.swing.JToolBar;
import javax.swing.LookAndFeel;
import javax.swing.UIManager;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.plaf.ComponentUI;
import javax.swing.plaf.UIResource;
import javax.swing.plaf.basic.BasicTextFieldUI;
import javax.swing.text.Caret;
import javax.swing.text.Document;
import javax.swing.text.JTextComponent;
import com.formdev.flatlaf.ui.FlatStylingSupport.Styleable;
import com.formdev.flatlaf.ui.FlatStylingSupport.StyleableUI;
@@ -103,6 +106,10 @@ public class FlatTextFieldUI
/** @since 2 */ @Styleable protected Icon trailingIcon;
/** @since 2 */ protected JComponent leadingComponent;
/** @since 2 */ protected JComponent trailingComponent;
/** @since 2 */ protected JComponent clearButton;
// only used via styling (not in UI defaults, but has likewise client properties)
/** @since 2 */ @Styleable protected boolean showClearButton;
private Color oldDisabledBackground;
private Color oldInactiveBackground;
@@ -110,6 +117,7 @@ public class FlatTextFieldUI
private Insets defaultMargin;
private FocusListener focusListener;
private DocumentListener documentListener;
private Map<String, Object> oldStyleValues;
private AtomicBoolean borderShared;
@@ -126,6 +134,7 @@ public class FlatTextFieldUI
installLeadingComponent();
installTrailingComponent();
installClearButton();
installStyle();
}
@@ -134,6 +143,7 @@ public class FlatTextFieldUI
public void uninstallUI( JComponent c ) {
uninstallLeadingComponent();
uninstallTrailingComponent();
uninstallClearButton();
super.uninstallUI( c );
@@ -196,6 +206,11 @@ public class FlatTextFieldUI
getComponent().removeFocusListener( focusListener );
focusListener = null;
if( documentListener != null ) {
getComponent().getDocument().removeDocumentListener( documentListener );
documentListener = null;
}
}
@Override
@@ -254,9 +269,47 @@ public class FlatTextFieldUI
c.revalidate();
c.repaint();
break;
case TEXT_FIELD_SHOW_CLEAR_BUTTON:
uninstallClearButton();
installClearButton();
c.revalidate();
c.repaint();
break;
case "enabled":
case "editable":
updateClearButton();
break;
case "document":
if( documentListener != null ) {
if( e.getOldValue() instanceof Document )
((Document)e.getOldValue()).removeDocumentListener( documentListener );
if( e.getNewValue() instanceof Document )
((Document)e.getNewValue()).addDocumentListener( documentListener );
updateClearButton();
}
break;
}
}
/** @since 2 */
protected void installDocumentListener() {
if( documentListener != null )
return;
documentListener = new FlatDocumentListener();
getComponent().getDocument().addDocumentListener( documentListener );
}
/** @since 2 */
protected void documentChanged( DocumentEvent e ) {
if( clearButton != null )
updateClearButton();
}
/** @since 2 */
protected void installStyle() {
try {
@@ -275,10 +328,15 @@ public class FlatTextFieldUI
protected void applyStyle( Object style ) {
oldDisabledBackground = disabledBackground;
oldInactiveBackground = inactiveBackground;
boolean oldShowClearButton = showClearButton;
oldStyleValues = FlatStylingSupport.parseAndApply( oldStyleValues, style, this::applyStyleProperty );
updateBackground();
if( showClearButton != oldShowClearButton ) {
uninstallClearButton();
installClearButton();
}
}
/** @since 2 */
@@ -474,10 +532,14 @@ debug*/
size.width += getLeadingIconWidth() + getTrailingIconWidth();
// add width of leading and trailing components
if( leadingComponent != null && leadingComponent.isVisible() )
size.width += leadingComponent.getPreferredSize().width;
if( trailingComponent != null && trailingComponent.isVisible() )
size.width += trailingComponent.getPreferredSize().width;
for( JComponent comp : getLeadingComponents() ) {
if( comp != null && comp.isVisible() )
size.width += comp.getPreferredSize().width;
}
for( JComponent comp : getTrailingComponents() ) {
if( comp != null && comp.isVisible() )
size.width += comp.getPreferredSize().width;
}
return size;
}
@@ -558,17 +620,24 @@ debug*/
boolean ltr = isLeftToRight();
// remove width of leading/trailing components
JComponent leftComponent = ltr ? leadingComponent : trailingComponent;
JComponent rightComponent = ltr ? trailingComponent : leadingComponent;
boolean leftVisible = leftComponent != null && leftComponent.isVisible();
boolean rightVisible = rightComponent != null && rightComponent.isVisible();
if( leftVisible ) {
int w = leftComponent.getPreferredSize().width;
r.x += w;
r.width -= w;
JComponent[] leftComponents = ltr ? getLeadingComponents() : getTrailingComponents();
JComponent[] rightComponents = ltr ? getTrailingComponents() : getLeadingComponents();
boolean leftVisible = false;
boolean rightVisible = false;
for( JComponent leftComponent : leftComponents ) {
if( leftComponent != null && leftComponent.isVisible() ) {
int w = leftComponent.getPreferredSize().width;
r.x += w;
r.width -= w;
leftVisible = true;
}
}
for( JComponent rightComponent : rightComponents ) {
if( rightComponent != null && rightComponent.isVisible() ) {
r.width -= rightComponent.getPreferredSize().width;
rightVisible = true;
}
}
if( rightVisible )
r.width -= rightComponent.getPreferredSize().width;
// if a leading/trailing icons (or components) are shown, then the left/right margins are reduced
// to the top margin, which places the icon nicely centered on left/right side
@@ -671,6 +740,76 @@ debug*/
}
}
/** @since 2 */
protected void installClearButton() {
JTextComponent c = getComponent();
if( clientPropertyBoolean( c, TEXT_FIELD_SHOW_CLEAR_BUTTON, showClearButton ) ) {
clearButton = createClearButton();
updateClearButton();
installDocumentListener();
installLayout();
c.add( clearButton );
}
}
/** @since 2 */
protected JComponent createClearButton() {
JButton button = new JButton();
button.putClientProperty( STYLE_CLASS, "clearButton" );
button.putClientProperty( BUTTON_TYPE, BUTTON_TYPE_TOOLBAR_BUTTON );
button.setCursor( Cursor.getDefaultCursor() );
button.addActionListener( e -> {
getComponent().setText( "" );
} );
return button;
}
/** @since 2 */
protected void uninstallClearButton() {
if( clearButton != null ) {
getComponent().remove( clearButton );
clearButton = null;
}
}
/** @since 2 */
protected void updateClearButton() {
if( clearButton == null )
return;
JTextComponent c = getComponent();
boolean visible = c.isEnabled() && c.isEditable() && c.getDocument().getLength() > 0;
if( visible != clearButton.isVisible() ) {
clearButton.setVisible( visible );
c.revalidate();
c.repaint();
}
}
/**
* Returns components placed at the leading side of the text field.
* The returned array may contain {@code null}.
* The default implementation returns {@link #leadingComponent}.
*
* @since 2
*/
protected JComponent[] getLeadingComponents() {
return new JComponent[] { leadingComponent };
}
/**
* Returns components placed at the trailing side of the text field.
* The returned array may contain {@code null}.
* The default implementation returns {@link #trailingComponent} and {@link #clearButton}.
* <p>
* <strong>Note</strong>: The components in the array must be in reverse (visual) order.
*
* @since 2
*/
protected JComponent[] getTrailingComponents() {
return new JComponent[] { trailingComponent, clearButton };
}
/** @since 2 */
protected void prepareLeadingOrTrailingComponent( JComponent c ) {
c.putClientProperty( STYLE_CLASS, "inTextField" );
@@ -686,7 +825,8 @@ debug*/
}
}
private void installLayout() {
/** @since 2 */
protected void installLayout() {
JTextComponent c = getComponent();
LayoutManager oldLayout = c.getLayout();
if( !(oldLayout instanceof FlatTextFieldLayout) )
@@ -731,25 +871,30 @@ debug*/
if( delegate != null )
delegate.layoutContainer( parent );
if( leadingComponent == null && trailingComponent == null )
return;
int ow = FlatUIUtils.getBorderFocusAndLineWidth( getComponent() );
int h = parent.getHeight() - ow - ow;
boolean ltr = isLeftToRight();
JComponent leftComponent = ltr ? leadingComponent : trailingComponent;
JComponent rightComponent = ltr ? trailingComponent : leadingComponent;
JComponent[] leftComponents = ltr ? getLeadingComponents() : getTrailingComponents();
JComponent[] rightComponents = ltr ? getTrailingComponents() : getLeadingComponents();
// layout left component
if( leftComponent != null && leftComponent.isVisible() ) {
int w = leftComponent.getPreferredSize().width;
leftComponent.setBounds( ow, ow, w, h );
// layout left components
int x = ow;
for( JComponent leftComponent : leftComponents ) {
if( leftComponent != null && leftComponent.isVisible() ) {
int cw = leftComponent.getPreferredSize().width;
leftComponent.setBounds( x, ow, cw, h );
x += cw;
}
}
// layout right component
if( rightComponent != null && rightComponent.isVisible() ) {
int w = rightComponent.getPreferredSize().width;
rightComponent.setBounds( parent.getWidth() - ow - w, ow, w, h );
// layout right components
x = parent.getWidth() - ow;
for( JComponent rightComponent : rightComponents ) {
if( rightComponent != null && rightComponent.isVisible() ) {
int cw = rightComponent.getPreferredSize().width;
x -= cw;
rightComponent.setBounds( x, ow, cw, h );
}
}
}
@@ -780,4 +925,24 @@ debug*/
((LayoutManager2)delegate).invalidateLayout( target );
}
}
//---- class FlatDocumentListener -----------------------------------------
private class FlatDocumentListener
implements DocumentListener
{
@Override
public void insertUpdate( DocumentEvent e ) {
documentChanged( e );
}
@Override
public void removeUpdate( DocumentEvent e ) {
documentChanged( e );
}
@Override
public void changedUpdate( DocumentEvent e ) {
documentChanged( e );
}
}
}

View File

@@ -907,3 +907,16 @@ Tree.icon.openColor = @icon
[style]ToolBarSeparator.inTextField = \
separatorWidth: 3
#---- clearButton ----
# for clear/cancel button in text fields
[style]Button.clearButton = \
icon: com.formdev.flatlaf.icons.FlatClearIcon; \
focusable: false; \
toolbar.margin: 1,1,1,1; \
toolbar.spacingInsets: 1,1,1,1; \
background: $TextField.background; \
toolbar.hoverBackground: $TextField.background; \
toolbar.pressedBackground: $TextField.background

View File

@@ -823,7 +823,8 @@ public class TestFlatStyleableInfo
"focusedBackground", Color.class,
"iconTextGap", int.class,
"leadingIcon", Icon.class,
"trailingIcon", Icon.class
"trailingIcon", Icon.class,
"showClearButton", boolean.class
);
// border

View File

@@ -1011,6 +1011,8 @@ public class TestFlatStyling
ui.applyStyle( "leadingIcon: com.formdev.flatlaf.icons.FlatSearchIcon" );
ui.applyStyle( "trailingIcon: com.formdev.flatlaf.icons.FlatClearIcon" );
ui.applyStyle( "showClearButton: true" );
// border
flatTextBorder( style -> ui.applyStyle( style ) );