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 }