1 /*
2  * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4  *
5  * This code is free software; you can redistribute it and/or modify it
6  * under the terms of the GNU General Public License version 2 only, as
7  * published by the Free Software Foundation.  Oracle designates this
8  * particular file as subject to the "Classpath" exception as provided
9  * by Oracle in the LICENSE file that accompanied this code.
10  *
11  * This code is distributed in the hope that it will be useful, but WITHOUT
12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14  * version 2 for more details (a copy is included in the LICENSE file that
15  * accompanied this code).
16  *
17  * You should have received a copy of the GNU General Public License version
18  * 2 along with this work; if not, write to the Free Software Foundation,
19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20  *
21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22  * or visit www.oracle.com if you need additional information or have any
23  * questions.
24  */
25 
26 /**
27  * @test
28  * @key headful
29  * @bug 8194327
30  * @summary [macosx] AWT windows have incorrect main/key window behaviors
31  * @author Alan Snyder
32  * @run main/othervm/native TestMainKeyWindow
33  * @requires (os.family == "mac")
34  */
35 
36 import java.awt.*;
37 import java.awt.event.ActionEvent;
38 import java.awt.event.InputEvent;
39 import java.awt.event.KeyEvent;
40 import java.io.IOException;
41 import java.lang.reflect.InvocationTargetException;
42 import java.util.Objects;
43 import javax.swing.*;
44 
45 public class TestMainKeyWindow
46 {
47     static TestMainKeyWindow theTest;
48 
49     KeyStroke commandT = KeyStroke.getKeyStroke(KeyEvent.VK_T, KeyEvent.META_DOWN_MASK);
50 
51     int nextX = 130;
52 
53     private final MyFrame frame1;
54     private final MyFrame frame2;
55     private final Object COLOR_PANEL = "Color Panel";
56     private final Object NATIVE_WINDOW = "Native Window";
57 
58     // these bounds must agree with the native code that creates the windows
59     private Rectangle colorPanelBounds = new Rectangle(130, 300, 225, 400);  // approximate is OK
60     private Rectangle nativeWindowBounds = new Rectangle(130, 200, 200, 100);
61 
62     private Robot robot;
63 
64     private int actionCounter;
65     private Object actionTarget;
66 
67     private int failureCount;
68     private boolean isApplicationOpened;
69 
TestMainKeyWindow()70     public TestMainKeyWindow()
71     {
72         System.loadLibrary("testMainKeyWindow");
73 
74         JMenuBar defaultMenuBar = createMenuBar("Application", true);
75         Desktop.getDesktop().setDefaultMenuBar(defaultMenuBar);
76 
77         setup();
78 
79         frame1 = new MyFrame("Frame 1");
80         frame2 = new MyFrame("Frame 2");
81         frame1.setVisible(true);
82         frame2.setVisible(true);
83 
84         try {
85             robot = new Robot();
86             robot.setAutoDelay(50);
87         } catch (AWTException ex) {
88             throw new RuntimeException(ex);
89         }
90     }
91 
92     class MyFrame
93         extends JFrame
94     {
MyFrame(String title)95         public MyFrame(String title)
96             throws HeadlessException
97         {
98             super(title);
99 
100             JMenuBar mainMenuBar = createMenuBar(title, true);
101             setJMenuBar(mainMenuBar);
102             setBounds(nextX, 60, 200, 90);
103             nextX += 250;
104             JComponent contentPane = new JPanel();
105             setContentPane(contentPane);
106             contentPane.setLayout(new FlowLayout());
107             contentPane.add(new JCheckBox("foo", true));
108             InputMap inputMap = contentPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
109             inputMap.put(commandT, "test");
110             ActionMap actionMap = contentPane.getActionMap();
111             actionMap.put("test", new MyAction(title + " Key"));
112         }
113     }
114 
runTest()115     private void runTest()
116     {
117         failureCount = 0;
118         robot.waitForIdle();
119         performTest(frame1, false);
120         performTest(frame1, true);
121         performTest(frame2, false);
122         performTest(frame2, true);
123         performTest(NATIVE_WINDOW, false);
124         performTest(NATIVE_WINDOW, true);
125         performTest(COLOR_PANEL, false);
126         if (failureCount > 0) {
127             throw new RuntimeException("Test failed: " + failureCount + " failure(s)");
128         }
129     }
130 
performTest(Object windowIdentification, boolean selectColorPanel)131     private void performTest(Object windowIdentification, boolean selectColorPanel)
132     {
133         setupWindows(windowIdentification, selectColorPanel);
134 
135         performMenuShortcutTest(windowIdentification, selectColorPanel);
136         performMenuItemTest(windowIdentification, selectColorPanel);
137 
138         // test deactivating and reactivating the application
139         // the window state and behavior should be restored
140 
141         openOtherApplication();
142         activateApplication();
143         robot.delay(1000);
144 
145         performMenuShortcutTest(windowIdentification, selectColorPanel);
146         performMenuItemTest(windowIdentification, selectColorPanel);
147     }
148 
openOtherApplication()149     private void openOtherApplication() {
150         try {
151             String[] cmd = { "/usr/bin/open", "/Applications/System Preferences.app" };
152             Runtime.getRuntime().exec(cmd);
153             if (!isApplicationOpened) {
154                 String[] cmd2 = { "/usr/bin/osascript", "-e",
155                     "tell application \"System Preferences\" to set bounds of window 1 to {400, 180, 1068, 821}" };
156                 Runtime.getRuntime().exec(cmd2);
157             }
158             isApplicationOpened = true;
159         } catch (IOException ex) {
160             throw new RuntimeException("Unable to deactivate test application");
161         }
162         robot.delay(1000);
163     }
164 
performMenuShortcutTest(Object windowIdentification, boolean selectColorPanel)165     private void performMenuShortcutTest(Object windowIdentification, boolean selectColorPanel)
166     {
167         int currentActionCount = actionCounter;
168 
169         // Perform the menu shortcut
170         robot.keyPress(KeyEvent.VK_META);
171         robot.keyPress(KeyEvent.VK_T);
172         robot.keyRelease(KeyEvent.VK_T);
173         robot.keyRelease(KeyEvent.VK_META);
174         robot.waitForIdle();
175 
176         Object target = waitForAction(currentActionCount + 1);
177         boolean isDirectKey = windowIdentification instanceof Window && !selectColorPanel;
178         Object expectedTarget = getExpectedTarget(windowIdentification, isDirectKey);
179         if (!Objects.equals(target, expectedTarget)) {
180             failureCount++;
181             String configuration = getConfigurationName(windowIdentification, selectColorPanel);
182             System.err.println("***** Menu shortcut test failed for " + configuration + ". Expected: " + expectedTarget + ", Actual: " + target);
183         }
184     }
185 
performMenuItemTest(Object windowIdentification, boolean selectColorPanel)186     private void performMenuItemTest(Object windowIdentification, boolean selectColorPanel)
187     {
188         int currentActionCount = actionCounter;
189 
190         // Find the menu on the screen menu bar
191         // The location depends upon the application name which is the name of the first menu.
192         // Unfortunately, the application name can vary based on how the application is run.
193         // The work around is to make the menu and the menu item names very long.
194 
195         int menuBarX = 250;
196         int menuBarY = 11;
197         int menuItemX = menuBarX;
198         int menuItemY = 34;
199 
200         robot.mouseMove(menuBarX, menuBarY);
201         robot.delay(100);
202         robot.mousePress(InputEvent.BUTTON1_DOWN_MASK);
203         robot.delay(100);
204         robot.mouseMove(menuItemX, menuItemY);
205         robot.delay(100);
206         robot.mouseRelease(InputEvent.BUTTON1_DOWN_MASK);
207         robot.waitForIdle();
208 
209         Object target = waitForAction(currentActionCount + 1);
210         Object expectedTarget = getExpectedTarget(windowIdentification, false);
211         if (!Objects.equals(target, expectedTarget)) {
212             failureCount++;
213             String configuration = getConfigurationName(windowIdentification, selectColorPanel);
214             System.err.println("***** Menu item test failed for " + configuration + ". Expected: " + expectedTarget + ", Actual: " + target);
215         }
216     }
217 
getConfigurationName(Object windowIdentification, boolean selectColorPanel)218     private String getConfigurationName(Object windowIdentification, boolean selectColorPanel)
219     {
220         String name = "Unknown";
221         if (windowIdentification instanceof Window) {
222             Window w = (Window) windowIdentification;
223             name = getWindowTitle(w);
224         } else if (windowIdentification == NATIVE_WINDOW) {
225             name = "Native Window";
226         } else if (windowIdentification == COLOR_PANEL) {
227             name = "Color Panel";
228         }
229         if (selectColorPanel) {
230             return name + " with color panel";
231         } else {
232             return name;
233         }
234     }
235 
getExpectedTarget(Object windowIdentification, boolean isDirectKey)236     private Object getExpectedTarget(Object windowIdentification, boolean isDirectKey)
237     {
238         if (windowIdentification instanceof Window) {
239             Window w = (Window) windowIdentification;
240             String title = getWindowTitle(w);
241             if (isDirectKey) {
242                 title = title + " Key";
243             }
244             return title;
245         }
246         return "Application";
247     }
248 
getWindowTitle(Window w)249     private String getWindowTitle(Window w)
250     {
251         if (w instanceof Frame) {
252             Frame f = (Frame) w;
253             return f.getTitle();
254         }
255         if (w instanceof Dialog) {
256             Dialog d = (Dialog) w;
257             return d.getTitle();
258         }
259         throw new IllegalStateException();
260     }
261 
registerAction(Object target)262     private synchronized void registerAction(Object target)
263     {
264         actionCounter++;
265         actionTarget = target;
266     }
267 
waitForAction(int count)268     private synchronized Object waitForAction(int count)
269     {
270         try {
271             for (int i = 0; i < 10; i++) {
272                 if (actionCounter == count) {
273                     return actionTarget;
274                 }
275                 if (actionCounter > count) {
276                     throw new IllegalStateException();
277                 }
278                 wait(100);
279             }
280         } catch (InterruptedException ex) {
281         }
282         return "No Action";
283     }
284 
setupWindows(Object windowIdentification, boolean selectColorPanel)285     private void setupWindows(Object windowIdentification, boolean selectColorPanel)
286     {
287         clickOnWindowTitleBar(windowIdentification);
288         if (selectColorPanel) {
289             clickOnWindowTitleBar(COLOR_PANEL);
290         }
291     }
292 
clickOnWindowTitleBar(Object windowIdentification)293     private void clickOnWindowTitleBar(Object windowIdentification)
294     {
295         Rectangle bounds = getWindowBounds(windowIdentification);
296         int x = bounds.x + 70;  // to the right of the stoplight buttons
297         int y = bounds.y + 12;  // in the title bar
298         robot.mouseMove(x, y);
299         robot.mousePress(InputEvent.BUTTON1_DOWN_MASK);
300         robot.waitForIdle();
301         robot.mouseRelease(InputEvent.BUTTON1_DOWN_MASK);
302         robot.waitForIdle();
303     }
304 
getWindowBounds(Object windowIdentification)305     private Rectangle getWindowBounds(Object windowIdentification)
306     {
307         if (windowIdentification instanceof Window) {
308             Window w = (Window) windowIdentification;
309             return w.getBounds();
310         }
311         if (windowIdentification == COLOR_PANEL) {
312             return colorPanelBounds;
313         }
314         if (windowIdentification == NATIVE_WINDOW) {
315             return nativeWindowBounds;
316         }
317         throw new IllegalArgumentException();
318     }
319 
createMenuBar(String text, boolean isEnabled)320     JMenuBar createMenuBar(String text, boolean isEnabled)
321     {
322         JMenuBar mb = new JMenuBar();
323         // A very long name makes it more likely that the robot will hit the menu
324         JMenu menu = new JMenu("TestTestTestTestTestTestTestTestTestTest");
325         mb.add(menu);
326         JMenuItem item = new JMenuItem("TestTestTestTestTestTestTestTestTestTest");
327         item.setAccelerator(commandT);
328         item.setEnabled(isEnabled);
329         item.addActionListener(ev -> {
330             registerAction(text);
331         });
332         menu.add(item);
333         return mb;
334     }
335 
dispose()336     void dispose()
337     {
338         frame1.setVisible(false);
339         frame2.setVisible(false);
340         frame1.dispose();
341         frame2.dispose();
342         takedown();
343         Desktop.getDesktop().setDefaultMenuBar(null);
344         if (isApplicationOpened) {
345             try {
346                 String[] cmd = { "/usr/bin/osascript", "-e", "tell application \"System Preferences\" to close window 1" };
347                 Process p = Runtime.getRuntime().exec(cmd);
348                 p.waitFor();
349             } catch (IOException | InterruptedException ex) {
350             }
351         }
352     }
353 
354     class MyAction
355         extends AbstractAction
356     {
357         String text;
358 
MyAction(String text)359         public MyAction(String text)
360         {
361             super("Test");
362 
363             this.text = text;
364         }
365 
366         @Override
actionPerformed(ActionEvent e)367         public void actionPerformed(ActionEvent e)
368         {
369             registerAction(text);
370         }
371     }
372 
setup()373     private static native void setup();
takedown()374     private static native void takedown();
activateApplication()375     private static native void activateApplication();
376 
main(String[] args)377     public static void main(String[] args)
378     {
379         if (!System.getProperty("os.name").contains("OS X")) {
380             System.out.println("This test is for MacOS only. Automatically passed on other platforms.");
381             return;
382         }
383 
384         System.setProperty("apple.laf.useScreenMenuBar", "true");
385 
386         try {
387             runSwing(() -> {
388                 theTest = new TestMainKeyWindow();
389             });
390             theTest.runTest();
391         } finally {
392             if (theTest != null) {
393                 runSwing(() -> {
394                     theTest.dispose();
395                 });
396             }
397         }
398     }
399 
runSwing(Runnable r)400     private static void runSwing(Runnable r)
401     {
402         try {
403             SwingUtilities.invokeAndWait(r);
404         } catch (InterruptedException e) {
405         } catch (InvocationTargetException e) {
406             throw new RuntimeException(e);
407         }
408     }
409 }
410