diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f3b62bc..a2f8635b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,8 @@ FlatLaf Change Log applications: `lighten()`, `darken()`, `saturate()`, `desaturate()`, `spin()`, `tint()`, `shade()` and `luma()`. - Support defining fonts in FlatLaf properties files. (issue #384) +- Extras: Added class `FlatDesktop` for easy integration into macOS screen menu + (About, Preferences and Quit) when using Java 8. #### Fixed bugs diff --git a/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/DemoFrame.java b/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/DemoFrame.java index be913508..f622b28e 100644 --- a/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/DemoFrame.java +++ b/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/DemoFrame.java @@ -37,6 +37,7 @@ import com.formdev.flatlaf.FlatLightLaf; import com.formdev.flatlaf.demo.HintManager.Hint; import com.formdev.flatlaf.demo.extras.*; import com.formdev.flatlaf.demo.intellijthemes.*; +import com.formdev.flatlaf.extras.FlatDesktop; import com.formdev.flatlaf.extras.FlatAnimatedLafChange; import com.formdev.flatlaf.extras.FlatSVGIcon; import com.formdev.flatlaf.extras.FlatUIDefaultsInspector; @@ -81,6 +82,22 @@ class DemoFrame if( tabIndex >= 0 && tabIndex < tabbedPane.getTabCount() && tabIndex != tabbedPane.getSelectedIndex() ) tabbedPane.setSelectedIndex( tabIndex ); + // hide some menu items on macOS + if( SystemInfo.isMacOS ) { + exitMenuItem.setVisible( false ); + aboutMenuItem.setVisible( false ); + + // do not use HTML text on macOS + htmlMenuItem.setText( "some text" ); + } + + // integrate into macOS screen menu + FlatDesktop.setAboutHandler( this::aboutActionPerformed ); + FlatDesktop.setPreferencesHandler( this::showPreferences ); + FlatDesktop.setQuitHandler( response -> { + response.performQuit(); + } ); + SwingUtilities.invokeLater( () -> { showHints(); } ); @@ -174,6 +191,13 @@ class DemoFrame "About", JOptionPane.PLAIN_MESSAGE ); } + private void showPreferences() { + JOptionPane.showMessageDialog( this, + "Sorry, but FlatLaf Demo does not have preferences. :(\n" + + "This dialog is here to demonstrate usage of class 'FlatDesktop' on macOS.", + "Preferences", JOptionPane.PLAIN_MESSAGE ); + } + private void selectedTabChanged() { DemoPrefs.getState().putInt( FlatLafDemo.KEY_TAB, tabbedPane.getSelectedIndex() ); } @@ -346,7 +370,7 @@ class DemoFrame private JLabel accentColorLabel; private void initAccentColors() { - accentColorLabel = new JLabel( "Accent color:" ); + accentColorLabel = new JLabel( "Accent color: " ); toolBar.add( Box.createHorizontalGlue() ); toolBar.add( accentColorLabel ); @@ -412,7 +436,7 @@ class DemoFrame JMenuItem openMenuItem = new JMenuItem(); JMenuItem saveAsMenuItem = new JMenuItem(); JMenuItem closeMenuItem = new JMenuItem(); - JMenuItem exitMenuItem = new JMenuItem(); + exitMenuItem = new JMenuItem(); JMenu editMenu = new JMenu(); JMenuItem undoMenuItem = new JMenuItem(); JMenuItem redoMenuItem = new JMenuItem(); @@ -431,7 +455,7 @@ class DemoFrame JMenuItem structureViewMenuItem = new JMenuItem(); JMenuItem propertiesViewMenuItem = new JMenuItem(); JMenuItem menuItem2 = new JMenuItem(); - JMenuItem menuItem1 = new JMenuItem(); + htmlMenuItem = new JMenuItem(); JRadioButtonMenuItem radioButtonMenuItem1 = new JRadioButtonMenuItem(); JRadioButtonMenuItem radioButtonMenuItem2 = new JRadioButtonMenuItem(); JRadioButtonMenuItem radioButtonMenuItem3 = new JRadioButtonMenuItem(); @@ -449,7 +473,7 @@ class DemoFrame JMenuItem showHintsMenuItem = new JMenuItem(); JMenuItem showUIDefaultsInspectorMenuItem = new JMenuItem(); JMenu helpMenu = new JMenu(); - JMenuItem aboutMenuItem = new JMenuItem(); + aboutMenuItem = new JMenuItem(); toolBar = new JToolBar(); JButton backButton = new JButton(); JButton forwardButton = new JButton(); @@ -638,9 +662,9 @@ class DemoFrame menuItem2.setEnabled(false); viewMenu.add(menuItem2); - //---- menuItem1 ---- - menuItem1.setText("some HTML text"); - viewMenu.add(menuItem1); + //---- htmlMenuItem ---- + htmlMenuItem.setText("some HTML text"); + viewMenu.add(htmlMenuItem); viewMenu.addSeparator(); //---- radioButtonMenuItem1 ---- @@ -886,6 +910,8 @@ class DemoFrame } // JFormDesigner - Variables declaration - DO NOT MODIFY //GEN-BEGIN:variables + private JMenuItem exitMenuItem; + private JMenuItem htmlMenuItem; private JMenu fontMenu; private JMenu optionsMenu; private JCheckBoxMenuItem windowDecorationsCheckBoxMenuItem; @@ -894,6 +920,7 @@ class DemoFrame private JCheckBoxMenuItem underlineMenuSelectionMenuItem; private JCheckBoxMenuItem alwaysShowMnemonicsMenuItem; private JCheckBoxMenuItem animatedLafChangeMenuItem; + private JMenuItem aboutMenuItem; private JToolBar toolBar; private JTabbedPane tabbedPane; private ControlBar controlBar; diff --git a/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/DemoFrame.jfd b/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/DemoFrame.jfd index b74c088c..326c9828 100644 --- a/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/DemoFrame.jfd +++ b/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/DemoFrame.jfd @@ -1,4 +1,4 @@ -JFDML JFormDesigner: "7.0.4.0.360" Java: "11.0.11" encoding: "UTF-8" +JFDML JFormDesigner: "7.0.4.0.360" Java: "17" encoding: "UTF-8" new FormModel { contentType: "form/swing" @@ -170,6 +170,9 @@ new FormModel { "text": "Exit" "accelerator": static javax.swing.KeyStroke getKeyStroke( 81, 4226, false ) "mnemonic": 88 + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "exitActionPerformed", false ) ) } ) } ) @@ -285,8 +288,11 @@ new FormModel { "enabled": false } ) add( new FormComponent( "javax.swing.JMenuItem" ) { - name: "menuItem1" + name: "htmlMenuItem" "text": "some HTML text" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } } ) add( new FormComponent( "javax.swing.JPopupMenu$Separator" ) { name: "separator8" @@ -415,6 +421,9 @@ new FormModel { name: "aboutMenuItem" "text": "About" "mnemonic": 65 + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "aboutActionPerformed", false ) ) } ) } ) diff --git a/flatlaf-extras/src/main/java/com/formdev/flatlaf/extras/FlatDesktop.java b/flatlaf-extras/src/main/java/com/formdev/flatlaf/extras/FlatDesktop.java new file mode 100644 index 00000000..927fb137 --- /dev/null +++ b/flatlaf-extras/src/main/java/com/formdev/flatlaf/extras/FlatDesktop.java @@ -0,0 +1,218 @@ +/* + * Copyright 2021 FormDev Software GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.formdev.flatlaf.extras; + +import java.awt.Desktop; +import java.awt.EventQueue; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.function.Consumer; +import com.formdev.flatlaf.util.LoggingFacade; +import com.formdev.flatlaf.util.SystemInfo; + +/** + * Supports interaction with desktop. + *

+ * Note: If you application requires Java 9 or later, + * then use class {@link java.awt.Desktop} instead of this class. + * + * @author Karl Tauber + * @since 2 + */ +public class FlatDesktop +{ + public enum Action { APP_ABOUT, APP_PREFERENCES, APP_QUIT_HANDLER }; + + /** + * Checks whether the given action is supported on the current platform. + */ + public static boolean isSupported( Action action ) { + if( SystemInfo.isJava_9_orLater ) { + try { + return Desktop.getDesktop().isSupported( Enum.valueOf( Desktop.Action.class, action.name() ) ); + } catch( Exception ex ) { + LoggingFacade.INSTANCE.logSevere( null, ex ); + return false; + } + } else if( SystemInfo.isMacOS ) + return true; + else + return false; + } + + /** + * Sets a handler to show a custom About dialog. + *

+ * Useful for macOS to enable menu item "MyApp > About". + *

+ * Uses: + *

+ */ + public static void setAboutHandler( Runnable aboutHandler ) { + if( !isSupported( Action.APP_ABOUT ) ) + return; + + String handlerClassName; + if( SystemInfo.isJava_9_orLater ) + handlerClassName = "java.awt.desktop.AboutHandler"; + else if( SystemInfo.isMacOS ) + handlerClassName = "com.apple.eawt.AboutHandler"; + else + return; + + setHandler( "setAboutHandler", handlerClassName, aboutHandler ); + } + + /** + * Sets a handler to show a custom Preferences dialog. + *

+ * Useful for macOS to enable menu item "MyApp > Preferences". + *

+ * Uses: + *

+ */ + public static void setPreferencesHandler( Runnable preferencesHandler ) { + if( !isSupported( Action.APP_PREFERENCES ) ) + return; + + String handlerClassName; + if( SystemInfo.isJava_9_orLater ) + handlerClassName = "java.awt.desktop.PreferencesHandler"; + else if( SystemInfo.isMacOS ) + handlerClassName = "com.apple.eawt.PreferencesHandler"; + else + return; + + setHandler( "setPreferencesHandler", handlerClassName, preferencesHandler ); + } + + private static void setHandler( String setHandlerMethodName, String handlerClassName, + Runnable handler ) + { + try { + Object desktopOrApplication = getDesktopOrApplication(); + Class handlerClass = Class.forName( handlerClassName ); + + Method m = desktopOrApplication.getClass().getMethod( setHandlerMethodName, handlerClass ); + m.invoke( desktopOrApplication, Proxy.newProxyInstance( FlatDesktop.class.getClassLoader(), + new Class[] { handlerClass }, + (proxy, method, args) -> { + // Use invokeLater to release the listener firing for the case + // that the action listener shows a modal dialog. + // This (hopefully) prevents application hunging. + EventQueue.invokeLater( () -> { + handler.run(); + } ); + return null; + } ) ); + } catch( Exception ex ) { + LoggingFacade.INSTANCE.logSevere( null, ex ); + } + } + + /** + * Sets a handler which is invoked when the application should quit. + * The handler must invoke either {@link QuitResponse#performQuit} or + * {@link QuitResponse#cancelQuit}. + *

+ * Useful for macOS to get notified when user clicks menu item "MyApp > Quit". + *

+ * Uses: + *

+ */ + public static void setQuitHandler( Consumer quitHandler ) { + if( !isSupported( Action.APP_QUIT_HANDLER ) ) + return; + + String handlerClassName; + if( SystemInfo.isJava_9_orLater ) + handlerClassName = "java.awt.desktop.QuitHandler"; + else if( SystemInfo.isMacOS ) + handlerClassName = "com.apple.eawt.QuitHandler"; + else + return; + + try { + Object desktopOrApplication = getDesktopOrApplication(); + Class handlerClass = Class.forName( handlerClassName ); + + Method m = desktopOrApplication.getClass().getMethod( "setQuitHandler", handlerClass ); + m.invoke( desktopOrApplication, Proxy.newProxyInstance( FlatDesktop.class.getClassLoader(), + new Class[] { handlerClass }, + (proxy, method, args) -> { + Object response = args[1]; + String responseClass = SystemInfo.isJava_9_orLater + ? "java.awt.desktop.QuitResponse" + : "com.apple.eawt.QuitResponse"; + quitHandler.accept( new QuitResponse() { + @Override + public void performQuit() { + try { + Class.forName( responseClass ).getMethod( "performQuit" ).invoke( response ); + } catch( Exception ex ) { + LoggingFacade.INSTANCE.logSevere( null, ex ); + } + } + + @Override + public void cancelQuit() { + try { + Class.forName( responseClass ).getMethod( "cancelQuit" ).invoke( response ); + } catch( Exception ex ) { + LoggingFacade.INSTANCE.logSevere( null, ex ); + } + } + + } ); + return null; + } ) ); + } catch( Exception ex ) { + LoggingFacade.INSTANCE.logSevere( null, ex ); + } + } + + private static Object getDesktopOrApplication() throws Exception { + if( SystemInfo.isJava_9_orLater ) + return Desktop.getDesktop(); + else if( SystemInfo.isMacOS ) { + try { + Class cls = Class.forName( "com.apple.eawt.Application" ); + return cls.getMethod( "getApplication" ).invoke( null ); + } catch( Exception ex ) { + LoggingFacade.INSTANCE.logSevere( null, ex ); + throw new UnsupportedOperationException(); + } + } else + throw new UnsupportedOperationException(); + } + + //---- interface QuitResponse --------------------------------------------- + + public interface QuitResponse { + void performQuit(); + void cancelQuit(); + } +}