<Swing Dev> Public API for internal Swing classes.

Walter Laan WLaan at costengineering.eu
Tue Aug 4 07:50:51 UTC 2015


Below a use case where I had to cast to sun.swing.UIAction to correctly check if it is enabled or not.
Let me know if I need to make the bug report or if you can add it as a comment to any existing issue.

The issue with sun.swing.UIAction is as follows:

It does not return the correct value from #isEnabled(), so you need to cast and call #isEnabled(Object) instead (check argument). This is because a single instance of UIAction is shared between all components with the same UI class, I assume originally for performance reasons.
If you just want to perform the action when really enabled, you can use SwingUtilities#notifyAction(Action, KeyStroke, KeyEvent, Object, int) where the Object is the 'sender' (the action event source component) which is passed to UIAction#isEnabled(Object).
It is technically possible to work around it with current public API, but that means not using any of the Swing UI classes at all and rewrite them without using UIAction.

Solutions:
1) Fix UIAction by removing #isEnabled(Object) and have an instance per component
Lots of work and large impact.
Swing API users can remove references to UIAction and just call Action#isEnabled()

2) Make UIAction public API (move to javax.swing.plaf?)
Simple refactor but lots of files changed
Swing API users only need to update their imports

3) Do nothing and force Swing API to do point 1 themselves by re-implementing the components and UI classes using only public API
Even more work than first solution but only for Swing API users
Or they can access through reflection if that is not blocked by the module system? It will probably depend on the SecurityManager I guess.

4) Provide boolean SwingUtilities.canNotifyAction(Action, Object) which returns true if #notifyAction(Action, KeyStroke, KeyEvent, Object, int) would call action performed
Minimal work and minimal impact
Swing API users need to change code from ((sun.swing.UIAction) action).isEnabled(component) to SwingUtilities.canNotifyAction(component, action)

5) Something else?

My preference would be solution 4 due to minimal impact - solution 2 also has not much impact but then you have public API which does not implement the Action interface correctly. Solution 1 would be the correct one but a lot of work.

A simple test case:

import java.awt.EventQueue;

import javax.swing.Action;
import javax.swing.JTable;
import javax.swing.table.DefaultTableModel;

public class TestUIAction {

    public static void main(String[] args) {
        EventQueue.invokeLater(new Runnable() {

            @Override
            public void run() {
                JTable table = new JTable(new DefaultTableModel(1, 1));

                Action action = table.getActionMap().get("cancel");
                System.out.println("JTable#isEditing() = " + table.isEditing());
                System.out.println("Action#isEnabled() = " + action.isEnabled() + " but should be false!");                                	System.out.println("UIAction#isEnabled(Object) = " + ((sun.swing.UIAction) action).isEnabled(table));
                
                table.editCellAt(0, 0);
                
                System.out.println("JTable#isEditing() = " + table.isEditing());
                System.out.println("Action#isEnabled() = " + action.isEnabled());                	System.out.println("UIAction#isEnabled(Object) = " + ((sun.swing.UIAction) action).isEnabled(table));
            }
        });
    }
}

Output:
JTable#isEditing() = false
Action#isEnabled() = true but should be false!
UIAction#isEnabled(Object) = false

JTable#isEditing() = true
Action#isEnabled() = true
UIAction#isEnabled(Object) = true


Below a (quite long) scenario is which I had to use it:

