InternalCellEditor.java

  1. package swingtree;

  2. import org.jspecify.annotations.Nullable;
  3. import org.slf4j.Logger;
  4. import org.slf4j.LoggerFactory;
  5. import sprouts.Problem;
  6. import sprouts.Result;

  7. import javax.swing.*;
  8. import javax.swing.border.Border;
  9. import javax.swing.border.LineBorder;
  10. import javax.swing.table.TableCellEditor;
  11. import javax.swing.table.TableCellRenderer;
  12. import javax.swing.tree.TreeCellEditor;
  13. import java.awt.Color;
  14. import java.awt.Component;
  15. import java.awt.event.*;
  16. import java.lang.reflect.Constructor;
  17. import java.util.ArrayList;
  18. import java.util.EventObject;
  19. import java.util.List;
  20. import java.util.Objects;

  21. final class InternalCellEditor extends AbstractCellEditor implements TableCellEditor, TreeCellEditor {

  22.     private final static Class<?>[] argTypes = new Class<?>[]{String.class};
  23.     private static final Logger log = LoggerFactory.getLogger(InternalCellEditor.class);
  24.     private @Nullable Constructor<?> constructor;
  25.     private Result<Object> editorOutputValue;

  26.     /** The Swing component being edited. */
  27.     private @Nullable JComponent editorComponent;
  28.     /**
  29.      * The delegate class which handles all methods sent from the
  30.      * <code>CellEditor</code>.
  31.      */
  32.     private @Nullable EditorDelegate delegate;
  33.     /**
  34.      * An integer specifying the number of clicks needed to start editing.
  35.      * Even if <code>clickCountToStart</code> is defined as zero, it
  36.      * will not initiate until a click occurs.
  37.      */
  38.     private int clickCountToStart = 1;

  39.     private boolean hasDefaultComponent;

  40.     private final Class<? extends JComponent> hostType;

  41.     private boolean isInitialized = false;


  42.     public InternalCellEditor(Class<? extends JComponent> hostType) {
  43.         this.hostType = hostType;
  44.         this.editorOutputValue = Result.of(Object.class);
  45.     }

  46.     public void ini(JComponent host, int row, int col) {
  47.         if ( !isInitialized ) {
  48.             isInitialized = true;
  49.             Border defaultBorder = null;
  50.             JComponent defaultEditorComponent = null;
  51.             if ( host instanceof JTable ) {
  52.                 JTable table = (JTable) host;
  53.                 Class<?> columnDataType = table.getColumnClass(col);
  54.                 if ( Boolean.class.isAssignableFrom(columnDataType) )
  55.                     defaultEditorComponent = new JCheckBox();
  56.             }
  57.             if ( defaultEditorComponent == null )
  58.                 defaultEditorComponent = new JTextField();

  59.             if (JTable.class.isAssignableFrom(hostType)) {
  60.                 defaultBorder = UIManager.getBorder("Table.editorBorder");
  61.                 defaultEditorComponent.setName("Table.editor");
  62.             } else if (JTree.class.isAssignableFrom(hostType)) {
  63.                 defaultBorder = UIManager.getBorder("Tree.editorBorder");
  64.             } else if (JList.class.isAssignableFrom(hostType)) {
  65.                 defaultBorder = UIManager.getBorder("List.editorBorder");
  66.             } else if (JComboBox.class.isAssignableFrom(hostType)) {
  67.                 defaultBorder = UIManager.getBorder("ComboBox.editorBorder");
  68.             }
  69.             if (defaultBorder == null)
  70.                 defaultBorder = new LineBorder(Color.BLACK);

  71.             defaultEditorComponent.setBorder(defaultBorder);
  72.             if ( defaultEditorComponent instanceof JTextField )
  73.                 _setEditor((JTextField) defaultEditorComponent);
  74.             else if ( defaultEditorComponent instanceof JCheckBox )
  75.                 setEditor((JCheckBox) defaultEditorComponent);

  76.             hasDefaultComponent = true;
  77.         }
  78.     }

  79.     private void _setUIManagerInfo(JComponent editor) {
  80.         String name = "";
  81.         String info = "";
  82.         if ( JTable.class.isAssignableFrom(hostType) )
  83.             info = "isTableCellEditor";
  84.         else if ( JTree.class.isAssignableFrom(hostType) )
  85.             info = "isTreeCellEditor";
  86.         else if ( JList.class.isAssignableFrom(hostType) )
  87.             info = "isListCellEditor";
  88.         else if ( JComboBox.class.isAssignableFrom(hostType) )
  89.             info = "isComboBoxCellEditor";

  90.         if ( editor instanceof JTextField )
  91.             name = "JTextField";
  92.         else if ( editor instanceof JCheckBox )
  93.             name = "JCheckBox";
  94.         else if ( editor instanceof JComboBox )
  95.             name = "JComboBox";

  96.         if ( !name.isEmpty() && !info.isEmpty() )
  97.             editor.putClientProperty(name+"."+info, Boolean.TRUE);
  98.     }

  99.     public boolean hasDefaultComponent() {
  100.         return hasDefaultComponent;
  101.     }

  102.     private void _setEditor(final JTextField textField) {
  103.         editorComponent = textField;
  104.         this.clickCountToStart = 2;
  105.         delegate = new EditorDelegate() {
  106.             @Override
  107.             public void setPresentationEntry(@Nullable Object value) {
  108.                 textField.setText((value != null) ? value.toString() : "");
  109.             }

  110.             @Override
  111.             public Object getCurrentCellEditorEntry() {
  112.                 return textField.getText();
  113.             }
  114.         };
  115.         textField.addActionListener(delegate);
  116.         _setUIManagerInfo(textField);
  117.     }

  118.     public void setEditor(final JTextField textField) {
  119.         _setEditor(textField);
  120.         hasDefaultComponent = false;
  121.     }

  122.     public void setEntry(
  123.         @Nullable Object presentationEntry, // Typically a nicely formatted string
  124.         @Nullable Object originalEntryFromModel, // The original value from the model
  125.         Class<?> targetedEntryType
  126.     ) {
  127.         try {
  128.             Objects.requireNonNull(delegate);
  129.             delegate.setValueAndTarget(presentationEntry, originalEntryFromModel, targetedEntryType);
  130.         } catch (Exception e) {
  131.             log.debug("Failed to internal cell editor value for host type '"+hostType.getName()+"'", e);
  132.         }
  133.     }

  134.     public @Nullable Component getComponent() {
  135.         return editorComponent;
  136.     }

  137.     public @Nullable Component getCustomComponent() {
  138.         if (hasDefaultComponent)
  139.             return null;
  140.         return editorComponent;
  141.     }

  142.     public void setEditor(final JCheckBox checkBox) {
  143.         editorComponent = checkBox;
  144.         delegate = new EditorDelegate() {
  145.             @Override
  146.             public void setPresentationEntry(@Nullable Object value) {
  147.                 boolean selected = false;
  148.                 if (value instanceof Boolean) {
  149.                     selected = (Boolean) value;
  150.                 }
  151.                 else if (value instanceof String) {
  152.                     selected = value.equals("true");
  153.                 }
  154.                 checkBox.setSelected(selected);
  155.             }

  156.             @Override
  157.             public Object getCurrentCellEditorEntry() {
  158.                 return checkBox.isSelected();
  159.             }
  160.         };
  161.         checkBox.addActionListener(delegate);
  162.         checkBox.setRequestFocusEnabled(false);
  163.         _setUIManagerInfo(checkBox);
  164.         hasDefaultComponent = false;
  165.     }

  166.     public void setEditor(final JComboBox<?> comboBox) {
  167.         editorComponent = comboBox;
  168.         delegate = new EditorDelegate() {
  169.             @Override
  170.             public void setPresentationEntry(@Nullable Object value) {
  171.                 comboBox.setSelectedItem(value);
  172.             }

  173.             @Override
  174.             public @Nullable Object getCurrentCellEditorEntry() {
  175.                 return comboBox.getSelectedItem();
  176.             }

  177.             @Override
  178.             public boolean shouldSelectCell(EventObject anEvent) {
  179.                 if (anEvent instanceof MouseEvent) {
  180.                     MouseEvent e = (MouseEvent)anEvent;
  181.                     return e.getID() != MouseEvent.MOUSE_DRAGGED;
  182.                 }
  183.                 return true;
  184.             }
  185.             @Override
  186.             public boolean stopCellEditing() {
  187.                 if (comboBox.isEditable()) {
  188.                     // Commit edited value.
  189.                     comboBox.actionPerformed(new ActionEvent(
  190.                             InternalCellEditor.this, 0, ""));
  191.                 }
  192.                 return super.stopCellEditing();
  193.             }
  194.         };
  195.         comboBox.addActionListener(delegate);
  196.         _setUIManagerInfo(comboBox);
  197.         hasDefaultComponent = false;
  198.     }

  199.     /**
  200.      * Specifies the number of clicks needed to start editing.
  201.      *
  202.      * @param count  an int specifying the number of clicks needed to start editing
  203.      * @see #getClickCountToStart
  204.      */
  205.     public void setClickCountToStart(int count) {
  206.         clickCountToStart = count;
  207.     }

  208.     /**
  209.      * Returns the number of clicks needed to start editing.
  210.      * @return the number of clicks needed to start editing
  211.      */
  212.     public int getClickCountToStart() {
  213.         return clickCountToStart;
  214.     }

  215.     /**
  216.      * Forwards the message from the <code>CellEditor</code> to
  217.      * the <code>delegate</code>.
  218.      * @see EditorDelegate#getCellEditorEntryAsTargetType
  219.      */
  220.     @Override
  221.     public @Nullable Object getCellEditorValue() {
  222.         if ( JTable.class.isAssignableFrom(hostType) )
  223.             return this.editorOutputValue.orElseNull();
  224.         Objects.requireNonNull(delegate);
  225.         return delegate.getCellEditorEntryAsTargetType().orElseNull();
  226.     }

  227.     /**
  228.      * Returns true if <code>anEvent</code> is <b>not</b> a
  229.      * <code>MouseEvent</code>.  Otherwise, it returns true
  230.      * if the necessary number of clicks have occurred, and
  231.      * returns false otherwise.
  232.      *
  233.      * @param   anEvent         the event
  234.      * @return  true  if cell is ready for editing, false otherwise
  235.      * @see #setClickCountToStart
  236.      * @see #shouldSelectCell
  237.      */
  238.     @Override
  239.     public boolean isCellEditable(EventObject anEvent) {
  240.         if (anEvent instanceof MouseEvent) {
  241.             return ((MouseEvent)anEvent).getClickCount() >= clickCountToStart;
  242.         }
  243.         return true;
  244.     }

  245.     /**
  246.      * Forwards the message from the <code>CellEditor</code> to
  247.      * the <code>delegate</code>.
  248.      * @see EditorDelegate#shouldSelectCell(EventObject)
  249.      */
  250.     @Override
  251.     public boolean shouldSelectCell(EventObject anEvent) {
  252.         Objects.requireNonNull(delegate);
  253.         return delegate.shouldSelectCell(anEvent);
  254.     }

  255.     /**
  256.      * Forwards the message from the <code>CellEditor</code> to
  257.      * the <code>delegate</code>.
  258.      * @see EditorDelegate#stopCellEditing
  259.      */
  260.     @Override
  261.     public boolean stopCellEditing() {
  262.         Objects.requireNonNull(delegate);
  263.         if ( JTable.class.isAssignableFrom(hostType) ) {
  264.             Result<Object> newValueResult = delegate.getCellEditorEntryAsTargetType();
  265.             @Nullable Object newValue = newValueResult.orElseNull();
  266.             this.editorOutputValue = newValueResult;
  267.             if ( constructor == null ) {
  268.                 return super.stopCellEditing();
  269.             } else {
  270.                 try {
  271.                     if ("".equals(newValue)) {
  272.                         return super.stopCellEditing();
  273.                     } else if (newValue != null) {
  274.                         if (constructor.getDeclaringClass().isAssignableFrom(newValue.getClass())) {
  275.                             return super.stopCellEditing();
  276.                         }
  277.                     }

  278.                     this.editorOutputValue = Result.of(constructor.newInstance(new Object[]{newValue}));
  279.                 } catch (Exception e) {
  280.                     if (editorComponent != null)
  281.                         editorComponent.setBorder(new LineBorder(Color.red));
  282.                     return false;
  283.                 }
  284.             }
  285.         }
  286.         return delegate.stopCellEditing();
  287.     }

  288.     /**
  289.      * Forwards the message from the <code>CellEditor</code> to
  290.      * the <code>delegate</code>.
  291.      * @see EditorDelegate#cancelCellEditing
  292.      */
  293.     @Override
  294.     public void cancelCellEditing() {
  295.         Objects.requireNonNull(delegate);
  296.         delegate.cancelCellEditing();
  297.     }

  298.     /** Implements the <code>TreeCellEditor</code> interface. */
  299.     @Override
  300.     public Component getTreeCellEditorComponent(JTree tree, @Nullable Object entryFromModel,
  301.                                                 boolean isSelected,
  302.                                                 boolean expanded,
  303.                                                 boolean leaf, int row) {
  304.         Objects.requireNonNull(delegate);
  305.         Objects.requireNonNull(editorComponent);
  306.         String entryAsString = tree.convertValueToText(entryFromModel, isSelected, expanded, leaf, row, false);
  307.         delegate.setValueAndTarget(entryAsString, entryFromModel, entryFromModel == null ? Object.class : entryFromModel.getClass());
  308.         return editorComponent;
  309.     }

  310. //
  311. //  Implementing the CellEditor Interface
  312. //
  313.     /** Implements the <code>TableCellEditor</code> interface. */
  314.     @Override
  315.     public Component getTableCellEditorComponent(JTable table, @Nullable Object entryFromModel,
  316.                                                  boolean isSelected,
  317.                                                  int row, int column) {
  318.         Objects.requireNonNull(delegate);
  319.         Objects.requireNonNull(editorComponent);
  320.         delegate.setValueAndTarget(entryFromModel, entryFromModel, entryFromModel == null ? Object.class : entryFromModel.getClass());
  321.         if (editorComponent instanceof JCheckBox) {
  322.             //in order to avoid a "flashing" effect when clicking a checkbox
  323.             //in a table, it is important for the editor to have as a border
  324.             //the same border that the renderer has, and have as the background
  325.             //the same color as the renderer has. This is primarily only
  326.             //needed for JCheckBox since this editor doesn't fill all the
  327.             //visual space of the table cell, unlike a text field.
  328.             TableCellRenderer renderer = table.getCellRenderer(row, column);
  329.             Component c = renderer.getTableCellRendererComponent(table, entryFromModel,
  330.                                                                   isSelected, true, row, column);
  331.             if (c != null) {
  332.                 editorComponent.setOpaque(true);
  333.                 editorComponent.setBackground(c.getBackground());
  334.                 if (c instanceof JComponent) {
  335.                     editorComponent.setBorder(((JComponent)c).getBorder());
  336.                 }
  337.             } else {
  338.                 editorComponent.setOpaque(false);
  339.             }
  340.         }
  341.         return editorComponent;
  342.     }

  343.     public void updateForTable(JTable table, int column) {
  344.         if ( JTable.class.isAssignableFrom(hostType) ) {
  345.             this.editorOutputValue = Result.of(Object.class);
  346.             try {
  347.                 Class<?> type = table.getColumnClass(column);
  348.                 if ( editorComponent instanceof JTextField ) {
  349.                     JTextField tf = (JTextField) editorComponent;
  350.                     int alignment = tf.getHorizontalAlignment();
  351.                     if (Number.class.isAssignableFrom(type)) {
  352.                         if ( alignment != JTextField.RIGHT )
  353.                             tf.setHorizontalAlignment(JTextField.RIGHT);
  354.                     } else {
  355.                         if ( alignment == JTextField.RIGHT )
  356.                             tf.setHorizontalAlignment(JTextField.LEADING);
  357.                     }
  358.                 }
  359.                 if ( editorComponent instanceof JCheckBox ) {
  360.                     JCheckBox cb = (JCheckBox) editorComponent;
  361.                     if ( Boolean.class.isAssignableFrom(type) ) {
  362.                         if ( cb.getHorizontalAlignment() != JCheckBox.CENTER )
  363.                             cb.setHorizontalAlignment(JCheckBox.CENTER);
  364.                     }
  365.                 }

  366.                 if (type != Object.class) {
  367.                     constructor = type.getConstructor(argTypes);
  368.                 }
  369.             }
  370.             catch (Exception e) {
  371.                 log.debug("Failed to update internal cell editor for host type '"+hostType.getName()+"'", e);
  372.             }
  373.         }
  374.     }

  375.     /**
  376.      * The protected <code>EditorDelegate</code> class.
  377.      */
  378.     protected abstract class EditorDelegate implements ActionListener, ItemListener {

  379.         private Class<?> targetType = Object.class;
  380.         private @Nullable Object originalValue;

  381.         public final void setValueAndTarget(
  382.             @Nullable Object entryToBePresentedAndEdited,
  383.             @Nullable Object originalEntryFromModel,
  384.             Class<?> targetType
  385.         ) {
  386.             this.targetType = targetType;
  387.             this.originalValue = originalEntryFromModel;
  388.             this.setPresentationEntry(entryToBePresentedAndEdited);
  389.         }

  390.         /**
  391.          * Returns the value of this cell directly from the editor component.
  392.          * @return the value of this cell
  393.          */
  394.         protected abstract @Nullable Object getCurrentCellEditorEntry();

  395.         /**
  396.          *  Tries to convert the value of this cell to the target type
  397.          *  and returns it as a {@link Result}, which may contain problems
  398.          *  if the conversion failed.
  399.          *
  400.          * @return the value of this cell as the target type if possible
  401.          *         or the raw value if conversion is not possible.
  402.          */
  403.         public final Result<@Nullable Object> getCellEditorEntryAsTargetType() {
  404.             Object value = getCurrentCellEditorEntry();
  405.             if ( value == null )
  406.                 return Result.of(Object.class);
  407.             if ( targetType == Object.class )
  408.                 return Result.of(value);
  409.             if ( targetType.isAssignableFrom(value.getClass()) )
  410.                 return Result.of(value);
  411.             try {
  412.                 return _tryConvert(value, targetType);
  413.             } catch (Exception e) {
  414.                 log.debug(
  415.                         "Failed to convert internal cell editor value " +
  416.                         "from '"+value+"' to target type '"+targetType.getName()+"' " +
  417.                         "for host component type '"+hostType.getName()+"'",
  418.                         e
  419.                     );
  420.             }

  421.             List<Problem> problems = new ArrayList<>();
  422.             problems.add(Problem.of(
  423.                     "Failed to convert internal cell editor value " +
  424.                         "from '"+value+"' to target type '"+targetType.getName()+"' " +
  425.                         "for host component type '"+hostType.getName()+"'"
  426.                     ));
  427.             Object restoredValue = this.originalValue;
  428.             try {
  429.                 if ( restoredValue != null )
  430.                     restoredValue = _tryConvert(restoredValue, targetType).orElseNullable(value);
  431.             } catch (Exception e) {
  432.                 problems.add(Problem.of(e));
  433.                 log.debug(
  434.                         "Failed to convert internal cell editor value received before editing " +
  435.                         "from '"+this.originalValue+"' to target type '"+targetType.getName()+"' " +
  436.                         "for host component type '"+hostType.getName()+"'",
  437.                         e
  438.                     );
  439.             }
  440.             return Result.of(
  441.                     Object.class,
  442.                     restoredValue,
  443.                     problems
  444.                 );
  445.         }

  446.         /**
  447.          * Sets the value of this cell.
  448.          * @param value the new value of this cell
  449.          */
  450.         protected abstract void setPresentationEntry(@Nullable Object value);

  451.         /**
  452.          * Returns true to indicate that the editing cell may
  453.          * be selected.
  454.          *
  455.          * @param   anEvent         the event
  456.          * @return  true
  457.          * @see #isCellEditable
  458.          */
  459.         public boolean shouldSelectCell(EventObject anEvent) {
  460.             return true;
  461.         }

  462.         /**
  463.          * Stops editing and
  464.          * returns true to indicate that editing has stopped.
  465.          * This method calls <code>fireEditingStopped</code>.
  466.          *
  467.          * @return  true
  468.          */
  469.         public boolean stopCellEditing() {
  470.             fireEditingStopped();
  471.             return true;
  472.         }

  473.         /**
  474.          * Cancels editing.  This method calls <code>fireEditingCanceled</code>.
  475.          */
  476.         public void cancelCellEditing() {
  477.             fireEditingCanceled();
  478.         }

  479.         /**
  480.          * When an action is performed, editing is ended.
  481.          * @param e the action event
  482.          * @see #stopCellEditing
  483.          */
  484.         @Override
  485.         public void actionPerformed(ActionEvent e) {
  486.             InternalCellEditor.this.stopCellEditing();
  487.         }

  488.         /**
  489.          * When an item's state changes, editing is ended.
  490.          * @param e the action event
  491.          * @see #stopCellEditing
  492.          */
  493.         @Override
  494.         public void itemStateChanged(ItemEvent e) {
  495.             InternalCellEditor.this.stopCellEditing();
  496.         }

  497.         private Result<@Nullable Object> _tryConvert(Object value, Class<?> targetType) throws Exception {
  498.             if ( targetType == String.class ) {
  499.                 if ( value instanceof String )
  500.                     return Result.of(value);
  501.                 else if ( value instanceof Number )
  502.                     return Result.of(value.toString());
  503.                 else if ( value instanceof Boolean )
  504.                     return Result.of(value.toString());
  505.                 else if ( value instanceof Character )
  506.                     return Result.of(value.toString());
  507.                 else
  508.                     return Result.of(value.toString());
  509.             } else if ( targetType == Character.class ) {
  510.                 if ( value instanceof Character )
  511.                     return Result.of(value);
  512.                 else if ( value instanceof String ) {
  513.                     String str = (String) value;
  514.                     if ( str.length() == 1 )
  515.                         return Result.of(str.charAt(0));
  516.                 }
  517.             } else if ( targetType == Boolean.class ) {
  518.                 if ( value instanceof Boolean )
  519.                     return Result.of(value);
  520.                 else if ( value instanceof String ) {
  521.                     String str = (String) value;
  522.                     if ( str.equalsIgnoreCase("true") )
  523.                         return Result.of(true);
  524.                     if ( str.equalsIgnoreCase("false") )
  525.                         return Result.of(false);
  526.                 }
  527.             } else if ( Number.class.isAssignableFrom(targetType) ) {
  528.                 if ( value instanceof Number ) {
  529.                     if ( targetType == Integer.class )
  530.                         return Result.of(((Number) value).intValue());
  531.                     if ( targetType == Long.class )
  532.                         return Result.of(((Number) value).longValue());
  533.                     if ( targetType == Float.class )
  534.                         return Result.of(((Number) value).floatValue());
  535.                     if ( targetType == Double.class )
  536.                         return Result.of(((Number) value).doubleValue());
  537.                     if ( targetType == Byte.class )
  538.                         return Result.of(((Number) value).byteValue());
  539.                     if ( targetType == Short.class )
  540.                         return Result.of(((Number) value).shortValue());
  541.                 } else if ( value instanceof String ) {
  542.                     String str = (String) value;
  543.                     if ( targetType == Integer.class )
  544.                         return Result.of(Integer.parseInt(str));
  545.                     if ( targetType == Long.class )
  546.                         return Result.of(Long.parseLong(str));
  547.                     if ( targetType == Float.class )
  548.                         return Result.of(Float.parseFloat(str));
  549.                     if ( targetType == Double.class )
  550.                         return Result.of(Double.parseDouble(str));
  551.                     if ( targetType == Byte.class )
  552.                         return Result.of(Byte.parseByte(str));
  553.                     if ( targetType == Short.class )
  554.                         return Result.of(Short.parseShort(str));
  555.                 } else if ( value instanceof Boolean ) {
  556.                     if ( targetType == Integer.class )
  557.                         return Result.of(((Boolean) value) ? 1 : 0);
  558.                     if ( targetType == Long.class )
  559.                         return Result.of(((Boolean) value) ? 1L : 0L);
  560.                     if ( targetType == Float.class )
  561.                         return Result.of(((Boolean) value) ? 1.0f : 0.0f);
  562.                     if ( targetType == Double.class )
  563.                         return Result.of(((Boolean) value) ? 1.0 : 0.0);
  564.                     if ( targetType == Byte.class )
  565.                         return Result.of(((Boolean) value) ? (byte) 1 : (byte) 0);
  566.                     if ( targetType == Short.class )
  567.                         return Result.of(((Boolean) value) ? (short) 1 : (short) 0);
  568.                 }
  569.             }
  570.             throw new IllegalArgumentException(
  571.                     "Cannot convert value '"+value+"' to target type '"+targetType.getName()+"'."
  572.             );
  573.         }

  574.     }

  575. }