0

i'm developing a simple application using java swing that i would like to used in Windows, MacOS and Linux. Of course i'm trying to integrate it with the OS the best i can.

For MacOS i have this code that allows me to set the app name in the global menu and the action for the "About" button.

I'm using the following code:

  if(System.getProperty("os.name").toUpperCase().startsWith("MAC")){
        System.setProperty("com.apple.mrj.application.apple.menu.about.name", "My app name");
        System.setProperty("apple.awt.application.name", "My app name");
        //Need for macos global menubar
        System.setProperty("apple.laf.useScreenMenuBar", "true");
        try{
        com.apple.eawt.Application app = com.apple.eawt.Application.getApplication();
        app.setDockIconImage(Toolkit.getDefaultToolkit().getImage(MainGUI.class.getResource("images/icon.png")));
        app.setAboutHandler(new com.apple.eawt.AboutHandler() {
            @Override
            public void handleAbout(com.apple.eawt.AppEvent.AboutEvent aboutEvent) {
                AboutDialog a = new AboutDialog();
                a.setTitle("About");
                a.pack();
                a.setResizable(false);
                centerDialogInScreen(a);
                a.setVisible(true);
            }
        });
        } catch (Throwable e){
            //This means that the application is not being run on MAC OS.
            //Just do nothing and go on...
        }
    }

When i run my application on a non-macos JAVA since its JRE doesn't have the com.apple.eawt.* classes the JVM should throw an NoDefClassFoundError that i'm catching and go on, right?

It doesn't seem to be doing that, when i launch my application ".jar" i get the following (on Windows):

Error: A JNI error has occurred, please check your installation and  try again
Exception in thread "main" java.lang.NoClassDefFoundError:  com/apple/eawt/AboutHandler
       at java.lang.Class.getDeclaredMethods0(Native Method)
       at java.lang.Class.privateGetDeclaredMethods(Unknown Source)
       at java.lang.Class.privateGetMethodRecursive(Unknown Source)
       at java.lang.Class.getMethod0(Unknown Source)
       at java.lang.Class.getMethod(Unknown Source)
       at sun.launcher.LauncherHelper.validateMainClass(Unknown Source)
       at sun.launcher.LauncherHelper.checkAndLoadMain(Unknown Source)
Caused by: java.lang.ClassNotFoundException: com.apple.eawt.AboutHandler
       at java.net.URLClassLoader.findClass(Unknown Source)
       at java.lang.ClassLoader.loadClass(Unknown Source)
       at sun.misc.Launcher$AppClassLoader.loadClass(Unknown Source)
       at java.lang.ClassLoader.loadClass(Unknown Source)
       ... 7 more

What am i missing here?

loveMeansNothing
  • 361
  • 1
  • 5
  • 20
  • I would create local copies of AboutHandler and AboutEvent with the same signature & package declaration as original – HRgiger Mar 15 '17 at 19:02
  • What is in your `else` part of the code? – Germann Arlington Mar 15 '17 at 19:07
  • @HRgiger I've tried to use OpenJDKs source code for that classes but found out that was a dumb attempt since it would increase the size of the package. If i created that files how would i make the MACOS JVM pick the installation ones instead of the ones on my package? As a last resort i could just create a different main file for each platform and generate a different application jar for each platform. But, since i'm lazy (in a good way i think) i want generate a single package for all platforms. – loveMeansNothing Mar 15 '17 at 19:12
  • @GermannArlington There's no else yet. FWIW that code lives inside the main method that later invokes: "SwingUtilities.invokeLater(new MainGUI());". – loveMeansNothing Mar 15 '17 at 19:13
  • You are instantiating `com.apple.eawt.Application` inside the `if` statement. What are you instantiating outside it? – Germann Arlington Mar 15 '17 at 19:16
  • @GermannArlington i'm instantiating nothing outside of the if statement. That main only has that if statement and "SwingUtilities.invokeLater(new MainGUI());" in the end. I even used "com.apple.eawt.Application" to avoid using import for that class. – loveMeansNothing Mar 15 '17 at 19:20
  • You must be instantiating something else in `MainGUI()`. It may be indirect instantiation or declaration – Germann Arlington Mar 15 '17 at 22:40

