From 8fa1eae3520213e643f2013b4d09d38cb55732e1 Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Fri, 22 Oct 2021 13:14:01 +0200 Subject: [PATCH] TextComponents: triple-click-and-drag now extends selection by whole lines triple-click-and-drag does not work in theme editor because drag is enabled, anyway a triple-click now selects the whole line before dragging starts --- CHANGELOG.md | 3 +- .../com/formdev/flatlaf/ui/FlatCaret.java | 66 +++++++++++----- .../testing/FlatTextComponentsTest.java | 60 ++++++++++----- .../testing/FlatTextComponentsTest.jfd | 30 +++++++- .../themeeditor/FlatRSyntaxTextAreaUI.java | 76 +++++++++++++------ 5 files changed, 165 insertions(+), 70 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a114d36..8d5012d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,8 @@ FlatLaf Change Log - TextField, FormattedTextField and PasswordField: Support leading and trailing icons (set client property `JTextField.leadingIcon` or `JTextField.trailingIcon` to an `Icon`). (PR #378; issue #368) -- TextComponents: Double-click-and-drag now extends selection by whole words. +- TextComponents: Double/triple-click-and-drag now extends selection by whole + words/lines. - Theming improvements: Reworks core themes to make it easier to create new themes (e.g. reduced explicit colors by using color functions). **Note**: There are minor incompatible changes in FlatLaf properties files. (PR #390) diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatCaret.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatCaret.java index 19898c3b..b274a4be 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatCaret.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatCaret.java @@ -19,14 +19,18 @@ package com.formdev.flatlaf.ui; import static com.formdev.flatlaf.FlatClientProperties.*; import java.awt.EventQueue; import java.awt.Rectangle; +import java.awt.event.ActionEvent; import java.awt.event.FocusEvent; import java.awt.event.MouseEvent; +import javax.swing.Action; +import javax.swing.ActionMap; import javax.swing.JFormattedTextField; import javax.swing.SwingUtilities; import javax.swing.UIManager; import javax.swing.plaf.UIResource; import javax.swing.text.BadLocationException; import javax.swing.text.DefaultCaret; +import javax.swing.text.DefaultEditorKit; import javax.swing.text.Document; import javax.swing.text.JTextComponent; import javax.swing.text.Utilities; @@ -52,8 +56,9 @@ public class FlatCaret private boolean wasTemporaryLost; private boolean isMousePressed; private boolean isWordSelection; - private int beginInitialWord; - private int endInitialWord; + private boolean isLineSelection; + private int dragSelectionStart; + private int dragSelectionEnd; public FlatCaret( String selectAllOnFocusPolicy, boolean selectAllOnMouseClick ) { this.selectAllOnFocusPolicy = selectAllOnFocusPolicy; @@ -153,11 +158,34 @@ public class FlatCaret isMousePressed = true; super.mousePressed( e ); + JTextComponent c = getComponent(); + // left double-click starts word selection isWordSelection = e.getClickCount() == 2 && SwingUtilities.isLeftMouseButton( e ) && !e.isConsumed(); - if( isWordSelection ) { - beginInitialWord = getMark(); - endInitialWord = getDot(); + + // left triple-click starts line selection + isLineSelection = e.getClickCount() == 3 && SwingUtilities.isLeftMouseButton( e ) && (!e.isConsumed() || c.getDragEnabled()); + + // select line + // (this is also done in DefaultCaret.mouseClicked(), but this event is + // sent when the mouse is released, which is too late for triple-click-and-drag) + if( isLineSelection ) { + ActionMap actionMap = c.getActionMap(); + Action selectLineAction = (actionMap != null) + ? actionMap.get( DefaultEditorKit.selectLineAction ) + : null; + if( selectLineAction != null ) { + selectLineAction.actionPerformed( new ActionEvent( c, + ActionEvent.ACTION_PERFORMED, null, e.getWhen(), e.getModifiers() ) ); + } + } + + // remember selection where word/line selection starts to keep it always selected while dragging + if( isWordSelection || isLineSelection ) { + int mark = getMark(); + int dot = getDot(); + dragSelectionStart = Math.min( dot, mark ); + dragSelectionEnd = Math.max( dot, mark ); } } @@ -165,33 +193,29 @@ public class FlatCaret public void mouseReleased( MouseEvent e ) { isMousePressed = false; isWordSelection = false; + isLineSelection = false; super.mouseReleased( e ); } @Override public void mouseDragged( MouseEvent e ) { - if( isWordSelection && !e.isConsumed() && SwingUtilities.isLeftMouseButton( e ) ) { - // fix Swing's double-click-and-drag behavior so that dragging after - // a double-click extends selection by whole words + if( (isWordSelection || isLineSelection) && + !e.isConsumed() && SwingUtilities.isLeftMouseButton( e ) ) + { + // fix Swing's double/triple-click-and-drag behavior so that dragging after + // a double/triple-click extends selection by whole words/lines JTextComponent c = getComponent(); int pos = c.viewToModel( e.getPoint() ); if( pos < 0 ) return; try { - int mark; - int dot; - if( pos > endInitialWord ) { - mark = beginInitialWord; - dot = Utilities.getWordEnd( c, pos ); - } else if( pos < beginInitialWord ) { - mark = endInitialWord; - dot = Utilities.getWordStart( c, pos ); - } else { - mark = beginInitialWord; - dot = endInitialWord; - } - select( mark, dot ); + if( pos > dragSelectionEnd ) + select( dragSelectionStart, isWordSelection ? Utilities.getWordEnd( c, pos ) : Utilities.getRowEnd( c, pos ) ); + else if( pos < dragSelectionStart ) + select( dragSelectionEnd, isWordSelection ? Utilities.getWordStart( c, pos ) : Utilities.getRowStart( c, pos ) ); + else + select( dragSelectionStart, dragSelectionEnd ); } catch( BadLocationException ex ) { UIManager.getLookAndFeel().provideErrorFeedback( c ); } diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatTextComponentsTest.java b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatTextComponentsTest.java index a63fb11f..e72cb16b 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatTextComponentsTest.java +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatTextComponentsTest.java @@ -81,6 +81,14 @@ public class FlatTextComponentsTest } } + private void dragEnabledChanged() { + boolean dragEnabled = dragEnabledCheckBox.isSelected(); + textField.setDragEnabled( dragEnabled ); + textArea.setDragEnabled( dragEnabled ); + textPane.setDragEnabled( dragEnabled ); + editorPane.setDragEnabled( dragEnabled ); + } + private void initComponents() { // JFormDesigner - Component initialization - DO NOT MODIFY //GEN-BEGIN:initComponents JLabel textFieldLabel = new JLabel(); @@ -138,14 +146,15 @@ public class FlatTextComponentsTest JComboBox comboBox6 = new JComboBox<>(); JSpinner spinner5 = new JSpinner(); JLabel label5 = new JLabel(); - JTextField textField4 = new JTextField(); + textField = new JTextField(); + dragEnabledCheckBox = new JCheckBox(); JLabel label6 = new JLabel(); JScrollPane scrollPane2 = new JScrollPane(); - JTextArea textArea2 = new JTextArea(); + textArea = new JTextArea(); JScrollPane scrollPane4 = new JScrollPane(); - JTextPane textPane4 = new JTextPane(); + textPane = new JTextPane(); JScrollPane scrollPane6 = new JScrollPane(); - JEditorPane editorPane5 = new JEditorPane(); + editorPane = new JEditorPane(); JPopupMenu popupMenu1 = new JPopupMenu(); JMenuItem cutMenuItem = new JMenuItem(); JMenuItem copyMenuItem = new JMenuItem(); @@ -535,10 +544,16 @@ public class FlatTextComponentsTest label5.setName("label5"); add(label5, "cell 0 16"); - //---- textField4 ---- - textField4.setText("123 456 789 abc def"); - textField4.setName("textField4"); - add(textField4, "cell 1 16 2 1,growx"); + //---- textField ---- + textField.setText("123 456 789 abc def"); + textField.setName("textField"); + add(textField, "cell 1 16 2 1,growx"); + + //---- dragEnabledCheckBox ---- + dragEnabledCheckBox.setText("Drag enabled"); + dragEnabledCheckBox.setName("dragEnabledCheckBox"); + dragEnabledCheckBox.addActionListener(e -> dragEnabledChanged()); + add(dragEnabledCheckBox, "cell 3 16 2 1,alignx left,growx 0"); //---- label6 ---- label6.setText("JTextArea
JTextPane
JEditorPane"); @@ -549,10 +564,10 @@ public class FlatTextComponentsTest { scrollPane2.setName("scrollPane2"); - //---- textArea2 ---- - textArea2.setText("1 123 456 789 abc def\n2 123 456 789 abc def\n3 123 456 789 abc def\n4 123 456 789 abc def\n5 123 456 789 abc def\n6 123 456 789 abc def\n7 123 456 789 abc def\n8 123 456 789 abc def"); - textArea2.setName("textArea2"); - scrollPane2.setViewportView(textArea2); + //---- textArea ---- + textArea.setText("1 123 456 789 abc def\n2 123 456 789 abc def\n3 123 456 789 abc def\n4 123 456 789 abc def\n5 123 456 789 abc def\n6 123 456 789 abc def\n7 123 456 789 abc def\n8 123 456 789 abc def"); + textArea.setName("textArea"); + scrollPane2.setViewportView(textArea); } add(scrollPane2, "cell 1 17 4 1,growx"); @@ -560,10 +575,10 @@ public class FlatTextComponentsTest { scrollPane4.setName("scrollPane4"); - //---- textPane4 ---- - textPane4.setText("1 123 456 789 abc def\n2 123 456 789 abc def\n3 123 456 789 abc def\n4 123 456 789 abc def\n5 123 456 789 abc def\n6 123 456 789 abc def\n7 123 456 789 abc def\n8 123 456 789 abc def"); - textPane4.setName("textPane4"); - scrollPane4.setViewportView(textPane4); + //---- textPane ---- + textPane.setText("1 123 456 789 abc def\n2 123 456 789 abc def\n3 123 456 789 abc def\n4 123 456 789 abc def\n5 123 456 789 abc def\n6 123 456 789 abc def\n7 123 456 789 abc def\n8 123 456 789 abc def"); + textPane.setName("textPane"); + scrollPane4.setViewportView(textPane); } add(scrollPane4, "cell 1 17 4 1,growx"); @@ -571,10 +586,10 @@ public class FlatTextComponentsTest { scrollPane6.setName("scrollPane6"); - //---- editorPane5 ---- - editorPane5.setText("1 123 456 789 abc def\n2 123 456 789 abc def\n3 123 456 789 abc def\n4 123 456 789 abc def\n5 123 456 789 abc def\n6 123 456 789 abc def\n7 123 456 789 abc def\n8 123 456 789 abc def"); - editorPane5.setName("editorPane5"); - scrollPane6.setViewportView(editorPane5); + //---- editorPane ---- + editorPane.setText("1 123 456 789 abc def\n2 123 456 789 abc def\n3 123 456 789 abc def\n4 123 456 789 abc def\n5 123 456 789 abc def\n6 123 456 789 abc def\n7 123 456 789 abc def\n8 123 456 789 abc def"); + editorPane.setName("editorPane"); + scrollPane6.setViewportView(editorPane); } add(scrollPane6, "cell 1 17 4 1,growx"); @@ -612,6 +627,11 @@ public class FlatTextComponentsTest private JSpinner bottomPaddingField; private JCheckBox leadingIconCheckBox; private JCheckBox trailingIconCheckBox; + private JTextField textField; + private JCheckBox dragEnabledCheckBox; + private JTextArea textArea; + private JTextPane textPane; + private JEditorPane editorPane; // JFormDesigner - End of variables declaration //GEN-END:variables //---- TestIcon ----------------------------------------------------------- diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatTextComponentsTest.jfd b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatTextComponentsTest.jfd index 6504d628..5a340765 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatTextComponentsTest.jfd +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatTextComponentsTest.jfd @@ -423,11 +423,24 @@ new FormModel { "value": "cell 0 16" } ) add( new FormComponent( "javax.swing.JTextField" ) { - name: "textField4" + name: "textField" "text": "123 456 789 abc def" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 1 16 2 1,growx" } ) + add( new FormComponent( "javax.swing.JCheckBox" ) { + name: "dragEnabledCheckBox" + "text": "Drag enabled" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "dragEnabledChanged", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 3 16 2 1,alignx left,growx 0" + } ) add( new FormComponent( "javax.swing.JLabel" ) { name: "label6" "text": "JTextArea
JTextPane
JEditorPane" @@ -437,8 +450,11 @@ new FormModel { add( new FormContainer( "javax.swing.JScrollPane", new FormLayoutManager( class javax.swing.JScrollPane ) ) { name: "scrollPane2" add( new FormComponent( "javax.swing.JTextArea" ) { - name: "textArea2" + name: "textArea" "text": "1 123 456 789 abc def\n2 123 456 789 abc def\n3 123 456 789 abc def\n4 123 456 789 abc def\n5 123 456 789 abc def\n6 123 456 789 abc def\n7 123 456 789 abc def\n8 123 456 789 abc def" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } } ) }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 1 17 4 1,growx" @@ -446,8 +462,11 @@ new FormModel { add( new FormContainer( "javax.swing.JScrollPane", new FormLayoutManager( class javax.swing.JScrollPane ) ) { name: "scrollPane4" add( new FormComponent( "javax.swing.JTextPane" ) { - name: "textPane4" + name: "textPane" "text": "1 123 456 789 abc def\n2 123 456 789 abc def\n3 123 456 789 abc def\n4 123 456 789 abc def\n5 123 456 789 abc def\n6 123 456 789 abc def\n7 123 456 789 abc def\n8 123 456 789 abc def" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } } ) }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 1 17 4 1,growx" @@ -455,8 +474,11 @@ new FormModel { add( new FormContainer( "javax.swing.JScrollPane", new FormLayoutManager( class javax.swing.JScrollPane ) ) { name: "scrollPane6" add( new FormComponent( "javax.swing.JEditorPane" ) { - name: "editorPane5" + name: "editorPane" "text": "1 123 456 789 abc def\n2 123 456 789 abc def\n3 123 456 789 abc def\n4 123 456 789 abc def\n5 123 456 789 abc def\n6 123 456 789 abc def\n7 123 456 789 abc def\n8 123 456 789 abc def" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } } ) }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 1 17 4 1,growx" diff --git a/flatlaf-theme-editor/src/main/java/com/formdev/flatlaf/themeeditor/FlatRSyntaxTextAreaUI.java b/flatlaf-theme-editor/src/main/java/com/formdev/flatlaf/themeeditor/FlatRSyntaxTextAreaUI.java index 873678d8..3a5046af 100644 --- a/flatlaf-theme-editor/src/main/java/com/formdev/flatlaf/themeeditor/FlatRSyntaxTextAreaUI.java +++ b/flatlaf-theme-editor/src/main/java/com/formdev/flatlaf/themeeditor/FlatRSyntaxTextAreaUI.java @@ -18,12 +18,16 @@ package com.formdev.flatlaf.themeeditor; import java.awt.Graphics; import java.awt.Rectangle; +import java.awt.event.ActionEvent; import java.awt.event.MouseEvent; +import javax.swing.Action; +import javax.swing.ActionMap; import javax.swing.JComponent; import javax.swing.SwingUtilities; import javax.swing.UIManager; import javax.swing.text.BadLocationException; import javax.swing.text.Caret; +import javax.swing.text.DefaultEditorKit; import javax.swing.text.JTextComponent; import javax.swing.text.Utilities; import org.fife.ui.rsyntaxtextarea.RSyntaxTextAreaUI; @@ -70,59 +74,83 @@ class FlatRSyntaxTextAreaUI extends ConfigurableCaret { private boolean isWordSelection; - private int beginInitialWord; - private int endInitialWord; + private boolean isLineSelection; + private int dragSelectionStart; + private int dragSelectionEnd; @Override public void mousePressed( MouseEvent e ) { super.mousePressed( e ); + JTextComponent c = getComponent(); + // left double-click starts word selection isWordSelection = e.getClickCount() == 2 && SwingUtilities.isLeftMouseButton( e ) && !e.isConsumed(); - if( isWordSelection ) { - beginInitialWord = getMark(); - endInitialWord = getDot(); + + // left triple-click starts line selection + isLineSelection = e.getClickCount() == 3 && SwingUtilities.isLeftMouseButton( e ) && (!e.isConsumed() || c.getDragEnabled()); + + // select line + // (this is also done in DefaultCaret.mouseClicked(), but this event is + // sent when the mouse is released, which is too late for triple-click-and-drag) + if( isLineSelection ) { + ActionMap actionMap = c.getActionMap(); + Action selectLineAction = (actionMap != null) + ? actionMap.get( DefaultEditorKit.selectLineAction ) + : null; + if( selectLineAction != null ) { + selectLineAction.actionPerformed( new ActionEvent( c, + ActionEvent.ACTION_PERFORMED, null, e.getWhen(), e.getModifiers() ) ); + } + } + + // remember selection where word/line selection starts to keep it always selected while dragging + if( isWordSelection || isLineSelection ) { + int mark = getMark(); + int dot = getDot(); + dragSelectionStart = Math.min( dot, mark ); + dragSelectionEnd = Math.max( dot, mark ); } } @Override public void mouseReleased( MouseEvent e ) { isWordSelection = false; + isLineSelection = false; super.mouseReleased( e ); } @Override public void mouseDragged( MouseEvent e ) { - if( isWordSelection && !e.isConsumed() && SwingUtilities.isLeftMouseButton( e ) ) { - // fix Swing's double-click-and-drag behavior so that dragging after - // a double-click extends selection by whole words + if( (isWordSelection || isLineSelection) && + !e.isConsumed() && SwingUtilities.isLeftMouseButton( e ) ) + { + // fix Swing's double/triple-click-and-drag behavior so that dragging after + // a double/triple-click extends selection by whole words/lines JTextComponent c = getComponent(); int pos = c.viewToModel( e.getPoint() ); if( pos < 0 ) return; try { - int mark; - int dot; - if( pos > endInitialWord ) { - mark = beginInitialWord; - dot = Utilities.getWordEnd( c, pos ); - } else if( pos < beginInitialWord ) { - mark = endInitialWord; - dot = Utilities.getWordStart( c, pos ); - } else { - mark = beginInitialWord; - dot = endInitialWord; - } - if( mark != getMark() ) - setDot( mark ); - if( dot != getDot() ) - moveDot( dot ); + if( pos > dragSelectionEnd ) + select( dragSelectionStart, isWordSelection ? Utilities.getWordEnd( c, pos ) : Utilities.getRowEnd( c, pos ) ); + else if( pos < dragSelectionStart ) + select( dragSelectionEnd, isWordSelection ? Utilities.getWordStart( c, pos ) : Utilities.getRowStart( c, pos ) ); + else + select( dragSelectionStart, dragSelectionEnd ); } catch( BadLocationException ex ) { UIManager.getLookAndFeel().provideErrorFeedback( c ); } } else super.mouseDragged( e ); } + + private void select( int mark, int dot ) { + if( mark != getMark() ) + setDot( mark ); + if( dot != getDot() ) + moveDot( dot ); + } } }