Using Jidesoft HierarchicalTable (see http://www.jidesoft.com/images/hierarchicaltable.png) which has (multiple) JTables as child components of a JTable. Their implementation has a work around to avoid keystrokes from being processed by the parent table if the focus is in a child component table. They do this by placing a JPanel between the parent and child table which registers a no-op action for each action in the JTable Input/ActionMap.

For example:
-top row is selected in the focused child table
-users presses up arrow
-child table up action is not enabled (since top row selection cannot move up)
-key press goes up to the inserted panel and is consumed (action performed that does nothing)
-parent table up arrow action does not get executed so its row selection is not changed

As mentioned this is a workaround to avoid the parent component handling the key input, but a 'correct' solution would require re-writing BasicTableUI (and all the sub-classes for all the look and feels) for their HierarchicalTable instead of re-using the JTable UI.
To improve the hack in the case of the escape key, which I want to use to close a JDialog the HierarchicalTable (skip the parent table actions instead of consuming it before gets there), I changed the no-op action to be only enabled if the original action is enabled in the parent table.

For example:
-child table is focused and not cell editing
-user presses escape
-child table 'cancel cell edit action' does not get notified because is not enabled
-key press goes up the inserted panel but does not consume the action because the parent table 'cancel cell edit' action is also not enabled
-parent table 'cancel cell edit action' does not get notified because is not enabled
-eventually key press comes to the JDialog root pane and executes the close dialog action

But since the 'cancel cell edit action' is an sun.swing.UIAction, to check if it is really enabled, I need to cast to the interal API and call #isEnabled(parentTable):

    /**
     * Mute an action by doing nothing if the original action is enabled
     */
    private static class MutedAction extends AbstractAction {
        private final Object sender;
        private final Action action;

        public MutedAction(Object sender, Action action) {
            this.sender = sender;
            this.action = action;
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            // muted
        }

        @Override
        public boolean isEnabled() {
            if(action instanceof sun.swing.UIAction) {
                return ((sun.swing.UIAction) action).isEnabled(sender);
            }
            else {
                return action.isEnabled();
            }
        }
    }


Kind regards,
Walter Laan
Cost Engineering Consultancy

-----Original Message-----
From: swing-dev [mailto:swing-dev-bounces at openjdk.java.net] On Behalf Of Alexander Scherbatiy
Sent: maandag 3 augustus 2015 13:22
To: Van Den Borre, Koen <Koen.VanDenBorre at esko.com>
Cc: swing-dev at openjdk.java.net; macosx-port-dev at openjdk.java.net
Subject: Re: <Swing Dev> Public API for internal Swing classes.



  Hello Koen,

  Are you using the isEnabled(Object sender) method just to separate a logic that checks that an action needs to be executed from the action execution in the same way as it it done in the UIAction?

  Could you file an enhancement on it and provide a simple use case: 
http://bugreport.java.com/bugreport

  Thanks,
  Alexandr.


On 7/27/2015 4:13 PM, Van Den Borre, Koen wrote:
> Hey,
>
> We are using sun.swing.UIAction in a custom ListUI where we override the following method and use the sender object:
>
> @Override
> public boolean isEnabled(Object sender)
>
> Regards,
>
> Koen
>
>
> On 27 Jul 2015, at 14:30, Alexander Scherbatiy <alexandr.scherbatiy at oracle.com> wrote:
>
>> According to the JEP 200: The Modular JDK (see 
>> http://openjdk.java.net/jeps/200) we expect that the standard Java SE modules will not export any internal packages.
>>
>> It means that classes from internal packages (like sun.swing) will not be accessible.
>>
>> For example:
>>   sun.swing.FilePane
>>   sun.swing.SwingUtilities2
>>   sun.swing.sun.swing.plaf.synth.SynthIcon
>> and others.
>>
>>
>> Please, let us known if you are using the internal Swing API and it is not possible to replace it by public API.
>>
>> There are some known requests:
>>
>>   JDK-8132119 Provide public API for text related methods in SwingUtilities2
>>     https://bugs.openjdk.java.net/browse/JDK-8132119
>>
>>   JDK-8132120 Provide public API for screen menu bar support on MacOS
>>     https://bugs.openjdk.java.net/browse/JDK-8132120
>>
>>   JDK-6274842 RFE: Provide a means for a custom look and feel to use desktop font antialiasing settings.
>>     https://bugs.openjdk.java.net/browse/JDK-6274842
>>
>>
>> If you don't know if you use these types (because you use 3rd party 
>> jars) you can use the JDK 8 "jdeps" tool to find such dependencies :-
>>
>> ~/jdk1.8/bin/jdeps
>> Usage: jdeps <options> <classes...>
>> where <classes> can be a pathname to a .class file, a directory, a 
>> JAR file, or a fully-qualified class name
>>
>> Thanks,
>> Alexandr.
>>




More information about the swing-dev mailing list