1 /*
   2  * Copyright (c) 2012, 2015, 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 
 158                 for (int i = from + removed.size() - 1; i >= from ; i--) {
 159                     List<Object> menuItemList = glassMenu.getItems();
 160                     if (i >= 0 && menuItemList.size() > i) {
 161                         glassMenu.remove(i);
 162                     }
 163                 }
 164                 for (int i = from; i < to; i++) {
 165                     MenuItemBase item = change.getList().get(i);
 166                     if (item instanceof MenuBase) {
 167                         insertMenu(glassMenu, (MenuBase)item, i);
 168                     } else {
 169                         insertMenuItem(glassMenu, item, i);
 170                     }
 171                 }
 172             }
 173         });
 174 
 175         for (MenuItemBase item : items) {
 176             if (item instanceof MenuBase) {
 177                 // submenu
 178                 addMenu(glassMenu, (MenuBase)item);
 179             } else {
 180                 // menu item
 181                 addMenuItem(glassMenu, item);
 182             }
 183         }
 184         glassMenu.setPixels(getPixels(mb));
 185 
 186         setMenuBindings(glassMenu, mb);
 187 
 188         if (parent != null) {
 189             parent.insert(glassMenu, pos);
 190         } else {
 191             glassSystemMenuBar.insert(glassMenu, pos);
 192         }
 193     }
 194 
 195     private void setMenuBindings(final Menu glassMenu, final MenuBase mb) {
 196         mb.textProperty().addListener(valueModel -> glassMenu.setTitle(parseText(mb)));
 197         mb.disableProperty().addListener(valueModel -> glassMenu.setEnabled(!mb.isDisable()));
 198         mb.mnemonicParsingProperty().addListener(valueModel -> glassMenu.setTitle(parseText(mb)));
 199     }
 200 
 201     private void addMenuItem(Menu parent, final MenuItemBase menuitem) {
 202         insertMenuItem(parent, menuitem, parent.getItems().size());
 203     }
 204 
 205     private void insertMenuItem(final Menu parent, final MenuItemBase menuitem, int pos) {
 206         Application app = Application.GetApplication();
 207 
 208         // There is no way of knowing if listener was already added.
 209         menuitem.visibleProperty().removeListener(visibilityListener);
 210         menuitem.visibleProperty().addListener(visibilityListener);
 211 
 212         if (!menuitem.isVisible()) {
 213             return;
 214         }
 215 
 216         if (menuitem instanceof SeparatorMenuItemBase) {
 217             if (menuitem.isVisible()) {
 218                 parent.insert(MenuItem.Separator, pos);
 219             }
 220         } else {
 221             MenuItem.Callback callback = new MenuItem.Callback() {
 222                 @Override public void action() {
 223                     // toggle state of check or radio items (from ContextMenuContent.java)
 224                     if (menuitem instanceof CheckMenuItemBase) {
 225                         CheckMenuItemBase checkItem = (CheckMenuItemBase)menuitem;
 226                         checkItem.setSelected(!checkItem.isSelected());
 227                     } else if (menuitem instanceof RadioMenuItemBase) {
 228                         // this is a radio button. If there is a toggleGroup specified, we
 229                         // simply set selected to true. If no toggleGroup is specified, we
 230                         // toggle the selected state, as there is no assumption of mutual
 231                         // exclusivity when no toggleGroup is set.
 232                         RadioMenuItemBase radioItem = (RadioMenuItemBase)menuitem;
 233                         // Note: The ToggleGroup is not exposed for RadioMenuItemBase,
 234                         // so we just assume that one has been set at this point.
 235                         //radioItem.setSelected(radioItem.getToggleGroup() != null ? true : !radioItem.isSelected());
 236                         radioItem.setSelected(true);
 237                     }
 238                     menuitem.fire();
 239                 }
 240                 @Override public void validate() {
 241                     Menu.EventHandler     meh  = parent.getEventHandler();
 242                     GlassMenuEventHandler gmeh = (GlassMenuEventHandler)meh;
 243 
 244                     if (gmeh.isMenuOpen()) {
 245                         return;
 246                     }
 247                     menuitem.fireValidation();
 248                 }
 249             };
 250 
 251             final MenuItem glassSubMenuItem = app.createMenuItem(parseText(menuitem), callback);
 252 
 253             menuitem.textProperty().addListener(valueModel -> glassSubMenuItem.setTitle(parseText(menuitem)));
 254 
 255             glassSubMenuItem.setPixels(getPixels(menuitem));
 256             menuitem.graphicProperty().addListener(valueModel -> {
 257                 glassSubMenuItem.setPixels(getPixels(menuitem));
 258             });
 259 
 260             glassSubMenuItem.setEnabled(! menuitem.isDisable());
 261             menuitem.disableProperty().addListener(valueModel -> glassSubMenuItem.setEnabled(!menuitem.isDisable()));
 262 
 263             setShortcut(glassSubMenuItem, menuitem);
 264             menuitem.acceleratorProperty().addListener(valueModel -> setShortcut(glassSubMenuItem, menuitem));
 265 
 266             menuitem.mnemonicParsingProperty().addListener(valueModel -> glassSubMenuItem.setTitle(parseText(menuitem)));
 267 
 268             if (menuitem instanceof CheckMenuItemBase) {
 269                 final CheckMenuItemBase checkItem = (CheckMenuItemBase)menuitem;
 270                 glassSubMenuItem.setChecked(checkItem.isSelected());
 271                 checkItem.selectedProperty().addListener(valueModel -> glassSubMenuItem.setChecked(checkItem.isSelected()));
 272             } else if (menuitem instanceof RadioMenuItemBase) {
 273                 final RadioMenuItemBase radioItem = (RadioMenuItemBase)menuitem;
 274                 glassSubMenuItem.setChecked(radioItem.isSelected());
 275                 radioItem.selectedProperty().addListener(valueModel -> glassSubMenuItem.setChecked(radioItem.isSelected()));
 276             }
 277 
 278             parent.insert(glassSubMenuItem, pos);
 279         }
 280     }
 281 
 282     private String parseText(MenuItemBase menuItem) {
 283         String text = menuItem.getText();
 284         if (text == null) {
 285             // don't pass null strings to Glass
 286             return "";
 287         } else if (!text.isEmpty() && menuItem.isMnemonicParsing()) {
 288             // \ufffc is a placeholder character
 289             //return text.replace("__", "\ufffc").replace("_", "").replace("\ufffc", "_");
 290             return text.replaceFirst("_([^_])", "$1");
 291         } else {
 292             return text;
 293         }
 294     }
 295 
 296     private Pixels getPixels(MenuItemBase menuItem) {
 297         if (menuItem.getGraphic() instanceof ImageView) {
 298             ImageView iv = (ImageView)menuItem.getGraphic();
 299             Image     im = iv.getImage();
 300             if (im == null) return null;
 301             
 302             String    url          = im.impl_getUrl();
 303 
 304             if (url == null || PixelUtils.supportedFormatType(url)) {
 305                 com.sun.prism.Image pi = (com.sun.prism.Image)im.impl_getPlatformImage();
 306 
 307                 return pi == null ? null : PixelUtils.imageToPixels(pi);
 308             }
 309         }
 310         return (null);
 311     }
 312 
 313     private void setShortcut(MenuItem glassSubMenuItem, MenuItemBase menuItem) {
 314         final KeyCombination accelerator = menuItem.getAccelerator();
 315         if (accelerator == null) {
 316             glassSubMenuItem.setShortcut(0, 0);
 317         } else if (accelerator instanceof KeyCodeCombination) {
 318             KeyCodeCombination kcc  = (KeyCodeCombination)accelerator;
 319             KeyCode            code = kcc.getCode();
 320             assert PlatformUtil.isMac() || PlatformUtil.isLinux();
 321             int modifier = glassModifiers(kcc);
 322             if (PlatformUtil.isMac()) {
 323                 int finalCode = code.isLetterKey() ? code.impl_getChar().toUpperCase().charAt(0)
 324                         : code.impl_getCode();
 325                 glassSubMenuItem.setShortcut(finalCode, modifier);
 326             } else if (PlatformUtil.isLinux()) {
 327                 String lower = code.impl_getChar().toLowerCase();
 328                 if ((modifier & KeyEvent.MODIFIER_CONTROL) != 0) {
 329                     glassSubMenuItem.setShortcut(lower.charAt(0), modifier);
 330                 } else {
 331                     glassSubMenuItem.setShortcut(0, 0);
 332                 }
 333             } else {
 334                 glassSubMenuItem.setShortcut(0, 0);
 335             }
 336         } else if (accelerator instanceof KeyCharacterCombination) {
 337             KeyCharacterCombination kcc = (KeyCharacterCombination)accelerator;
 338             String kchar = kcc.getCharacter();
 339             glassSubMenuItem.setShortcut(kchar.charAt(0), glassModifiers(kcc));
 340         }
 341     }
 342 
 343     private int glassModifiers(KeyCombination kcc) {
 344         int ret = 0;
 345         if (kcc.getShift() == KeyCombination.ModifierValue.DOWN) {
 346             ret += KeyEvent.MODIFIER_SHIFT;
 347         }
 348         if (kcc.getControl() == KeyCombination.ModifierValue.DOWN) {
 349             ret += KeyEvent.MODIFIER_CONTROL;
 350         }
 351         if (kcc.getAlt() == KeyCombination.ModifierValue.DOWN) {
 352             ret += KeyEvent.MODIFIER_ALT;
 353         }
 354         if (kcc.getShortcut() == KeyCombination.ModifierValue.DOWN) {
 355             if (PlatformUtil.isLinux()) {
 356                 ret += KeyEvent.MODIFIER_CONTROL;
 357             } else if (PlatformUtil.isMac()) {
 358                 ret += KeyEvent.MODIFIER_COMMAND;
 359             }
 360         }
 361         if (kcc.getMeta() == KeyCombination.ModifierValue.DOWN) {
 362             if (PlatformUtil.isLinux()) {
 363                 ret += KeyEvent.MODIFIER_WINDOWS;   // RT-19326 - Linux shortcut support
 364             } else if (PlatformUtil.isMac()) {
 365                 ret += KeyEvent.MODIFIER_COMMAND;
 366             }
 367         }
 368 
 369         if (kcc instanceof KeyCodeCombination) {
 370             KeyCode kcode = ((KeyCodeCombination)kcc).getCode();
 371             int     code  = kcode.impl_getCode();
 372 
 373             if (((code >= KeyCode.F1.impl_getCode())  && (code <= KeyCode.F12.impl_getCode())) ||
 374                 ((code >= KeyCode.F13.impl_getCode()) && (code <= KeyCode.F24.impl_getCode()))) {
 375                 ret += KeyEvent.MODIFIER_FUNCTION;
 376             }
 377         }
 378                        
 379         return (ret);
 380     }
 381         
 382 }