فهرست منبع

Merge pull request #5395 from ssaket/clipboardEvents

Clipboard events in GUI (AdvanceDynamicTexture)
David Catuhe 6 سال پیش
والد
کامیت
f6e2b10917

+ 1 - 0
Tools/Gulp/config.json

@@ -158,6 +158,7 @@
                 "../../src/babylon.types.js",
                 "../../src/Events/babylon.keyboardEvents.js",
                 "../../src/Events/babylon.pointerEvents.js",
+                "../../src/Events/babylon.clipboardEvents.js",
                 "../../src/Tools/babylon.tools.js",
                 "../../src/Math/babylon.math.js",
                 "../../src/Math/babylon.math.scalar.js",

+ 5 - 0
dist/preview release/what's new.md

@@ -16,6 +16,7 @@
   - WebXR camera rotation using parent container ([TrevorDev](https://github.com/TrevorDev))
 - GUI:
   - Added new [ImageBasedSlider](http://doc.babylonjs.com/how_to/gui#imagebasedslider) to let users customize sliders using images ([Deltakosh](https://github.com/deltakosh))
+  - Added support for clipboard events to let users perform `cut`, `copy` and `paste` events ([Saket Saurabh](https://github.com/ssaket))
 
 ## Updates
 
@@ -25,6 +26,10 @@
 - Added `sldier.displayThumb` to show/hide slider's thumb ([Deltakosh](https://github.com/deltakosh))
 - Added `grid.rowCount`, `grid.columnCount` and `grid.getChildrenAt()` ([Deltakosh](https://github.com/deltakosh))
 - Added `Control.AllowAlphaInheritance` to let users control the way alpha is used (inherited or not) ([Deltakosh](https://github.com/deltakosh))
+- Added support for performing operations like select all, text highlight, delete selected in `inputText` ([Saket Saurabh](https://github.com/ssaket))
+- Added `inputText.onTextCopyObservable`, `inputText.onTextCutObservable` and `inputText.onTextPasteObservable` to inputText ([Saket Saurabh](https://github.com/ssaket))
+- Added `AdvancedDynamicTexture.onClipboardObservable` to observe for clipboard events in AdvancedDynamicTexture([Saket Saurabh](https://github.com/ssaket))
+
 
 ### Core Engine
 

+ 59 - 2
gui/src/2D/advancedDynamicTexture.ts

@@ -1,4 +1,4 @@
-import { DynamicTexture, Nullable, Observer, Camera, Engine, KeyboardInfoPre, PointerInfoPre, PointerInfo, Layer, Viewport, Scene, Texture, KeyboardEventTypes, Vector3, Matrix, Vector2, Tools, PointerEventTypes, AbstractMesh, StandardMaterial, Color3 } from "babylonjs";
+import { DynamicTexture, Nullable, Observer, Camera, Engine, KeyboardInfoPre, PointerInfoPre, PointerInfo, ClipboardEventTypes, Layer, Viewport, Scene, Texture, KeyboardEventTypes, Vector3, Matrix, Vector2, Tools, PointerEventTypes, AbstractMesh, StandardMaterial, Color3, Observable, ClipboardInfo } from 'babylonjs';
 import { Container } from "./controls/container";
 import { Control } from "./controls/control";
 import { Style } from "./style";
@@ -68,6 +68,16 @@ export class AdvancedDynamicTexture extends DynamicTexture {
     private _blockNextFocusCheck = false;
     private _renderScale = 1;
     private _rootCanvas: Nullable<HTMLCanvasElement>;
+    /**
+     * Define type to string to ensure compatibility across browsers
+     * Safari doesn't support DataTransfer constructor
+     */
+    private _clipboardData: string = "";
+
+    /**
+     * Observable event triggered each time an clipboard event is received from the rendering canvas
+     */
+    public onClipboardObservable = new Observable<ClipboardInfo>();
 
     /**
      * Gets or sets a boolean defining if alpha is stored as premultiplied
@@ -237,6 +247,16 @@ export class AdvancedDynamicTexture extends DynamicTexture {
     }
 
     /**
+     * Gets or set information about clipboardData
+     */
+    public get clipboardData(): string {
+        return this._clipboardData;
+    }
+    public set clipboardData(value: string) {
+        this._clipboardData = value;
+    }
+
+     /**
      * Creates a new AdvancedDynamicTexture
      * @param name defines the name of the texture
      * @param width defines the width of the texture
@@ -380,6 +400,7 @@ export class AdvancedDynamicTexture extends DynamicTexture {
         }
 
         this._rootContainer.dispose();
+        this.onClipboardObservable.clear();
 
         super.dispose();
     }
@@ -596,7 +617,7 @@ export class AdvancedDynamicTexture extends DynamicTexture {
             if (pi.type !== PointerEventTypes.POINTERMOVE
                 && pi.type !== PointerEventTypes.POINTERUP
                 && pi.type !== PointerEventTypes.POINTERDOWN) {
-                return;
+                    return;
             }
 
             if (!scene) {
@@ -626,6 +647,42 @@ export class AdvancedDynamicTexture extends DynamicTexture {
         this._attachToOnPointerOut(scene);
     }
 
+    /** @hidden */
+    private onClipboardCopy = (evt: ClipboardEvent) => {
+        let ev = new ClipboardInfo(ClipboardEventTypes.COPY, evt);
+        this.onClipboardObservable.notifyObservers(ev);
+        evt.preventDefault();
+    }
+     /** @hidden */
+    private onClipboardCut = (evt: ClipboardEvent) => {
+        let ev = new ClipboardInfo(ClipboardEventTypes.CUT, evt);
+        this.onClipboardObservable.notifyObservers(ev);
+        evt.preventDefault();
+    }
+    /** @hidden */
+    private onClipboardPaste = (evt: ClipboardEvent) => {
+        let ev = new ClipboardInfo(ClipboardEventTypes.PASTE, evt);
+        this.onClipboardObservable.notifyObservers(ev);
+        evt.preventDefault();
+    }
+
+   /**
+    * Register the clipboard Events onto the canvas
+    */
+    public registerClipboardEvents(): void {
+        self.addEventListener("copy", this.onClipboardCopy, false);
+        self.addEventListener("cut", this.onClipboardCut, false);
+        self.addEventListener("paste", this.onClipboardPaste, false);
+    }
+    /**
+     * Unregister the clipboard Events from the canvas
+     */
+    public unRegisterClipboardEvents(): void {
+        self.removeEventListener("copy", this.onClipboardCopy);
+        self.removeEventListener("cut",  this.onClipboardCut);
+        self.removeEventListener("paste", this.onClipboardPaste);
+    }
+
     /**
      * Connect the texture to a hosting mesh to enable interactions
      * @param mesh defines the mesh to attach to

+ 245 - 2
gui/src/2D/controls/inputText.ts

@@ -1,7 +1,7 @@
 import { Control } from "./control";
 import { IFocusableControl } from "../advancedDynamicTexture";
 import { ValueAndUnit } from "../valueAndUnit";
-import { Nullable, Observable, Vector2 } from "babylonjs";
+import { Nullable, Observable, Observer, Vector2, ClipboardEventTypes, ClipboardInfo, PointerInfo } from 'babylonjs';
 import { Measure } from "../measure";
 import { VirtualKeyboard } from "./virtualKeyboard";
 
@@ -28,6 +28,14 @@ export class InputText extends Control implements IFocusableControl {
     private _deadKey = false;
     private _addKey = true;
     private _currentKey = "";
+    private _isTextHighlightOn = false;
+    private _textHighlightColor = "#d5e0ff";
+    private _highligherOpacity = 0.4;
+    private _highlightedText = "";
+    private _startHighlightIndex = 0;
+    private _endHighlightIndex = 0;
+    private _onClipboardObserver: Nullable<Observer<ClipboardInfo>>;
+    private _onPointerDblTapObserver: Nullable<Observer<PointerInfo>>;
 
     /** @hidden */
     public _connectedVirtualKeyboard: Nullable<VirtualKeyboard>;
@@ -43,6 +51,14 @@ export class InputText extends Control implements IFocusableControl {
     public onFocusObservable = new Observable<InputText>();
     /** Observable raised when the control loses the focus */
     public onBlurObservable = new Observable<InputText>();
+    /**Observable raised when the text is highlighted */
+    public onTextHighlightObservable = new Observable<InputText>();
+    /**Observable raised when copy event is triggered */
+    public onTextCopyObservable = new Observable<InputText>();
+    /** Observable raised when cut event is triggered */
+    public onTextCutObservable = new Observable<InputText>();
+    /** Observable raised when paste event is triggered */
+    public onTextPasteObservable = new Observable<InputText>();
 
     /** Gets or sets the maximum width allowed by the control */
     public get maxWidth(): string | number {
@@ -64,6 +80,32 @@ export class InputText extends Control implements IFocusableControl {
         }
     }
 
+    /** Gets and sets the text highlighter transparency; default: 0.4 */
+    public get highligherOpacity(): number {
+        return this._highligherOpacity;
+    }
+
+    public set highligherOpacity(value: number) {
+        if (this._highligherOpacity === value) {
+            return;
+        }
+        this._highligherOpacity = value;
+        this._markAsDirty();
+    }
+
+    /** Gets and sets the text hightlight color */
+    public get textHighlightColor(): string {
+        return this._textHighlightColor;
+    }
+
+    public set textHighlightColor(value: string) {
+        if (this._textHighlightColor === value) {
+            return;
+        }
+        this._textHighlightColor = value;
+        this._markAsDirty();
+    }
+
     /** Gets or sets control margin */
     public get margin(): string {
         return this._margin.toString(this._host);
@@ -176,6 +218,18 @@ export class InputText extends Control implements IFocusableControl {
         this._deadKey = flag;
     }
 
+    /** Gets or sets the highlight text */
+    public get highlightedText(): string {
+        return this._highlightedText;
+    }
+    public set highlightedText(text: string) {
+        if (this._highlightedText === text) {
+            return;
+        }
+        this._highlightedText = text;
+        this._markAsDirty();
+    }
+
     /** Gets or sets if the current key should be added */
     public get addKey(): boolean {
         return this._addKey;
@@ -246,6 +300,15 @@ export class InputText extends Control implements IFocusableControl {
         this._markAsDirty();
 
         this.onBlurObservable.notifyObservers(this);
+
+        this._host.unRegisterClipboardEvents();
+        if (this._onClipboardObserver) {
+            this._host.onClipboardObservable.remove(this._onClipboardObserver);
+        }
+        let scene = this._host.getScene();
+        if (this._onPointerDblTapObserver && scene) {
+            scene.onPointerObservable.remove(this._onPointerDblTapObserver);
+        }
     }
 
     /** @hidden */
@@ -270,6 +333,41 @@ export class InputText extends Control implements IFocusableControl {
             this._host.focusedControl = null;
             return;
         }
+
+        this._host.registerClipboardEvents();
+
+        this._onClipboardObserver = this._host.onClipboardObservable.add((clipboardInfo) => {
+            // process clipboard event, can be configured.
+             switch (clipboardInfo.type){
+                 case ClipboardEventTypes.COPY:
+                         this._onCopyText(clipboardInfo.event);
+                         this.onTextCopyObservable.notifyObservers(this);
+                         break;
+                 case ClipboardEventTypes.CUT:
+                         this._onCutText(clipboardInfo.event);
+                         this.onTextCutObservable.notifyObservers(this);
+                         break;
+                 case ClipboardEventTypes.PASTE:
+                         this._onPasteText(clipboardInfo.event);
+                         this.onTextPasteObservable.notifyObservers(this);
+                         break;
+                 default: return;
+              }
+        });
+
+        let scene = this._host.getScene();
+        if (scene) {
+            //register the pointer double tap event
+            this._onPointerDblTapObserver = scene.onPointerObservable.add((pointerInfo) => {
+                if (!this._isFocused) {
+                    return;
+                }
+                if (pointerInfo.type === BABYLON.PointerEventTypes.POINTERDOUBLETAP) {
+                    this._processDblClick(pointerInfo);
+                }
+            });
+        }
+
     }
 
     protected _getTypeName(): string {
@@ -289,6 +387,29 @@ export class InputText extends Control implements IFocusableControl {
 
     /** @hidden */
     public processKey(keyCode: number, key?: string, evt?: KeyboardEvent) {
+
+        //return if clipboard event keys (i.e -ctr/cmd + c,v,x)
+        if (evt && (evt.ctrlKey || evt.metaKey) && (keyCode === 67 || keyCode === 86 || keyCode === 88)) {
+            return;
+        }
+
+        //select all
+        if (evt && (evt.ctrlKey || evt.metaKey) && keyCode === 65) {
+
+            this._blinkIsEven = false;
+            this._isTextHighlightOn = true;
+            evt.preventDefault();
+
+            //if already highlighted pass
+            if (this._highlightedText) {
+                return;
+            }
+
+            this._startHighlightIndex = 0;
+            this._endHighlightIndex = this._text.length;
+            this._cursorOffset = 0;
+            return;
+        }
         // Specific cases
         switch (keyCode) {
             case 32: //SPACE
@@ -301,6 +422,18 @@ export class InputText extends Control implements IFocusableControl {
                 break;
             case 8: // BACKSPACE
                 if (this._text && this._text.length > 0) {
+                    //delete the highlighted text
+                    if (this._isTextHighlightOn) {
+                        this.text = this._text.slice(0, this._startHighlightIndex) + this._text.slice(this._endHighlightIndex);
+                        this._isTextHighlightOn = false;
+                        this._cursorOffset =  this.text.length - this._startHighlightIndex;
+                        this._blinkIsEven = false;
+                        if (evt) {
+                            evt.preventDefault();
+                        }
+                        return;
+                    }
+                    //delete single character
                     if (this._cursorOffset === 0) {
                         this.text = this._text.substr(0, this._text.length - 1);
                     } else {
@@ -315,11 +448,27 @@ export class InputText extends Control implements IFocusableControl {
                 }
                 return;
             case 46: // DELETE
+                if (this._isTextHighlightOn) {
+                    this.text = this._text.slice(0, this._startHighlightIndex) + this._text.slice(this._endHighlightIndex);
+                    let decrementor = (this._endHighlightIndex - this._startHighlightIndex);
+                    while (decrementor > 0 && this._cursorOffset > 0) {
+                        this._cursorOffset--;
+                    }
+                    this._isTextHighlightOn = false;
+                    this._cursorOffset = this.text.length - this._startHighlightIndex;
+                    if (evt) {
+                        evt.preventDefault();
+                    }
+                    return;
+                }
                 if (this._text && this._text.length > 0  && this._cursorOffset > 0) {
                     let deletePosition = this._text.length - this._cursorOffset;
                     this.text = this._text.slice(0, deletePosition) + this._text.slice(deletePosition + 1);
                     this._cursorOffset--;
                 }
+                if (evt) {
+                    evt.preventDefault();
+                }
                 return;
             case 13: // RETURN
                 this._host.focusedControl = null;
@@ -335,6 +484,14 @@ export class InputText extends Control implements IFocusableControl {
                 this._markAsDirty();
                 return;
             case 37: // LEFT
+                if (evt && evt.shiftKey) {
+                    if (!this._isTextHighlightOn) {
+                        this._isTextHighlightOn = true;
+                        this._endHighlightIndex = this._text.length - this._cursorOffset;
+                        this._startHighlightIndex = this._endHighlightIndex;
+                    }
+                    (this._startHighlightIndex < 0) ? 0 : --this._startHighlightIndex;
+                }
                 this._cursorOffset++;
                 if (this._cursorOffset > this._text.length) {
                     this._cursorOffset = this._text.length;
@@ -343,6 +500,14 @@ export class InputText extends Control implements IFocusableControl {
                 this._markAsDirty();
                 return;
             case 39: // RIGHT
+                if (evt && evt.shiftKey) {
+                    if (!this._isTextHighlightOn) {
+                        this._isTextHighlightOn = true;
+                        this._startHighlightIndex = this._text.length - this._cursorOffset;
+                        this._endHighlightIndex = this._startHighlightIndex;
+                    }
+                    (this._endHighlightIndex > this._text.length) ? this._text.length - 1 : ++this._endHighlightIndex;
+                }
                 this._cursorOffset--;
                 if (this._cursorOffset < 0) {
                     this._cursorOffset = 0;
@@ -357,6 +522,7 @@ export class InputText extends Control implements IFocusableControl {
                 this.deadKey = true;
                 break;
         }
+        this._isTextHighlightOn = false;
 
         // Printable characters
         if (key &&
@@ -381,12 +547,70 @@ export class InputText extends Control implements IFocusableControl {
             }
         }
     }
-
     /** @hidden */
+    private _processDblClick(evt: PointerInfo) {
+        //pre-find the start and end index of the word under cursor, speeds up the rendering
+        this._startHighlightIndex = this._text.length - this._cursorOffset;
+        this._endHighlightIndex = this._startHighlightIndex;
+        for (let rWord = /\w+/g, left = 1, right = 1; this._startHighlightIndex > 0 && this._endHighlightIndex < this._text.length && (left || right);) {
+            right = (this._text[this._endHighlightIndex].search(rWord) !== -1) ? ++this._endHighlightIndex : 0;
+            left =  (this._text[this._startHighlightIndex - 1 ].search(rWord) !== -1) ? --this._startHighlightIndex : 0;
+        }
+        this.onTextHighlightObservable.notifyObservers(this);
+        this._isTextHighlightOn = true;
+        this._blinkIsEven = false;
+    }
+
+    /**
+     * Handles the keyboard event
+     * @param evt Defines the KeyboardEvent
+     */
     public processKeyboard(evt: KeyboardEvent): void {
+        // process pressed key
         this.processKey(evt.keyCode, evt.key, evt);
     }
 
+    /** @hidden */
+    private _onCopyText(ev: ClipboardEvent): void {
+        this._isTextHighlightOn = false;
+        //when write permission to clipbaord data is denied
+        try {
+            ev.clipboardData.setData("text/plain", this._highlightedText);
+        }
+        catch {} //pass
+        this._host.clipboardData = this._highlightedText;
+    }
+    /** @hidden */
+    private _onCutText(ev: ClipboardEvent): void {
+        if (!this._highlightedText) {
+            return;
+        }
+        this.text = this._text.slice(0, this._startHighlightIndex) + this._text.slice(this._endHighlightIndex);
+        this._isTextHighlightOn = false;
+        this._cursorOffset = this.text.length - this._startHighlightIndex;
+        //when write permission to clipbaord data is denied
+        try {
+            ev.clipboardData.setData("text/plain", this._highlightedText);
+        }
+        catch { } //pass
+
+        this._host.clipboardData = this._highlightedText;
+        this._highlightedText = "";
+    }
+    /** @hidden */
+    private _onPasteText(ev: ClipboardEvent): void {
+        let data: string = "";
+        if (ev.clipboardData && ev.clipboardData.types.indexOf("text/plain") !== -1) {
+            data = ev.clipboardData.getData("text/plain");
+        }
+        else {
+            //get the cached data; returns blank string by default
+            data = this._host.clipboardData;
+        }
+        let insertPosition = this._text.length - this._cursorOffset;
+        this.text = this._text.slice(0, insertPosition) + data + this._text.slice(insertPosition);
+    }
+
     public _draw(parentMeasure: Measure, context: CanvasRenderingContext2D): void {
         context.save();
 
@@ -514,6 +738,19 @@ export class InputText extends Control implements IFocusableControl {
                     this._blinkIsEven = !this._blinkIsEven;
                     this._markAsDirty();
                 }, 500);
+
+                 //show the highlighted text
+                if (this._isTextHighlightOn) {
+                    clearTimeout(this._blinkTimeout);
+                    let highlightCursorOffsetWidth = context.measureText(this.text.substring(this._startHighlightIndex)).width;
+                    let highlightCursorLeft = this._scrollLeft + this._textWidth - highlightCursorOffsetWidth;
+                    this._highlightedText = this.text.substring(this._startHighlightIndex, this._endHighlightIndex);
+                    //for transparancy
+                    context.globalAlpha = this._highligherOpacity;
+                    context.fillStyle = this._textHighlightColor;
+                    context.fillRect(highlightCursorLeft, this._currentMeasure.top + (this._currentMeasure.height - this._fontOffset.height) / 2, context.measureText(this.text.substring(this._startHighlightIndex, this._endHighlightIndex)).width, this._fontOffset.height);
+                    context.globalAlpha = 1.0;
+                }
             }
 
             context.restore();
@@ -538,6 +775,8 @@ export class InputText extends Control implements IFocusableControl {
         }
 
         this._clickedCoordinate = coordinates.x;
+        this._isTextHighlightOn = false;
+        this._highlightedText = "";
         if (this._host.focusedControl === this) {
             // Move cursor
             clearTimeout(this._blinkTimeout);
@@ -566,5 +805,9 @@ export class InputText extends Control implements IFocusableControl {
         this.onBlurObservable.clear();
         this.onFocusObservable.clear();
         this.onTextChangedObservable.clear();
+        this.onTextCopyObservable.clear();
+        this.onTextCutObservable.clear();
+        this.onTextPasteObservable.clear();
+        this.onTextHighlightObservable.clear();
     }
 }

+ 57 - 0
src/Events/babylon.clipboardEvents.ts

@@ -0,0 +1,57 @@
+
+module BABYLON {
+    /**
+     * Gather the list of clipboard event types as constants.
+     */
+    export class ClipboardEventTypes {
+        /**
+         * The clipboard event is fired when a copy command is active (pressed).
+         */
+        public static readonly COPY = 0x01; //
+        /**
+         *  The clipboard event is fired when a cut command is active (pressed).
+         */
+        public static readonly CUT = 0x02;
+
+        /**
+         * The clipboard event is fired when a paste command is active (pressed).
+         */
+        public static readonly PASTE = 0x03;
+    }
+    /**
+     * This class is used to store clipboard related info for the onClipboardObservable event.
+     */
+    export class ClipboardInfo {
+        /**
+         *Creates an instance of ClipboardInfo.
+         * @param {number} type
+         * @param {ClipboardEvent} event
+         */
+        constructor(
+            /**
+             * Defines the type of event (BABYLON.ClipboardEventTypes)
+             */
+            public type: number,
+            /**
+             * Defines the related dom event
+             */
+            public event: ClipboardEvent) {
+        }
+
+       /**
+        *  Get the clipboard event's type from the keycode.
+        * @param keyCode Defines the keyCode for the current keyboard event.
+        * @return {number}
+        */
+        public static GetTypeFromCharacter(keyCode: number): number {
+            let charCode = keyCode;
+            //TODO: add codes for extended ASCII
+            switch (charCode){
+                case 67: return ClipboardEventTypes.COPY;
+                case 86: return ClipboardEventTypes.PASTE;
+                case 88: return ClipboardEventTypes.CUT;
+                default: return -1;
+            }
+        }
+    }
+}