1 /*
   2  * Copyright (c) 2010, 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.glass.events.GestureEvent;
  29 import com.sun.glass.events.KeyEvent;
  30 import com.sun.glass.events.MouseEvent;
  31 import com.sun.glass.events.ViewEvent;
  32 import com.sun.glass.events.TouchEvent;
  33 import com.sun.glass.events.SwipeGesture;
  34 import com.sun.glass.ui.Accessible;
  35 import com.sun.glass.ui.Clipboard;
  36 import com.sun.glass.ui.ClipboardAssistance;
  37 import com.sun.glass.ui.View;
  38 import com.sun.glass.ui.Window;
  39 import com.sun.javafx.PlatformUtil;
  40 import com.sun.javafx.collections.TrackableObservableList;
  41 import com.sun.javafx.logging.PulseLogger;
  42 import static com.sun.javafx.logging.PulseLogger.PULSE_LOGGING_ENABLED;
  43 import com.sun.javafx.scene.input.KeyCodeMap;
  44 
  45 import javafx.collections.ListChangeListener;
  46 import javafx.collections.ObservableList;
  47 
  48 import javafx.event.EventType;
  49 import javafx.geometry.Point2D;
  50 import javafx.scene.input.InputMethodEvent;
  51 import javafx.scene.input.InputMethodHighlight;
  52 import javafx.scene.input.InputMethodTextRun;
  53 import javafx.scene.input.MouseButton;
  54 import javafx.scene.input.RotateEvent;
  55 import javafx.scene.input.ScrollEvent;
  56 import javafx.scene.input.SwipeEvent;
  57 import javafx.scene.input.TouchPoint;
  58 import javafx.scene.input.TransferMode;
  59 import javafx.scene.input.ZoomEvent;
  60 
  61 import java.security.AccessController;
  62 import java.security.PrivilegedAction;
  63 
  64 class GlassViewEventHandler extends View.EventHandler {
  65 
  66     static boolean zoomGestureEnabled;
  67     static boolean rotateGestureEnabled;
  68     static boolean scrollGestureEnabled;
  69     static {
  70         AccessController.doPrivileged((PrivilegedAction<Void>) () -> {
  71             zoomGestureEnabled = Boolean.valueOf(System.getProperty("com.sun.javafx.gestures.zoom", "false"));
  72             rotateGestureEnabled = Boolean.valueOf(System.getProperty("com.sun.javafx.gestures.rotate", "false"));
  73             scrollGestureEnabled = Boolean.valueOf(System.getProperty("com.sun.javafx.gestures.scroll", "false"));
  74             return null;
  75         });
  76     }    
  77 
  78     private ViewScene scene;
  79     private final GlassSceneDnDEventHandler dndHandler;
  80     private final GestureRecognizers gestures;
  81 
  82     public GlassViewEventHandler(final ViewScene scene) {
  83         this.scene = scene;
  84         
  85         dndHandler = new GlassSceneDnDEventHandler(scene);
  86 
  87         gestures = new GestureRecognizers();
  88         if (PlatformUtil.isWindows() || PlatformUtil.isIOS() || PlatformUtil.isEmbedded()) {
  89             gestures.add(new SwipeGestureRecognizer(scene));
  90         }
  91         if (zoomGestureEnabled) {
  92             gestures.add(new ZoomGestureRecognizer(scene));
  93         }
  94         if (rotateGestureEnabled) {
  95             gestures.add(new RotateGestureRecognizer(scene));
  96         }
  97         if (scrollGestureEnabled) {
  98             gestures.add(new ScrollGestureRecognizer(scene));
  99         }
 100     }
 101 
 102     // Default fullscreen allows limited keyboard input.
 103     // It will only receive events from the following keys:
 104     // DOWN, UP, LEFT, RIGHT, SPACE, TAB, PAGE_UP, PAGE_DOWN,
 105     // HOME, END, ENTER.
 106     private static boolean allowableFullScreenKeys(int key) {
 107         switch (key) {
 108             case KeyEvent.VK_DOWN:
 109             case KeyEvent.VK_UP:
 110             case KeyEvent.VK_LEFT:
 111             case KeyEvent.VK_RIGHT:
 112             case KeyEvent.VK_SPACE:
 113             case KeyEvent.VK_TAB:
 114             case KeyEvent.VK_PAGE_DOWN:
 115             case KeyEvent.VK_PAGE_UP:
 116             case KeyEvent.VK_HOME:
 117             case KeyEvent.VK_END:
 118             case KeyEvent.VK_ENTER:
 119                 return true;
 120         }
 121         return false;
 122     }
 123 
 124     private boolean checkFullScreenKeyEvent(int type, int key, char chars[], int modifiers) {
 125         return scene.getWindowStage().isTrustedFullScreen() || allowableFullScreenKeys(key);
 126     }
 127 
 128     private final PaintCollector collector = PaintCollector.getInstance();
 129 
 130     private static EventType<javafx.scene.input.KeyEvent> keyEventType(int glassType) {
 131         switch (glassType) {
 132             case com.sun.glass.events.KeyEvent.PRESS:
 133                 return javafx.scene.input.KeyEvent.KEY_PRESSED;
 134             case com.sun.glass.events.KeyEvent.RELEASE:
 135                 return javafx.scene.input.KeyEvent.KEY_RELEASED;
 136             case com.sun.glass.events.KeyEvent.TYPED:
 137                 return javafx.scene.input.KeyEvent.KEY_TYPED;
 138             default:
 139                 if (QuantumToolkit.verbose) {
 140                     System.err.println("Unknown Glass key event type: " + glassType);
 141                 }
 142                 return null;
 143         }
 144     }
 145 
 146     private final KeyEventNotification keyNotification = new KeyEventNotification();
 147     private class KeyEventNotification implements PrivilegedAction<Void> {
 148         View view;
 149         long time;
 150         int type;
 151         int key;
 152         char[] chars;
 153         int modifiers;
 154 
 155         @Override
 156         public Void run() {
 157             if (PULSE_LOGGING_ENABLED) {
 158                 PulseLogger.newInput(keyEventType(type).toString());
 159             }
 160             WindowStage stage = scene.getWindowStage();
 161             try {
 162                 if (stage != null) {
 163                     stage.setInEventHandler(true);
 164                 }
 165 
 166                 boolean shiftDown = (modifiers & KeyEvent.MODIFIER_SHIFT) != 0;
 167                 boolean controlDown = (modifiers & KeyEvent.MODIFIER_CONTROL) != 0;
 168                 boolean altDown = (modifiers & KeyEvent.MODIFIER_ALT) != 0;
 169                 boolean metaDown = (modifiers & KeyEvent.MODIFIER_WINDOWS) != 0;
 170 
 171                 String str = new String(chars);
 172                 String text = str; // TODO: this must be a text like "HOME", "F1", or "A"
 173 
 174                 javafx.scene.input.KeyEvent keyEvent = new javafx.scene.input.KeyEvent(
 175                         keyEventType(type),
 176                         str, text, 
 177                         KeyCodeMap.valueOf(key) ,
 178                         shiftDown, controlDown, altDown, metaDown);
 179 
 180                 switch (type) {
 181                     case com.sun.glass.events.KeyEvent.PRESS:
 182                         if (view.isInFullscreen() && stage != null) {
 183                             if (stage.getSavedFullScreenExitKey() != null
 184                                 && stage.getSavedFullScreenExitKey().match(keyEvent)) {
 185                                 stage.exitFullScreen();
 186                             }
 187                         }
 188                         /* NOBREAK */
 189                     case com.sun.glass.events.KeyEvent.RELEASE:
 190                     case com.sun.glass.events.KeyEvent.TYPED:
 191                         if (view.isInFullscreen()) {
 192                             if (!checkFullScreenKeyEvent(type, key, chars, modifiers)) {
 193                                 break;
 194                             }
 195                         }
 196                         if (scene.sceneListener != null) {
 197                             scene.sceneListener.keyEvent(keyEvent);
 198                         }
 199                         break;
 200                     default:
 201                         if (QuantumToolkit.verbose) {
 202                             System.out.println("handleKeyEvent: unhandled type: " + type);
 203                         }
 204                 }
 205             } finally {
 206                 if (stage != null) {
 207                     stage.setInEventHandler(false);
 208                 }
 209                 if (PULSE_LOGGING_ENABLED) {
 210                     PulseLogger.newInput(null);
 211                 }
 212             }
 213             return null;
 214         }
 215     }
 216 
 217     @Override public void handleKeyEvent(View view, long time, int type, int key,
 218                                          char[] chars, int modifiers)
 219     {
 220         keyNotification.view = view;
 221         keyNotification.time = time;
 222         keyNotification.type = type;
 223         keyNotification.key = key;
 224         keyNotification.chars = chars;
 225         keyNotification.modifiers = modifiers;
 226 
 227         QuantumToolkit.runWithoutRenderLock(() -> {
 228             return AccessController.doPrivileged(keyNotification, scene.getAccessControlContext());
 229         });
 230     }
 231 
 232     private static EventType<javafx.scene.input.MouseEvent> mouseEventType(int glassType) {
 233         switch (glassType) {
 234             case com.sun.glass.events.MouseEvent.DOWN:
 235                 return javafx.scene.input.MouseEvent.MOUSE_PRESSED;
 236             case com.sun.glass.events.MouseEvent.UP:
 237                 return javafx.scene.input.MouseEvent.MOUSE_RELEASED;
 238             case com.sun.glass.events.MouseEvent.ENTER:
 239                 return javafx.scene.input.MouseEvent.MOUSE_ENTERED;
 240             case com.sun.glass.events.MouseEvent.EXIT:
 241                 return javafx.scene.input.MouseEvent.MOUSE_EXITED;
 242             case com.sun.glass.events.MouseEvent.MOVE:
 243                 return javafx.scene.input.MouseEvent.MOUSE_MOVED;
 244             case com.sun.glass.events.MouseEvent.DRAG:
 245                 return javafx.scene.input.MouseEvent.MOUSE_DRAGGED;
 246             case com.sun.glass.events.MouseEvent.WHEEL:
 247                 throw new IllegalArgumentException("WHEEL event cannot "
 248                         + "be translated to MouseEvent, must be translated to "
 249                         + "ScrollEvent");
 250             default:
 251                 if (QuantumToolkit.verbose) {
 252                     System.err.println("Unknown Glass mouse event type: " + glassType);
 253                 }
 254                 return null;
 255         }
 256     }
 257 
 258     private static MouseButton mouseEventButton(int glassButton) {
 259         switch (glassButton) {
 260             case com.sun.glass.events.MouseEvent.BUTTON_LEFT:
 261                 return MouseButton.PRIMARY;
 262             case com.sun.glass.events.MouseEvent.BUTTON_RIGHT:
 263                 return MouseButton.SECONDARY;
 264             case com.sun.glass.events.MouseEvent.BUTTON_OTHER:
 265                 return MouseButton.MIDDLE;
 266             default:
 267                 return MouseButton.NONE;
 268         }
 269     }
 270 
 271     // Used to determine whether a DRAG operation has been initiated on this window
 272     private int mouseButtonPressedMask = 0;
 273 
 274     private final MouseEventNotification mouseNotification = new MouseEventNotification();
 275     private class MouseEventNotification implements PrivilegedAction<Void> {
 276         View view;
 277         long time;
 278         int type;
 279         int button;
 280         int x, y, xAbs, yAbs;
 281         int modifiers;
 282         boolean isPopupTrigger;
 283         boolean isSynthesized;
 284 
 285         @Override
 286         public Void run() {
 287             if (PULSE_LOGGING_ENABLED) {
 288                 PulseLogger.newInput(mouseEventType(type).toString());
 289             }
 290                     
 291             int buttonMask;
 292             switch (button) {
 293                 case MouseEvent.BUTTON_LEFT:
 294                     buttonMask = KeyEvent.MODIFIER_BUTTON_PRIMARY;
 295                     break;
 296                 case MouseEvent.BUTTON_OTHER:
 297                     buttonMask = KeyEvent.MODIFIER_BUTTON_MIDDLE;
 298                     break;
 299                 case MouseEvent.BUTTON_RIGHT:
 300                     buttonMask = KeyEvent.MODIFIER_BUTTON_SECONDARY;
 301                     break;
 302                 default:
 303                     buttonMask = 0;
 304                     break;
 305             }
 306 
 307             switch (type) {
 308                 case MouseEvent.MOVE:
 309                     if (button != MouseEvent.BUTTON_NONE) {
 310                         //RT-11305: the drag hasn't been started on this window -- ignore the event
 311                         return null;
 312                     }
 313                     break;
 314                 case MouseEvent.UP:
 315                     if ((mouseButtonPressedMask & buttonMask) == 0) {
 316                         //RT-11305: the mouse button hasn't been pressed on this window -- ignore the event
 317                         return null;
 318                     }
 319                     mouseButtonPressedMask &= ~buttonMask;
 320                     break;
 321                 case MouseEvent.DOWN:
 322                     mouseButtonPressedMask |= buttonMask;
 323                     break;
 324                 case MouseEvent.ENTER:
 325                 case MouseEvent.EXIT:
 326                     break;
 327                 case MouseEvent.CLICK:
 328                     // Don't send click events to FX, as they are generated in Scene
 329                     return null;
 330                 default:
 331                     if (QuantumToolkit.verbose) {
 332                         System.out.println("handleMouseEvent: unhandled type: " + type);
 333                     }
 334             }
 335 
 336             WindowStage stage = scene.getWindowStage();
 337             try {
 338                 if (stage != null) {
 339                     stage.setInEventHandler(true);
 340                 }
 341                 if (scene.sceneListener != null) {
 342                     boolean shiftDown = (modifiers & KeyEvent.MODIFIER_SHIFT) != 0;
 343                     boolean controlDown = (modifiers & KeyEvent.MODIFIER_CONTROL) != 0;
 344                     boolean altDown = (modifiers & KeyEvent.MODIFIER_ALT) != 0;
 345                     boolean metaDown = (modifiers & KeyEvent.MODIFIER_WINDOWS) != 0;
 346                     boolean primaryButtonDown = (modifiers & KeyEvent.MODIFIER_BUTTON_PRIMARY) != 0;
 347                     boolean middleButtonDown = (modifiers & KeyEvent.MODIFIER_BUTTON_MIDDLE) != 0;
 348                     boolean secondaryButtonDown = (modifiers & KeyEvent.MODIFIER_BUTTON_SECONDARY) != 0;
 349 
 350                     scene.sceneListener.mouseEvent(mouseEventType(type), x, y, xAbs, yAbs,
 351                             mouseEventButton(button), isPopupTrigger, isSynthesized,
 352                             shiftDown, controlDown, altDown, metaDown,
 353                             primaryButtonDown, middleButtonDown, secondaryButtonDown);
 354                 }
 355             } finally {
 356                 if (stage != null) {
 357                     stage.setInEventHandler(false);
 358                 }
 359                 if (PULSE_LOGGING_ENABLED) {
 360                     PulseLogger.newInput(null);
 361                 }
 362             }
 363             return null;
 364         }
 365     }
 366 
 367     @Override
 368     public void handleMouseEvent(View view, long time, int type, int button,
 369                                  int x, int y, int xAbs, int yAbs,
 370                                  int modifiers, boolean isPopupTrigger, boolean isSynthesized)
 371     {
 372         mouseNotification.view = view;
 373         mouseNotification.time = time;
 374         mouseNotification.type = type;
 375         mouseNotification.button = button;
 376         mouseNotification.x = x;
 377         mouseNotification.y = y;
 378         mouseNotification.xAbs = xAbs;
 379         mouseNotification.yAbs = yAbs;
 380         mouseNotification.modifiers = modifiers;
 381         mouseNotification.isPopupTrigger = isPopupTrigger;
 382         mouseNotification.isSynthesized = isSynthesized;
 383 
 384         QuantumToolkit.runWithoutRenderLock(() -> {
 385             return AccessController.doPrivileged(mouseNotification, scene.getAccessControlContext());
 386         });
 387     }
 388 
 389     @Override public void handleMenuEvent(final View view,
 390                                           final int x, final int y, final int xAbs, final int yAbs,
 391                                           final boolean isKeyboardTrigger)
 392     {
 393         if (PULSE_LOGGING_ENABLED) {
 394             PulseLogger.newInput("MENU_EVENT");
 395         }
 396         WindowStage stage = scene.getWindowStage();
 397         try {
 398             if (stage != null) {
 399                 stage.setInEventHandler(true);
 400             }
 401             QuantumToolkit.runWithoutRenderLock(() -> {
 402                 return AccessController.doPrivileged((PrivilegedAction<Void>) () -> {
 403                     if (scene.sceneListener != null) {
 404                         scene.sceneListener.menuEvent(x, y, xAbs, yAbs, isKeyboardTrigger);
 405                     }
 406                     return null;
 407                 }, scene.getAccessControlContext());
 408             });
 409         } finally {
 410             if (stage != null) {
 411                 stage.setInEventHandler(false);
 412             }
 413             if (PULSE_LOGGING_ENABLED) {
 414                 PulseLogger.newInput(null);
 415             }
 416         }
 417     }
 418 
 419     @Override public void handleScrollEvent(final View view, final long time,
 420                                             final int x, final int y, final int xAbs, final int yAbs,
 421                                             final double deltaX, final double deltaY, final int modifiers,
 422                                             final int lines, final int chars,
 423                                             final int defaultLines, final int defaultChars,
 424                                             final double xMultiplier, final double yMultiplier)
 425     {
 426         if (PULSE_LOGGING_ENABLED) {
 427             PulseLogger.newInput("SCROLL_EVENT");
 428         }
 429         WindowStage stage = scene.getWindowStage();
 430         try {
 431             if (stage != null) {
 432                 stage.setInEventHandler(true);
 433             }
 434             QuantumToolkit.runWithoutRenderLock(() -> {
 435                 return AccessController.doPrivileged((PrivilegedAction<Void>) () -> {
 436                     if (scene.sceneListener != null) {
 437                         scene.sceneListener.scrollEvent(ScrollEvent.SCROLL,
 438                             deltaX, deltaY, 0, 0,
 439                             xMultiplier, yMultiplier,
 440                             0, // touchCount
 441                             chars, lines, defaultChars, defaultLines,
 442                             x, y, xAbs, yAbs,
 443                             (modifiers & KeyEvent.MODIFIER_SHIFT) != 0,
 444                             (modifiers & KeyEvent.MODIFIER_CONTROL) != 0,
 445                             (modifiers & KeyEvent.MODIFIER_ALT) != 0,
 446                             (modifiers & KeyEvent.MODIFIER_WINDOWS) != 0,
 447                             false, // this is always indirect
 448                             false); // this has no inertia
 449                     }
 450                     return null;
 451                 }, scene.getAccessControlContext());
 452             });
 453         } finally {
 454             if (stage != null) {
 455                 stage.setInEventHandler(false);
 456             }
 457             if (PULSE_LOGGING_ENABLED) {
 458                 PulseLogger.newInput(null);
 459             }
 460         }
 461     }
 462 
 463     private static byte inputMethodEventAttrValue(int pos, int[] attrBoundary, byte[] attrValue) {
 464         if (attrBoundary != null) {
 465             for (int current = 0; current < attrBoundary.length-1; current++) {
 466                 if (pos >= attrBoundary[current] &&
 467                     pos < attrBoundary[current+1]) {
 468                     return attrValue[current];
 469                 }
 470             }
 471         }
 472         return View.IME_ATTR_INPUT_ERROR;
 473     }
 474 
 475     private static ObservableList<InputMethodTextRun> inputMethodEventComposed(
 476             String text, int commitCount, int[] clauseBoundary, int[] attrBoundary, byte[] attrValue)
 477     {
 478         ObservableList<InputMethodTextRun> composed = new TrackableObservableList<InputMethodTextRun>() {
 479             @Override
 480             protected void onChanged(ListChangeListener.Change<InputMethodTextRun> c) {
 481             }
 482         };
 483 
 484         if (commitCount < text.length()) {
 485             if (clauseBoundary == null) {
 486                 // Create one single segment as UNSELECTED_RAW
 487                 composed.add(new InputMethodTextRun(
 488                         text.substring(commitCount),
 489                         InputMethodHighlight.UNSELECTED_RAW));
 490             } else {
 491                 for (int current = 0; current < clauseBoundary.length-1; current++) {
 492                     if (clauseBoundary[current] < commitCount) {
 493                         continue;
 494                     }
 495 
 496                     InputMethodHighlight highlight;
 497                     switch (inputMethodEventAttrValue(clauseBoundary[current], attrBoundary, attrValue)) {
 498                         case View.IME_ATTR_TARGET_CONVERTED:
 499                             highlight = InputMethodHighlight.SELECTED_CONVERTED;
 500                             break;
 501                         case View.IME_ATTR_CONVERTED:
 502                             highlight = InputMethodHighlight.UNSELECTED_CONVERTED;
 503                             break;
 504                         case View.IME_ATTR_TARGET_NOTCONVERTED:
 505                             highlight = InputMethodHighlight.SELECTED_RAW;
 506                             break;
 507                         case View.IME_ATTR_INPUT:
 508                         case View.IME_ATTR_INPUT_ERROR:
 509                         default:
 510                             highlight = InputMethodHighlight.UNSELECTED_RAW;
 511                             break;
 512                     }
 513                     composed.add(new InputMethodTextRun(
 514                             text.substring(clauseBoundary[current],
 515                                            clauseBoundary[current+1]),
 516                             highlight));
 517                 }
 518             }
 519         }
 520         return composed;
 521     }
 522 
 523     @Override public void handleInputMethodEvent(final long time, final String text,
 524                                                  final int[] clauseBoundary,
 525                                                  final int[] attrBoundary, final byte[] attrValue,
 526                                                  final int commitCount, final int cursorPos)
 527     {
 528         if (PULSE_LOGGING_ENABLED) {
 529             PulseLogger.newInput("INPUT_METHOD_EVENT");
 530         }
 531         WindowStage stage = scene.getWindowStage();
 532         try {
 533             if (stage != null) {
 534                 stage.setInEventHandler(true);
 535             }
 536             QuantumToolkit.runWithoutRenderLock(() -> {
 537                 return AccessController.doPrivileged((PrivilegedAction<Void>) () -> {
 538                     if (scene.sceneListener != null) {
 539                         String t = text != null ? text : "";
 540                         EventType<InputMethodEvent> eventType =
 541                                 InputMethodEvent.INPUT_METHOD_TEXT_CHANGED;
 542                         ObservableList<InputMethodTextRun> composed = inputMethodEventComposed(
 543                                 t, commitCount, clauseBoundary, attrBoundary, attrValue);
 544                         String committed = t.substring(0, commitCount);
 545                         scene.sceneListener.inputMethodEvent(eventType, composed, committed, cursorPos);
 546                     }
 547                     return null;
 548                 }, scene.getAccessControlContext());
 549             });
 550         } finally {
 551             if (stage != null) {
 552                 stage.setInEventHandler(false);
 553             }
 554             if (PULSE_LOGGING_ENABLED) {
 555                 PulseLogger.newInput(null);
 556             }
 557         }
 558     }
 559 
 560     @Override
 561     public double[] getInputMethodCandidatePos(int offset) {
 562         Point2D p2d = scene.inputMethodRequests.getTextLocation(offset);
 563         double[] ret = new double[2];
 564         ret[0] = p2d.getX();
 565         ret[1] = p2d.getY();
 566         return ret;
 567     }
 568 
 569     private static TransferMode actionToTransferMode(int dropActions) {
 570         if (dropActions == Clipboard.ACTION_NONE) {
 571             return null;
 572         } else if (dropActions == Clipboard.ACTION_COPY
 573                 //IE drop action for URL copy;  XXX: should be fixed in Glass
 574                 || dropActions == (Clipboard.ACTION_COPY | Clipboard.ACTION_REFERENCE) )
 575         {
 576             return TransferMode.COPY;
 577         } else if (dropActions == Clipboard.ACTION_MOVE
 578                 //IE drop action for URL move;  XXX: should be fixed in Glass
 579                 || dropActions == (Clipboard.ACTION_MOVE | Clipboard.ACTION_REFERENCE) )
 580         {
 581             return TransferMode.MOVE;
 582         } else if (dropActions == Clipboard.ACTION_REFERENCE) {
 583             return TransferMode.LINK;
 584         } else if (dropActions == Clipboard.ACTION_COPY_OR_MOVE) {
 585             if (QuantumToolkit.verbose) {
 586                 System.err.println("Ambiguous drop action: " + Integer.toHexString(dropActions));
 587             }
 588         } else {
 589             if (QuantumToolkit.verbose) {
 590                 System.err.println("Unknown drop action: " + Integer.toHexString(dropActions));
 591             }
 592         }
 593         return null;
 594     }
 595 
 596     private static int transferModeToAction(TransferMode tm) {
 597         if (tm == null) {
 598             return Clipboard.ACTION_NONE;
 599         }
 600 
 601         switch (tm) {
 602             case COPY:
 603                 return Clipboard.ACTION_COPY;
 604             case MOVE:
 605                 return Clipboard.ACTION_MOVE;
 606             case LINK:
 607                 return Clipboard.ACTION_REFERENCE;
 608             default:
 609                 return Clipboard.ACTION_NONE;
 610         }
 611     }
 612 
 613     @Override public int handleDragEnter(View view,
 614                                          final int x, final int y, final int xAbs, final int yAbs,
 615                                          final int recommendedDropAction,
 616                                          final ClipboardAssistance dropTargetAssistant)
 617     {
 618         if (PULSE_LOGGING_ENABLED) {
 619             PulseLogger.newInput("DRAG_ENTER");
 620         }
 621         TransferMode action;
 622         try {
 623             action = QuantumToolkit.runWithoutRenderLock(() -> {
 624                 return dndHandler.handleDragEnter(x, y, xAbs, yAbs,
 625                         actionToTransferMode(recommendedDropAction),
 626                         dropTargetAssistant);
 627             });
 628         } finally {
 629             if (PULSE_LOGGING_ENABLED) {
 630                 PulseLogger.newInput(null);
 631             }
 632         }
 633         return transferModeToAction(action);
 634     }
 635 
 636     @Override public void handleDragLeave(View view, final ClipboardAssistance dropTargetAssistant) {
 637         if (PULSE_LOGGING_ENABLED) {
 638             PulseLogger.newInput("DRAG_LEAVE");
 639         }
 640         try {
 641             QuantumToolkit.runWithoutRenderLock(() -> {
 642                 dndHandler.handleDragLeave(dropTargetAssistant);
 643                 return null;
 644             });
 645         } finally {
 646             if (PULSE_LOGGING_ENABLED) {
 647                 PulseLogger.newInput(null);
 648             }
 649         }
 650     }
 651 
 652     @Override public int handleDragDrop(View view,
 653                                         final int x, final int y, final int xAbs, final int yAbs,
 654                                         final int recommendedDropAction,
 655                                         final ClipboardAssistance dropTargetAssistant)
 656     {
 657         if (PULSE_LOGGING_ENABLED) {
 658             PulseLogger.newInput("DRAG_DROP");
 659         }
 660         TransferMode action;
 661         try {
 662             action = QuantumToolkit.runWithoutRenderLock(() -> {
 663                 return dndHandler.handleDragDrop(x, y, xAbs, yAbs,
 664                     actionToTransferMode(recommendedDropAction),
 665                     dropTargetAssistant);
 666             });
 667         } finally {
 668             if (PULSE_LOGGING_ENABLED) {
 669                 PulseLogger.newInput(null);
 670             }
 671         }
 672         return transferModeToAction(action);
 673     }
 674 
 675     @Override public int handleDragOver(View view,
 676                                         final int x, final int y, final int xAbs, final int yAbs,
 677                                         final int recommendedDropAction,
 678                                         final ClipboardAssistance dropTargetAssistant)
 679     {
 680         if (PULSE_LOGGING_ENABLED) {
 681             PulseLogger.newInput("DRAG_OVER");
 682         }
 683         TransferMode action;
 684         try {
 685             action = QuantumToolkit.runWithoutRenderLock(() -> {
 686                 return dndHandler.handleDragOver(x, y, xAbs, yAbs,
 687                     actionToTransferMode(recommendedDropAction),
 688                     dropTargetAssistant);
 689             });
 690         } finally {
 691             if (PULSE_LOGGING_ENABLED) {
 692                 PulseLogger.newInput(null);
 693             }
 694         }
 695         return transferModeToAction(action);
 696     }
 697 
 698     private ClipboardAssistance dropSourceAssistant;
 699     
 700     @Override public void handleDragStart(View view, final int button,
 701                                           final int x, final int y, final int xAbs, final int yAbs,
 702                                           final ClipboardAssistance assistant)
 703     {
 704         if (PULSE_LOGGING_ENABLED) {
 705             PulseLogger.newInput("DRAG_START");
 706         }
 707         dropSourceAssistant = assistant;
 708         try {
 709             QuantumToolkit.runWithoutRenderLock(() -> {
 710                 dndHandler.handleDragStart(button, x, y, xAbs, yAbs, assistant);
 711                 return null;
 712             });
 713         } finally {
 714             if (PULSE_LOGGING_ENABLED) {
 715                 PulseLogger.newInput(null);
 716             }
 717         }
 718     }
 719 
 720     @Override public void handleDragEnd(View view, final int performedAction) {
 721         if (PULSE_LOGGING_ENABLED) {
 722             PulseLogger.newInput("DRAG_END");
 723         }
 724         try {
 725             QuantumToolkit.runWithoutRenderLock(() -> {
 726                 dndHandler.handleDragEnd(actionToTransferMode(performedAction), dropSourceAssistant);
 727                 return null;
 728             });
 729         } finally {
 730             if (PULSE_LOGGING_ENABLED) {
 731                 PulseLogger.newInput(null);
 732             }
 733         }
 734     }
 735 
 736     // TODO - dropTargetListener.dropActionChanged
 737     // TODO - dragSourceListener.dropActionChanged
 738 
 739     private final ViewEventNotification viewNotification = new ViewEventNotification();
 740     private class ViewEventNotification implements PrivilegedAction<Void> {
 741         View view;
 742         long time;
 743         int type;
 744 
 745         @Override
 746         public Void run() {
 747             if (scene.sceneListener == null) {
 748                 return null;
 749             }
 750             switch (type) {
 751                 case ViewEvent.REPAINT: {
 752                     Window w = view.getWindow();
 753                     if (w != null && w.getMinimumWidth() == view.getWidth() && !w.isVisible()) {
 754                         // RT-21057 - ignore initial minimum size setting if not visible
 755                         break;
 756                     }
 757                     if (QuantumToolkit.drawInPaint && w != null && w.isVisible()) {
 758                         WindowStage stage = scene.getWindowStage();
 759                         if (stage != null && !stage.isApplet()) {
 760                             collector.liveRepaintRenderJob(scene);
 761                         }
 762                     }
 763                     scene.entireSceneNeedsRepaint();
 764                     break;
 765                 }
 766                 case ViewEvent.RESIZE: {
 767                     Window w = view.getWindow();
 768                     scene.sceneListener.changedSize(view.getWidth(), view.getHeight());
 769                     scene.entireSceneNeedsRepaint();
 770                     QuantumToolkit.runWithRenderLock(() -> {
 771                         scene.updateSceneState();
 772                         return null;
 773                     });
 774                     if (QuantumToolkit.liveResize && w != null && w.isVisible()) {
 775                         WindowStage stage = scene.getWindowStage();
 776                         if (stage != null && !stage.isApplet()) {
 777                             collector.liveRepaintRenderJob(scene);
 778                         }
 779                     }
 780                     break;
 781                 }
 782                 case ViewEvent.MOVE:
 783                     scene.sceneListener.changedLocation(view.getX(), view.getY());
 784                     break;
 785                 case ViewEvent.FULLSCREEN_ENTER:
 786                 case ViewEvent.FULLSCREEN_EXIT:
 787                     if (scene.getWindowStage() != null) {
 788                         scene.getWindowStage().fullscreenChanged(type == ViewEvent.FULLSCREEN_ENTER);
 789                     }
 790                     break;
 791                 case ViewEvent.ADD:
 792                 case ViewEvent.REMOVE:
 793                     // unhandled
 794                     break;
 795                 default:
 796                     throw new RuntimeException("handleViewEvent: unhandled type: " + type);
 797             }
 798             return null;
 799         }
 800     }
 801 
 802     @Override public void handleViewEvent(View view, long time, final int type) {
 803         if (PULSE_LOGGING_ENABLED) {
 804             PulseLogger.newInput("VIEW_EVENT: "+ViewEvent.getTypeString(type));
 805         }
 806         viewNotification.view = view;
 807         viewNotification.time = time;
 808         viewNotification.type = type;
 809         try {
 810             QuantumToolkit.runWithoutRenderLock(() -> {
 811                 return AccessController.doPrivileged(viewNotification, scene.getAccessControlContext());
 812             });
 813         }
 814         finally {
 815             if (PULSE_LOGGING_ENABLED) {
 816                 PulseLogger.newInput(null);
 817             }
 818         }
 819     }
 820 
 821     @Override public void handleScrollGestureEvent(
 822             View view, final long time, final int type,
 823             final int modifiers, final boolean isDirect, final boolean isInertia, final int touchCount,
 824             final int x, final int y, final int xAbs, final int yAbs, final double dx, final double dy,
 825             final double totaldx, final double totaldy, final double multiplierX, final double multiplierY)
 826     {
 827         if (PULSE_LOGGING_ENABLED) {
 828             PulseLogger.newInput("SCROLL_GESTURE_EVENT");
 829         }
 830         WindowStage stage = scene.getWindowStage();
 831         try {
 832             if (stage != null) {
 833                 stage.setInEventHandler(true);
 834             }
 835             QuantumToolkit.runWithoutRenderLock(() -> {
 836                 return AccessController.doPrivileged((PrivilegedAction<Void>) () -> {
 837                     if (scene.sceneListener != null) {
 838                         EventType<ScrollEvent> eventType;
 839                         switch(type) {
 840                             case GestureEvent.GESTURE_STARTED:
 841                                 eventType = ScrollEvent.SCROLL_STARTED;
 842                                 break;
 843                             case GestureEvent.GESTURE_PERFORMED:
 844                                 eventType = ScrollEvent.SCROLL;
 845                                 break;
 846                             case GestureEvent.GESTURE_FINISHED:
 847                                 eventType = ScrollEvent.SCROLL_FINISHED;
 848                                 break;
 849                             default:
 850                                 throw new RuntimeException("Unknown scroll event type: " + type);
 851                         }
 852                         scene.sceneListener.scrollEvent(eventType, dx, dy, totaldx, totaldy,
 853                                 multiplierX, multiplierY,
 854                                 touchCount,
 855                                 0, 0, 0, 0,
 856                                 x == View.GESTURE_NO_VALUE ? Double.NaN : x,
 857                                 y == View.GESTURE_NO_VALUE ? Double.NaN : y,
 858                                 xAbs == View.GESTURE_NO_VALUE ? Double.NaN : xAbs,
 859                                 yAbs == View.GESTURE_NO_VALUE ? Double.NaN : yAbs,
 860                                 (modifiers & KeyEvent.MODIFIER_SHIFT) != 0,
 861                                 (modifiers & KeyEvent.MODIFIER_CONTROL) != 0,
 862                                 (modifiers & KeyEvent.MODIFIER_ALT) != 0,
 863                                 (modifiers & KeyEvent.MODIFIER_WINDOWS) != 0,
 864                                 isDirect, isInertia);
 865                     }
 866                     return null;
 867                 }, scene.getAccessControlContext());
 868             });
 869         } finally {
 870             if (stage != null) {
 871                 stage.setInEventHandler(false);
 872             }
 873             if (PULSE_LOGGING_ENABLED) {
 874                 PulseLogger.newInput(null);
 875             }
 876         }
 877     }
 878 
 879     @Override public void handleZoomGestureEvent(
 880             View view, final long time, final int type,
 881             final int modifiers, final boolean isDirect, final boolean isInertia,
 882             final int originx, final int originy,
 883             final int originxAbs, final int originyAbs,
 884             final double scale, double expansion,
 885             final double totalscale, double totalexpansion)
 886     {
 887         if (PULSE_LOGGING_ENABLED) {
 888             PulseLogger.newInput("ZOOM_GESTURE_EVENT");
 889         }
 890         WindowStage stage = scene.getWindowStage();
 891         try {
 892             if (stage != null) {
 893                 stage.setInEventHandler(true);
 894             }
 895             QuantumToolkit.runWithoutRenderLock(() -> {
 896                 return AccessController.doPrivileged((PrivilegedAction<Void>) () -> {
 897                     if (scene.sceneListener != null) {
 898                         EventType<ZoomEvent> eventType;
 899                         switch (type) {
 900                             case GestureEvent.GESTURE_STARTED:
 901                                 eventType = ZoomEvent.ZOOM_STARTED;
 902                                 break;
 903                             case GestureEvent.GESTURE_PERFORMED:
 904                                 eventType = ZoomEvent.ZOOM;
 905                                 break;
 906                             case GestureEvent.GESTURE_FINISHED:
 907                                 eventType = ZoomEvent.ZOOM_FINISHED;
 908                                 break;
 909                             default:
 910                                 throw new RuntimeException("Unknown scroll event type: " + type);
 911                         }
 912                         scene.sceneListener.zoomEvent(eventType, scale, totalscale,
 913                                 originx == View.GESTURE_NO_VALUE ? Double.NaN : originx,
 914                                 originy == View.GESTURE_NO_VALUE ? Double.NaN : originy,
 915                                 originxAbs == View.GESTURE_NO_VALUE ? Double.NaN : originxAbs,
 916                                 originyAbs == View.GESTURE_NO_VALUE ? Double.NaN : originyAbs,
 917                                 (modifiers & KeyEvent.MODIFIER_SHIFT) != 0,
 918                                 (modifiers & KeyEvent.MODIFIER_CONTROL) != 0,
 919                                 (modifiers & KeyEvent.MODIFIER_ALT) != 0,
 920                                 (modifiers & KeyEvent.MODIFIER_WINDOWS) != 0,
 921                                 isDirect, isInertia);
 922                     }
 923                     return null;
 924                 }, scene.getAccessControlContext());
 925             });
 926         } finally {
 927             if (stage != null) {
 928                 stage.setInEventHandler(false);
 929             }
 930             if (PULSE_LOGGING_ENABLED) {
 931                 PulseLogger.newInput(null);
 932             }
 933         }
 934     }
 935 
 936     @Override public void handleRotateGestureEvent(
 937             View view, final long time, final int type,
 938             final int modifiers, final boolean isDirect, final boolean isInertia,
 939             final int originx, final int originy,
 940             final int originxAbs, final int originyAbs,
 941             final double dangle, final double totalangle)
 942     {
 943         if (PULSE_LOGGING_ENABLED) {
 944             PulseLogger.newInput("ROTATE_GESTURE_EVENT");
 945         }
 946         WindowStage stage = scene.getWindowStage();
 947         try {
 948             if (stage != null) {
 949                 stage.setInEventHandler(true);
 950             }
 951             QuantumToolkit.runWithoutRenderLock(() -> {
 952                 return AccessController.doPrivileged((PrivilegedAction<Void>) () -> {
 953                     if (scene.sceneListener != null) {
 954                         EventType<RotateEvent> eventType;
 955                         switch (type) {
 956                             case GestureEvent.GESTURE_STARTED:
 957                                 eventType = RotateEvent.ROTATION_STARTED;
 958                                 break;
 959                             case GestureEvent.GESTURE_PERFORMED:
 960                                 eventType = RotateEvent.ROTATE;
 961                                 break;
 962                             case GestureEvent.GESTURE_FINISHED:
 963                                 eventType = RotateEvent.ROTATION_FINISHED;
 964                                 break;
 965                             default:
 966                                 throw new RuntimeException("Unknown scroll event type: " + type);
 967                         }
 968                         scene.sceneListener.rotateEvent(eventType, dangle, totalangle,
 969                                 originx == View.GESTURE_NO_VALUE ? Double.NaN : originx,
 970                                 originy == View.GESTURE_NO_VALUE ? Double.NaN : originy,
 971                                 originxAbs == View.GESTURE_NO_VALUE ? Double.NaN : originxAbs,
 972                                 originyAbs == View.GESTURE_NO_VALUE ? Double.NaN : originyAbs,
 973                                 (modifiers & KeyEvent.MODIFIER_SHIFT) != 0,
 974                                 (modifiers & KeyEvent.MODIFIER_CONTROL) != 0,
 975                                 (modifiers & KeyEvent.MODIFIER_ALT) != 0,
 976                                 (modifiers & KeyEvent.MODIFIER_WINDOWS) != 0,
 977                                 isDirect, isInertia);
 978                     }
 979                     return null;
 980                 }, scene.getAccessControlContext());
 981             });
 982         } finally {
 983             if (stage != null) {
 984                 stage.setInEventHandler(false);
 985             }
 986             if (PULSE_LOGGING_ENABLED) {
 987                 PulseLogger.newInput(null);
 988             }
 989         }
 990     }
 991 
 992     @Override public void handleSwipeGestureEvent(
 993             View view, final long time, int type,
 994             final int modifiers, final boolean isDirect,
 995             boolean isInertia, final int touchCount,
 996             final int dir, final int x, final int y, final int xAbs, final int yAbs)
 997     {
 998         if (PULSE_LOGGING_ENABLED) {
 999             PulseLogger.newInput("SWIPE_EVENT");
1000         }
1001         WindowStage stage = scene.getWindowStage();
1002         try {
1003             if (stage != null) {
1004                 stage.setInEventHandler(true);
1005             }
1006             QuantumToolkit.runWithoutRenderLock(() -> {
1007                 return AccessController.doPrivileged((PrivilegedAction<Void>) () -> {
1008                     if (scene.sceneListener != null) {
1009                         EventType<SwipeEvent> eventType;
1010                         switch (dir) {
1011                             case SwipeGesture.DIR_UP:
1012                                 eventType = SwipeEvent.SWIPE_UP;
1013                                 break;
1014                             case SwipeGesture.DIR_DOWN:
1015                                 eventType = SwipeEvent.SWIPE_DOWN;
1016                                 break;
1017                             case SwipeGesture.DIR_LEFT:
1018                                 eventType = SwipeEvent.SWIPE_LEFT;
1019                                 break;
1020                             case SwipeGesture.DIR_RIGHT:
1021                                 eventType = SwipeEvent.SWIPE_RIGHT;
1022                                 break;
1023                             default:
1024                                 throw new RuntimeException("Unknown swipe event direction: " + dir);
1025                         }
1026                         scene.sceneListener.swipeEvent(eventType, touchCount,
1027                                 x == View.GESTURE_NO_VALUE ? Double.NaN : x,
1028                                 y == View.GESTURE_NO_VALUE ? Double.NaN : y,
1029                                 xAbs == View.GESTURE_NO_VALUE ? Double.NaN : xAbs,
1030                                 yAbs == View.GESTURE_NO_VALUE ? Double.NaN : yAbs,
1031                                 (modifiers & KeyEvent.MODIFIER_SHIFT) != 0,
1032                                 (modifiers & KeyEvent.MODIFIER_CONTROL) != 0,
1033                                 (modifiers & KeyEvent.MODIFIER_ALT) != 0,
1034                                 (modifiers & KeyEvent.MODIFIER_WINDOWS) != 0,
1035                                 isDirect);
1036                     }
1037                     return null;
1038                 }, scene.getAccessControlContext());
1039             });
1040         } finally {
1041             if (stage != null) {
1042                 stage.setInEventHandler(false);
1043             }
1044             if (PULSE_LOGGING_ENABLED) {
1045                 PulseLogger.newInput(null);
1046             }
1047         }
1048     }
1049 
1050     @Override public void handleBeginTouchEvent(
1051             View view, final long time, final int modifiers,
1052             final boolean isDirect, final int touchEventCount)
1053     {
1054         if (PULSE_LOGGING_ENABLED) {
1055             PulseLogger.newInput("BEGIN_TOUCH_EVENT");
1056         }
1057         WindowStage stage = scene.getWindowStage();
1058         try {
1059             if (stage != null) {
1060                 stage.setInEventHandler(true);
1061             }
1062             QuantumToolkit.runWithoutRenderLock(() -> {
1063                 return AccessController.doPrivileged((PrivilegedAction<Void>) () -> {
1064                     if (scene.sceneListener != null) {
1065                         scene.sceneListener.touchEventBegin(time, touchEventCount,
1066                                 isDirect,
1067                                 (modifiers & KeyEvent.MODIFIER_SHIFT) != 0,
1068                                 (modifiers & KeyEvent.MODIFIER_CONTROL) != 0,
1069                                 (modifiers & KeyEvent.MODIFIER_ALT) != 0,
1070                                 (modifiers & KeyEvent.MODIFIER_WINDOWS) != 0);
1071                     }
1072                     return null;
1073                 }, scene.getAccessControlContext());
1074             });
1075         } finally {
1076             if (stage != null) {
1077                 stage.setInEventHandler(false);
1078             }
1079             if (PULSE_LOGGING_ENABLED) {
1080                 PulseLogger.newInput(null);
1081             }
1082         }
1083 
1084         gestures.notifyBeginTouchEvent(time, modifiers, isDirect, touchEventCount);
1085     }
1086 
1087     @Override public void handleNextTouchEvent(
1088             View view, final long time, final int type, final long touchId,
1089             final int x, final int y, final int xAbs, final int yAbs)
1090     {
1091         if (PULSE_LOGGING_ENABLED) {
1092             PulseLogger.newInput("NEXT_TOUCH_EVENT");
1093         }
1094         WindowStage stage = scene.getWindowStage();
1095         try {
1096             if (stage != null) {
1097                 stage.setInEventHandler(true);
1098             }
1099             QuantumToolkit.runWithoutRenderLock(() -> {
1100                     return AccessController.doPrivileged((PrivilegedAction<Void>) () -> {
1101                     if (scene.sceneListener != null) {
1102                         TouchPoint.State state;
1103                         switch (type) {
1104                             case TouchEvent.TOUCH_PRESSED:
1105                                 state = TouchPoint.State.PRESSED;
1106                                 break;
1107                             case TouchEvent.TOUCH_MOVED:
1108                                 state = TouchPoint.State.MOVED;
1109                                 break;
1110                             case TouchEvent.TOUCH_STILL:
1111                                 state = TouchPoint.State.STATIONARY;
1112                                 break;
1113                             case TouchEvent.TOUCH_RELEASED:
1114                                 state = TouchPoint.State.RELEASED;
1115                                 break;
1116                             default:
1117                                 throw new RuntimeException("Unknown touch state: " + type);
1118                         }
1119                         scene.sceneListener.touchEventNext(state, touchId, x, y, xAbs, yAbs);
1120                     }
1121                     return null;
1122                 }, scene.getAccessControlContext());
1123             });
1124         } finally {
1125             if (stage != null) {
1126                 stage.setInEventHandler(false);
1127             }
1128             if (PULSE_LOGGING_ENABLED) {
1129                 PulseLogger.newInput(null);
1130             }
1131         }
1132 
1133         gestures.notifyNextTouchEvent(time, type, touchId, x, y, xAbs, yAbs);
1134     }
1135 
1136     @Override public void handleEndTouchEvent(View view, long time) {
1137         if (PULSE_LOGGING_ENABLED) {
1138             PulseLogger.newInput("END_TOUCH_EVENT");
1139         }
1140         WindowStage stage = scene.getWindowStage();
1141         try {
1142             if (stage != null) {
1143                 stage.setInEventHandler(true);
1144             }
1145             QuantumToolkit.runWithoutRenderLock(() -> {
1146                 return AccessController.doPrivileged((PrivilegedAction<Void>) () -> {
1147                     if (scene.sceneListener != null) {
1148                         scene.sceneListener.touchEventEnd();
1149                     }
1150                     return null;
1151                 }, scene.getAccessControlContext());
1152             });
1153         } finally {
1154             if (stage != null) {
1155                 stage.setInEventHandler(false);
1156             }
1157             if (PULSE_LOGGING_ENABLED) {
1158                 PulseLogger.newInput(null);
1159             }
1160         }
1161 
1162         gestures.notifyEndTouchEvent(time);
1163     }
1164 
1165     @Override
1166     public Accessible getSceneAccessible() {
1167         if (scene != null && scene.sceneListener != null) {
1168             return scene.sceneListener.getSceneAccessible();
1169         }
1170         return null;
1171     }
1172 }