View Javadoc
1   /*******************************************************************************
2    * Copyhacked (H) 2012-2025.
3    * This program and the accompanying materials
4    * are made available under no term at all, use it like
5    * you want, but share and discuss it
6    * every time possible with every body.
7    *
8    * Contributors:
9    *      ron190 at ymail dot com - initial implementation
10   *******************************************************************************/
11  package com.jsql.view.swing.text;
12  
13  import com.jsql.util.LogLevelUtil;
14  import com.jsql.view.swing.popupmenu.JPopupMenuText;
15  import com.jsql.view.swing.text.action.SilentDeleteTextAction;
16  import org.apache.logging.log4j.LogManager;
17  import org.apache.logging.log4j.Logger;
18  
19  import javax.swing.*;
20  import javax.swing.text.DefaultEditorKit;
21  import javax.swing.text.JTextComponent;
22  import javax.swing.undo.CannotRedoException;
23  import javax.swing.undo.CannotUndoException;
24  import javax.swing.undo.CompoundEdit;
25  import javax.swing.undo.UndoManager;
26  import java.awt.event.ActionEvent;
27  
28  /**
29   * A swing JTextComponent with Undo/Redo functionality.
30   * @param <T> Component like JTextField or JTextArea to decorate
31   */
32  public class JPopupTextComponent<T extends JTextComponent> extends JPopupComponent<T> implements DecoratorJComponent<T> {
33      
34      private static final Logger LOGGER = LogManager.getRootLogger();
35  
36      /**
37       * Save the component to decorate, add the Undo/Redo.
38       * @param proxy Swing component to decorate
39       */
40      public JPopupTextComponent(final T proxy) {
41          super(proxy);
42  
43          this.getProxy().setComponentPopupMenu(new JPopupMenuText(this.getProxy()));
44          this.getProxy().setDragEnabled(true);
45  
46          var undoRedoManager = new UndoManager();
47  
48          // Time-based grouping for words undo/redo instead of chars
49          final CompoundEdit[] currentEdit = {new CompoundEdit()};
50          Timer commitTimer = new Timer(500, e -> {
51              currentEdit[0].end();
52              undoRedoManager.addEdit(currentEdit[0]);
53              currentEdit[0] = new CompoundEdit();
54          });
55          commitTimer.setRepeats(false);
56  
57          // Listen for undo and redo events
58          var doc = this.getProxy().getDocument();
59          doc.addUndoableEditListener(e -> {
60              currentEdit[0].addEdit(e.getEdit());
61              commitTimer.restart();
62          });
63  
64          this.initUndo(undoRedoManager);
65          this.initRedo(undoRedoManager);
66          this.makeDeleteSilent();
67      }
68  
69      private void initUndo(final UndoManager undo) {
70          final var undoIdentifier = "Undo";  // Create an undo action and add it to the text component
71          
72          this.getProxy().getActionMap().put(undoIdentifier, new AbstractAction(undoIdentifier) {
73              @Override
74              public void actionPerformed(ActionEvent evt) {
75                  // Unhandled ArrayIndexOutOfBoundsException #92146 on undo()
76                  try {
77                      if (undo.canUndo()) {
78                          undo.undo();
79                      }
80                  } catch (ArrayIndexOutOfBoundsException | CannotUndoException e) {
81                      LOGGER.log(LogLevelUtil.CONSOLE_JAVA, e, e);
82                  }
83              }
84         });
85  
86          // Bind the undo action to ctl-Z
87          this.getProxy().getInputMap().put(KeyStroke.getKeyStroke("control Z"), undoIdentifier);
88      }
89  
90      private void initRedo(final UndoManager undo) {
91          final var redoIdentifier = "Redo";  // Create a redo action and add it to the text component
92          
93          this.getProxy().getActionMap().put(redoIdentifier, new AbstractAction(redoIdentifier) {
94              @Override
95              public void actionPerformed(ActionEvent evt) {
96                  try {
97                      if (undo.canRedo()) {
98                          undo.redo();
99                      }
100                 } catch (CannotRedoException e) {
101                     LOGGER.log(LogLevelUtil.CONSOLE_JAVA, e, e);
102                 }
103             }
104         });
105 
106         // Bind the redo action to ctl-Y
107         this.getProxy().getInputMap().put(KeyStroke.getKeyStroke("control Y"), redoIdentifier);
108     }
109 
110     private void makeDeleteSilent() {
111         var actionMap = this.getProxy().getActionMap();  // Silent delete
112 
113         String key = DefaultEditorKit.deletePrevCharAction;
114         actionMap.put(key, new SilentDeleteTextAction(key, actionMap.get(key)));
115 
116         key = DefaultEditorKit.deleteNextCharAction;
117         actionMap.put(key, new SilentDeleteTextAction(key, actionMap.get(key)));
118     }
119 }