1 /* 2 * Copyright (c) 2012, 2015, 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 package com.sun.javafx.tk.quantum; 27 28 import com.sun.javafx.menu.CheckMenuItemBase; 29 import com.sun.javafx.menu.MenuBase; 30 import com.sun.javafx.menu.MenuItemBase; 31 import com.sun.javafx.menu.RadioMenuItemBase; 32 import com.sun.javafx.menu.SeparatorMenuItemBase; 33 import com.sun.javafx.PlatformUtil; 34 import com.sun.javafx.tk.TKSystemMenu; 35 import com.sun.glass.events.KeyEvent; 36 import com.sun.glass.ui.Application; 37 import com.sun.glass.ui.Menu; 38 import com.sun.glass.ui.MenuBar; 39 import com.sun.glass.ui.MenuItem; 40 import com.sun.glass.ui.Pixels; 41 42 import java.util.ArrayList; 43 import java.util.List; 44 45 import javafx.collections.ListChangeListener; 46 import javafx.collections.ObservableList; 47 import javafx.beans.InvalidationListener; 48 import javafx.beans.Observable; 49 import javafx.event.ActionEvent; 50 import javafx.event.EventHandler; 51 import javafx.scene.image.Image; 52 import javafx.scene.image.ImageView; 53 import javafx.scene.input.KeyCode; 54 import javafx.scene.input.KeyCombination; 55 import javafx.scene.input.KeyCharacterCombination; 56 import javafx.scene.input.KeyCodeCombination; 57 import javafx.scene.Node; 58 59 class GlassSystemMenu implements TKSystemMenu { 60 61 private List<MenuBase> systemMenus = null; 62 private MenuBar glassSystemMenuBar = null; 63 64 private InvalidationListener visibilityListener = valueModel -> { 65 if (systemMenus != null) { 66 setMenus(systemMenus); 67 } 68 }; 69 70 protected void createMenuBar() { 71 if (glassSystemMenuBar == null) { 72 Application app = Application.GetApplication(); 73 glassSystemMenuBar = app.createMenuBar(); 74 app.installDefaultMenus(glassSystemMenuBar); 75 76 if (systemMenus != null) { 77 setMenus(systemMenus); 78 } 79 } 80 } 81 82 protected MenuBar getMenuBar() { 83 return glassSystemMenuBar; 84 } 85 86 @Override public boolean isSupported() { 87 return Application.GetApplication().supportsSystemMenu(); 88 } 89 90 @Override public void setMenus(List<MenuBase> menus) { 91 systemMenus = menus; 92 if (glassSystemMenuBar != null) { 93 94 /* 95 * Remove existing menus 96 */ 97 List<Menu> existingMenus = glassSystemMenuBar.getMenus(); 98 int existingSize = existingMenus.size(); 99 100 /* 101 * Leave the Apple menu in place 102 */ 103 for (int index = existingSize - 1; index >= 1; index--) { 104 Menu menu = existingMenus.get(index); 105 clearMenu(menu); 106 glassSystemMenuBar.remove(index); 107 } 108 109 for (MenuBase menu : menus) { 110 addMenu(null, menu); 111 } 112 } 113 } 114 115 // Clear the menu to prevent a memory leak, as outlined in RT-34779 116 private void clearMenu(Menu menu) { 117 for (int i = menu.getItems().size() - 1; i >= 0; i--) { 118 Object o = menu.getItems().get(i); 119 120 if (o instanceof MenuItem) { 121 ((MenuItem)o).setCallback(null); 122 } else if (o instanceof Menu) { 123 clearMenu((Menu) o); 124 } 125 } 126 menu.setEventHandler(null); 127 } 128 129 private void addMenu(final Menu parent, final MenuBase mb) { 130 if (parent != null) { 131 insertMenu(parent, mb, parent.getItems().size()); 132 } else { 133 insertMenu(parent, mb, glassSystemMenuBar.getMenus().size()); 134 } 135 } 136 137 private void insertMenu(final Menu parent, final MenuBase mb, int pos) { 138 Application app = Application.GetApplication(); 139 final Menu glassMenu = app.createMenu(parseText(mb), ! mb.isDisable()); 140 glassMenu.setEventHandler(new GlassMenuEventHandler(mb)); 141 142 // There is no way of knowing if listener was already added. 143 mb.visibleProperty().removeListener(visibilityListener); 144 mb.visibleProperty().addListener(visibilityListener); 145 146 if (!mb.isVisible()) { 147 return; 148 } 149 150 final ObservableList<MenuItemBase> items = mb.getItemsBase(); 151 152 items.addListener((ListChangeListener.Change<? extends MenuItemBase> change) -> { 153 while (change.next()) { 154 int from = change.getFrom(); 155 int to = change.getTo(); 156 List<? extends MenuItemBase> removed = change.getRemoved(); 157 158 for (int i = from + removed.size() - 1; i >= from ; i--) { 159 List<Object> menuItemList = glassMenu.getItems(); 160 if (i >= 0 && menuItemList.size() > i) { 161 glassMenu.remove(i); 162 } 163 } 164 for (int i = from; i < to; i++) { 165 MenuItemBase item = change.getList().get(i); 166 if (item instanceof MenuBase) { 167 insertMenu(glassMenu, (MenuBase)item, i); 168 } else { 169 insertMenuItem(glassMenu, item, i); 170 } 171 } 172 } 173 }); 174 175 for (MenuItemBase item : items) { 176 if (item instanceof MenuBase) { 177 // submenu 178 addMenu(glassMenu, (MenuBase)item); 179 } else { 180 // menu item 181 addMenuItem(glassMenu, item); 182 } 183 } 184 glassMenu.setPixels(getPixels(mb)); 185 186 setMenuBindings(glassMenu, mb); 187 188 if (parent != null) { 189 parent.insert(glassMenu, pos); 190 } else { 191 glassSystemMenuBar.insert(glassMenu, pos); 192 } 193 } 194 195 private void setMenuBindings(final Menu glassMenu, final MenuBase mb) { 196 mb.textProperty().addListener(valueModel -> glassMenu.setTitle(parseText(mb))); 197 mb.disableProperty().addListener(valueModel -> glassMenu.setEnabled(!mb.isDisable())); 198 mb.mnemonicParsingProperty().addListener(valueModel -> glassMenu.setTitle(parseText(mb))); 199 } 200 201 private void addMenuItem(Menu parent, final MenuItemBase menuitem) { 202 insertMenuItem(parent, menuitem, parent.getItems().size()); 203 } 204 205 private void insertMenuItem(final Menu parent, final MenuItemBase menuitem, int pos) { 206 Application app = Application.GetApplication(); 207 208 // There is no way of knowing if listener was already added. 209 menuitem.visibleProperty().removeListener(visibilityListener); 210 menuitem.visibleProperty().addListener(visibilityListener); 211 212 if (!menuitem.isVisible()) { 213 return; 214 } 215 216 if (menuitem instanceof SeparatorMenuItemBase) { 217 if (menuitem.isVisible()) { 218 parent.insert(MenuItem.Separator, pos); 219 } 220 } else { 221 MenuItem.Callback callback = new MenuItem.Callback() { 222 @Override public void action() { 223 // toggle state of check or radio items (from ContextMenuContent.java) 224 if (menuitem instanceof CheckMenuItemBase) { 225 CheckMenuItemBase checkItem = (CheckMenuItemBase)menuitem; 226 checkItem.setSelected(!checkItem.isSelected()); 227 } else if (menuitem instanceof RadioMenuItemBase) { 228 // this is a radio button. If there is a toggleGroup specified, we 229 // simply set selected to true. If no toggleGroup is specified, we 230 // toggle the selected state, as there is no assumption of mutual 231 // exclusivity when no toggleGroup is set. 232 RadioMenuItemBase radioItem = (RadioMenuItemBase)menuitem; 233 // Note: The ToggleGroup is not exposed for RadioMenuItemBase, 234 // so we just assume that one has been set at this point. 235 //radioItem.setSelected(radioItem.getToggleGroup() != null ? true : !radioItem.isSelected()); 236 radioItem.setSelected(true); 237 } 238 menuitem.fire(); 239 } 240 @Override public void validate() { 241 Menu.EventHandler meh = parent.getEventHandler(); 242 GlassMenuEventHandler gmeh = (GlassMenuEventHandler)meh; 243 244 if (gmeh.isMenuOpen()) { 245 return; 246 } 247 menuitem.fireValidation(); 248 } 249 }; 250 251 final MenuItem glassSubMenuItem = app.createMenuItem(parseText(menuitem), callback); 252 253 menuitem.textProperty().addListener(valueModel -> glassSubMenuItem.setTitle(parseText(menuitem))); 254 255 glassSubMenuItem.setPixels(getPixels(menuitem)); 256 menuitem.graphicProperty().addListener(valueModel -> { 257 glassSubMenuItem.setPixels(getPixels(menuitem)); 258 }); 259 260 glassSubMenuItem.setEnabled(! menuitem.isDisable()); 261 menuitem.disableProperty().addListener(valueModel -> glassSubMenuItem.setEnabled(!menuitem.isDisable())); 262 263 setShortcut(glassSubMenuItem, menuitem); 264 menuitem.acceleratorProperty().addListener(valueModel -> setShortcut(glassSubMenuItem, menuitem)); 265 266 menuitem.mnemonicParsingProperty().addListener(valueModel -> glassSubMenuItem.setTitle(parseText(menuitem))); 267 268 if (menuitem instanceof CheckMenuItemBase) { 269 final CheckMenuItemBase checkItem = (CheckMenuItemBase)menuitem; 270 glassSubMenuItem.setChecked(checkItem.isSelected()); 271 checkItem.selectedProperty().addListener(valueModel -> glassSubMenuItem.setChecked(checkItem.isSelected())); 272 } else if (menuitem instanceof RadioMenuItemBase) { 273 final RadioMenuItemBase radioItem = (RadioMenuItemBase)menuitem; 274 glassSubMenuItem.setChecked(radioItem.isSelected()); 275 radioItem.selectedProperty().addListener(valueModel -> glassSubMenuItem.setChecked(radioItem.isSelected())); 276 } 277 278 parent.insert(glassSubMenuItem, pos); 279 } 280 } 281 282 private String parseText(MenuItemBase menuItem) { 283 String text = menuItem.getText(); 284 if (text == null) { 285 // don't pass null strings to Glass 286 return ""; 287 } else if (!text.isEmpty() && menuItem.isMnemonicParsing()) { 288 // \ufffc is a placeholder character 289 //return text.replace("__", "\ufffc").replace("_", "").replace("\ufffc", "_"); 290 return text.replaceFirst("_([^_])", "$1"); 291 } else { 292 return text; 293 } 294 } 295 296 private Pixels getPixels(MenuItemBase menuItem) { 297 if (menuItem.getGraphic() instanceof ImageView) { 298 ImageView iv = (ImageView)menuItem.getGraphic(); 299 Image im = iv.getImage(); 300 if (im == null) return null; 301 302 String url = im.impl_getUrl(); 303 304 if (url == null || PixelUtils.supportedFormatType(url)) { 305 com.sun.prism.Image pi = (com.sun.prism.Image)im.impl_getPlatformImage(); 306 307 return pi == null ? null : PixelUtils.imageToPixels(pi); 308 } 309 } 310 return (null); 311 } 312 313 private void setShortcut(MenuItem glassSubMenuItem, MenuItemBase menuItem) { 314 final KeyCombination accelerator = menuItem.getAccelerator(); 315 if (accelerator == null) { 316 glassSubMenuItem.setShortcut(0, 0); 317 } else if (accelerator instanceof KeyCodeCombination) { 318 KeyCodeCombination kcc = (KeyCodeCombination)accelerator; 319 KeyCode code = kcc.getCode(); 320 assert PlatformUtil.isMac() || PlatformUtil.isLinux(); 321 int modifier = glassModifiers(kcc); 322 if (PlatformUtil.isMac()) { 323 int finalCode = code.isLetterKey() ? code.impl_getChar().toUpperCase().charAt(0) 324 : code.impl_getCode(); 325 glassSubMenuItem.setShortcut(finalCode, modifier); 326 } else if (PlatformUtil.isLinux()) { 327 String lower = code.impl_getChar().toLowerCase(); 328 if ((modifier & KeyEvent.MODIFIER_CONTROL) != 0) { 329 glassSubMenuItem.setShortcut(lower.charAt(0), modifier); 330 } else { 331 glassSubMenuItem.setShortcut(0, 0); 332 } 333 } else { 334 glassSubMenuItem.setShortcut(0, 0); 335 } 336 } else if (accelerator instanceof KeyCharacterCombination) { 337 KeyCharacterCombination kcc = (KeyCharacterCombination)accelerator; 338 String kchar = kcc.getCharacter(); 339 glassSubMenuItem.setShortcut(kchar.charAt(0), glassModifiers(kcc)); 340 } 341 } 342 343 private int glassModifiers(KeyCombination kcc) { 344 int ret = 0; 345 if (kcc.getShift() == KeyCombination.ModifierValue.DOWN) { 346 ret += KeyEvent.MODIFIER_SHIFT; 347 } 348 if (kcc.getControl() == KeyCombination.ModifierValue.DOWN) { 349 ret += KeyEvent.MODIFIER_CONTROL; 350 } 351 if (kcc.getAlt() == KeyCombination.ModifierValue.DOWN) { 352 ret += KeyEvent.MODIFIER_ALT; 353 } 354 if (kcc.getShortcut() == KeyCombination.ModifierValue.DOWN) { 355 if (PlatformUtil.isLinux()) { 356 ret += KeyEvent.MODIFIER_CONTROL; 357 } else if (PlatformUtil.isMac()) { 358 ret += KeyEvent.MODIFIER_COMMAND; 359 } 360 } 361 if (kcc.getMeta() == KeyCombination.ModifierValue.DOWN) { 362 if (PlatformUtil.isLinux()) { 363 ret += KeyEvent.MODIFIER_WINDOWS; // RT-19326 - Linux shortcut support 364 } else if (PlatformUtil.isMac()) { 365 ret += KeyEvent.MODIFIER_COMMAND; 366 } 367 } 368 369 if (kcc instanceof KeyCodeCombination) { 370 KeyCode kcode = ((KeyCodeCombination)kcc).getCode(); 371 int code = kcode.impl_getCode(); 372 373 if (((code >= KeyCode.F1.impl_getCode()) && (code <= KeyCode.F12.impl_getCode())) || 374 ((code >= KeyCode.F13.impl_getCode()) && (code <= KeyCode.F24.impl_getCode()))) { 375 ret += KeyEvent.MODIFIER_FUNCTION; 376 } 377 } 378 379 return (ret); 380 } 381 382 }