1 /* 2 * Copyright (c) 2012, 2014, 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 for (int i = from + removed.size() - 1; i >= from ; i--) { 158 glassMenu.remove(i); 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.impl_getUrl(); 299 300 if (url == null || PixelUtils.supportedFormatType(url)) { 301 com.sun.prism.Image pi = (com.sun.prism.Image)im.impl_getPlatformImage(); 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.impl_getChar().toUpperCase().charAt(0) 320 : code.impl_getCode(); 321 glassSubMenuItem.setShortcut(finalCode, modifier); 322 } else if (PlatformUtil.isLinux()) { 323 String lower = code.impl_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.impl_getCode(); 368 369 if (((code >= KeyCode.F1.impl_getCode()) && (code <= KeyCode.F12.impl_getCode())) || 370 ((code >= KeyCode.F13.impl_getCode()) && (code <= KeyCode.F24.impl_getCode()))) { 371 ret += KeyEvent.MODIFIER_FUNCTION; 372 } 373 } 374 375 return (ret); 376 } 377 378 }