TextComponents: selectAllOnFocusPolicy related changes:

- No longer select all text if selection (or caret position) was changed by
  application and `selectAllOnFocusPolicy` is `once` (the default). (issue #983)
- FormattedTextField and Spinner: `selectAllOnFocusPolicy = once` behaves now
  as `always` (was `never` before), which means that all text is selected when
  component gains focus. This is because of special behavior of
  `JFormattedTextField` that did not allow implementation of `once`.
- Client property `JTextField.selectAllOnFocusPolicy` now also works on
  (editable) `JComboBox` and on `JSpinner`.
- Added client property `JTextField.selectAllOnMouseClick` to override UI
  property `TextComponent.selectAllOnMouseClick`. (issue #961)
- For `selectAllOnMouseClick = true`, clicking with the mouse into the text
  field, to focus it, now always selects all text, even if
  `selectAllOnFocusPolicy` is `once`.
This commit is contained in:
Karl Tauber
2025-03-16 11:10:02 +01:00
parent 666b99971d
commit d8e59f2cf3
8 changed files with 976 additions and 77 deletions

View File

@@ -21,6 +21,8 @@ import java.awt.IllegalComponentStateException;
import java.awt.Window;
import java.util.Objects;
import javax.swing.JComponent;
import javax.swing.JFormattedTextField;
import javax.swing.JSpinner;
import javax.swing.SwingConstants;
/**
@@ -1209,12 +1211,15 @@ public interface FlatClientProperties
/**
* Specifies whether all text is selected when the text component gains focus.
* <p>
* <strong>Component</strong> {@link javax.swing.JTextField} (and subclasses)<br>
* <strong>Components</strong> {@link javax.swing.text.JTextComponent} (and subclasses),
* {@link javax.swing.JComboBox} (since 3.6) and {@link javax.swing.JSpinner} (since 3.6)<br>
* <strong>Value type</strong> {@link java.lang.String}<br>
* <strong>Allowed Values</strong>
* {@link #SELECT_ALL_ON_FOCUS_POLICY_NEVER},
* {@link #SELECT_ALL_ON_FOCUS_POLICY_ONCE} (default) or
* {@link #SELECT_ALL_ON_FOCUS_POLICY_ALWAYS}
*
* @see #SELECT_ALL_ON_MOUSE_CLICK
*/
String SELECT_ALL_ON_FOCUS_POLICY = "JTextField.selectAllOnFocusPolicy";
@@ -1229,6 +1234,12 @@ public interface FlatClientProperties
* Select all text when the text component gains focus for the first time
* and selection was not modified (is at end of text).
* This is the default.
* <p>
* <b>Limitations:</b>
* For {@link JFormattedTextField} and {@link JSpinner} this behaves
* as {@link #SELECT_ALL_ON_FOCUS_POLICY_ALWAYS}.
* This is because of special behavior of {@link JFormattedTextField}
* that did not allow implementation of {@code "once"}.
*
* @see #SELECT_ALL_ON_FOCUS_POLICY
*/
@@ -1241,6 +1252,19 @@ public interface FlatClientProperties
*/
String SELECT_ALL_ON_FOCUS_POLICY_ALWAYS = "always";
/**
* Specifies whether all text is selected when when clicking with the mouse
* into the text field (and if "select all on focus" policy is enabled).
* <p>
* <strong>Components</strong> {@link javax.swing.text.JTextComponent} (and subclasses),
* {@link javax.swing.JComboBox} and {@link javax.swing.JSpinner}<br>
* <strong>Value type</strong> {@link java.lang.Boolean}
*
* @see #SELECT_ALL_ON_FOCUS_POLICY
* @since 3.6
*/
String SELECT_ALL_ON_MOUSE_CLICK = "JTextField.selectAllOnMouseClick";
/**
* Placeholder text that is only painted if the text field is empty.
* <p>

View File

@@ -17,6 +17,7 @@
package com.formdev.flatlaf.ui;
import static com.formdev.flatlaf.FlatClientProperties.*;
import java.awt.Container;
import java.awt.EventQueue;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
@@ -24,7 +25,9 @@ import java.awt.event.FocusEvent;
import java.awt.event.MouseEvent;
import javax.swing.Action;
import javax.swing.ActionMap;
import javax.swing.JComboBox;
import javax.swing.JFormattedTextField;
import javax.swing.JSpinner;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.plaf.UIResource;
@@ -33,6 +36,7 @@ import javax.swing.text.DefaultCaret;
import javax.swing.text.DefaultEditorKit;
import javax.swing.text.Document;
import javax.swing.text.JTextComponent;
import javax.swing.text.Position;
import javax.swing.text.Utilities;
/**
@@ -48,12 +52,15 @@ public class FlatCaret
{
private static final String KEY_CARET_INFO = "FlatLaf.internal.caretInfo";
// selectAllOnFocusPolicy
private static final int NEVER = 0, ONCE = 1, ALWAYS = 2;
private final String selectAllOnFocusPolicy;
private final boolean selectAllOnMouseClick;
private boolean inInstall;
private boolean wasFocused;
private boolean wasTemporaryLost;
private boolean wasFocusTemporaryLost;
private boolean isMousePressed;
private boolean isWordSelection;
private boolean isLineSelection;
@@ -94,6 +101,9 @@ public class FlatCaret
// restore selection
select( (int) ci[1], (int) ci[0] );
if( ci[4] != 0 )
wasFocused = true;
// if text component is focused, then caret and selection are visible,
// but when switching theme, the component does not yet have
// a highlighter and the selection is not painted
@@ -121,6 +131,7 @@ public class FlatCaret
getMark(),
getBlinkRate(),
System.currentTimeMillis(),
wasFocused ? 1 : 0,
} );
super.deinstall( c );
@@ -140,11 +151,36 @@ public class FlatCaret
super.adjustVisibility( nloc );
}
@Override
public void setDot( int dot ) {
super.setDot( dot );
// mark as focused if invoked from JTextComponent.setCaretPosition()
// to disable SELECT_ALL_ON_FOCUS_POLICY_ONCE if application explicitly changes selection
if( !wasFocused &&
getSelectAllOnFocusPolicy() == ONCE &&
StackUtils.wasInvokedFrom( JTextComponent.class.getName(), "setCaretPosition", 6 ) )
wasFocused = true;
}
@Override
public void moveDot( int dot ) {
super.moveDot( dot );
// mark as focused if invoked from JTextComponent.moveCaretPosition()
// to disable SELECT_ALL_ON_FOCUS_POLICY_ONCE if application explicitly changes selection
if( !wasFocused &&
getSelectAllOnFocusPolicy() == ONCE &&
StackUtils.wasInvokedFrom( JTextComponent.class.getName(), "moveCaretPosition", 6 ) )
wasFocused = true;
}
@Override
public void focusGained( FocusEvent e ) {
if( !inInstall && !wasTemporaryLost && (!isMousePressed || selectAllOnMouseClick) )
if( !inInstall && !wasFocusTemporaryLost && (!isMousePressed || isSelectAllOnMouseClick()) )
selectAllOnFocusGained();
wasTemporaryLost = false;
wasFocusTemporaryLost = false;
wasFocused = true;
super.focusGained( e );
@@ -152,7 +188,7 @@ public class FlatCaret
@Override
public void focusLost( FocusEvent e ) {
wasTemporaryLost = e.isTemporary();
wasFocusTemporaryLost = e.isTemporary();
super.focusLost( e );
}
@@ -232,24 +268,13 @@ public class FlatCaret
if( doc == null || !c.isEnabled() || !c.isEditable() || FlatUIUtils.isCellEditor( c ) )
return;
Object selectAllOnFocusPolicy = c.getClientProperty( SELECT_ALL_ON_FOCUS_POLICY );
if( selectAllOnFocusPolicy == null )
selectAllOnFocusPolicy = this.selectAllOnFocusPolicy;
if( selectAllOnFocusPolicy == null || SELECT_ALL_ON_FOCUS_POLICY_NEVER.equals( selectAllOnFocusPolicy ) )
int selectAllOnFocusPolicy = getSelectAllOnFocusPolicy();
if( selectAllOnFocusPolicy == NEVER )
return;
if( !SELECT_ALL_ON_FOCUS_POLICY_ALWAYS.equals( selectAllOnFocusPolicy ) ) {
// policy is "once" (or null or unknown)
if( selectAllOnFocusPolicy == ONCE && !isMousePressed ) {
// was already focused?
if( wasFocused )
return;
// check whether selection was modified before gaining focus
int dot = getDot();
int mark = getMark();
if( dot != mark || dot != doc.getLength() )
if( wasFocused && !(c instanceof JFormattedTextField) )
return;
}
@@ -265,16 +290,51 @@ public class FlatCaret
select( 0, c2.getDocument().getLength() );
} );
} else {
} else
select( 0, doc.getLength() );
}
}
private void select( int mark, int dot ) {
if( mark != getMark() )
setDot( mark );
setDot( mark, Position.Bias.Forward );
if( dot != getDot() )
moveDot( dot );
moveDot( dot, Position.Bias.Forward );
}
private int getSelectAllOnFocusPolicy() {
Object value = getClientProperty( SELECT_ALL_ON_FOCUS_POLICY );
// Note: using String.valueOf() because selectAllOnFocusPolicy may be null
switch( String.valueOf( value instanceof String ? value : selectAllOnFocusPolicy ) ) {
default:
case SELECT_ALL_ON_FOCUS_POLICY_NEVER: return NEVER;
case SELECT_ALL_ON_FOCUS_POLICY_ONCE: return ONCE;
case SELECT_ALL_ON_FOCUS_POLICY_ALWAYS: return ALWAYS;
}
}
private boolean isSelectAllOnMouseClick() {
Object value = getClientProperty( SELECT_ALL_ON_MOUSE_CLICK );
return (value instanceof Boolean) ? (boolean) value : selectAllOnMouseClick;
}
private Object getClientProperty( String key ) {
JTextComponent c = getComponent();
if( c == null )
return null;
Object value = c.getClientProperty( key );
if( value != null )
return value;
Container parent = c.getParent();
if( parent instanceof JComboBox )
return ((JComboBox<?>)parent).getClientProperty( key );
if( parent instanceof JSpinner.DefaultEditor ) {
parent = parent.getParent();
if( parent instanceof JSpinner )
return ((JSpinner)parent).getClientProperty( key );
}
return null;
}
/** @since 1.4 */