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.
This commit is contained in:
parent
440ec8a0b6
commit
8be924c9d9
|
@ -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>
|
|
Before Width: | Height: | Size: 6.8 KiB |
|
@ -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>
|
|
Before Width: | Height: | Size: 6.8 KiB |
|
@ -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>
|
|
Before Width: | Height: | Size: 6.8 KiB |
|
@ -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>
|
|
Before Width: | Height: | Size: 6.8 KiB |
31
app/ui.js
31
app/ui.js
|
@ -234,14 +234,6 @@ const UI = {
|
||||||
},
|
},
|
||||||
|
|
||||||
addTouchSpecificHandlers() {
|
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")
|
document.getElementById("noVNC_keyboard_button")
|
||||||
.addEventListener('click', UI.toggleVirtualKeyboard);
|
.addEventListener('click', UI.toggleVirtualKeyboard);
|
||||||
|
|
||||||
|
@ -430,7 +422,6 @@ const UI = {
|
||||||
UI.disableSetting('port');
|
UI.disableSetting('port');
|
||||||
UI.disableSetting('path');
|
UI.disableSetting('path');
|
||||||
UI.disableSetting('repeaterID');
|
UI.disableSetting('repeaterID');
|
||||||
UI.setMouseButton(1);
|
|
||||||
|
|
||||||
// Hide the controlbar after 2 seconds
|
// Hide the controlbar after 2 seconds
|
||||||
UI.closeControlbarTimeout = setTimeout(UI.closeControlbar, 2000);
|
UI.closeControlbarTimeout = setTimeout(UI.closeControlbar, 2000);
|
||||||
|
@ -1633,24 +1624,6 @@ const UI = {
|
||||||
* MISC
|
* MISC
|
||||||
* ------v------*/
|
* ------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() {
|
updateViewOnly() {
|
||||||
if (!UI.rfb) return;
|
if (!UI.rfb) return;
|
||||||
UI.rfb.viewOnly = UI.getSetting('view_only');
|
UI.rfb.viewOnly = UI.getSetting('view_only');
|
||||||
|
@ -1661,8 +1634,6 @@ const UI = {
|
||||||
.classList.add('noVNC_hidden');
|
.classList.add('noVNC_hidden');
|
||||||
document.getElementById('noVNC_toggle_extra_keys_button')
|
document.getElementById('noVNC_toggle_extra_keys_button')
|
||||||
.classList.add('noVNC_hidden');
|
.classList.add('noVNC_hidden');
|
||||||
document.getElementById('noVNC_mouse_button' + UI.rfb.touchButton)
|
|
||||||
.classList.add('noVNC_hidden');
|
|
||||||
document.getElementById('noVNC_clipboard_button')
|
document.getElementById('noVNC_clipboard_button')
|
||||||
.classList.add('noVNC_hidden');
|
.classList.add('noVNC_hidden');
|
||||||
} else {
|
} else {
|
||||||
|
@ -1670,8 +1641,6 @@ const UI = {
|
||||||
.classList.remove('noVNC_hidden');
|
.classList.remove('noVNC_hidden');
|
||||||
document.getElementById('noVNC_toggle_extra_keys_button')
|
document.getElementById('noVNC_toggle_extra_keys_button')
|
||||||
.classList.remove('noVNC_hidden');
|
.classList.remove('noVNC_hidden');
|
||||||
document.getElementById('noVNC_mouse_button' + UI.rfb.touchButton)
|
|
||||||
.classList.remove('noVNC_hidden');
|
|
||||||
document.getElementById('noVNC_clipboard_button')
|
document.getElementById('noVNC_clipboard_button')
|
||||||
.classList.remove('noVNC_hidden');
|
.classList.remove('noVNC_hidden');
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 } };
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,7 +5,6 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as Log from '../util/logging.js';
|
import * as Log from '../util/logging.js';
|
||||||
import { isTouchDevice } from '../util/browser.js';
|
|
||||||
import { setCapture, stopEvent, getPointerEvent } from '../util/events.js';
|
import { setCapture, stopEvent, getPointerEvent } from '../util/events.js';
|
||||||
|
|
||||||
const WHEEL_STEP = 10; // Delta threshold for a mouse wheel step
|
const WHEEL_STEP = 10; // Delta threshold for a mouse wheel step
|
||||||
|
@ -16,9 +15,6 @@ export default class Mouse {
|
||||||
constructor(target) {
|
constructor(target) {
|
||||||
this._target = target || document;
|
this._target = target || document;
|
||||||
|
|
||||||
this._doubleClickTimer = null;
|
|
||||||
this._lastTouchPos = null;
|
|
||||||
|
|
||||||
this._pos = null;
|
this._pos = null;
|
||||||
this._wheelStepXTimer = null;
|
this._wheelStepXTimer = null;
|
||||||
this._wheelStepYTimer = null;
|
this._wheelStepYTimer = null;
|
||||||
|
@ -33,11 +29,6 @@ export default class Mouse {
|
||||||
'mousedisable': this._handleMouseDisable.bind(this)
|
'mousedisable': this._handleMouseDisable.bind(this)
|
||||||
};
|
};
|
||||||
|
|
||||||
// ===== PROPERTIES =====
|
|
||||||
|
|
||||||
this.touchButton = 1; // Button mask (1, 2, 4) for touch devices
|
|
||||||
// (0 means ignore clicks)
|
|
||||||
|
|
||||||
// ===== EVENT HANDLERS =====
|
// ===== EVENT HANDLERS =====
|
||||||
|
|
||||||
this.onmousebutton = () => {}; // Handler for mouse button press/release
|
this.onmousebutton = () => {}; // Handler for mouse button press/release
|
||||||
|
@ -55,39 +46,7 @@ export default class Mouse {
|
||||||
let pos = this._pos;
|
let pos = this._pos;
|
||||||
|
|
||||||
let bmask;
|
let bmask;
|
||||||
if (e.touches || e.changedTouches) {
|
if (e.which) {
|
||||||
// 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) {
|
|
||||||
/* everything except IE */
|
/* everything except IE */
|
||||||
bmask = 1 << e.button;
|
bmask = 1 << e.button;
|
||||||
} else {
|
} else {
|
||||||
|
@ -105,10 +64,7 @@ export default class Mouse {
|
||||||
}
|
}
|
||||||
|
|
||||||
_handleMouseDown(e) {
|
_handleMouseDown(e) {
|
||||||
// Touch events have implicit capture
|
setCapture(this._target);
|
||||||
if (e.type === "mousedown") {
|
|
||||||
setCapture(this._target);
|
|
||||||
}
|
|
||||||
|
|
||||||
this._handleMouseButton(e, 1);
|
this._handleMouseButton(e, 1);
|
||||||
}
|
}
|
||||||
|
@ -242,11 +198,6 @@ export default class Mouse {
|
||||||
|
|
||||||
grab() {
|
grab() {
|
||||||
const t = this._target;
|
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('mousedown', this._eventHandlers.mousedown);
|
||||||
t.addEventListener('mouseup', this._eventHandlers.mouseup);
|
t.addEventListener('mouseup', this._eventHandlers.mouseup);
|
||||||
t.addEventListener('mousemove', this._eventHandlers.mousemove);
|
t.addEventListener('mousemove', this._eventHandlers.mousemove);
|
||||||
|
@ -265,11 +216,6 @@ export default class Mouse {
|
||||||
|
|
||||||
this._resetWheelStepTimers();
|
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('mousedown', this._eventHandlers.mousedown);
|
||||||
t.removeEventListener('mouseup', this._eventHandlers.mouseup);
|
t.removeEventListener('mouseup', this._eventHandlers.mouseup);
|
||||||
t.removeEventListener('mousemove', this._eventHandlers.mousemove);
|
t.removeEventListener('mousemove', this._eventHandlers.mousemove);
|
||||||
|
|
183
core/rfb.js
183
core/rfb.js
|
@ -11,12 +11,14 @@ import { toUnsigned32bit, toSigned32bit } from './util/int.js';
|
||||||
import * as Log from './util/logging.js';
|
import * as Log from './util/logging.js';
|
||||||
import { encodeUTF8, decodeUTF8 } from './util/strings.js';
|
import { encodeUTF8, decodeUTF8 } from './util/strings.js';
|
||||||
import { dragThreshold } from './util/browser.js';
|
import { dragThreshold } from './util/browser.js';
|
||||||
|
import { clientToElement } from './util/element.js';
|
||||||
import EventTargetMixin from './util/eventtarget.js';
|
import EventTargetMixin from './util/eventtarget.js';
|
||||||
import Display from "./display.js";
|
import Display from "./display.js";
|
||||||
import Inflator from "./inflator.js";
|
import Inflator from "./inflator.js";
|
||||||
import Deflator from "./deflator.js";
|
import Deflator from "./deflator.js";
|
||||||
import Keyboard from "./input/keyboard.js";
|
import Keyboard from "./input/keyboard.js";
|
||||||
import Mouse from "./input/mouse.js";
|
import Mouse from "./input/mouse.js";
|
||||||
|
import GestureHandler from "./input/gesturehandler.js";
|
||||||
import Cursor from "./util/cursor.js";
|
import Cursor from "./util/cursor.js";
|
||||||
import Websock from "./websock.js";
|
import Websock from "./websock.js";
|
||||||
import DES from "./des.js";
|
import DES from "./des.js";
|
||||||
|
@ -39,6 +41,12 @@ const DEFAULT_BACKGROUND = 'rgb(40, 40, 40)';
|
||||||
// Minimum wait (ms) between two mouse moves
|
// Minimum wait (ms) between two mouse moves
|
||||||
const MOUSE_MOVE_DELAY = 17;
|
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
|
// Extended clipboard pseudo-encoding formats
|
||||||
const extendedClipboardFormatText = 1;
|
const extendedClipboardFormatText = 1;
|
||||||
/*eslint-disable no-unused-vars */
|
/*eslint-disable no-unused-vars */
|
||||||
|
@ -118,6 +126,7 @@ export default class RFB extends EventTargetMixin {
|
||||||
this._flushing = false; // Display flushing state
|
this._flushing = false; // Display flushing state
|
||||||
this._keyboard = null; // Keyboard input handler object
|
this._keyboard = null; // Keyboard input handler object
|
||||||
this._mouse = null; // Mouse input handler object
|
this._mouse = null; // Mouse input handler object
|
||||||
|
this._gestures = null; // Gesture input handler object
|
||||||
|
|
||||||
// Timers
|
// Timers
|
||||||
this._disconnTimer = null; // disconnection timer
|
this._disconnTimer = null; // disconnection timer
|
||||||
|
@ -144,10 +153,17 @@ export default class RFB extends EventTargetMixin {
|
||||||
this._viewportDragPos = {};
|
this._viewportDragPos = {};
|
||||||
this._viewportHasMoved = false;
|
this._viewportHasMoved = false;
|
||||||
|
|
||||||
|
// Gesture state
|
||||||
|
this._gestureLastTapTime = null;
|
||||||
|
this._gestureFirstDoubleTapEv = null;
|
||||||
|
this._gestureLastMagnitudeX = 0;
|
||||||
|
this._gestureLastMagnitudeY = 0;
|
||||||
|
|
||||||
// Bound event handlers
|
// Bound event handlers
|
||||||
this._eventHandlers = {
|
this._eventHandlers = {
|
||||||
focusCanvas: this._focusCanvas.bind(this),
|
focusCanvas: this._focusCanvas.bind(this),
|
||||||
windowResize: this._windowResize.bind(this),
|
windowResize: this._windowResize.bind(this),
|
||||||
|
handleGesture: this._handleGesture.bind(this),
|
||||||
};
|
};
|
||||||
|
|
||||||
// main setup
|
// main setup
|
||||||
|
@ -210,6 +226,8 @@ export default class RFB extends EventTargetMixin {
|
||||||
this._mouse.onmousebutton = this._handleMouseButton.bind(this);
|
this._mouse.onmousebutton = this._handleMouseButton.bind(this);
|
||||||
this._mouse.onmousemove = this._handleMouseMove.bind(this);
|
this._mouse.onmousemove = this._handleMouseMove.bind(this);
|
||||||
|
|
||||||
|
this._gestures = new GestureHandler();
|
||||||
|
|
||||||
this._sock = new Websock();
|
this._sock = new Websock();
|
||||||
this._sock.on('message', () => {
|
this._sock.on('message', () => {
|
||||||
this._handleMessage();
|
this._handleMessage();
|
||||||
|
@ -306,8 +324,8 @@ export default class RFB extends EventTargetMixin {
|
||||||
|
|
||||||
get capabilities() { return this._capabilities; }
|
get capabilities() { return this._capabilities; }
|
||||||
|
|
||||||
get touchButton() { return this._mouse.touchButton; }
|
get touchButton() { return 0; }
|
||||||
set touchButton(button) { this._mouse.touchButton = button; }
|
set touchButton(button) { Log.Warn("Using old API!"); }
|
||||||
|
|
||||||
get clipViewport() { return this._clipViewport; }
|
get clipViewport() { return this._clipViewport; }
|
||||||
set clipViewport(viewport) {
|
set clipViewport(viewport) {
|
||||||
|
@ -501,6 +519,8 @@ export default class RFB extends EventTargetMixin {
|
||||||
// Make our elements part of the page
|
// Make our elements part of the page
|
||||||
this._target.appendChild(this._screen);
|
this._target.appendChild(this._screen);
|
||||||
|
|
||||||
|
this._gestures.attach(this._canvas);
|
||||||
|
|
||||||
this._cursor.attach(this._canvas);
|
this._cursor.attach(this._canvas);
|
||||||
this._refreshCursor();
|
this._refreshCursor();
|
||||||
|
|
||||||
|
@ -512,17 +532,26 @@ export default class RFB extends EventTargetMixin {
|
||||||
this._canvas.addEventListener("mousedown", this._eventHandlers.focusCanvas);
|
this._canvas.addEventListener("mousedown", this._eventHandlers.focusCanvas);
|
||||||
this._canvas.addEventListener("touchstart", 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");
|
Log.Debug("<< RFB.connect");
|
||||||
}
|
}
|
||||||
|
|
||||||
_disconnect() {
|
_disconnect() {
|
||||||
Log.Debug(">> RFB.disconnect");
|
Log.Debug(">> RFB.disconnect");
|
||||||
this._cursor.detach();
|
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("mousedown", this._eventHandlers.focusCanvas);
|
||||||
this._canvas.removeEventListener("touchstart", this._eventHandlers.focusCanvas);
|
this._canvas.removeEventListener("touchstart", this._eventHandlers.focusCanvas);
|
||||||
window.removeEventListener('resize', this._eventHandlers.windowResize);
|
window.removeEventListener('resize', this._eventHandlers.windowResize);
|
||||||
this._keyboard.ungrab();
|
this._keyboard.ungrab();
|
||||||
this._mouse.ungrab();
|
this._mouse.ungrab();
|
||||||
|
this._gestures.detach();
|
||||||
this._sock.close();
|
this._sock.close();
|
||||||
try {
|
try {
|
||||||
this._target.removeChild(this._screen);
|
this._target.removeChild(this._screen);
|
||||||
|
@ -910,6 +939,156 @@ export default class RFB extends EventTargetMixin {
|
||||||
this._display.absY(y), mask);
|
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
|
// Message Handlers
|
||||||
|
|
||||||
_negotiateProtocolVersion() {
|
_negotiateProtocolVersion() {
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -26,12 +26,6 @@ protocol stream.
|
||||||
moved to the remote session when a `mousedown` or `touchstart`
|
moved to the remote session when a `mousedown` or `touchstart`
|
||||||
event is received. Enabled by default.
|
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`
|
`clipViewport`
|
||||||
- Is a `boolean` indicating if the remote session should be clipped
|
- Is a `boolean` indicating if the remote session should be clipped
|
||||||
to its container. When disabled scrollbars will be shown to handle
|
to its container. When disabled scrollbars will be shown to handle
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -34,7 +34,6 @@ describe('Mouse Event Handling', function () {
|
||||||
e.preventDefault = sinon.spy();
|
e.preventDefault = sinon.spy();
|
||||||
return e;
|
return e;
|
||||||
};
|
};
|
||||||
const touchevent = mouseevent;
|
|
||||||
|
|
||||||
describe('Decode Mouse Events', function () {
|
describe('Decode Mouse Events', function () {
|
||||||
it('should decode mousedown events', function (done) {
|
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 () {
|
describe('Accumulate mouse wheel events with small delta', function () {
|
||||||
|
|
||||||
beforeEach(function () { this.clock = sinon.useFakeTimers(); });
|
beforeEach(function () { this.clock = sinon.useFakeTimers(); });
|
||||||
|
|
|
@ -7,6 +7,8 @@ import { deflateInit, deflate } from "../vendor/pako/lib/zlib/deflate.js";
|
||||||
import { encodings } from '../core/encodings.js';
|
import { encodings } from '../core/encodings.js';
|
||||||
import { toUnsigned32bit } from '../core/util/int.js';
|
import { toUnsigned32bit } from '../core/util/int.js';
|
||||||
import { encodeUTF8 } from '../core/util/strings.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';
|
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
|
// message events
|
||||||
it('should do nothing if we receive an empty message and have nothing in the queue', function () {
|
it('should do nothing if we receive an empty message and have nothing in the queue', function () {
|
||||||
client._normalMsg = sinon.spy();
|
client._normalMsg = sinon.spy();
|
||||||
|
|
12
vnc.html
12
vnc.html
|
@ -94,18 +94,6 @@
|
||||||
|
|
||||||
<!--noVNC Touch Device only buttons-->
|
<!--noVNC Touch Device only buttons-->
|
||||||
<div id="noVNC_mobile_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"
|
<input type="image" alt="Keyboard" src="app/images/keyboard.svg"
|
||||||
id="noVNC_keyboard_button" class="noVNC_button" title="Show Keyboard">
|
id="noVNC_keyboard_button" class="noVNC_button" title="Show Keyboard">
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Reference in New Issue