4 Answers4

2

The exact time when a NoClassDefFoundError will be thrown is not fixed, but may depend on JVM implementation details. In your case, it’s the verification of the class. In HotSpot, the Verifier will not load all referenced classes, but apparently it tried to load AboutHandler in your case, perhaps caused by the fact that your class has an inner class that implements AboutHandler and the Verifier wanted to check the consistency of the type hierarchy, i.e. whether AboutHandler really is an interface. The exact details are not so important as even when you manage to work-around it, the result would be fragile and may break in other versions or alternative JVM implementations.

If you want to be on the safe side, you must not have a direct reference to classes that might not be present. So you could do the entire operation reflectively, using Class.forName, Method.invoke, etc., but this would make any nontrivial code cumbersome. The simpler solution is to put the entire MacOS specific code into a class of its own, say MacSetup. Then, you only have to load this class via Class.forName (only after checking that you are running on MacOS) to detach it.

public class MacSetup implements Runnable {
    @Override
    public void run() {
        System.setProperty("com.apple.mrj.application.apple.menu.about.name", "My app name");
        System.setProperty("apple.awt.application.name", "My app name");
        //Need for macos global menubar
        System.setProperty("apple.laf.useScreenMenuBar", "true");
        com.apple.eawt.Application app = com.apple.eawt.Application.getApplication();
        app.setDockIconImage(Toolkit.getDefaultToolkit().getImage(MainGUI.class.getResource("images/icon.png")));
        app.setAboutHandler(new com.apple.eawt.AboutHandler() {
            @Override
            public void handleAbout(com.apple.eawt.AppEvent.AboutEvent aboutEvent) {
                AboutDialog a = new AboutDialog();
                a.setTitle("About");
                a.pack();
                a.setResizable(false);
                centerDialogInScreen(a);
                a.setVisible(true);
            }
        });
    }
}

Main class:

if(System.getProperty("os.name").toUpperCase().startsWith("MAC")) {
    try {
        Class.forName("MacSetup").asSubclass(Runnable.class).newInstance().run();
    } catch (ClassNotFoundException | InstantiationException | IllegalAccessException ex) {
        // since this code is only executed when we *are* on MacOS, we should report it
        Logger.getLogger("MacSetup").log(Level.SEVERE, null, ex);
    }
}

So the main class is detached from the MacSetup class and all of its references, as it loads this class reflectively and invokes its implementation method via the always-present Runnable interface, reducing the reflective operations to the necessary minimum.

Holger
  • 285,553
  • 42
  • 434
  • 765
  • You definitely know your java sir. The main problem with that is that the set app icon and set about handle code must be run in the main thread (at least is the only way it seems to work). – loveMeansNothing Mar 20 '17 at 09:16
  • It does run in the main thread. The use of the `Runnable` interface does not imply multi-threading, here, it’s just used as ordinary interface have a method without parameters returning `void`. – Holger Mar 20 '17 at 09:22
  • I see. Your approach seems the broke the icon and aboutHandler in macOS, but it sure is a clever and clean one... – loveMeansNothing Mar 20 '17 at 09:27
  • It shouldn’t, so it’s worth investigating, i.e. debugging, the issue. I used a class without `package` for simplification, but you likely want to place `MacSetup` into the same package as your main class and have to adjust the `Class.forName("MacSetup")` statement. Further, it might help checking whether `MainGUI.class.getResource("images/icon.png")` works in the context of the other class. – Holger Mar 20 '17 at 09:35
  • That's exactly what i've done. I've lost some time doing different tests. But then i got it working (the icon at least) using reflection (see my answer) and didn't looked back again. – loveMeansNothing Mar 20 '17 at 09:41
  • I assume, it would be great to find out, why this doesn’t work, e.g. for future readers. If resolving the resource only works from the main class, it could help turning the `MacSetup` into a `Consumer` instead of `Runnable` and let the main class pass the already resolved resource as argument to the `accept` method. However, if you don’t want to invest more time, never mind. – Holger Mar 20 '17 at 09:46
  • I might have some time in the next weeks. I'll keep posting until i figure it out. Anyway, thanks for you support! – loveMeansNothing Mar 20 '17 at 09:48
