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 }