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.scene.control;
  27 
  28 import java.util.Map;
  29 
  30 import com.sun.javafx.scene.control.skin.*;
  31 import javafx.beans.InvalidationListener;
  32 import javafx.beans.Observable;
  33 import javafx.collections.ListChangeListener;
  34 import javafx.event.ActionEvent;
  35 import javafx.event.Event;
  36 import javafx.geometry.HPos;
  37 import javafx.geometry.VPos;
  38 import javafx.scene.control.*;
  39 import javafx.scene.control.skin.TextAreaSkin;
  40 import javafx.scene.control.skin.TextFieldSkin;
  41 import javafx.scene.layout.HBox;
  42 import javafx.scene.layout.StackPane;
  43 
  44 /**
  45  * The embedded context menu for a text input control.
  46  * The menu will be displayed horizontally underneath the cursor
  47  * with the available text editing options i.e. cut, copy, paste, select all.
  48  *
  49  */
  50 public class EmbeddedTextContextMenuContent extends StackPane {
  51 
  52     private ContextMenu contextMenu;
  53     private StackPane pointer;
  54     private HBox menuBox;
  55 
  56     public EmbeddedTextContextMenuContent(final ContextMenu popupMenu) {
  57         this.contextMenu = popupMenu;
  58         this.menuBox = new HBox();
  59         this.pointer = new StackPane();
  60         pointer.getStyleClass().add("pointer");
  61 
  62         updateMenuItemContainer();
  63         getChildren().addAll(pointer, menuBox);
  64 
  65         contextMenu.ownerNodeProperty().addListener(arg0 -> {
  66             if (contextMenu.getOwnerNode() instanceof TextArea) {
  67                 TextAreaSkin tas = (TextAreaSkin)((TextArea)contextMenu.getOwnerNode()).getSkin();
  68                 tas.getSkinnable().getProperties().addListener(new InvalidationListener() {
  69                     @Override public void invalidated(Observable arg0) {
  70                         requestLayout();
  71                     }
  72                 });
  73             } else if (contextMenu.getOwnerNode() instanceof TextField) {
  74                 TextFieldSkin tfs = (TextFieldSkin)((TextField)contextMenu.getOwnerNode()).getSkin();
  75                 tfs.getSkinnable().getProperties().addListener(new InvalidationListener() {
  76                     @Override public void invalidated(Observable arg0) {
  77                         requestLayout();
  78                     }
  79                 });
  80             }
  81         });
  82 
  83         contextMenu.getItems().addListener((ListChangeListener<MenuItem>) c -> {
  84             // Listener to items in PopupMenu to update items in PopupMenuContent
  85             updateMenuItemContainer();
  86         });
  87     }
  88 
  89     private void updateMenuItemContainer() {
  90         menuBox.getChildren().clear();
  91         for (MenuItem item: contextMenu.getItems()) {
  92             MenuItemContainer menuItemContainer = new MenuItemContainer(item);
  93             menuItemContainer.visibleProperty().bind(item.visibleProperty());
  94             menuBox.getChildren().add(menuItemContainer);
  95         }
  96     }
  97 
  98     private void hideAllMenus(MenuItem item) {
  99         contextMenu.hide();
 100 
 101         Menu parentMenu;
 102         while ((parentMenu = item.getParentMenu()) != null) {
 103             parentMenu.hide();
 104             item = parentMenu;
 105         }
 106         if (parentMenu == null && item.getParentPopup() != null) {
 107             item.getParentPopup().hide();
 108         }
 109     }
 110 
 111     @Override protected double computePrefHeight(double width) {
 112         final double pointerHeight = snapSizeY(pointer.prefHeight(width));
 113         final double menuBoxHeight = snapSizeY(menuBox.prefHeight(width));
 114         return snappedTopInset() + pointerHeight + menuBoxHeight + snappedBottomInset();
 115     }
 116 
 117     @Override protected double computePrefWidth(double height) {
 118         final double menuBoxWidth = snapSizeX(menuBox.prefWidth(height));
 119         return snappedLeftInset() + menuBoxWidth + snappedRightInset();
 120     }
 121 
 122     @Override protected void layoutChildren() {
 123         final double left = snappedLeftInset();
 124         final double right = snappedRightInset();
 125         final double top = snappedTopInset();
 126         final double width = getWidth() - (left + right);
 127         final double pointerWidth = snapSizeX(Utils.boundedSize(pointer.prefWidth(-1), pointer.minWidth(-1), pointer.maxWidth(-1)));
 128         final double pointerHeight = snapSizeY(Utils.boundedSize(pointer.prefWidth(-1), pointer.minWidth(-1), pointer.maxWidth(-1)));
 129         final double menuBoxWidth = snapSizeX(Utils.boundedSize(menuBox.prefWidth(-1), menuBox.minWidth(-1), menuBox.maxWidth(-1)));
 130         final double menuBoxHeight = snapSizeY(Utils.boundedSize(menuBox.prefWidth(-1), menuBox.minWidth(-1), menuBox.maxWidth(-1)));
 131         double sceneX = 0;
 132         double screenX = 0;
 133         double pointerX = 0;
 134 
 135         // Get the positions of the cursor from the TextArea/TextField and draw the arrow underneath it.
 136         Map<Object,Object> properties = null;
 137         if (contextMenu.getOwnerNode() instanceof TextArea) {
 138             properties = ((TextArea)contextMenu.getOwnerNode()).getProperties();
 139         } else if (contextMenu.getOwnerNode() instanceof TextField) {
 140             properties = ((TextField)contextMenu.getOwnerNode()).getProperties();
 141         }
 142 
 143         if (properties != null) {
 144             if (properties.containsKey("CONTEXT_MENU_SCENE_X")) {
 145                 sceneX = Double.valueOf(properties.get("CONTEXT_MENU_SCENE_X").toString());
 146                 properties.remove("CONTEXT_MENU_SCENE_X");
 147             }
 148 
 149             if (properties.containsKey("CONTEXT_MENU_SCREEN_X")) {
 150                 screenX = Double.valueOf(properties.get("CONTEXT_MENU_SCREEN_X").toString());
 151                 properties.remove("CONTEXT_MENU_SCREEN_X");
 152             }
 153         }
 154 
 155         if (sceneX == 0) {
 156             pointerX = width/2;
 157         } else {
 158             pointerX = (screenX - sceneX - contextMenu.getX()) + sceneX;
 159         }
 160 
 161         pointer.resize(pointerWidth, pointerHeight);
 162         positionInArea(pointer, pointerX, top, pointerWidth, pointerHeight, 0, HPos.CENTER, VPos.CENTER);
 163         menuBox.resize(menuBoxWidth, menuBoxHeight);
 164         positionInArea(menuBox, left, top + pointerHeight, menuBoxWidth, menuBoxHeight, 0, HPos.CENTER, VPos.CENTER);
 165     }
 166 
 167     class MenuItemContainer extends Button {
 168         private MenuItem item;
 169 
 170         public MenuItemContainer(MenuItem item){
 171             getStyleClass().addAll(item.getStyleClass());
 172             setId(item.getId());
 173             this.item = item;
 174             setText(item.getText());
 175             setStyle(item.getStyle());
 176 
 177             // bind to text property in menu item
 178             textProperty().bind(item.textProperty());
 179         }
 180 
 181         public MenuItem getItem() {
 182             return item;
 183         }
 184 
 185         @Override public void fire() {
 186             Event.fireEvent(item, new ActionEvent());
 187             if (!Boolean.TRUE.equals((Boolean)item.getProperties().get("refreshMenu"))) {
 188                 hideAllMenus(item);
 189             }
 190         }
 191     }
 192 }