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