summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPierre Ossman <ossman@cendio.se>2020-05-18 15:17:06 +0200
committerSamuel Mannehed <samuel@cendio.se>2020-06-12 09:18:46 +0200
commit8be924c9d96324800ac38ea086e500ee71255d0d (patch)
treee2d5e1a4163214efdc0c6bb25ad796dbcc606ba7
parent440ec8a0b696640ce1276b0359ed573762dce245 (diff)
downloadnovnc-8be924c9d96324800ac38ea086e500ee71255d0d.tar.gz
Add touch gestures for mouse emulation
Add several single and multitouch gestures to simulate various mouse actions that would otherwise be impossible to perform. This replaces the old system where you could select which mouse button a single touch would generate.
-rw-r--r--app/images/mouse_left.svg92
-rw-r--r--app/images/mouse_middle.svg92
-rw-r--r--app/images/mouse_none.svg92
-rw-r--r--app/images/mouse_right.svg92
-rw-r--r--app/ui.js31
-rw-r--r--core/input/gesturehandler.js567
-rw-r--r--core/input/mouse.js58
-rw-r--r--core/rfb.js183
-rw-r--r--core/util/element.js32
-rw-r--r--docs/API.md6
-rw-r--r--tests/test.gesturehandler.js1031
-rw-r--r--tests/test.mouse.js126
-rw-r--r--tests/test.rfb.js750
-rw-r--r--vnc.html12
14 files changed, 2562 insertions, 602 deletions
diff --git a/app/images/mouse_left.svg b/app/images/mouse_left.svg
deleted file mode 100644
index ce4cca4..0000000
--- a/app/images/mouse_left.svg
+++ /dev/null
@@ -1,92 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="25"
- height="25"
- viewBox="0 0 25 25"
- id="svg2"
- version="1.1"
- inkscape:version="0.91 r13725"
- sodipodi:docname="mouse_left.svg"
- inkscape:export-filename="/home/ossman/devel/noVNC/images/drag.png"
- inkscape:export-xdpi="90"
- inkscape:export-ydpi="90">
- <defs
- id="defs4" />
- <sodipodi:namedview
- id="base"
- pagecolor="#959595"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:zoom="11.313708"
- inkscape:cx="15.551515"
- inkscape:cy="12.205592"
- inkscape:document-units="px"
- inkscape:current-layer="layer1"
- showgrid="false"
- units="px"
- inkscape:snap-bbox="true"
- inkscape:bbox-paths="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:object-paths="true"
- showguides="true"
- inkscape:window-width="1920"
- inkscape:window-height="1136"
- inkscape:window-x="1920"
- inkscape:window-y="27"
- inkscape:window-maximized="1"
- inkscape:snap-smooth-nodes="true"
- inkscape:object-nodes="true"
- inkscape:snap-intersection-paths="true"
- inkscape:snap-nodes="true"
- inkscape:snap-global="true">
- <inkscape:grid
- type="xygrid"
- id="grid4136" />
- </sodipodi:namedview>
- <metadata
- id="metadata7">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title></dc:title>
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:label="Layer 1"
- inkscape:groupmode="layer"
- id="layer1"
- transform="translate(0,-1027.3622)">
- <path
- style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#0068f6;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
- d="m 8,1030.3622 c -2.1987124,0 -4,1.8013 -4,4 l 0,2 5,0 0,-2 c 0,-1.4738 1.090393,-2.7071 2.5,-2.9492 l 0,-1.0508 -3.5,0 z"
- id="path6219" />
- <path
- style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
- d="m 13.5,1030.3622 0,1.0508 c 1.409607,0.2421 2.5,1.4754 2.5,2.9492 l 0,2 5,0 0,-2 c 0,-2.1987 -1.801288,-4 -4,-4 l -3.5,0 z"
- id="path6217" />
- <path
- style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
- d="m 12,1033.3622 c -0.571311,0 -1,0.4287 -1,1 l 0,5 c 0,0.5713 0.428689,1 1,1 l 1,0 c 0.571311,0 1,-0.4287 1,-1 l 0,-5 c 0,-0.5713 -0.428689,-1 -1,-1 l -1,0 z"
- id="path6215" />
- <path
- style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
- d="m 4,1038.3622 0,3.5 c 0,4.1377 3.362302,7.5 7.5,7.5 l 2,0 c 4.137698,0 7.5,-3.3623 7.5,-7.5 l 0,-3.5 -5,0 0,1 c 0,1.6447 -1.355293,3 -3,3 l -1,0 c -1.644707,0 -3,-1.3553 -3,-3 l 0,-1 -5,0 z"
- id="rect6178" />
- </g>
-</svg>
diff --git a/app/images/mouse_middle.svg b/app/images/mouse_middle.svg
deleted file mode 100644
index 6603425..0000000
--- a/app/images/mouse_middle.svg
+++ /dev/null
@@ -1,92 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="25"
- height="25"
- viewBox="0 0 25 25"
- id="svg2"
- version="1.1"
- inkscape:version="0.91 r13725"
- sodipodi:docname="mouse_middle.svg"
- inkscape:export-filename="/home/ossman/devel/noVNC/images/drag.png"
- inkscape:export-xdpi="90"
- inkscape:export-ydpi="90">
- <defs
- id="defs4" />
- <sodipodi:namedview
- id="base"
- pagecolor="#959595"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:zoom="11.313708"
- inkscape:cx="15.551515"
- inkscape:cy="12.205592"
- inkscape:document-units="px"
- inkscape:current-layer="layer1"
- showgrid="false"
- units="px"
- inkscape:snap-bbox="true"
- inkscape:bbox-paths="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:object-paths="true"
- showguides="true"
- inkscape:window-width="1920"
- inkscape:window-height="1136"
- inkscape:window-x="1920"
- inkscape:window-y="27"
- inkscape:window-maximized="1"
- inkscape:snap-smooth-nodes="true"
- inkscape:object-nodes="true"
- inkscape:snap-intersection-paths="true"
- inkscape:snap-nodes="true"
- inkscape:snap-global="true">
- <inkscape:grid
- type="xygrid"
- id="grid4136" />
- </sodipodi:namedview>
- <metadata
- id="metadata7">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title></dc:title>
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:label="Layer 1"
- inkscape:groupmode="layer"
- id="layer1"
- transform="translate(0,-1027.3622)">
- <path
- style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
- d="m 8,1030.3622 c -2.1987124,0 -4,1.8013 -4,4 l 0,2 5,0 0,-2 c 0,-1.4738 1.090393,-2.7071 2.5,-2.9492 l 0,-1.0508 -3.5,0 z"
- id="path6219" />
- <path
- style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
- d="m 13.5,1030.3622 0,1.0508 c 1.409607,0.2421 2.5,1.4754 2.5,2.9492 l 0,2 5,0 0,-2 c 0,-2.1987 -1.801288,-4 -4,-4 l -3.5,0 z"
- id="path6217" />
- <path
- style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#0068f6;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
- d="m 12,1033.3622 c -0.571311,0 -1,0.4287 -1,1 l 0,5 c 0,0.5713 0.428689,1 1,1 l 1,0 c 0.571311,0 1,-0.4287 1,-1 l 0,-5 c 0,-0.5713 -0.428689,-1 -1,-1 l -1,0 z"
- id="path6215" />
- <path
- style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
- d="m 4,1038.3622 0,3.5 c 0,4.1377 3.362302,7.5 7.5,7.5 l 2,0 c 4.137698,0 7.5,-3.3623 7.5,-7.5 l 0,-3.5 -5,0 0,1 c 0,1.6447 -1.355293,3 -3,3 l -1,0 c -1.644707,0 -3,-1.3553 -3,-3 l 0,-1 -5,0 z"
- id="rect6178" />
- </g>
-</svg>
diff --git a/app/images/mouse_none.svg b/app/images/mouse_none.svg
deleted file mode 100644
index 3e0f838..0000000
--- a/app/images/mouse_none.svg
+++ /dev/null
@@ -1,92 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="25"
- height="25"
- viewBox="0 0 25 25"
- id="svg2"
- version="1.1"
- inkscape:version="0.91 r13725"
- sodipodi:docname="mouse_none.svg"
- inkscape:export-filename="/home/ossman/devel/noVNC/images/drag.png"
- inkscape:export-xdpi="90"
- inkscape:export-ydpi="90">
- <defs
- id="defs4" />
- <sodipodi:namedview
- id="base"
- pagecolor="#959595"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:zoom="16"
- inkscape:cx="23.160825"
- inkscape:cy="13.208262"
- inkscape:document-units="px"
- inkscape:current-layer="layer1"
- showgrid="false"
- units="px"
- inkscape:snap-bbox="true"
- inkscape:bbox-paths="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:object-paths="true"
- showguides="true"
- inkscape:window-width="1920"
- inkscape:window-height="1136"
- inkscape:window-x="1920"
- inkscape:window-y="27"
- inkscape:window-maximized="1"
- inkscape:snap-smooth-nodes="true"
- inkscape:object-nodes="true"
- inkscape:snap-intersection-paths="true"
- inkscape:snap-nodes="true"
- inkscape:snap-global="true">
- <inkscape:grid
- type="xygrid"
- id="grid4136" />
- </sodipodi:namedview>
- <metadata
- id="metadata7">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title></dc:title>
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:label="Layer 1"
- inkscape:groupmode="layer"
- id="layer1"
- transform="translate(0,-1027.3622)">
- <path
- style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
- d="m 8,1030.3622 c -2.1987124,0 -4,1.8013 -4,4 l 0,2 5,0 0,-2 c 0,-1.4738 1.090393,-2.7071 2.5,-2.9492 l 0,-1.0508 -3.5,0 z"
- id="path6219" />
- <path
- style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
- d="m 13.5,1030.3622 0,1.0508 c 1.409607,0.2421 2.5,1.4754 2.5,2.9492 l 0,2 5,0 0,-2 c 0,-2.1987 -1.801288,-4 -4,-4 l -3.5,0 z"
- id="path6217" />
- <path
- style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
- d="m 12,1033.3622 c -0.571311,0 -1,0.4287 -1,1 l 0,5 c 0,0.5713 0.428689,1 1,1 l 1,0 c 0.571311,0 1,-0.4287 1,-1 l 0,-5 c 0,-0.5713 -0.428689,-1 -1,-1 l -1,0 z"
- id="path6215" />
- <path
- style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
- d="m 4,1038.3622 0,3.5 c 0,4.1377 3.362302,7.5 7.5,7.5 l 2,0 c 4.137698,0 7.5,-3.3623 7.5,-7.5 l 0,-3.5 -5,0 0,1 c 0,1.6447 -1.355293,3 -3,3 l -1,0 c -1.644707,0 -3,-1.3553 -3,-3 l 0,-1 -5,0 z"
- id="rect6178" />
- </g>
-</svg>
diff --git a/app/images/mouse_right.svg b/app/images/mouse_right.svg
deleted file mode 100644
index f4bad76..0000000
--- a/app/images/mouse_right.svg
+++ /dev/null
@@ -1,92 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="25"
- height="25"
- viewBox="0 0 25 25"
- id="svg2"
- version="1.1"
- inkscape:version="0.91 r13725"
- sodipodi:docname="mouse_right.svg"
- inkscape:export-filename="/home/ossman/devel/noVNC/images/drag.png"
- inkscape:export-xdpi="90"
- inkscape:export-ydpi="90">
- <defs
- id="defs4" />
- <sodipodi:namedview
- id="base"
- pagecolor="#959595"
- bordercolor="#666666"
- borderopacity="1.0"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:zoom="11.313708"
- inkscape:cx="15.551515"
- inkscape:cy="12.205592"
- inkscape:document-units="px"
- inkscape:current-layer="layer1"
- showgrid="false"
- units="px"
- inkscape:snap-bbox="true"
- inkscape:bbox-paths="true"
- inkscape:bbox-nodes="true"
- inkscape:snap-bbox-edge-midpoints="true"
- inkscape:object-paths="true"
- showguides="true"
- inkscape:window-width="1920"
- inkscape:window-height="1136"
- inkscape:window-x="1920"
- inkscape:window-y="27"
- inkscape:window-maximized="1"
- inkscape:snap-smooth-nodes="true"
- inkscape:object-nodes="true"
- inkscape:snap-intersection-paths="true"
- inkscape:snap-nodes="true"
- inkscape:snap-global="true">
- <inkscape:grid
- type="xygrid"
- id="grid4136" />
- </sodipodi:namedview>
- <metadata
- id="metadata7">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title></dc:title>
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <g
- inkscape:label="Layer 1"
- inkscape:groupmode="layer"
- id="layer1"
- transform="translate(0,-1027.3622)">
- <path
- style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
- d="m 8,1030.3622 c -2.1987124,0 -4,1.8013 -4,4 l 0,2 5,0 0,-2 c 0,-1.4738 1.090393,-2.7071 2.5,-2.9492 l 0,-1.0508 -3.5,0 z"
- id="path6219" />
- <path
- style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#0068f6;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
- d="m 13.5,1030.3622 0,1.0508 c 1.409607,0.2421 2.5,1.4754 2.5,2.9492 l 0,2 5,0 0,-2 c 0,-2.1987 -1.801288,-4 -4,-4 l -3.5,0 z"
- id="path6217" />
- <path
- style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
- d="m 12,1033.3622 c -0.571311,0 -1,0.4287 -1,1 l 0,5 c 0,0.5713 0.428689,1 1,1 l 1,0 c 0.571311,0 1,-0.4287 1,-1 l 0,-5 c 0,-0.5713 -0.428689,-1 -1,-1 l -1,0 z"
- id="path6215" />
- <path
- style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
- d="m 4,1038.3622 0,3.5 c 0,4.1377 3.362302,7.5 7.5,7.5 l 2,0 c 4.137698,0 7.5,-3.3623 7.5,-7.5 l 0,-3.5 -5,0 0,1 c 0,1.6447 -1.355293,3 -3,3 l -1,0 c -1.644707,0 -3,-1.3553 -3,-3 l 0,-1 -5,0 z"
- id="rect6178" />
- </g>
-</svg>
diff --git a/app/ui.js b/app/ui.js
index a039e94..c70743d 100644
--- a/app/ui.js
+++ b/app/ui.js
@@ -234,14 +234,6 @@ const UI = {
},
addTouchSpecificHandlers() {
- document.getElementById("noVNC_mouse_button0")
- .addEventListener('click', () => UI.setMouseButton(1));
- document.getElementById("noVNC_mouse_button1")
- .addEventListener('click', () => UI.setMouseButton(2));
- document.getElementById("noVNC_mouse_button2")
- .addEventListener('click', () => UI.setMouseButton(4));
- document.getElementById("noVNC_mouse_button4")
- .addEventListener('click', () => UI.setMouseButton(0));
document.getElementById("noVNC_keyboard_button")
.addEventListener('click', UI.toggleVirtualKeyboard);
@@ -430,7 +422,6 @@ const UI = {
UI.disableSetting('port');
UI.disableSetting('path');
UI.disableSetting('repeaterID');
- UI.setMouseButton(1);
// Hide the controlbar after 2 seconds
UI.closeControlbarTimeout = setTimeout(UI.closeControlbar, 2000);
@@ -1633,24 +1624,6 @@ const UI = {
* MISC
* ------v------*/
- setMouseButton(num) {
- const viewOnly = UI.rfb.viewOnly;
- if (UI.rfb && !viewOnly) {
- UI.rfb.touchButton = num;
- }
-
- const blist = [0, 1, 2, 4];
- for (let b = 0; b < blist.length; b++) {
- const button = document.getElementById('noVNC_mouse_button' +
- blist[b]);
- if (blist[b] === num && !viewOnly) {
- button.classList.remove("noVNC_hidden");
- } else {
- button.classList.add("noVNC_hidden");
- }
- }
- },
-
updateViewOnly() {
if (!UI.rfb) return;
UI.rfb.viewOnly = UI.getSetting('view_only');
@@ -1661,8 +1634,6 @@ const UI = {
.classList.add('noVNC_hidden');
document.getElementById('noVNC_toggle_extra_keys_button')
.classList.add('noVNC_hidden');
- document.getElementById('noVNC_mouse_button' + UI.rfb.touchButton)
- .classList.add('noVNC_hidden');
document.getElementById('noVNC_clipboard_button')
.classList.add('noVNC_hidden');
} else {
@@ -1670,8 +1641,6 @@ const UI = {
.classList.remove('noVNC_hidden');
document.getElementById('noVNC_toggle_extra_keys_button')
.classList.remove('noVNC_hidden');
- document.getElementById('noVNC_mouse_button' + UI.rfb.touchButton)
- .classList.remove('noVNC_hidden');
document.getElementById('noVNC_clipboard_button')
.classList.remove('noVNC_hidden');
}
diff --git a/core/input/gesturehandler.js b/core/input/gesturehandler.js
new file mode 100644
index 0000000..6fa72d2
--- /dev/null
+++ b/core/input/gesturehandler.js
@@ -0,0 +1,567 @@
+/*
+ * noVNC: HTML5 VNC client
+ * Copyright (C) 2020 The noVNC Authors
+ * Licensed under MPL 2.0 (see LICENSE.txt)
+ *
+ * See README.md for usage and integration instructions.
+ *
+ */
+
+const GH_NOGESTURE = 0;
+const GH_ONETAP = 1;
+const GH_TWOTAP = 2;
+const GH_THREETAP = 4;
+const GH_DRAG = 8;
+const GH_LONGPRESS = 16;
+const GH_TWODRAG = 32;
+const GH_PINCH = 64;
+
+const GH_INITSTATE = 127;
+
+const GH_MOVE_THRESHOLD = 50;
+const GH_ANGLE_THRESHOLD = 90; // Degrees
+
+// Timeout when waiting for gestures (ms)
+const GH_MULTITOUCH_TIMEOUT = 250;
+
+// Maximum time between press and release for a tap (ms)
+const GH_TAP_TIMEOUT = 1000;
+
+// Timeout when waiting for longpress (ms)
+const GH_LONGPRESS_TIMEOUT = 1000;
+
+// Timeout when waiting to decide between PINCH and TWODRAG (ms)
+const GH_TWOTOUCH_TIMEOUT = 50;
+
+export default class GestureHandler {
+ constructor() {
+ this._target = null;
+
+ this._state = GH_INITSTATE;
+
+ this._tracked = [];
+ this._ignored = [];
+
+ this._waitingRelease = false;
+ this._releaseStart = 0.0;
+
+ this._longpressTimeoutId = null;
+ this._twoTouchTimeoutId = null;
+
+ this._boundEventHandler = this._eventHandler.bind(this);
+ }
+
+ attach(target) {
+ this.detach();
+
+ this._target = target;
+ this._target.addEventListener('touchstart',
+ this._boundEventHandler);
+ this._target.addEventListener('touchmove',
+ this._boundEventHandler);
+ this._target.addEventListener('touchend',
+ this._boundEventHandler);
+ this._target.addEventListener('touchcancel',
+ this._boundEventHandler);
+ }
+
+ detach() {
+ if (!this._target) {
+ return;
+ }
+
+ this._stopLongpressTimeout();
+ this._stopTwoTouchTimeout();
+
+ this._target.removeEventListener('touchstart',
+ this._boundEventHandler);
+ this._target.removeEventListener('touchmove',
+ this._boundEventHandler);
+ this._target.removeEventListener('touchend',
+ this._boundEventHandler);
+ this._target.removeEventListener('touchcancel',
+ this._boundEventHandler);
+ this._target = null;
+ }
+
+ _eventHandler(e) {
+ let fn;
+
+ e.stopPropagation();
+ e.preventDefault();
+
+ switch (e.type) {
+ case 'touchstart':
+ fn = this._touchStart;
+ break;
+ case 'touchmove':
+ fn = this._touchMove;
+ break;
+ case 'touchend':
+ case 'touchcancel':
+ fn = this._touchEnd;
+ break;
+ }
+
+ for (let i = 0; i < e.changedTouches.length; i++) {
+ let touch = e.changedTouches[i];
+ fn.call(this, touch.identifier, touch.clientX, touch.clientY);
+ }
+ }
+
+ _touchStart(id, x, y) {
+ // Ignore any new touches if there is already an active gesture,
+ // or we're in a cleanup state
+ if (this._hasDetectedGesture() || (this._state === GH_NOGESTURE)) {
+ this._ignored.push(id);
+ return;
+ }
+
+ // Did it take too long between touches that we should no longer
+ // consider this a single gesture?
+ if ((this._tracked.length > 0) &&
+ ((Date.now() - this._tracked[0].started) > GH_MULTITOUCH_TIMEOUT)) {
+ this._state = GH_NOGESTURE;
+ this._ignored.push(id);
+ return;
+ }
+
+ // If we're waiting for fingers to release then we should no longer
+ // recognize new touches
+ if (this._waitingRelease) {
+ this._state = GH_NOGESTURE;
+ this._ignored.push(id);
+ return;
+ }
+
+ this._tracked.push({
+ id: id,
+ started: Date.now(),
+ active: true,
+ firstX: x,
+ firstY: y,
+ lastX: x,
+ lastY: y,
+ angle: 0
+ });
+
+ switch (this._tracked.length) {
+ case 1:
+ this._startLongpressTimeout();
+ break;
+
+ case 2:
+ this._state &= ~(GH_ONETAP | GH_DRAG | GH_LONGPRESS);
+ this._stopLongpressTimeout();
+ break;
+
+ case 3:
+ this._state &= ~(GH_TWOTAP | GH_TWODRAG | GH_PINCH);
+ break;
+
+ default:
+ this._state = GH_NOGESTURE;
+ }
+ }
+
+ _touchMove(id, x, y) {
+ let touch = this._tracked.find(t => t.id === id);
+
+ // If this is an update for a touch we're not tracking, ignore it
+ if (touch === undefined) {
+ return;
+ }
+
+ // Update the touches last position with the event coordinates
+ touch.lastX = x;
+ touch.lastY = y;
+
+ let deltaX = x - touch.firstX;
+ let deltaY = y - touch.firstY;
+
+ // Update angle when the touch has moved
+ if ((touch.firstX !== touch.lastX) ||
+ (touch.firstY !== touch.lastY)) {
+ touch.angle = Math.atan2(deltaY, deltaX) * 180 / Math.PI;
+ }
+
+ if (!this._hasDetectedGesture()) {
+ // Ignore moves smaller than the minimum threshold
+ if (Math.hypot(deltaX, deltaY) < GH_MOVE_THRESHOLD) {
+ return;
+ }
+
+ // Can't be a tap or long press as we've seen movement
+ this._state &= ~(GH_ONETAP | GH_TWOTAP | GH_THREETAP | GH_LONGPRESS);
+ this._stopLongpressTimeout();
+
+ if (this._tracked.length !== 1) {
+ this._state &= ~(GH_DRAG);
+ }
+ if (this._tracked.length !== 2) {
+ this._state &= ~(GH_TWODRAG | GH_PINCH);
+ }
+
+ // We need to figure out which of our different two touch gestures
+ // this might be
+ if (this._tracked.length === 2) {
+
+ // The other touch is the one where the id doesn't match
+ let prevTouch = this._tracked.find(t => t.id !== id);
+
+ // How far the previous touch point has moved since start
+ let prevDeltaMove = Math.hypot(prevTouch.firstX - prevTouch.lastX,
+ prevTouch.firstY - prevTouch.lastY);
+
+ // We know that the current touch moved far enough,
+ // but unless both touches moved further than their
+ // threshold we don't want to disqualify any gestures
+ if (prevDeltaMove > GH_MOVE_THRESHOLD) {
+
+ // The angle difference between the direction of the touch points
+ let deltaAngle = Math.abs(touch.angle - prevTouch.angle);
+ deltaAngle = Math.abs(((deltaAngle + 180) % 360) - 180);
+
+ // PINCH or TWODRAG can be eliminated depending on the angle
+ if (deltaAngle > GH_ANGLE_THRESHOLD) {
+ this._state &= ~GH_TWODRAG;
+ } else {
+ this._state &= ~GH_PINCH;
+ }
+
+ if (this._isTwoTouchTimeoutRunning()) {
+ this._stopTwoTouchTimeout();
+ }
+ } else if (!this._isTwoTouchTimeoutRunning()) {
+ // We can't determine the gesture right now, let's
+ // wait and see if more events are on their way
+ this._startTwoTouchTimeout();
+ }
+ }
+
+ if (!this._hasDetectedGesture()) {
+ return;
+ }
+
+ this._pushEvent('gesturestart');
+ }
+
+ this._pushEvent('gesturemove');
+ }
+
+ _touchEnd(id, x, y) {
+ // Check if this is an ignored touch
+ if (this._ignored.indexOf(id) !== -1) {
+ // Remove this touch from ignored
+ this._ignored.splice(this._ignored.indexOf(id), 1);
+
+ // And reset the state if there are no more touches
+ if ((this._ignored.length === 0) &&
+ (this._tracked.length === 0)) {
+ this._state = GH_INITSTATE;
+ this._waitingRelease = false;
+ }
+ return;
+ }
+
+ // We got a touchend before the timer triggered,
+ // this cannot result in a gesture anymore.
+ if (!this._hasDetectedGesture() &&
+ this._isTwoTouchTimeoutRunning()) {
+ this._stopTwoTouchTimeout();
+ this._state = GH_NOGESTURE;
+ }
+
+ // Some gestures don't trigger until a touch is released
+ if (!this._hasDetectedGesture()) {
+ // Can't be a gesture that relies on movement
+ this._state &= ~(GH_DRAG | GH_TWODRAG | GH_PINCH);
+ // Or something that relies on more time
+ this._state &= ~GH_LONGPRESS;
+ this._stopLongpressTimeout();
+
+ if (!this._waitingRelease) {
+ this._releaseStart = Date.now();
+ this._waitingRelease = true;
+
+ // Can't be a tap that requires more touches than we current have
+ switch (this._tracked.length) {
+ case 1:
+ this._state &= ~(GH_TWOTAP | GH_THREETAP);
+ break;
+
+ case 2:
+ this._state &= ~(GH_ONETAP | GH_THREETAP);
+ break;
+ }
+ }
+ }
+
+ // Waiting for all touches to release? (i.e. some tap)
+ if (this._waitingRelease) {
+ // Were all touches released at roughly the same time?
+ if ((Date.now() - this._releaseStart) > GH_MULTITOUCH_TIMEOUT) {
+ this._state = GH_NOGESTURE;
+ }
+
+ // Did too long time pass between press and release?
+ if (this._tracked.some(t => (Date.now() - t.started) > GH_TAP_TIMEOUT)) {
+ this._state = GH_NOGESTURE;
+ }
+
+ let touch = this._tracked.find(t => t.id === id);
+ touch.active = false;
+
+ // Are we still waiting for more releases?
+ if (this._hasDetectedGesture()) {
+ this._pushEvent('gesturestart');
+ } else {
+ // Have we reached a dead end?
+ if (this._state !== GH_NOGESTURE) {
+ return;
+ }
+ }
+ }
+
+ if (this._hasDetectedGesture()) {
+ this._pushEvent('gestureend');
+ }
+
+ // Ignore any remaining touches until they are ended
+ for (let i = 0; i < this._tracked.length; i++) {
+ if (this._tracked[i].active) {
+ this._ignored.push(this._tracked[i].id);
+ }
+ }
+ this._tracked = [];
+
+ this._state = GH_NOGESTURE;
+
+ // Remove this touch from ignored if it's in there
+ if (this._ignored.indexOf(id) !== -1) {
+ this._ignored.splice(this._ignored.indexOf(id), 1);
+ }
+
+ // We reset the state if ignored is empty
+ if ((this._ignored.length === 0)) {
+ this._state = GH_INITSTATE;
+ this._waitingRelease = false;
+ }
+ }
+
+ _hasDetectedGesture() {
+ if (this._state === GH_NOGESTURE) {
+ return false;
+ }
+ // Check to see if the bitmask value is a power of 2
+ // (i.e. only one bit set). If it is, we have a state.
+ if (this._state & (this._state - 1)) {
+ return false;
+ }
+
+ // For taps we also need to have all touches released
+ // before we've fully detected the gesture
+ if (this._state & (GH_ONETAP | GH_TWOTAP | GH_THREETAP)) {
+ if (this._tracked.some(t => t.active)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ _startLongpressTimeout() {
+ this._stopLongpressTimeout();
+ this._longpressTimeoutId = setTimeout(() => this._longpressTimeout(),
+ GH_LONGPRESS_TIMEOUT);
+ }
+
+ _stopLongpressTimeout() {
+ clearTimeout(this._longpressTimeoutId);
+ this._longpressTimeoutId = null;
+ }
+
+ _longpressTimeout() {
+ if (this._hasDetectedGesture()) {
+ throw new Error("A longpress gesture failed, conflict with a different gesture");
+ }
+
+ this._state = GH_LONGPRESS;
+ this._pushEvent('gesturestart');
+ }
+
+ _startTwoTouchTimeout() {
+ this._stopTwoTouchTimeout();
+ this._twoTouchTimeoutId = setTimeout(() => this._twoTouchTimeout(),
+ GH_TWOTOUCH_TIMEOUT);
+ }
+
+ _stopTwoTouchTimeout() {
+ clearTimeout(this._twoTouchTimeoutId);
+ this._twoTouchTimeoutId = null;
+ }
+
+ _isTwoTouchTimeoutRunning() {
+ return this._twoTouchTimeoutId !== null;
+ }
+
+ _twoTouchTimeout() {
+ if (this._tracked.length === 0) {
+ throw new Error("A pinch or two drag gesture failed, no tracked touches");
+ }
+
+ // How far each touch point has moved since start
+ let avgM = this._getAverageMovement();
+ let avgMoveH = Math.abs(avgM.x);
+ let avgMoveV = Math.abs(avgM.y);
+
+ // The difference in the distance between where
+ // the touch points started and where they are now
+ let avgD = this._getAverageDistance();
+ let deltaTouchDistance = Math.abs(Math.hypot(avgD.first.x, avgD.first.y) -
+ Math.hypot(avgD.last.x, avgD.last.y));
+
+ if ((avgMoveV < deltaTouchDistance) &&
+ (avgMoveH < deltaTouchDistance)) {
+ this._state = GH_PINCH;
+ } else {
+ this._state = GH_TWODRAG;
+ }
+
+ this._pushEvent('gesturestart');
+ this._pushEvent('gesturemove');
+ }
+
+ _pushEvent(type) {
+ let detail = { type: this._stateToGesture(this._state) };
+
+ // For most gesture events the current (average) position is the
+ // most useful
+ let avg = this._getPosition();
+ let pos = avg.last;
+
+ // However we have a slight distance to detect gestures, so for the
+ // first gesture event we want to use the first positions we saw
+ if (type === 'gesturestart') {
+ pos = avg.first;
+ }
+
+ // For these gestures, we always want the event coordinates
+ // to be where the gesture began, not the current touch location.
+ switch (this._state) {
+ case GH_TWODRAG:
+ case GH_PINCH:
+ pos = avg.first;
+ break;
+ }
+
+ detail['clientX'] = pos.x;
+ detail['clientY'] = pos.y;
+
+ // FIXME: other coordinates?
+
+ // Some gestures also have a magnitude
+ if (this._state === GH_PINCH) {
+ let distance = this._getAverageDistance();
+ if (type === 'gesturestart') {
+ detail['magnitudeX'] = distance.first.x;
+ detail['magnitudeY'] = distance.first.y;
+ } else {
+ detail['magnitudeX'] = distance.last.x;
+ detail['magnitudeY'] = distance.last.y;
+ }
+ } else if (this._state === GH_TWODRAG) {
+ if (type === 'gesturestart') {
+ detail['magnitudeX'] = 0.0;
+ detail['magnitudeY'] = 0.0;
+ } else {
+ let movement = this._getAverageMovement();
+ detail['magnitudeX'] = movement.x;
+ detail['magnitudeY'] = movement.y;
+ }
+ }
+
+ let gev = new CustomEvent(type, { detail: detail });
+ this._target.dispatchEvent(gev);
+ }
+
+ _stateToGesture(state) {
+ switch (state) {
+ case GH_ONETAP:
+ return 'onetap';
+ case GH_TWOTAP:
+ return 'twotap';
+ case GH_THREETAP:
+ return 'threetap';
+ case GH_DRAG:
+ return 'drag';
+ case GH_LONGPRESS:
+ return 'longpress';
+ case GH_TWODRAG:
+ return 'twodrag';
+ case GH_PINCH:
+ return 'pinch';
+ }
+
+ throw new Error("Unknown gesture state: " + state);
+ }
+
+ _getPosition() {
+ if (this._tracked.length === 0) {
+ throw new Error("Failed to get gesture position, no tracked touches");
+ }
+
+ let size = this._tracked.length;
+ let fx = 0, fy = 0, lx = 0, ly = 0;
+
+ for (let i = 0; i < this._tracked.length; i++) {
+ fx += this._tracked[i].firstX;
+ fy += this._tracked[i].firstY;
+ lx += this._tracked[i].lastX;
+ ly += this._tracked[i].lastY;
+ }
+
+ return { first: { x: fx / size,
+ y: fy / size },
+ last: { x: lx / size,
+ y: ly / size } };
+ }
+
+ _getAverageMovement() {
+ if (this._tracked.length === 0) {
+ throw new Error("Failed to get gesture movement, no tracked touches");
+ }
+
+ let totalH, totalV;
+ totalH = totalV = 0;
+ let size = this._tracked.length;
+
+ for (let i = 0; i < this._tracked.length; i++) {
+ totalH += this._tracked[i].lastX - this._tracked[i].firstX;
+ totalV += this._tracked[i].lastY - this._tracked[i].firstY;
+ }
+
+ return { x: totalH / size,
+ y: totalV / size };
+ }
+
+ _getAverageDistance() {
+ if (this._tracked.length === 0) {
+ throw new Error("Failed to get gesture distance, no tracked touches");
+ }
+
+ // Distance between the first and last tracked touches
+
+ let first = this._tracked[0];
+ let last = this._tracked[this._tracked.length - 1];
+
+ let fdx = Math.abs(last.firstX - first.firstX);
+ let fdy = Math.abs(last.firstY - first.firstY);
+
+ let ldx = Math.abs(last.lastX - first.lastX);
+ let ldy = Math.abs(last.lastY - first.lastY);
+
+ return { first: { x: fdx, y: fdy },
+ last: { x: ldx, y: ldy } };
+ }
+}
diff --git a/core/input/mouse.js b/core/input/mouse.js
index 5f1b6b4..82ea335 100644
--- a/core/input/mouse.js
+++ b/core/input/mouse.js
@@ -5,7 +5,6 @@
*/
import * as Log from '../util/logging.js';
-import { isTouchDevice } from '../util/browser.js';
import { setCapture, stopEvent, getPointerEvent } from '../util/events.js';
const WHEEL_STEP = 10; // Delta threshold for a mouse wheel step
@@ -16,9 +15,6 @@ export default class Mouse {
constructor(target) {
this._target = target || document;
- this._doubleClickTimer = null;
- this._lastTouchPos = null;
-
this._pos = null;
this._wheelStepXTimer = null;
this._wheelStepYTimer = null;
@@ -33,11 +29,6 @@ export default class Mouse {
'mousedisable': this._handleMouseDisable.bind(this)
};
- // ===== PROPERTIES =====
-
- this.touchButton = 1; // Button mask (1, 2, 4) for touch devices
- // (0 means ignore clicks)
-
// ===== EVENT HANDLERS =====
this.onmousebutton = () => {}; // Handler for mouse button press/release
@@ -55,39 +46,7 @@ export default class Mouse {
let pos = this._pos;
let bmask;
- if (e.touches || e.changedTouches) {
- // Touch device
-
- // When two touches occur within 500 ms of each other and are
- // close enough together a double click is triggered.
- if (down == 1) {
- if (this._doubleClickTimer === null) {
- this._lastTouchPos = pos;
- } else {
- clearTimeout(this._doubleClickTimer);
-
- // When the distance between the two touches is small enough
- // force the position of the latter touch to the position of
- // the first.
-
- const xs = this._lastTouchPos.x - pos.x;
- const ys = this._lastTouchPos.y - pos.y;
- const d = Math.sqrt((xs * xs) + (ys * ys));
-
- // The goal is to trigger on a certain physical width,
- // the devicePixelRatio brings us a bit closer but is
- // not optimal.
- const threshold = 20 * (window.devicePixelRatio || 1);
- if (d < threshold) {
- pos = this._lastTouchPos;
- }
- }
- this._doubleClickTimer =
- setTimeout(this._resetDoubleClickTimer.bind(this), 500);
- }
- bmask = this.touchButton;
- // If bmask is set
- } else if (e.which) {
+ if (e.which) {
/* everything except IE */
bmask = 1 << e.button;
} else {
@@ -105,10 +64,7 @@ export default class Mouse {
}
_handleMouseDown(e) {
- // Touch events have implicit capture
- if (e.type === "mousedown") {
- setCapture(this._target);
- }
+ setCapture(this._target);
this._handleMouseButton(e, 1);
}
@@ -242,11 +198,6 @@ export default class Mouse {
grab() {
const t = this._target;
- if (isTouchDevice) {
- t.addEventListener('touchstart', this._eventHandlers.mousedown);
- t.addEventListener('touchend', this._eventHandlers.mouseup);
- t.addEventListener('touchmove', this._eventHandlers.mousemove);
- }
t.addEventListener('mousedown', this._eventHandlers.mousedown);
t.addEventListener('mouseup', this._eventHandlers.mouseup);
t.addEventListener('mousemove', this._eventHandlers.mousemove);
@@ -265,11 +216,6 @@ export default class Mouse {
this._resetWheelStepTimers();
- if (isTouchDevice) {
- t.removeEventListener('touchstart', this._eventHandlers.mousedown);
- t.removeEventListener('touchend', this._eventHandlers.mouseup);
- t.removeEventListener('touchmove', this._eventHandlers.mousemove);
- }
t.removeEventListener('mousedown', this._eventHandlers.mousedown);
t.removeEventListener('mouseup', this._eventHandlers.mouseup);
t.removeEventListener('mousemove', this._eventHandlers.mousemove);
diff --git a/core/rfb.js b/core/rfb.js
index ca50779..9db375f 100644
--- a/core/rfb.js
+++ b/core/rfb.js
@@ -11,12 +11,14 @@ import { toUnsigned32bit, toSigned32bit } from './util/int.js';
import * as Log from './util/logging.js';
import { encodeUTF8, decodeUTF8 } from './util/strings.js';
import { dragThreshold } from './util/browser.js';
+import { clientToElement } from './util/element.js';
import EventTargetMixin from './util/eventtarget.js';
import Display from "./display.js";
import Inflator from "./inflator.js";
import Deflator from "./deflator.js";
import Keyboard from "./input/keyboard.js";
import Mouse from "./input/mouse.js";
+import GestureHandler from "./input/gesturehandler.js";
import Cursor from "./util/cursor.js";
import Websock from "./websock.js";
import DES from "./des.js";
@@ -39,6 +41,12 @@ const DEFAULT_BACKGROUND = 'rgb(40, 40, 40)';
// Minimum wait (ms) between two mouse moves
const MOUSE_MOVE_DELAY = 17;
+// Gesture thresholds
+const GESTURE_ZOOMSENS = 75;
+const GESTURE_SCRLSENS = 50;
+const DOUBLE_TAP_TIMEOUT = 1000;
+const DOUBLE_TAP_THRESHOLD = 50;
+
// Extended clipboard pseudo-encoding formats
const extendedClipboardFormatText = 1;
/*eslint-disable no-unused-vars */
@@ -118,6 +126,7 @@ export default class RFB extends EventTargetMixin {
this._flushing = false; // Display flushing state
this._keyboard = null; // Keyboard input handler object
this._mouse = null; // Mouse input handler object
+ this._gestures = null; // Gesture input handler object
// Timers
this._disconnTimer = null; // disconnection timer
@@ -144,10 +153,17 @@ export default class RFB extends EventTargetMixin {
this._viewportDragPos = {};
this._viewportHasMoved = false;
+ // Gesture state
+ this._gestureLastTapTime = null;
+ this._gestureFirstDoubleTapEv = null;
+ this._gestureLastMagnitudeX = 0;
+ this._gestureLastMagnitudeY = 0;
+
// Bound event handlers
this._eventHandlers = {
focusCanvas: this._focusCanvas.bind(this),
windowResize: this._windowResize.bind(this),
+ handleGesture: this._handleGesture.bind(this),
};
// main setup
@@ -210,6 +226,8 @@ export default class RFB extends EventTargetMixin {
this._mouse.onmousebutton = this._handleMouseButton.bind(this);
this._mouse.onmousemove = this._handleMouseMove.bind(this);
+ this._gestures = new GestureHandler();
+
this._sock = new Websock();
this._sock.on('message', () => {
this._handleMessage();
@@ -306,8 +324,8 @@ export default class RFB extends EventTargetMixin {
get capabilities() { return this._capabilities; }
- get touchButton() { return this._mouse.touchButton; }
- set touchButton(button) { this._mouse.touchButton = button; }
+ get touchButton() { return 0; }
+ set touchButton(button) { Log.Warn("Using old API!"); }
get clipViewport() { return this._clipViewport; }
set clipViewport(viewport) {
@@ -501,6 +519,8 @@ export default class RFB extends EventTargetMixin {
// Make our elements part of the page
this._target.appendChild(this._screen);
+ this._gestures.attach(this._canvas);
+
this._cursor.attach(this._canvas);
this._refreshCursor();
@@ -512,17 +532,26 @@ export default class RFB extends EventTargetMixin {
this._canvas.addEventListener("mousedown", this._eventHandlers.focusCanvas);
this._canvas.addEventListener("touchstart", this._eventHandlers.focusCanvas);
+ // Gesture events
+ this._canvas.addEventListener("gesturestart", this._eventHandlers.handleGesture);
+ this._canvas.addEventListener("gesturemove", this._eventHandlers.handleGesture);
+ this._canvas.addEventListener("gestureend", this._eventHandlers.handleGesture);
+
Log.Debug("<< RFB.connect");
}
_disconnect() {
Log.Debug(">> RFB.disconnect");
this._cursor.detach();
+ this._canvas.removeEventListener("gesturestart", this._eventHandlers.handleGesture);
+ this._canvas.removeEventListener("gesturemove", this._eventHandlers.handleGesture);
+ this._canvas.removeEventListener("gestureend", this._eventHandlers.handleGesture);
this._canvas.removeEventListener("mousedown", this._eventHandlers.focusCanvas);
this._canvas.removeEventListener("touchstart", this._eventHandlers.focusCanvas);
window.removeEventListener('resize', this._eventHandlers.windowResize);
this._keyboard.ungrab();
this._mouse.ungrab();
+ this._gestures.detach();
this._sock.close();
try {
this._target.removeChild(this._screen);
@@ -910,6 +939,156 @@ export default class RFB extends EventTargetMixin {
this._display.absY(y), mask);
}
+ _handleTapEvent(ev, bmask) {
+ let pos = clientToElement(ev.detail.clientX, ev.detail.clientY,
+ this._canvas);
+
+ // If the user quickly taps multiple times we assume they meant to
+ // hit the same spot, so slightly adjust coordinates
+
+ if ((this._gestureLastTapTime !== null) &&
+ ((Date.now() - this._gestureLastTapTime) < DOUBLE_TAP_TIMEOUT) &&
+ (this._gestureFirstDoubleTapEv.detail.type === ev.detail.type)) {
+ let dx = this._gestureFirstDoubleTapEv.detail.clientX - ev.detail.clientX;
+ let dy = this._gestureFirstDoubleTapEv.detail.clientY - ev.detail.clientY;
+ let distance = Math.hypot(dx, dy);
+
+ if (distance < DOUBLE_TAP_THRESHOLD) {
+ pos = clientToElement(this._gestureFirstDoubleTapEv.detail.clientX,
+ this._gestureFirstDoubleTapEv.detail.clientY,
+ this._canvas);
+ } else {
+ this._gestureFirstDoubleTapEv = ev;
+ }
+ } else {
+ this._gestureFirstDoubleTapEv = ev;
+ }
+ this._gestureLastTapTime = Date.now();
+
+ this._handleMouseMove(pos.x, pos.y);
+ this._handleMouseButton(pos.x, pos.y, true, bmask);
+ this._handleMouseButton(pos.x, pos.y, false, bmask);
+ }
+
+ _handleGesture(ev) {
+ let magnitude;
+
+ let pos = clientToElement(ev.detail.clientX, ev.detail.clientY,
+ this._canvas);
+ switch (ev.type) {
+ case 'gesturestart':
+ switch (ev.detail.type) {
+ case 'onetap':
+ this._handleTapEvent(ev, 0x1);
+ break;
+ case 'twotap':
+ this._handleTapEvent(ev, 0x4);
+ break;
+ case 'threetap':
+ this._handleTapEvent(ev, 0x2);
+ break;
+ case 'drag':
+ this._handleMouseMove(pos.x, pos.y);
+ this._handleMouseButton(pos.x, pos.y, true, 0x1);
+ break;
+ case 'longpress':
+ this._handleMouseMove(pos.x, pos.y);
+ this._handleMouseButton(pos.x, pos.y, true, 0x4);
+ break;
+
+ case 'twodrag':
+ this._gestureLastMagnitudeX = ev.detail.magnitudeX;
+ this._gestureLastMagnitudeY = ev.detail.magnitudeY;
+ this._handleMouseMove(pos.x, pos.y);
+ break;
+ case 'pinch':
+ this._gestureLastMagnitudeX = Math.hypot(ev.detail.magnitudeX,
+ ev.detail.magnitudeY);
+ this._handleMouseMove(pos.x, pos.y);
+ break;
+ }
+ break;
+
+ case 'gesturemove':
+ switch (ev.detail.type) {
+ case 'onetap':
+ case 'twotap':
+ case 'threetap':
+ break;
+ case 'drag':
+ case 'longpress':
+ this._handleMouseMove(pos.x, pos.y);
+ break;
+ case 'twodrag':
+ // Always scroll in the same position.
+ // We don't know if the mouse was moved so we need to move it
+ // every update.
+ this._handleMouseMove(pos.x, pos.y);
+ while ((ev.detail.magnitudeY - this._gestureLastMagnitudeY) > GESTURE_SCRLSENS) {
+ this._handleMouseButton(pos.x, pos.y, true, 0x8);
+ this._handleMouseButton(pos.x, pos.y, false, 0x8);
+ this._gestureLastMagnitudeY += GESTURE_SCRLSENS;
+ }
+ while ((ev.detail.magnitudeY - this._gestureLastMagnitudeY) < -GESTURE_SCRLSENS) {
+ this._handleMouseButton(pos.x, pos.y, true, 0x10);
+ this._handleMouseButton(pos.x, pos.y, false, 0x10);
+ this._gestureLastMagnitudeY -= GESTURE_SCRLSENS;
+ }
+ while ((ev.detail.magnitudeX - this._gestureLastMagnitudeX) > GESTURE_SCRLSENS) {
+ this._handleMouseButton(pos.x, pos.y, true, 0x20);
+ this._handleMouseButton(pos.x, pos.y, false, 0x20);
+ this._gestureLastMagnitudeX += GESTURE_SCRLSENS;
+ }
+ while ((ev.detail.magnitudeX - this._gestureLastMagnitudeX) < -GESTURE_SCRLSENS) {
+ this._handleMouseButton(pos.x, pos.y, true, 0x40);
+ this._handleMouseButton(pos.x, pos.y, false, 0x40);
+ this._gestureLastMagnitudeX -= GESTURE_SCRLSENS;
+ }
+ break;
+ case 'pinch':
+ // Always scroll in the same position.
+ // We don't know if the mouse was moved so we need to move it
+ // every update.
+ this._handleMouseMove(pos.x, pos.y);
+ magnitude = Math.hypot(ev.detail.magnitudeX, ev.detail.magnitudeY);
+ if (Math.abs(magnitude - this._gestureLastMagnitudeX) > GESTURE_ZOOMSENS) {
+ this._handleKeyEvent(KeyTable.XK_Control_L, "ControlLeft", true);
+ while ((magnitude - this._gestureLastMagnitudeX) > GESTURE_ZOOMSENS) {
+ this._handleMouseButton(pos.x, pos.y, true, 0x8);
+ this._handleMouseButton(pos.x, pos.y, false, 0x8);
+ this._gestureLastMagnitudeX += GESTURE_ZOOMSENS;
+ }
+ while ((magnitude - this._gestureLastMagnitudeX) < -GESTURE_ZOOMSENS) {
+ this._handleMouseButton(pos.x, pos.y, true, 0x10);
+ this._handleMouseButton(pos.x, pos.y, false, 0x10);
+ this._gestureLastMagnitudeX -= GESTURE_ZOOMSENS;
+ }
+ }
+ this._handleKeyEvent(KeyTable.XK_Control_L, "ControlLeft", false);
+ }
+ break;
+
+ case 'gestureend':
+ switch (ev.detail.type) {
+ case 'onetap':
+ case 'twotap':
+ case 'threetap':
+ case 'pinch':
+ case 'twodrag':
+ break;
+ case 'drag':
+ this._handleMouseMove(pos.x, pos.y);
+ this._handleMouseButton(pos.x, pos.y, false, 0x1);
+ break;
+ case 'longpress':
+ this._handleMouseMove(pos.x, pos.y);
+ this._handleMouseButton(pos.x, pos.y, false, 0x4);
+ break;
+ }
+ break;
+ }
+ }
+
// Message Handlers
_negotiateProtocolVersion() {
diff --git a/core/util/element.js b/core/util/element.js
new file mode 100644
index 0000000..466a745
--- /dev/null
+++ b/core/util/element.js
@@ -0,0 +1,32 @@
+/*
+ * noVNC: HTML5 VNC client
+ * Copyright (C) 2020 The noVNC Authors
+ * Licensed under MPL 2.0 (see LICENSE.txt)
+ *
+ * See README.md for usage and integration instructions.
+ */
+
+/*
+ * HTML element utility functions
+ */
+
+export function clientToElement(x, y, elem) {
+ const bounds = elem.getBoundingClientRect();
+ let pos = { x: 0, y: 0 };
+ // Clip to target bounds
+ if (x < bounds.left) {
+ pos.x = 0;
+ } else if (x >= bounds.right) {
+ pos.x = bounds.width - 1;
+ } else {
+ pos.x = x - bounds.left;
+ }
+ if (y < bounds.top) {
+ pos.y = 0;
+ } else if (y >= bounds.bottom) {
+ pos.y = bounds.height - 1;
+ } else {
+ pos.y = y - bounds.top;
+ }
+ return pos;
+}
diff --git a/docs/API.md b/docs/API.md
index 59b7cf1..349fdc8 100644
--- a/docs/API.md
+++ b/docs/API.md
@@ -26,12 +26,6 @@ protocol stream.
moved to the remote session when a `mousedown` or `touchstart`
event is received. Enabled by default.
-`touchButton`
- - Is a `long` controlling the button mask that should be simulated
- when a touch event is recieved. Uses the same values as
- [`MouseEvent.button`](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button).
- Is set to `1` by default.
-
`clipViewport`
- Is a `boolean` indicating if the remote session should be clipped
to its container. When disabled scrollbars will be shown to handle
diff --git a/tests/test.gesturehandler.js b/tests/test.gesturehandler.js
new file mode 100644
index 0000000..bfb2621
--- /dev/null
+++ b/tests/test.gesturehandler.js
@@ -0,0 +1,1031 @@
+const expect = chai.expect;
+
+import EventTargetMixin from '../core/util/eventtarget.js';
+
+import GestureHandler from '../core/input/gesturehandler.js';
+import * as browser from '../core/util/browser.js';
+
+class DummyTarget extends EventTargetMixin {
+}
+
+describe('Gesture handler', function () {
+ let target, handler;
+ let gestures;
+ let clock;
+ let touches;
+
+ before(function () {
+ clock = sinon.useFakeTimers();
+ });
+
+ after(function () {
+ clock.restore();
+ });
+
+ beforeEach(function () {
+ // Touch events and gestures are not supported on IE
+ if (browser.isIE()) {
+ this.skip();
+ return;
+ }
+
+ target = new DummyTarget();
+ gestures = sinon.spy();
+ target.addEventListener('gesturestart', gestures);
+ target.addEventListener('gesturemove', gestures);
+ target.addEventListener('gestureend', gestures);
+ touches = [];
+ handler = new GestureHandler();
+ handler.attach(target);
+ });
+
+ afterEach(function () {
+ handler.detach();
+ target = null;
+ gestures = null;
+ });
+
+ function touchStart(id, x, y) {
+ let touch = { identifier: id,
+ clientX: x, clientY: y };
+ touches.push(touch);
+ let ev = { type: 'touchstart',
+ touches: touches,
+ targetTouches: touches,
+ changedTouches: [ touch ],
+ stopPropagation: sinon.spy(),
+ preventDefault: sinon.spy() };
+ target.dispatchEvent(ev);
+ }
+
+ function touchMove(id, x, y) {
+ let touch = touches.find(t => t.identifier === id);
+ touch.clientX = x;
+ touch.clientY = y;
+ let ev = { type: 'touchmove',
+ touches: touches,
+ targetTouches: touches,
+ changedTouches: [ touch ],
+ stopPropagation: sinon.spy(),
+ preventDefault: sinon.spy() };
+ target.dispatchEvent(ev);
+ }
+
+ function touchEnd(id) {
+ let idx = touches.findIndex(t => t.identifier === id);
+ let touch = touches.splice(idx, 1)[0];
+ let ev = { type: 'touchend',
+ touches: touches,
+ targetTouches: touches,
+ changedTouches: [ touch ],
+ stopPropagation: sinon.spy(),
+ preventDefault: sinon.spy() };
+ target.dispatchEvent(ev);
+ }
+
+ describe('Single finger tap', function () {
+ it('should handle single finger tap', function () {
+ touchStart(1, 20.0, 30.0);
+
+ expect(gestures).to.not.have.been.called;
+
+ touchEnd(1);
+
+ expect(gestures).to.have.been.calledTwice;
+
+ expect(gestures.firstCall).to.have.been.calledWith(
+ sinon.match({ type: 'gesturestart',
+ detail: { type: 'onetap',
+ clientX: 20.0,
+ clientY: 30.0 } }));
+
+ expect(gestures.secondCall).to.have.been.calledWith(
+ sinon.match({ type: 'gestureend',
+ detail: { type: 'onetap',
+ clientX: 20.0,
+ clientY: 30.0 } }));
+ });
+ });
+
+ describe('Two finger tap', function () {
+ it('should handle two finger tap', function () {
+ touchStart(1, 20.0, 30.0);
+ touchStart(2, 30.0, 50.0);
+
+ expect(gestures).to.not.have.been.called;
+
+ touchEnd(1);
+
+ expect(gestures).to.not.have.been.called;
+
+ touchEnd(2);
+
+ expect(gestures).to.have.been.calledTwice;
+
+ expect(gestures.firstCall).to.have.been.calledWith(
+ sinon.match({ type: 'gesturestart',
+ detail: { type: 'twotap',
+ clientX: 25.0,
+ clientY: 40.0 } }));
+
+ expect(gestures.secondCall).to.have.been.calledWith(
+ sinon.match({ type: 'gestureend',
+ detail: { type: 'twotap',
+ clientX: 25.0,
+ clientY: 40.0 } }));
+ });
+
+ it('should ignore slow starting two finger tap', function () {
+ touchStart(1, 20.0, 30.0);
+
+ clock.tick(500);
+
+ touchStart(2, 30.0, 50.0);
+ touchEnd(1);
+ touchEnd(2);
+
+ expect(gestures).to.not.have.been.called;
+ });
+
+ it('should ignore slow ending two finger tap', function () {
+ touchStart(1, 20.0, 30.0);
+ touchStart(2, 30.0, 50.0);
+ touchEnd(1);
+
+ clock.tick(500);
+
+ touchEnd(2);
+
+ expect(gestures).to.not.have.been.called;
+ });
+
+ it('should ignore slow two finger tap', function () {
+ touchStart(1, 20.0, 30.0);
+ touchStart(2, 30.0, 50.0);
+
+ clock.tick(1500);
+
+ touchEnd(1);
+ touchEnd(2);
+
+ expect(gestures).to.not.have.been.called;
+ });
+ });
+
+ describe('Three finger tap', function () {
+ it('should handle three finger tap', function () {
+ touchStart(1, 20.0, 30.0);
+ touchStart(2, 30.0, 50.0);
+ touchStart(3, 40.0, 40.0);
+
+ expect(gestures).to.not.have.been.called;
+
+ touchEnd(1);
+
+ expect(gestures).to.not.have.been.called;
+
+ touchEnd(2);
+
+ expect(gestures).to.not.have.been.called;
+
+ touchEnd(3);
+
+ expect(gestures).to.have.been.calledTwice;
+
+ expect(gestures.firstCall).to.have.been.calledWith(
+ sinon.match({ type: 'gesturestart',
+ detail: { type: 'threetap',
+ clientX: 30.0,
+ clientY: 40.0 } }));
+
+ expect(gestures.secondCall).to.have.been.calledWith(
+ sinon.match({ type: 'gestureend',
+ detail: { type: 'threetap',
+ clientX: 30.0,
+ clientY: 40.0 } }));
+ });
+
+ it('should ignore slow starting three finger tap', function () {
+ touchStart(1, 20.0, 30.0);
+ touchStart(2, 30.0, 50.0);
+
+ clock.tick(500);
+
+ touchStart(3, 40.0, 40.0);
+ touchEnd(1);
+ touchEnd(2);
+ touchEnd(3);
+
+ expect(gestures).to.not.have.been.called;
+ });
+
+ it('should ignore slow ending three finger tap', function () {
+ touchStart(1, 20.0, 30.0);
+ touchStart(2, 30.0, 50.0);
+ touchStart(3, 40.0, 40.0);
+ touchEnd(1);
+ touchEnd(2);
+
+ clock.tick(500);
+
+ touchEnd(3);
+
+ expect(gestures).to.not.have.been.called;
+ });
+
+ it('should ignore three finger drag', function () {
+ touchStart(1, 20.0, 30.0);
+ touchStart(2, 30.0, 50.0);
+ touchStart(3, 40.0, 40.0);
+
+ touchMove(1, 120.0, 130.0);
+ touchMove(2, 130.0, 150.0);
+ touchMove(3, 140.0, 140.0);
+
+ touchEnd(1);
+ touchEnd(2);
+ touchEnd(3);
+
+ expect(gestures).to.not.have.been.called;
+ });
+
+ it('should ignore slow three finger tap', function () {
+ touchStart(1, 20.0, 30.0);
+ touchStart(2, 30.0, 50.0);
+ touchStart(3, 40.0, 40.0);
+
+ clock.tick(1500);
+
+ touchEnd(1);
+ touchEnd(2);
+ touchEnd(3);
+
+ expect(gestures).to.not.have.been.called;
+ });
+ });
+
+ describe('Single finger drag', function () {
+ it('should handle horizontal single finger drag', function () {
+ touchStart(1, 20.0, 30.0);
+
+ expect(gestures).to.not.have.been.called;
+
+ touchMove(1, 40.0, 30.0);
+
+ expect(gestures).to.not.have.been.called;
+
+ touchMove(1, 80.0, 30.0);
+
+ expect(gestures).to.have.been.calledTwice;
+
+ expect(gestures.firstCall).to.have.been.calledWith(
+ sinon.match({ type: 'gesturestart',
+ detail: { type: 'drag',
+ clientX: 20.0,
+ clientY: 30.0 } }));
+
+ expect(gestures.secondCall).to.have.been.calledWith(
+ sinon.match({ type: 'gesturemove',
+ detail: { type: 'drag',
+ clientX: 80.0,
+ clientY: 30.0 } }));
+
+ gestures.resetHistory();
+
+ touchEnd(1);
+
+ expect(gestures).to.have.been.calledOnceWith(
+ sinon.match({ type: 'gestureend',
+ detail: { type: 'drag',
+ clientX: 80.0,
+ clientY: 30.0 } }));
+ });
+
+ it('should handle vertical single finger drag', function () {
+ touchStart(1, 20.0, 30.0);
+
+ expect(gestures).to.not.have.been.called;
+
+ touchMove(1, 20.0, 50.0);
+
+ expect(gestures).to.not.have.been.called;
+
+ touchMove(1, 20.0, 90.0);
+
+ expect(gestures).to.have.been.calledTwice;
+
+ expect(gestures.firstCall).to.have.been.calledWith(
+ sinon.match({ type: 'gesturestart',
+ detail: { type: 'drag',
+ clientX: 20.0,
+ clientY: 30.0 } }));
+
+ expect(gestures.secondCall).to.have.been.calledWith(
+ sinon.match({ type: 'gesturemove',
+ detail: { type: 'drag',
+ clientX: 20.0,
+ clientY: 90.0 } }));
+
+ gestures.resetHistory();
+
+ touchEnd(1);
+
+ expect(gestures).to.have.been.calledOnceWith(
+ sinon.match({ type: 'gestureend',
+ detail: { type: 'drag',
+ clientX: 20.0,
+ clientY: 90.0 } }));
+ });
+
+ it('should handle diagonal single finger drag', function () {
+ touchStart(1, 120.0, 130.0);
+
+ expect(gestures).to.not.have.been.called;
+
+ touchMove(1, 90.0, 100.0);
+
+ expect(gestures).to.not.have.been.called;
+
+ touchMove(1, 60.0, 70.0);
+
+ expect(gestures).to.have.been.calledTwice;
+
+ expect(gestures.firstCall).to.have.been.calledWith(
+ sinon.match({ type: 'gesturestart',
+ detail: { type: 'drag',
+ clientX: 120.0,
+ clientY: 130.0 } }));
+
+ expect(gestures.secondCall).to.have.been.calledWith(
+ sinon.match({ type: 'gesturemove',
+ detail: { type: 'drag',
+ clientX: 60.0,
+ clientY: 70.0 } }));
+
+ gestures.resetHistory();
+
+ touchEnd(1);
+
+ expect(gestures).to.have.been.calledOnceWith(
+ sinon.match({ type: 'gestureend',
+ detail: { type: 'drag',
+ clientX: 60.0,
+ clientY: 70.0 } }));
+ });
+ });
+
+ describe('Long press', function () {
+ it('should handle long press', function () {
+ touchStart(1, 20.0, 30.0);
+
+ expect(gestures).to.not.have.been.called;
+
+ clock.tick(1500);
+
+ expect(gestures).to.have.been.calledOnceWith(
+ sinon.match({ type: 'gesturestart',
+ detail: { type: 'longpress',
+ clientX: 20.0,
+ clientY: 30.0 } }));
+
+ gestures.resetHistory();
+
+ touchEnd(1);
+
+ expect(gestures).to.have.been.calledOnceWith(
+ sinon.match({ type: 'gestureend',
+ detail: { type: 'longpress',
+ clientX: 20.0,
+ clientY: 30.0 } }));
+ });
+
+ it('should handle long press drag', function () {
+ touchStart(1, 20.0, 30.0);
+
+ expect(gestures).to.not.have.been.called;
+
+ clock.tick(1500);
+
+ expect(gestures).to.have.been.calledOnceWith(
+ sinon.match({ type: 'gesturestart',
+ detail: { type: 'longpress',
+ clientX: 20.0,
+ clientY: 30.0 } }));
+
+ gestures.resetHistory();
+
+ touchMove(1, 120.0, 50.0);
+
+ expect(gestures).to.have.been.calledOnceWith(
+ sinon.match({ type: 'gesturemove',
+ detail: { type: 'longpress',
+ clientX: 120.0,
+ clientY: 50.0 } }));
+
+ gestures.resetHistory();
+
+ touchEnd(1);
+
+ expect(gestures).to.have.been.calledOnceWith(
+ sinon.match({ type: 'gestureend',
+ detail: { type: 'longpress',
+ clientX: 120.0,
+ clientY: 50.0 } }));
+ });
+ });
+
+ describe('Two finger drag', function () {
+ it('should handle fast and distinct horizontal two finger drag', function () {
+ touchStart(1, 20.0, 30.0);
+ touchStart(2, 30.0, 30.0);
+
+ expect(gestures).to.not.have.been.called;
+
+ touchMove(1, 40.0, 30.0);
+ touchMove(2, 50.0, 30.0);
+
+ expect(gestures).to.not.have.been.called;
+
+ touchMove(2, 90.0, 30.0);
+ touchMove(1, 80.0, 30.0);
+
+ expect(gestures).to.have.been.calledTwice;
+
+ expect(gestures.firstCall).to.have.been.calledWith(
+ sinon.match({ type: 'gesturestart',
+ detail: { type: 'twodrag',
+ clientX: 25.0,
+ clientY: 30.0,
+ magnitudeX: 0.0,
+ magnitudeY: 0.0 } }));
+
+ expect(gestures.secondCall).to.have.been.calledWith(
+ sinon.match({ type: 'gesturemove',
+ detail: { type: 'twodrag',
+ clientX: 25.0,
+ clientY: 30.0,
+ magnitudeX: 60.0,
+ magnitudeY: 0.0 } }));
+
+ gestures.resetHistory();
+
+ touchEnd(1);
+
+ expect(gestures).to.have.been.calledOnceWith(
+ sinon.match({ type: 'gestureend',
+ detail: { type: 'twodrag',
+ clientX: 25.0,
+ clientY: 30.0,
+ magnitudeX: 60.0,
+ magnitudeY: 0.0 } }));
+ });
+
+ it('should handle fast and distinct vertical two finger drag', function () {
+ touchStart(1, 20.0, 30.0);
+ touchStart(2, 30.0, 30.0);
+
+ expect(gestures).to.not.have.been.called;
+
+ touchMove(1, 20.0, 100.0);
+ touchMove(2, 30.0, 40.0);
+
+ expect(gestures).to.not.have.been.called;
+
+ touchMove(2, 30.0, 90.0);
+
+ expect(gestures).to.have.been.calledTwice;
+
+ expect(gestures.firstCall).to.have.been.calledWith(
+ sinon.match({ type: 'gesturestart',
+ detail: { type: 'twodrag',
+ clientX: 25.0,
+ clientY: 30.0,
+ magnitudeX: 0.0,
+ magnitudeY: 0.0 } }));
+
+ expect(gestures.secondCall).to.have.been.calledWith(
+ sinon.match({ type: 'gesturemove',
+ detail: { type: 'twodrag',
+ clientX: 25.0,
+ clientY: 30.0,
+ magnitudeX: 0.0,
+ magnitudeY: 65.0 } }));
+
+ gestures.resetHistory();
+
+ touchEnd(1);
+
+ expect(gestures).to.have.been.calledOnceWith(
+ sinon.match({ type: 'gestureend',
+ detail: { type: 'twodrag',
+ clientX: 25.0,
+ clientY: 30.0,
+ magnitudeX: 0.0,
+ magnitudeY: 65.0 } }));
+ });
+
+ it('should handle fast and distinct diagonal two finger drag', function () {
+ touchStart(1, 120.0, 130.0);
+ touchStart(2, 130.0, 130.0);
+
+ expect(gestures).to.not.have.been.called;
+
+ touchMove(1, 80.0, 90.0);
+ touchMove(2, 100.0, 130.0);
+
+ expect(gestures).to.not.have.been.called;
+
+ touchMove(2, 60.0, 70.0);
+
+ expect(gestures).to.have.been.calledTwice;
+
+ expect(gestures.firstCall).to.have.been.calledWith(
+ sinon.match({ type: 'gesturestart',
+ detail: { type: 'twodrag',
+ clientX: 125.0,
+ clientY: 130.0,
+ magnitudeX: 0.0,
+ magnitudeY: 0.0 } }));
+
+ expect(gestures.secondCall).to.have.been.calledWith(
+ sinon.match({ type: 'gesturemove',
+ detail: { type: 'twodrag',
+ clientX: 125.0,
+ clientY: 130.0,
+ magnitudeX: -55.0,
+ magnitudeY: -50.0 } }));
+
+ gestures.resetHistory();
+
+ touchEnd(1);
+
+ expect(gestures).to.have.been.calledOnceWith(
+ sinon.match({ type: 'gestureend',
+ detail: { type: 'twodrag',
+ clientX: 125.0,
+ clientY: 130.0,
+ magnitudeX: -55.0,
+ magnitudeY: -50.0 } }));
+ });
+
+ it('should ignore fast almost two finger dragging', function () {
+ touchStart(1, 20.0, 30.0);
+ touchStart(2, 30.0, 30.0);
+ touchMove(1, 80.0, 30.0);
+ touchMove(2, 70.0, 30.0);
+ touchEnd(1);
+ touchEnd(2);
+
+ expect(gestures).to.not.have.been.called;
+
+ clock.tick(1500);
+
+ expect(gestures).to.not.have.been.called;
+ });
+
+ it('should handle slow horizontal two finger drag', function () {
+ touchStart(1, 50.0, 40.0);
+ touchStart(2, 60.0, 40.0);
+ touchMove(1, 80.0, 40.0);
+ touchMove(2, 110.0, 40.0);
+
+ expect(gestures).to.not.have.been.called;
+
+ clock.tick(60);
+
+ expect(gestures).to.have.been.calledTwice;
+
+ expect(gestures.firstCall).to.have.been.calledWith(
+ sinon.match({ type: 'gesturestart',
+ detail: { type: 'twodrag',
+ clientX: 55.0,
+ clientY: 40.0,
+ magnitudeX: 0.0,
+ magnitudeY: 0.0 } }));
+
+ expect(gestures.secondCall).to.have.been.calledWith(
+ sinon.match({ type: 'gesturemove',
+ detail: { type: 'twodrag',
+ clientX: 55.0,
+ clientY: 40.0,
+ magnitudeX: 40.0,
+ magnitudeY: 0.0 } }));
+ });
+
+ it('should handle slow vertical two finger drag', function () {
+ touchStart(1, 40.0, 40.0);
+ touchStart(2, 40.0, 60.0);
+ touchMove(2, 40.0, 80.0);
+ touchMove(1, 40.0, 100.0);
+
+ expect(gestures).to.not.have.been.called;
+
+ clock.tick(60);
+
+ expect(gestures).to.have.been.calledTwice;
+
+ expect(gestures.firstCall).to.have.been.calledWith(
+ sinon.match({ type: 'gesturestart',
+ detail: { type: 'twodrag',
+ clientX: 40.0,
+ clientY: 50.0,
+ magnitudeX: 0.0,
+ magnitudeY: 0.0 } }));
+
+ expect(gestures.secondCall).to.have.been.calledWith(
+ sinon.match({ type: 'gesturemove',
+ detail: { type: 'twodrag',
+ clientX: 40.0,
+ clientY: 50.0,
+ magnitudeX: 0.0,
+ magnitudeY: 40.0 } }));
+ });
+
+ it('should handle slow diagonal two finger drag', function () {
+ touchStart(1, 50.0, 40.0);
+ touchStart(2, 40.0, 60.0);
+ touchMove(1, 70.0, 60.0);
+ touchMove(2, 90.0, 110.0);
+
+ expect(gestures).to.not.have.been.called;
+
+ clock.tick(60);
+
+ expect(gestures).to.have.been.calledTwice;
+
+ expect(gestures.firstCall).to.have.been.calledWith(
+ sinon.match({ type: 'gesturestart',
+ detail: { type: 'twodrag',
+ clientX: 45.0,
+ clientY: 50.0,
+ magnitudeX: 0.0,
+ magnitudeY: 0.0 } }));
+
+ expect(gestures.secondCall).to.have.been.calledWith(
+ sinon.match({ type: 'gesturemove',
+ detail: { type: 'twodrag',
+ clientX: 45.0,
+ clientY: 50.0,
+ magnitudeX: 35.0,
+ magnitudeY: 35.0 } }));
+ });
+
+ it('should ignore too slow two finger drag', function () {
+ touchStart(1, 20.0, 30.0);
+
+ clock.tick(500);
+
+ touchStart(2, 30.0, 30.0);
+ touchMove(1, 40.0, 30.0);
+ touchMove(2, 50.0, 30.0);
+ touchMove(1, 80.0, 30.0);
+
+ expect(gestures).to.not.have.been.called;
+ });
+ });
+
+ describe('Pinch', function () {
+ it('should handle pinching distinctly and fast inwards', function () {
+ touchStart(1, 0.0, 0.0);
+ touchStart(2, 130.0, 130.0);
+
+ expect(gestures).to.not.have.been.called;
+
+ touchMove(1, 50.0, 40.0);
+ touchMove(2, 100.0, 130.0);
+
+ expect(gestures).to.not.have.been.called;
+
+ touchMove(2, 60.0, 70.0);
+
+ expect(gestures).to.have.been.calledTwice;
+
+ expect(gestures.firstCall).to.have.been.calledWith(
+ sinon.match({ type: 'gesturestart',
+ detail: { type: 'pinch',
+ clientX: 65.0,
+ clientY: 65.0,
+ magnitudeX: 130.0,
+ magnitudeY: 130.0 } }));
+
+ expect(gestures.secondCall).to.have.been.calledWith(
+ sinon.match({ type: 'gesturemove',
+ detail: { type: 'pinch',
+ clientX: 65.0,
+ clientY: 65.0,
+ magnitudeX: 10.0,
+ magnitudeY: 30.0 } }));
+
+ gestures.resetHistory();
+
+ touchEnd(1);
+
+ expect(gestures).to.have.been.calledOnceWith(
+ sinon.match({ type: 'gestureend',
+ detail: { type: 'pinch',
+ clientX: 65.0,
+ clientY: 65.0,
+ magnitudeX: 10.0,
+ magnitudeY: 30.0 } }));
+ });
+
+ it('should handle pinching fast and distinctly outwards', function () {
+ touchStart(1, 100.0, 100.0);
+ touchStart(2, 110.0, 100.0);
+
+ expect(gestures).to.not.have.been.called;
+
+ touchMove(1, 130.0, 70.0);
+ touchMove(2, 0.0, 200.0);
+
+ expect(gestures).to.not.have.been.called;
+
+ touchMove(1, 180.0, 20.0);
+
+ expect(gestures).to.have.been.calledTwice;
+
+ expect(gestures.firstCall).to.have.been.calledWith(
+ sinon.match({ type: 'gesturestart',
+ detail: { type: 'pinch',
+ clientX: 105.0,
+ clientY: 100.0,
+ magnitudeX: 10.0,
+ magnitudeY: 0.0 } }));
+
+ expect(gestures.secondCall).to.have.been.calledWith(
+ sinon.match({ type: 'gesturemove',
+ detail: { type: 'pinch',
+ clientX: 105.0,
+ clientY: 100.0,
+ magnitudeX: 180.0,
+ magnitudeY: 180.0 } }));
+
+ gestures.resetHistory();
+
+ touchEnd(1);
+
+ expect(gestures).to.have.been.calledOnceWith(
+ sinon.match({ type: 'gestureend',
+ detail: { type: 'pinch',
+ clientX: 105.0,
+ clientY: 100.0,
+ magnitudeX: 180.0,
+ magnitudeY: 180.0 } }));
+ });
+
+ it('should ignore fast almost pinching', function () {
+ touchStart(1, 20.0, 30.0);
+ touchStart(2, 130.0, 130.0);
+ touchMove(1, 80.0, 70.0);
+ touchEnd(1);
+ touchEnd(2);
+
+ expect(gestures).to.not.have.been.called;
+
+ clock.tick(1500);
+
+ expect(gestures).to.not.have.been.called;
+ });
+
+ it('should handle pinching inwards slowly', function () {
+ touchStart(1, 0.0, 0.0);
+ touchStart(2, 130.0, 130.0);
+ touchMove(1, 50.0, 40.0);
+ touchMove(2, 100.0, 130.0);
+
+ expect(gestures).to.not.have.been.called;
+
+ clock.tick(60);
+
+ expect(gestures).to.have.been.calledTwice;
+
+ expect(gestures.firstCall).to.have.been.calledWith(
+ sinon.match({ type: 'gesturestart',
+ detail: { type: 'pinch',
+ clientX: 65.0,
+ clientY: 65.0,
+ magnitudeX: 130.0,
+ magnitudeY: 130.0 } }));
+
+ expect(gestures.secondCall).to.have.been.calledWith(
+ sinon.match({ type: 'gesturemove',
+ detail: { type: 'pinch',
+ clientX: 65.0,
+ clientY: 65.0,
+ magnitudeX: 50.0,
+ magnitudeY: 90.0 } }));
+ });
+
+ it('should handle pinching outwards slowly', function () {
+ touchStart(1, 100.0, 130.0);
+ touchStart(2, 110.0, 130.0);
+ touchMove(2, 200.0, 130.0);
+
+ expect(gestures).to.not.have.been.called;
+
+ clock.tick(60);
+
+ expect(gestures).to.have.been.calledTwice;
+
+ expect(gestures.firstCall).to.have.been.calledWith(
+ sinon.match({ type: 'gesturestart',
+ detail: { type: 'pinch',
+ clientX: 105.0,
+ clientY: 130.0,
+ magnitudeX: 10.0,
+ magnitudeY: 0.0 } }));
+
+ expect(gestures.secondCall).to.have.been.calledWith(
+ sinon.match({ type: 'gesturemove',
+ detail: { type: 'pinch',
+ clientX: 105.0,
+ clientY: 130.0,
+ magnitudeX: 100.0,
+ magnitudeY: 0.0 } }));
+ });
+
+ it('should ignore pinching too slowly', function () {
+ touchStart(1, 0.0, 0.0);
+
+ clock.tick(500);
+
+ touchStart(2, 130.0, 130.0);
+ touchMove(2, 100.0, 130.0);
+ touchMove(1, 50.0, 40.0);
+
+ expect(gestures).to.not.have.been.called;
+ });
+ });
+
+ describe('Ignoring', function () {
+ it('should ignore extra touches during gesture', function () {
+ touchStart(1, 20.0, 30.0);
+ touchMove(1, 40.0, 30.0);
+ touchMove(1, 80.0, 30.0);
+
+ expect(gestures).to.have.been.calledTwice;
+
+ expect(gestures.firstCall).to.have.been.calledWith(
+ sinon.match({ type: 'gesturestart',
+ detail: { type: 'drag' } }));
+ expect(gestures.secondCall).to.have.been.calledWith(
+ sinon.match({ type: 'gesturemove',
+ detail: { type: 'drag' } }));
+
+ gestures.resetHistory();
+
+ touchStart(2, 10.0, 10.0);
+
+ expect(gestures).to.not.have.been.called;
+
+ touchMove(1, 100.0, 50.0);
+
+ expect(gestures).to.have.been.calledOnceWith(
+ sinon.match({ type: 'gesturemove',
+ detail: { type: 'drag',
+ clientX: 100.0,
+ clientY: 50.0 } }));
+
+ gestures.resetHistory();
+
+ touchEnd(1);
+
+ expect(gestures).to.have.been.calledOnceWith(
+ sinon.match({ type: 'gestureend',
+ detail: { type: 'drag',
+ clientX: 100.0,
+ clientY: 50.0 } }));
+ });
+
+ it('should ignore extra touches when waiting for gesture to end', function () {
+ touchStart(1, 20.0, 30.0);
+ touchStart(2, 30.0, 30.0);
+ touchMove(1, 40.0, 30.0);
+ touchMove(2, 90.0, 30.0);
+ touchMove(1, 80.0, 30.0);
+
+ expect(gestures).to.have.been.calledTwice;
+
+ expect(gestures.firstCall).to.have.been.calledWith(
+ sinon.match({ type: 'gesturestart',
+ detail: { type: 'twodrag' } }));
+ expect(gestures.secondCall).to.have.been.calledWith(
+ sinon.match({ type: 'gesturemove',
+ detail: { type: 'twodrag' } }));
+
+ gestures.resetHistory();
+
+ touchEnd(1);
+
+ expect(gestures).to.have.been.calledOnceWith(
+ sinon.match({ type: 'gestureend',
+ detail: { type: 'twodrag' } }));
+
+ gestures.resetHistory();
+
+ touchStart(3, 10.0, 10.0);
+ touchEnd(3);
+
+ expect(gestures).to.not.have.been.called;
+ });
+
+ it('should ignore extra touches after gesture', function () {
+ touchStart(1, 20.0, 30.0);
+ touchMove(1, 40.0, 30.0);
+ touchMove(1, 80.0, 30.0);
+
+ expect(gestures).to.have.been.calledTwice;
+
+ expect(gestures.firstCall).to.have.been.calledWith(
+ sinon.match({ type: 'gesturestart',
+ detail: { type: 'drag' } }));
+ expect(gestures.secondCall).to.have.been.calledWith(
+ sinon.match({ type: 'gesturemove',
+ detail: { type: 'drag' } }));
+
+ gestures.resetHistory();
+
+ touchStart(2, 10.0, 10.0);
+
+ expect(gestures).to.not.have.been.called;
+
+ touchMove(1, 100.0, 50.0);
+
+ expect(gestures).to.have.been.calledOnceWith(
+ sinon.match({ type: 'gesturemove',
+ detail: { type: 'drag' } }));
+
+ gestures.resetHistory();
+
+ touchEnd(1);
+
+ expect(gestures).to.have.been.calledOnceWith(
+ sinon.match({ type: 'gestureend',
+ detail: { type: 'drag' } }));
+
+ gestures.resetHistory();
+
+ touchEnd(2);
+
+ expect(gestures).to.not.have.been.called;
+
+ // Check that everything is reseted after trailing ignores are released
+
+ touchStart(3, 20.0, 30.0);
+ touchEnd(3);
+
+ expect(gestures).to.have.been.calledTwice;
+
+ expect(gestures.firstCall).to.have.been.calledWith(
+ sinon.match({ type: 'gesturestart',
+ detail: { type: 'onetap' } }));
+ expect(gestures.secondCall).to.have.been.calledWith(
+ sinon.match({ type: 'gestureend',
+ detail: { type: 'onetap' } }));
+ });
+
+ it('should properly reset after a gesture', function () {
+ touchStart(1, 20.0, 30.0);
+
+ expect(gestures).to.not.have.been.called;
+
+ touchEnd(1);
+
+ expect(gestures).to.have.been.calledTwice;
+
+ expect(gestures.firstCall).to.have.been.calledWith(
+ sinon.match({ type: 'gesturestart',
+ detail: { type: 'onetap',
+ clientX: 20.0,
+ clientY: 30.0 } }));
+
+ expect(gestures.secondCall).to.have.been.calledWith(
+ sinon.match({ type: 'gestureend',
+ detail: { type: 'onetap',
+ clientX: 20.0,
+ clientY: 30.0 } }));
+
+ gestures.resetHistory();
+
+ touchStart(2, 70.0, 80.0);
+
+ expect(gestures).to.not.have.been.called;
+
+ touchEnd(2);
+
+ expect(gestures).to.have.been.calledTwice;
+
+ expect(gestures.firstCall).to.have.been.calledWith(
+ sinon.match({ type: 'gesturestart',
+ detail: { type: 'onetap',
+ clientX: 70.0,
+ clientY: 80.0 } }));
+
+ expect(gestures.secondCall).to.have.been.calledWith(
+ sinon.match({ type: 'gestureend',
+ detail: { type: 'onetap',
+ clientX: 70.0,
+ clientY: 80.0 } }));
+ });
+ });
+});
diff --git a/tests/test.mouse.js b/tests/test.mouse.js
index 8e066c1..8830c4b 100644
--- a/tests/test.mouse.js
+++ b/tests/test.mouse.js
@@ -34,7 +34,6 @@ describe('Mouse Event Handling', function () {
e.preventDefault = sinon.spy();
return e;
};
- const touchevent = mouseevent;
describe('Decode Mouse Events', function () {
it('should decode mousedown events', function (done) {
@@ -89,131 +88,6 @@ describe('Mouse Event Handling', function () {
});
});
- describe('Double-click for Touch', function () {
-
- beforeEach(function () { this.clock = sinon.useFakeTimers(); });
- afterEach(function () { this.clock.restore(); });
-
- it('should use same pos for 2nd tap if close enough', function (done) {
- let calls = 0;
- const mouse = new Mouse(target);
- mouse.onmousebutton = (x, y, down, bmask) => {
- calls++;
- if (calls === 1) {
- expect(down).to.be.equal(1);
- expect(x).to.be.equal(68);
- expect(y).to.be.equal(36);
- } else if (calls === 3) {
- expect(down).to.be.equal(1);
- expect(x).to.be.equal(68);
- expect(y).to.be.equal(36);
- done();
- }
- };
- // touch events are sent in an array of events
- // with one item for each touch point
- mouse._handleMouseDown(touchevent(
- 'touchstart', { touches: [{ clientX: 78, clientY: 46 }]}));
- this.clock.tick(10);
- mouse._handleMouseUp(touchevent(
- 'touchend', { touches: [{ clientX: 79, clientY: 45 }]}));
- this.clock.tick(200);
- mouse._handleMouseDown(touchevent(
- 'touchstart', { touches: [{ clientX: 67, clientY: 35 }]}));
- this.clock.tick(10);
- mouse._handleMouseUp(touchevent(
- 'touchend', { touches: [{ clientX: 66, clientY: 36 }]}));
- });
-
- it('should not modify 2nd tap pos if far apart', function (done) {
- let calls = 0;
- const mouse = new Mouse(target);
- mouse.onmousebutton = (x, y, down, bmask) => {
- calls++;
- if (calls === 1) {
- expect(down).to.be.equal(1);
- expect(x).to.be.equal(68);
- expect(y).to.be.equal(36);
- } else if (calls === 3) {
- expect(down).to.be.equal(1);
- expect(x).to.not.be.equal(68);
- expect(y).to.not.be.equal(36);
- done();
- }
- };
- mouse._handleMouseDown(touchevent(
- 'touchstart', { touches: [{ clientX: 78, clientY: 46 }]}));
- this.clock.tick(10);
- mouse._handleMouseUp(touchevent(
- 'touchend', { touches: [{ clientX: 79, clientY: 45 }]}));
- this.clock.tick(200);
- mouse._handleMouseDown(touchevent(
- 'touchstart', { touches: [{ clientX: 57, clientY: 35 }]}));
- this.clock.tick(10);
- mouse._handleMouseUp(touchevent(
- 'touchend', { touches: [{ clientX: 56, clientY: 36 }]}));
- });
-
- it('should not modify 2nd tap pos if not soon enough', function (done) {
- let calls = 0;
- const mouse = new Mouse(target);
- mouse.onmousebutton = (x, y, down, bmask) => {
- calls++;
- if (calls === 1) {
- expect(down).to.be.equal(1);
- expect(x).to.be.equal(68);
- expect(y).to.be.equal(36);
- } else if (calls === 3) {
- expect(down).to.be.equal(1);
- expect(x).to.not.be.equal(68);
- expect(y).to.not.be.equal(36);
- done();
- }
- };
- mouse._handleMouseDown(touchevent(
- 'touchstart', { touches: [{ clientX: 78, clientY: 46 }]}));
- this.clock.tick(10);
- mouse._handleMouseUp(touchevent(
- 'touchend', { touches: [{ clientX: 79, clientY: 45 }]}));
- this.clock.tick(500);
- mouse._handleMouseDown(touchevent(
- 'touchstart', { touches: [{ clientX: 67, clientY: 35 }]}));
- this.clock.tick(10);
- mouse._handleMouseUp(touchevent(
- 'touchend', { touches: [{ clientX: 66, clientY: 36 }]}));
- });
-
- it('should not modify 2nd tap pos if not touch', function (done) {
- let calls = 0;
- const mouse = new Mouse(target);
- mouse.onmousebutton = (x, y, down, bmask) => {
- calls++;
- if (calls === 1) {
- expect(down).to.be.equal(1);
- expect(x).to.be.equal(68);
- expect(y).to.be.equal(36);
- } else if (calls === 3) {
- expect(down).to.be.equal(1);
- expect(x).to.not.be.equal(68);
- expect(y).to.not.be.equal(36);
- done();
- }
- };
- mouse._handleMouseDown(mouseevent(
- 'mousedown', { button: '0x01', clientX: 78, clientY: 46 }));
- this.clock.tick(10);
- mouse._handleMouseUp(mouseevent(
- 'mouseup', { button: '0x01', clientX: 79, clientY: 45 }));
- this.clock.tick(200);
- mouse._handleMouseDown(mouseevent(
- 'mousedown', { button: '0x01', clientX: 67, clientY: 35 }));
- this.clock.tick(10);
- mouse._handleMouseUp(mouseevent(
- 'mouseup', { button: '0x01', clientX: 66, clientY: 36 }));
- });
-
- });
-
describe('Accumulate mouse wheel events with small delta', function () {
beforeEach(function () { this.clock = sinon.useFakeTimers(); });
diff --git a/tests/test.rfb.js b/tests/test.rfb.js
index 62efefd..e46f40c 100644
--- a/tests/test.rfb.js
+++ b/tests/test.rfb.js
@@ -7,6 +7,8 @@ import { deflateInit, deflate } from "../vendor/pako/lib/zlib/deflate.js";
import { encodings } from '../core/encodings.js';
import { toUnsigned32bit } from '../core/util/int.js';
import { encodeUTF8 } from '../core/util/strings.js';
+import KeyTable from '../core/input/keysym.js';
+import * as browser from '../core/util/browser.js';
import FakeWebSocket from './fake.websocket.js';
@@ -2892,7 +2894,753 @@ describe('Remote Frame Buffer Protocol Client', function () {
});
});
- describe('WebSocket event handlers', function () {
+ describe('Gesture event handlers', function () {
+ let pointerEvent;
+
+ beforeEach(function () {
+ // Touch events and gestures are not supported on IE
+ if (browser.isIE()) {
+ this.skip();
+ return;
+ }
+
+ pointerEvent = sinon.spy(RFB.messages, 'pointerEvent');
+
+ client._display.resize(100, 100);
+ });
+
+ afterEach(function () {
+ pointerEvent.restore();
+ });
+
+ function elementToClient(x, y) {
+ let res = { x: 0, y: 0 };
+
+ let bounds = client._canvas.getBoundingClientRect();
+
+ /*
+ * If the canvas is on a fractional position we will calculate
+ * a fractional mouse position. But that gets truncated when we
+ * send the event, AND the same thing happens in RFB when it
+ * generates the PointerEvent message. To compensate for that
+ * fact we round the value upwards here.
+ */
+ res.x = Math.ceil(bounds.left + x);
+ res.y = Math.ceil(bounds.top + y);
+
+ return res;
+ }
+
+ function gestureStart(gestureType, x, y,
+ magnitudeX = 0, magnitudeY = 0) {
+ let pos = elementToClient(x, y);
+ let detail = {type: gestureType, clientX: pos.x, clientY: pos.y};
+
+ detail.magnitudeX = magnitudeX;
+ detail.magnitudeY = magnitudeY;
+
+ let ev = new CustomEvent('gesturestart', { detail: detail });
+ client._canvas.dispatchEvent(ev);
+ }
+
+ function gestureMove(gestureType, x, y,
+ magnitudeX = 0, magnitudeY = 0) {
+ let pos = elementToClient(x, y);
+ let detail = {type: gestureType, clientX: pos.x, clientY: pos.y};
+
+ detail.magnitudeX = magnitudeX;
+ detail.magnitudeY = magnitudeY;
+
+ let ev = new CustomEvent('gesturemove', { detail: detail });
+ client._canvas.dispatchEvent(ev);
+ }
+
+ function gestureEnd(gestureType, x, y) {
+ let pos = elementToClient(x, y);
+ let detail = {type: gestureType, clientX: pos.x, clientY: pos.y};
+ let ev = new CustomEvent('gestureend', { detail: detail });
+ client._canvas.dispatchEvent(ev);
+ }
+
+ describe('Gesture onetap', function () {
+ it('should handle onetap events', function () {
+ let bmask = 0x1;
+
+ gestureStart('onetap', 20, 40);
+ gestureEnd('onetap', 20, 40);
+
+ expect(pointerEvent).to.have.been.calledThrice;
+ expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,
+ 20, 40, 0x0);
+ expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,
+ 20, 40, bmask);
+ expect(pointerEvent.thirdCall).to.have.been.calledWith(client._sock,
+ 20, 40, 0x0);
+ });
+
+ it('should keep same position for multiple onetap events', function () {
+ let bmask = 0x1;
+
+ gestureStart('onetap', 20, 40);
+ gestureEnd('onetap', 20, 40);
+
+ expect(pointerEvent).to.have.been.calledThrice;
+ expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,
+ 20, 40, 0x0);
+ expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,
+ 20, 40, bmask);
+ expect(pointerEvent.thirdCall).to.have.been.calledWith(client._sock,
+ 20, 40, 0x0);
+
+ pointerEvent.resetHistory();
+
+ gestureStart('onetap', 20, 50);
+ gestureEnd('onetap', 20, 50);
+
+ expect(pointerEvent).to.have.been.calledThrice;
+ expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,
+ 20, 40, 0x0);
+ expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,
+ 20, 40, bmask);
+ expect(pointerEvent.thirdCall).to.have.been.calledWith(client._sock,
+ 20, 40, 0x0);
+
+ pointerEvent.resetHistory();
+
+ gestureStart('onetap', 30, 50);
+ gestureEnd('onetap', 30, 50);
+
+ expect(pointerEvent).to.have.been.calledThrice;
+ expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,
+ 20, 40, 0x0);
+ expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,
+ 20, 40, bmask);
+ expect(pointerEvent.thirdCall).to.have.been.calledWith(client._sock,
+ 20, 40, 0x0);
+ });
+
+ it('should not keep same position for onetap events when too far apart', function () {
+ let bmask = 0x1;
+
+ gestureStart('onetap', 20, 40);
+ gestureEnd('onetap', 20, 40);
+
+ expect(pointerEvent).to.have.been.calledThrice;
+ expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,
+ 20, 40, 0x0);
+ expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,
+ 20, 40, bmask);
+ expect(pointerEvent.thirdCall).to.have.been.calledWith(client._sock,
+ 20, 40, 0x0);
+
+ pointerEvent.resetHistory();
+
+ gestureStart('onetap', 80, 95);
+ gestureEnd('onetap', 80, 95);
+
+ expect(pointerEvent).to.have.been.calledThrice;
+ expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,
+ 80, 95, 0x0);
+ expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,
+ 80, 95, bmask);
+ expect(pointerEvent.thirdCall).to.have.been.calledWith(client._sock,
+ 80, 95, 0x0);
+ });
+
+ it('should not keep same position for onetap events when enough time inbetween', function () {
+ let bmask = 0x1;
+
+ gestureStart('onetap', 10, 20);
+ gestureEnd('onetap', 10, 20);
+
+ expect(pointerEvent).to.have.been.calledThrice;
+ expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,
+ 10, 20, 0x0);
+ expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,
+ 10, 20, bmask);
+ expect(pointerEvent.thirdCall).to.have.been.calledWith(client._sock,
+ 10, 20, 0x0);
+
+ pointerEvent.resetHistory();
+ this.clock.tick(1500);
+
+ gestureStart('onetap', 15, 20);
+ gestureEnd('onetap', 15, 20);
+
+ expect(pointerEvent).to.have.been.calledThrice;
+ expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,
+ 15, 20, 0x0);
+ expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,
+ 15, 20, bmask);
+ expect(pointerEvent.thirdCall).to.have.been.calledWith(client._sock,
+ 15, 20, 0x0);
+
+ pointerEvent.resetHistory();
+ });
+ });
+
+ describe('Gesture twotap', function () {
+ it('should handle gesture twotap events', function () {
+ let bmask = 0x4;
+
+ gestureStart("twotap", 20, 40);
+
+ expect(pointerEvent).to.have.been.calledThrice;
+ expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,
+ 20, 40, 0x0);
+ expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,
+ 20, 40, bmask);
+ expect(pointerEvent.thirdCall).to.have.been.calledWith(client._sock,
+ 20, 40, 0x0);
+ });
+
+ it('should keep same position for multiple twotap events', function () {
+ let bmask = 0x4;
+
+ for (let offset = 0;offset < 30;offset += 10) {
+ pointerEvent.resetHistory();
+
+ gestureStart('twotap', 20, 40 + offset);
+ gestureEnd('twotap', 20, 40 + offset);
+
+ expect(pointerEvent).to.have.been.calledThrice;
+ expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,
+ 20, 40, 0x0);
+ expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,
+ 20, 40, bmask);
+ expect(pointerEvent.thirdCall).to.have.been.calledWith(client._sock,
+ 20, 40, 0x0);
+ }
+ });
+ });
+
+ describe('Gesture threetap', function () {
+ it('should handle gesture start for threetap events', function () {
+ let bmask = 0x2;
+
+ gestureStart("threetap", 20, 40);
+
+ expect(pointerEvent).to.have.been.calledThrice;
+ expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,
+ 20, 40, 0x0);
+ expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,
+ 20, 40, bmask);
+ expect(pointerEvent.thirdCall).to.have.been.calledWith(client._sock,
+ 20, 40, 0x0);
+ });
+
+ it('should keep same position for multiple threetap events', function () {
+ let bmask = 0x2;
+
+ for (let offset = 0;offset < 30;offset += 10) {
+ pointerEvent.resetHistory();
+
+ gestureStart('threetap', 20, 40 + offset);
+ gestureEnd('threetap', 20, 40 + offset);
+
+ expect(pointerEvent).to.have.been.calledThrice;
+ expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,
+ 20, 40, 0x0);
+ expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,
+ 20, 40, bmask);
+ expect(pointerEvent.thirdCall).to.have.been.calledWith(client._sock,
+ 20, 40, 0x0);
+ }
+ });
+ });
+
+ describe('Gesture drag', function () {
+ it('should handle gesture drag events', function () {
+ let bmask = 0x1;
+
+ gestureStart('drag', 20, 40);
+
+ expect(pointerEvent).to.have.been.calledTwice;
+ expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,
+ 20, 40, 0x0);
+ expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,
+ 20, 40, bmask);
+
+ pointerEvent.resetHistory();
+
+ gestureMove('drag', 30, 50);
+ clock.tick(50);
+
+ expect(pointerEvent).to.have.been.calledOnce;
+ expect(pointerEvent).to.have.been.calledWith(client._sock,
+ 30, 50, bmask);
+
+ pointerEvent.resetHistory();
+
+ gestureEnd('drag', 30, 50);
+
+ expect(pointerEvent).to.have.been.calledTwice;
+ expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,
+ 30, 50, bmask);
+ expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,
+ 30, 50, 0x0);
+ });
+ });
+
+ describe('Gesture long press', function () {
+ it('should handle long press events', function () {
+ let bmask = 0x4;
+
+ gestureStart('longpress', 20, 40);
+
+ expect(pointerEvent).to.have.been.calledTwice;
+ expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,
+ 20, 40, 0x0);
+ expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,
+ 20, 40, bmask);
+ pointerEvent.resetHistory();
+
+ gestureMove('longpress', 40, 60);
+ clock.tick(50);
+
+ expect(pointerEvent).to.have.been.calledOnceWith(client._sock,
+ 40, 60, bmask);
+
+ pointerEvent.resetHistory();
+
+ gestureEnd('longpress', 40, 60);
+
+ expect(pointerEvent).to.have.been.calledTwice;
+ expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,
+ 40, 60, bmask);
+ expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,
+ 40, 60, 0x0);
+ });
+ });
+
+ describe('Gesture twodrag', function () {
+ it('should handle gesture twodrag up events', function () {
+ let bmask = 0x10; // Button mask for scroll down
+
+ gestureStart('twodrag', 20, 40, 0, 0);
+
+ expect(pointerEvent).to.have.been.calledOnceWith(client._sock,
+ 20, 40, 0x0);
+
+ pointerEvent.resetHistory();
+
+ gestureMove('twodrag', 20, 40, 0, -60);
+
+ expect(pointerEvent).to.have.been.calledThrice;
+ expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,
+ 20, 40, 0x0);
+ expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,
+ 20, 40, bmask);
+ expect(pointerEvent.thirdCall).to.have.been.calledWith(client._sock,
+ 20, 40, 0x0);
+ });
+
+ it('should handle gesture twodrag down events', function () {
+ let bmask = 0x8; // Button mask for scroll up
+
+ gestureStart('twodrag', 20, 40, 0, 0);
+
+ expect(pointerEvent).to.have.been.calledOnceWith(client._sock,
+ 20, 40, 0x0);
+
+ pointerEvent.resetHistory();
+
+ gestureMove('twodrag', 20, 40, 0, 60);
+
+ expect(pointerEvent).to.have.been.calledThrice;
+ expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,
+ 20, 40, 0x0);
+ expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,
+ 20, 40, bmask);
+ expect(pointerEvent.thirdCall).to.have.been.calledWith(client._sock,
+ 20, 40, 0x0);
+ });
+
+ it('should handle gesture twodrag right events', function () {
+ let bmask = 0x20; // Button mask for scroll right
+
+ gestureStart('twodrag', 20, 40, 0, 0);
+
+ expect(pointerEvent).to.have.been.calledOnceWith(client._sock,
+ 20, 40, 0x0);
+
+ pointerEvent.resetHistory();
+
+ gestureMove('twodrag', 20, 40, 60, 0);
+
+ expect(pointerEvent).to.have.been.calledThrice;
+ expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,
+ 20, 40, 0x0);
+ expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,
+ 20, 40, bmask);
+ expect(pointerEvent.thirdCall).to.have.been.calledWith(client._sock,
+ 20, 40, 0x0);
+ });
+
+ it('should handle gesture twodrag left events', function () {
+ let bmask = 0x40; // Button mask for scroll left
+
+ gestureStart('twodrag', 20, 40, 0, 0);
+
+ expect(pointerEvent).to.have.been.calledOnceWith(client._sock,
+ 20, 40, 0x0);
+
+ pointerEvent.resetHistory();
+
+ gestureMove('twodrag', 20, 40, -60, 0);
+
+ expect(pointerEvent).to.have.been.calledThrice;
+ expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,
+ 20, 40, 0x0);
+ expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,
+ 20, 40, bmask);
+ expect(pointerEvent.thirdCall).to.have.been.calledWith(client._sock,
+ 20, 40, 0x0);
+ });
+
+ it('should handle gesture twodrag diag events', function () {
+ let scrlUp = 0x8; // Button mask for scroll up
+ let scrlRight = 0x20; // Button mask for scroll right
+
+ gestureStart('twodrag', 20, 40, 0, 0);
+
+ expect(pointerEvent).to.have.been.calledOnceWith(client._sock,
+ 20, 40, 0x0);
+
+ pointerEvent.resetHistory();
+
+ gestureMove('twodrag', 20, 40, 60, 60);
+
+ expect(pointerEvent).to.have.been.callCount(5);
+ expect(pointerEvent.getCall(0)).to.have.been.calledWith(client._sock,
+ 20, 40, 0x0);
+ expect(pointerEvent.getCall(1)).to.have.been.calledWith(client._sock,
+ 20, 40, scrlUp);
+ expect(pointerEvent.getCall(2)).to.have.been.calledWith(client._sock,
+ 20, 40, 0x0);
+ expect(pointerEvent.getCall(3)).to.have.been.calledWith(client._sock,
+ 20, 40, scrlRight);
+ expect(pointerEvent.getCall(4)).to.have.been.calledWith(client._sock,
+ 20, 40, 0x0);
+ });
+
+ it('should handle multiple small gesture twodrag events', function () {
+ let bmask = 0x8; // Button mask for scroll up
+
+ gestureStart('twodrag', 20, 40, 0, 0);
+
+ expect(pointerEvent).to.have.been.calledOnceWith(client._sock,
+ 20, 40, 0x0);
+
+ pointerEvent.resetHistory();
+
+ gestureMove('twodrag', 20, 40, 0, 10);
+ clock.tick(50);
+
+ expect(pointerEvent).to.have.been.calledOnceWith(client._sock,
+ 20, 40, 0x0);
+
+ pointerEvent.resetHistory();
+
+ gestureMove('twodrag', 20, 40, 0, 20);
+ clock.tick(50);
+
+ expect(pointerEvent).to.have.been.calledOnceWith(client._sock,
+ 20, 40, 0x0);
+
+ pointerEvent.resetHistory();
+
+ gestureMove('twodrag', 20, 40, 0, 60);
+
+ expect(pointerEvent).to.have.been.calledThrice;
+ expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,
+ 20, 40, 0x0);
+ expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,
+ 20, 40, bmask);
+ expect(pointerEvent.thirdCall).to.have.been.calledWith(client._sock,
+ 20, 40, 0x0);
+ });
+
+ it('should handle large gesture twodrag events', function () {
+ let bmask = 0x8; // Button mask for scroll up
+
+ gestureStart('twodrag', 30, 50, 0, 0);
+
+ expect(pointerEvent).
+ to.have.been.calledOnceWith(client._sock, 30, 50, 0x0);
+
+ pointerEvent.resetHistory();
+
+ gestureMove('twodrag', 30, 50, 0, 200);
+
+ expect(pointerEvent).to.have.callCount(7);
+ expect(pointerEvent.getCall(0)).to.have.been.calledWith(client._sock,
+ 30, 50, 0x0);
+ expect(pointerEvent.getCall(1)).to.have.been.calledWith(client._sock,
+ 30, 50, bmask);
+ expect(pointerEvent.getCall(2)).to.have.been.calledWith(client._sock,
+ 30, 50, 0x0);
+ expect(pointerEvent.getCall(3)).to.have.been.calledWith(client._sock,
+ 30, 50, bmask);
+ expect(pointerEvent.getCall(4)).to.have.been.calledWith(client._sock,
+ 30, 50, 0x0);
+ expect(pointerEvent.getCall(5)).to.have.been.calledWith(client._sock,
+ 30, 50, bmask);
+ expect(pointerEvent.getCall(6)).to.have.been.calledWith(client._sock,
+ 30, 50, 0x0);
+ });
+ });
+
+ describe('Gesture pinch', function () {
+ let keyEvent;
+ let qemuKeyEvent;
+
+ beforeEach(function () {
+ // Touch events and gestures are not supported on IE
+ if (browser.isIE()) {
+ this.skip();
+ return;
+ }
+
+ keyEvent = sinon.spy(RFB.messages, 'keyEvent');
+ qemuKeyEvent = sinon.spy(RFB.messages, 'QEMUExtendedKeyEvent');
+ });
+
+ afterEach(function () {
+ keyEvent.restore();
+ qemuKeyEvent.restore();
+ });
+
+ it('should handle gesture pinch in events', function () {
+ let keysym = KeyTable.XK_Control_L;
+ let bmask = 0x10; // Button mask for scroll down
+
+ gestureStart('pinch', 20, 40, 90, 90);
+
+ expect(pointerEvent).to.have.been.calledOnceWith(client._sock,
+ 20, 40, 0x0);
+ expect(keyEvent).to.not.have.been.called;
+
+ pointerEvent.resetHistory();
+
+ gestureMove('pinch', 20, 40, 30, 30);
+
+ expect(pointerEvent).to.have.been.calledThrice;
+ expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,
+ 20, 40, 0x0);
+ expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,
+ 20, 40, bmask);
+ expect(pointerEvent.thirdCall).to.have.been.calledWith(client._sock,
+ 20, 40, 0x0);
+
+ expect(keyEvent).to.have.been.calledTwice;
+ expect(keyEvent.firstCall).to.have.been.calledWith(client._sock,
+ keysym, 1);
+ expect(keyEvent.secondCall).to.have.been.calledWith(client._sock,
+ keysym, 0);
+
+ expect(keyEvent.firstCall).to.have.been.calledBefore(pointerEvent.secondCall);
+ expect(keyEvent.lastCall).to.have.been.calledAfter(pointerEvent.lastCall);
+
+ pointerEvent.resetHistory();
+ keyEvent.resetHistory();
+
+ gestureEnd('pinch', 20, 40);
+
+ expect(pointerEvent).to.not.have.been.called;
+ expect(keyEvent).to.not.have.been.called;
+ });
+
+ it('should handle gesture pinch out events', function () {
+ let keysym = KeyTable.XK_Control_L;
+ let bmask = 0x8; // Button mask for scroll up
+
+ gestureStart('pinch', 10, 20, 10, 20);
+
+ expect(pointerEvent).to.have.been.calledOnceWith(client._sock,
+ 10, 20, 0x0);
+ expect(keyEvent).to.not.have.been.called;
+
+ pointerEvent.resetHistory();
+
+ gestureMove('pinch', 10, 20, 70, 80);
+
+ expect(pointerEvent).to.have.been.calledThrice;
+ expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,
+ 10, 20, 0x0);
+ expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,
+ 10, 20, bmask);
+ expect(pointerEvent.thirdCall).to.have.been.calledWith(client._sock,
+ 10, 20, 0x0);
+
+ expect(keyEvent).to.have.been.calledTwice;
+ expect(keyEvent.firstCall).to.have.been.calledWith(client._sock,
+ keysym, 1);
+ expect(keyEvent.secondCall).to.have.been.calledWith(client._sock,
+ keysym, 0);
+
+ expect(keyEvent.firstCall).to.have.been.calledBefore(pointerEvent.secondCall);
+ expect(keyEvent.lastCall).to.have.been.calledAfter(pointerEvent.lastCall);
+
+ pointerEvent.resetHistory();
+ keyEvent.resetHistory();
+
+ gestureEnd('pinch', 10, 20);
+
+ expect(pointerEvent).to.not.have.been.called;
+ expect(keyEvent).to.not.have.been.called;
+ });
+
+ it('should handle large gesture pinch', function () {
+ let keysym = KeyTable.XK_Control_L;
+ let bmask = 0x10; // Button mask for scroll down
+
+ gestureStart('pinch', 20, 40, 150, 150);
+
+ expect(pointerEvent).to.have.been.calledOnceWith(client._sock,
+ 20, 40, 0x0);
+ expect(keyEvent).to.not.have.been.called;
+
+ pointerEvent.resetHistory();
+
+ gestureMove('pinch', 20, 40, 30, 30);
+
+ expect(pointerEvent).to.have.been.callCount(5);
+ expect(pointerEvent.getCall(0)).to.have.been.calledWith(client._sock,
+ 20, 40, 0x0);
+ expect(pointerEvent.getCall(1)).to.have.been.calledWith(client._sock,
+ 20, 40, bmask);
+ expect(pointerEvent.getCall(2)).to.have.been.calledWith(client._sock,
+ 20, 40, 0x0);
+ expect(pointerEvent.getCall(3)).to.have.been.calledWith(client._sock,
+ 20, 40, bmask);
+ expect(pointerEvent.getCall(4)).to.have.been.calledWith(client._sock,
+ 20, 40, 0x0);
+
+ expect(keyEvent).to.have.been.calledTwice;
+ expect(keyEvent.firstCall).to.have.been.calledWith(client._sock,
+ keysym, 1);
+ expect(keyEvent.secondCall).to.have.been.calledWith(client._sock,
+ keysym, 0);
+
+ expect(keyEvent.firstCall).to.have.been.calledBefore(pointerEvent.secondCall);
+ expect(keyEvent.lastCall).to.have.been.calledAfter(pointerEvent.lastCall);
+
+ pointerEvent.resetHistory();
+ keyEvent.resetHistory();
+
+ gestureEnd('pinch', 20, 40);
+
+ expect(pointerEvent).to.not.have.been.called;
+ expect(keyEvent).to.not.have.been.called;
+ });
+
+ it('should handle multiple small gesture pinch out events', function () {
+ let keysym = KeyTable.XK_Control_L;
+ let bmask = 0x8; // Button mask for scroll down
+
+ gestureStart('pinch', 20, 40, 0, 10);
+
+ expect(pointerEvent).to.have.been.calledOnceWith(client._sock,
+ 20, 40, 0x0);
+ expect(keyEvent).to.not.have.been.called;
+
+ pointerEvent.resetHistory();
+
+ gestureMove('pinch', 20, 40, 0, 30);
+ clock.tick(50);
+
+ expect(pointerEvent).to.have.been.calledWith(client._sock,
+ 20, 40, 0x0);
+
+ pointerEvent.resetHistory();
+
+ gestureMove('pinch', 20, 40, 0, 60);
+ clock.tick(50);
+
+ expect(pointerEvent).to.have.been.calledWith(client._sock,
+ 20, 40, 0x0);
+
+ pointerEvent.resetHistory();
+ keyEvent.resetHistory();
+
+ gestureMove('pinch', 20, 40, 0, 90);
+
+ expect(pointerEvent).to.have.been.calledThrice;
+ expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,
+ 20, 40, 0x0);
+ expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,
+ 20, 40, bmask);
+ expect(pointerEvent.thirdCall).to.have.been.calledWith(client._sock,
+ 20, 40, 0x0);
+
+ expect(keyEvent).to.have.been.calledTwice;
+ expect(keyEvent.firstCall).to.have.been.calledWith(client._sock,
+ keysym, 1);
+ expect(keyEvent.secondCall).to.have.been.calledWith(client._sock,
+ keysym, 0);
+
+ expect(keyEvent.firstCall).to.have.been.calledBefore(pointerEvent.secondCall);
+ expect(keyEvent.lastCall).to.have.been.calledAfter(pointerEvent.lastCall);
+
+ pointerEvent.resetHistory();
+ keyEvent.resetHistory();
+
+ gestureEnd('pinch', 20, 40);
+
+ expect(keyEvent).to.not.have.been.called;
+ });
+
+ it('should send correct key control code', function () {
+ let keysym = KeyTable.XK_Control_L;
+ let code = 0x1d;
+ let bmask = 0x10; // Button mask for scroll down
+
+ client._qemuExtKeyEventSupported = true;
+
+ gestureStart('pinch', 20, 40, 90, 90);
+
+ expect(pointerEvent).to.have.been.calledOnceWith(client._sock,
+ 20, 40, 0x0);
+ expect(qemuKeyEvent).to.not.have.been.called;
+
+ pointerEvent.resetHistory();
+
+ gestureMove('pinch', 20, 40, 30, 30);
+
+ expect(pointerEvent).to.have.been.calledThrice;
+ expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,
+ 20, 40, 0x0);
+ expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,
+ 20, 40, bmask);
+ expect(pointerEvent.thirdCall).to.have.been.calledWith(client._sock,
+ 20, 40, 0x0);
+
+ expect(qemuKeyEvent).to.have.been.calledTwice;
+ expect(qemuKeyEvent.firstCall).to.have.been.calledWith(client._sock,
+ keysym,
+ true,
+ code);
+ expect(qemuKeyEvent.secondCall).to.have.been.calledWith(client._sock,
+ keysym,
+ false,
+ code);
+
+ expect(qemuKeyEvent.firstCall).to.have.been.calledBefore(pointerEvent.secondCall);
+ expect(qemuKeyEvent.lastCall).to.have.been.calledAfter(pointerEvent.lastCall);
+
+ pointerEvent.resetHistory();
+ qemuKeyEvent.resetHistory();
+
+ gestureEnd('pinch', 20, 40);
+
+ expect(pointerEvent).to.not.have.been.called;
+ expect(qemuKeyEvent).to.not.have.been.called;
+ });
+ });
+ });
+
+ describe('WebSocket Events', function () {
// message events
it('should do nothing if we receive an empty message and have nothing in the queue', function () {
client._normalMsg = sinon.spy();
diff --git a/vnc.html b/vnc.html
index 0f2a3b3..32f356f 100644
--- a/vnc.html
+++ b/vnc.html
@@ -94,18 +94,6 @@
<!--noVNC Touch Device only buttons-->
<div id="noVNC_mobile_buttons">
- <input type="image" alt="No mousebutton" src="app/images/mouse_none.svg"
- id="noVNC_mouse_button0" class="noVNC_button"
- title="Active Mouse Button">
- <input type="image" alt="Left mousebutton" src="app/images/mouse_left.svg"
- id="noVNC_mouse_button1" class="noVNC_button"
- title="Active Mouse Button">
- <input type="image" alt="Middle mousebutton" src="app/images/mouse_middle.svg"
- id="noVNC_mouse_button2" class="noVNC_button"
- title="Active Mouse Button">
- <input type="image" alt="Right mousebutton" src="app/images/mouse_right.svg"
- id="noVNC_mouse_button4" class="noVNC_button"
- title="Active Mouse Button">
<input type="image" alt="Keyboard" src="app/images/keyboard.svg"
id="noVNC_keyboard_button" class="noVNC_button" title="Show Keyboard">
</div>