1

The apple classes are not standard Java classes and thus will not be available outside the Mac platform. If you want your application to be cross platform the best option is to avoid using those classes, even if it means less integration with MacOS.

On the other hand, you could create a specific 'About' class for different operating systems and have that extended for specific operating systems (e.g. MacAbout, WinAbout,....)

Determine the class' name runtime, and use Class.forName to load the class dynamically, avoiding that the JRE needs to resolve the non-existing apple classes.

But, honestly, I'd go for the first option and make a real platform independent application.

M. le Rutte
  • 3,525
  • 3
  • 18
  • 31
  • 1
    This compete [example](http://stackoverflow.com/a/30308671/230513) illustrates the `Class.forName()` approach; see also `OSXAdapter`, cited [here](http://stackoverflow.com/a/2061318/230513). – trashgod Mar 15 '17 at 22:28
0

Well, i managed to solve part of the issue using reflection. The icon part is working fine, and i think the "AboutHandler" part would work just fine if i were able to translate the code to reflection. Since it's not critical and i have some bigger fish to fry i didn't bothered too much. If somebody manages how to get the the AboutHandler using reflection i'll mark it as correct.

if(System.getProperty("os.name").toUpperCase().startsWith("MAC")) {
System.setProperty("com.apple.mrj.application.apple.menu.about.name", "My App Name");
System.setProperty("apple.awt.application.name", "My App Short name");
//Need for macos global menubar
System.setProperty("apple.laf.useScreenMenuBar", "true");

try {
    Class application = Class.forName("com.apple.eawt.Application");
    Method getApplication = application.getMethod("getApplication");
    Object instance = getApplication.invoke(application);

    Class[] params = new Class[1];
    params[0] = Image.class;
    Method setIcon = application.getMethod("setDockIconImage",params);
    setIcon.invoke(instance,Toolkit.getDefaultToolkit().getImage(MainGUI.class.getResource("images/icon.png")));

} catch (ClassNotFoundException | NoSuchMethodException |
        SecurityException | IllegalAccessException |
        IllegalArgumentException | InvocationTargetException exp) {
    exp.printStackTrace(System.err);
}
}
loveMeansNothing
  • 361
  • 1
  • 5
  • 20
0

the com.apple java packages are deprecated and removed in java9 (now default on MacOS high sierra)

see http://openjdk.java.net/jeps/272 on the new api to use

package java.awt;

public class Desktop {

    /* ... */

    /**
     * Adds sub-types of {@link AppEventListener} to listen for notifications
     * from the native system.
     *
     * @param listener
     * @see AppForegroundListener
     * @see AppHiddenListener
     * @see AppReOpenedListener
     * @see AppScreenSleepListener
     * @see AppSystemSleepListener
     * @see AppUserSessionListener
     */

    public void addAppEventListener(final AppEventListener listener) {}

    /**
     * Requests user attention to this application (usually through bouncing the Dock icon).
     * Critical requests will continue to bounce the Dock icon until the app is activated.
     *
     */
    public void requestUserAttention(final boolean critical) {}

    /**
     * Attaches the contents of the provided PopupMenu to the application's Dock icon.
     */
    public void setDockMenu(final PopupMenu menu) {}

    /**
     * Changes this application's Dock icon to the provided image.
     */
    public void setDockIconImage(final Image image) {}


    /**
     * Affixes a small system provided badge to this application's Dock icon. Usually a number.
     */
    public void setDockIconBadge(final String badge) {}

    /**
     * Displays or hides a progress bar or other indicator in
     * the dock.
     *
     * @see DockProgressState.NORMAL
     * @see DockProgressState.PAUSED
     * @see DockProgressState.ERROR
     *
     * @see #setDockProgressValue
     */
    public void setDockProgressState(int state) {}

    /**
     * Sets the progress bar's current value to {@code n}.
     */
    public void setDockProgressValue(int n) {}

    /**
     * Tests whether a feature is supported on the current platform.
     */

    public boolean isSupportedFeature(Feature f) {}

    /* ... */
}
Jens Timmerman
  • 9,316
  • 1
  • 42
  • 48