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