Merge pull request #1414 from CendioOssman/gesture

Add gesture handling
This commit is contained in:
Samuel Mannehed 2020-06-12 16:11:30 +02:00 committed by GitHub
commit bb09e766ba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 3084 additions and 1203 deletions

View File

@ -25,6 +25,7 @@
"brace-style": ["error", "1tbs", { "allowSingleLine": true }], "brace-style": ["error", "1tbs", { "allowSingleLine": true }],
"indent": ["error", 4, { "SwitchCase": 1, "indent": ["error", 4, { "SwitchCase": 1,
"FunctionDeclaration": { "parameters": "first" },
"CallExpression": { "arguments": "first" }, "CallExpression": { "arguments": "first" },
"ArrayExpression": "first", "ArrayExpression": "first",
"ObjectExpression": "first", "ObjectExpression": "first",

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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');
} }

View File

@ -9,6 +9,7 @@
import * as Log from './util/logging.js'; import * as Log from './util/logging.js';
import Base64 from "./base64.js"; import Base64 from "./base64.js";
import { supportsImageMetadata } from './util/browser.js'; import { supportsImageMetadata } from './util/browser.js';
import { toSigned32bit } from './util/int.js';
export default class Display { export default class Display {
constructor(target) { constructor(target) {
@ -190,14 +191,14 @@ export default class Display {
if (this._scale === 0) { if (this._scale === 0) {
return 0; return 0;
} }
return x / this._scale + this._viewportLoc.x; return toSigned32bit(x / this._scale + this._viewportLoc.x);
} }
absY(y) { absY(y) {
if (this._scale === 0) { if (this._scale === 0) {
return 0; return 0;
} }
return y / this._scale + this._viewportLoc.y; return toSigned32bit(y / this._scale + this._viewportLoc.y);
} }
resize(width, height) { resize(width, height) {

View File

@ -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 } };
}
}

View File

@ -1,282 +0,0 @@
/*
* noVNC: HTML5 VNC client
* Copyright (C) 2019 The noVNC Authors
* Licensed under MPL 2.0 or any later version (see LICENSE.txt)
*/
import * as Log from '../util/logging.js';
import { isTouchDevice } from '../util/browser.js';
import { setCapture, stopEvent, getPointerEvent } from '../util/events.js';
const WHEEL_STEP = 10; // Delta threshold for a mouse wheel step
const WHEEL_STEP_TIMEOUT = 50; // ms
const WHEEL_LINE_HEIGHT = 19;
export default class Mouse {
constructor(target) {
this._target = target || document;
this._doubleClickTimer = null;
this._lastTouchPos = null;
this._pos = null;
this._wheelStepXTimer = null;
this._wheelStepYTimer = null;
this._accumulatedWheelDeltaX = 0;
this._accumulatedWheelDeltaY = 0;
this._eventHandlers = {
'mousedown': this._handleMouseDown.bind(this),
'mouseup': this._handleMouseUp.bind(this),
'mousemove': this._handleMouseMove.bind(this),
'mousewheel': this._handleMouseWheel.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 =====
this.onmousebutton = () => {}; // Handler for mouse button press/release
this.onmousemove = () => {}; // Handler for mouse movement
}
// ===== PRIVATE METHODS =====
_resetDoubleClickTimer() {
this._doubleClickTimer = null;
}
_handleMouseButton(e, down) {
this._updateMousePosition(e);
let pos = this._pos;
let bmask;
if (e.touches || e.changedTouches) {
// Touch device
// When two touches occur within 500 ms of each other and are
// close enough together a double click is triggered.
if (down == 1) {
if (this._doubleClickTimer === null) {
this._lastTouchPos = pos;
} else {
clearTimeout(this._doubleClickTimer);
// When the distance between the two touches is small enough
// force the position of the latter touch to the position of
// the first.
const xs = this._lastTouchPos.x - pos.x;
const ys = this._lastTouchPos.y - pos.y;
const d = Math.sqrt((xs * xs) + (ys * ys));
// The goal is to trigger on a certain physical width,
// the devicePixelRatio brings us a bit closer but is
// not optimal.
const threshold = 20 * (window.devicePixelRatio || 1);
if (d < threshold) {
pos = this._lastTouchPos;
}
}
this._doubleClickTimer =
setTimeout(this._resetDoubleClickTimer.bind(this), 500);
}
bmask = this.touchButton;
// If bmask is set
} else if (e.which) {
/* everything except IE */
bmask = 1 << e.button;
} else {
/* IE including 9 */
bmask = (e.button & 0x1) + // Left
(e.button & 0x2) * 2 + // Right
(e.button & 0x4) / 2; // Middle
}
Log.Debug("onmousebutton " + (down ? "down" : "up") +
", x: " + pos.x + ", y: " + pos.y + ", bmask: " + bmask);
this.onmousebutton(pos.x, pos.y, down, bmask);
stopEvent(e);
}
_handleMouseDown(e) {
// Touch events have implicit capture
if (e.type === "mousedown") {
setCapture(this._target);
}
this._handleMouseButton(e, 1);
}
_handleMouseUp(e) {
this._handleMouseButton(e, 0);
}
// Mouse wheel events are sent in steps over VNC. This means that the VNC
// protocol can't handle a wheel event with specific distance or speed.
// Therefor, if we get a lot of small mouse wheel events we combine them.
_generateWheelStepX() {
if (this._accumulatedWheelDeltaX < 0) {
this.onmousebutton(this._pos.x, this._pos.y, 1, 1 << 5);
this.onmousebutton(this._pos.x, this._pos.y, 0, 1 << 5);
} else if (this._accumulatedWheelDeltaX > 0) {
this.onmousebutton(this._pos.x, this._pos.y, 1, 1 << 6);
this.onmousebutton(this._pos.x, this._pos.y, 0, 1 << 6);
}
this._accumulatedWheelDeltaX = 0;
}
_generateWheelStepY() {
if (this._accumulatedWheelDeltaY < 0) {
this.onmousebutton(this._pos.x, this._pos.y, 1, 1 << 3);
this.onmousebutton(this._pos.x, this._pos.y, 0, 1 << 3);
} else if (this._accumulatedWheelDeltaY > 0) {
this.onmousebutton(this._pos.x, this._pos.y, 1, 1 << 4);
this.onmousebutton(this._pos.x, this._pos.y, 0, 1 << 4);
}
this._accumulatedWheelDeltaY = 0;
}
_resetWheelStepTimers() {
window.clearTimeout(this._wheelStepXTimer);
window.clearTimeout(this._wheelStepYTimer);
this._wheelStepXTimer = null;
this._wheelStepYTimer = null;
}
_handleMouseWheel(e) {
this._resetWheelStepTimers();
this._updateMousePosition(e);
let dX = e.deltaX;
let dY = e.deltaY;
// Pixel units unless it's non-zero.
// Note that if deltamode is line or page won't matter since we aren't
// sending the mouse wheel delta to the server anyway.
// The difference between pixel and line can be important however since
// we have a threshold that can be smaller than the line height.
if (e.deltaMode !== 0) {
dX *= WHEEL_LINE_HEIGHT;
dY *= WHEEL_LINE_HEIGHT;
}
this._accumulatedWheelDeltaX += dX;
this._accumulatedWheelDeltaY += dY;
// Generate a mouse wheel step event when the accumulated delta
// for one of the axes is large enough.
// Small delta events that do not pass the threshold get sent
// after a timeout.
if (Math.abs(this._accumulatedWheelDeltaX) > WHEEL_STEP) {
this._generateWheelStepX();
} else {
this._wheelStepXTimer =
window.setTimeout(this._generateWheelStepX.bind(this),
WHEEL_STEP_TIMEOUT);
}
if (Math.abs(this._accumulatedWheelDeltaY) > WHEEL_STEP) {
this._generateWheelStepY();
} else {
this._wheelStepYTimer =
window.setTimeout(this._generateWheelStepY.bind(this),
WHEEL_STEP_TIMEOUT);
}
stopEvent(e);
}
_handleMouseMove(e) {
this._updateMousePosition(e);
this.onmousemove(this._pos.x, this._pos.y);
stopEvent(e);
}
_handleMouseDisable(e) {
/*
* Stop propagation if inside canvas area
* Note: This is only needed for the 'click' event as it fails
* to fire properly for the target element so we have
* to listen on the document element instead.
*/
if (e.target == this._target) {
stopEvent(e);
}
}
// Update coordinates relative to target
_updateMousePosition(e) {
e = getPointerEvent(e);
const bounds = this._target.getBoundingClientRect();
let x;
let y;
// Clip to target bounds
if (e.clientX < bounds.left) {
x = 0;
} else if (e.clientX >= bounds.right) {
x = bounds.width - 1;
} else {
x = e.clientX - bounds.left;
}
if (e.clientY < bounds.top) {
y = 0;
} else if (e.clientY >= bounds.bottom) {
y = bounds.height - 1;
} else {
y = e.clientY - bounds.top;
}
this._pos = {x: x, y: y};
}
// ===== PUBLIC METHODS =====
grab() {
const t = this._target;
if (isTouchDevice) {
t.addEventListener('touchstart', this._eventHandlers.mousedown);
t.addEventListener('touchend', this._eventHandlers.mouseup);
t.addEventListener('touchmove', this._eventHandlers.mousemove);
}
t.addEventListener('mousedown', this._eventHandlers.mousedown);
t.addEventListener('mouseup', this._eventHandlers.mouseup);
t.addEventListener('mousemove', this._eventHandlers.mousemove);
t.addEventListener('wheel', this._eventHandlers.mousewheel);
// Prevent middle-click pasting (see above for why we bind to document)
document.addEventListener('click', this._eventHandlers.mousedisable);
// preventDefault() on mousedown doesn't stop this event for some
// reason so we have to explicitly block it
t.addEventListener('contextmenu', this._eventHandlers.mousedisable);
}
ungrab() {
const t = this._target;
this._resetWheelStepTimers();
if (isTouchDevice) {
t.removeEventListener('touchstart', this._eventHandlers.mousedown);
t.removeEventListener('touchend', this._eventHandlers.mouseup);
t.removeEventListener('touchmove', this._eventHandlers.mousemove);
}
t.removeEventListener('mousedown', this._eventHandlers.mousedown);
t.removeEventListener('mouseup', this._eventHandlers.mouseup);
t.removeEventListener('mousemove', this._eventHandlers.mousemove);
t.removeEventListener('wheel', this._eventHandlers.mousewheel);
document.removeEventListener('click', this._eventHandlers.mousedisable);
t.removeEventListener('contextmenu', this._eventHandlers.mousedisable);
}
}

View File

@ -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 { setCapture } from './util/events.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 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,16 @@ 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;
// Wheel thresholds
const WHEEL_STEP = 50; // Pixels needed for one step
const WHEEL_LINE_HEIGHT = 19; // Assumed pixels for one line step
// 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 */
@ -117,7 +129,7 @@ export default class RFB extends EventTargetMixin {
this._display = null; // Display object this._display = null; // Display object
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._gestures = null; // Gesture input handler object
// Timers // Timers
this._disconnTimer = null; // disconnection timer this._disconnTimer = null; // disconnection timer
@ -143,11 +155,22 @@ export default class RFB extends EventTargetMixin {
this._viewportDragging = false; this._viewportDragging = false;
this._viewportDragPos = {}; this._viewportDragPos = {};
this._viewportHasMoved = false; this._viewportHasMoved = false;
this._accumulatedWheelDeltaX = 0;
this._accumulatedWheelDeltaY = 0;
// 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),
handleMouse: this._handleMouse.bind(this),
handleWheel: this._handleWheel.bind(this),
handleGesture: this._handleGesture.bind(this),
}; };
// main setup // main setup
@ -206,9 +229,7 @@ export default class RFB extends EventTargetMixin {
this._keyboard = new Keyboard(this._canvas); this._keyboard = new Keyboard(this._canvas);
this._keyboard.onkeyevent = this._handleKeyEvent.bind(this); this._keyboard.onkeyevent = this._handleKeyEvent.bind(this);
this._mouse = new Mouse(this._canvas); this._gestures = new GestureHandler();
this._mouse.onmousebutton = this._handleMouseButton.bind(this);
this._mouse.onmousemove = this._handleMouseMove.bind(this);
this._sock = new Websock(); this._sock = new Websock();
this._sock.on('message', () => { this._sock.on('message', () => {
@ -296,18 +317,16 @@ export default class RFB extends EventTargetMixin {
this._rfbConnectionState === "connected") { this._rfbConnectionState === "connected") {
if (viewOnly) { if (viewOnly) {
this._keyboard.ungrab(); this._keyboard.ungrab();
this._mouse.ungrab();
} else { } else {
this._keyboard.grab(); this._keyboard.grab();
this._mouse.grab();
} }
} }
} }
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 +520,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 +533,44 @@ 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);
// Mouse events
this._canvas.addEventListener('mousedown', this._eventHandlers.handleMouse);
this._canvas.addEventListener('mouseup', this._eventHandlers.handleMouse);
this._canvas.addEventListener('mousemove', this._eventHandlers.handleMouse);
// Prevent middle-click pasting (see handler for why we bind to document)
this._canvas.addEventListener('click', this._eventHandlers.handleMouse);
// preventDefault() on mousedown doesn't stop this event for some
// reason so we have to explicitly block it
this._canvas.addEventListener('contextmenu', this._eventHandlers.handleMouse);
// Wheel events
this._canvas.addEventListener("wheel", this._eventHandlers.handleWheel);
// 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("wheel", this._eventHandlers.handleWheel);
this._canvas.removeEventListener('mousedown', this._eventHandlers.handleMouse);
this._canvas.removeEventListener('mouseup', this._eventHandlers.handleMouse);
this._canvas.removeEventListener('mousemove', this._eventHandlers.handleMouse);
this._canvas.removeEventListener('click', this._eventHandlers.handleMouse);
this._canvas.removeEventListener('contextmenu', this._eventHandlers.handleMouse);
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._gestures.detach();
this._sock.close(); this._sock.close();
try { try {
this._target.removeChild(this._screen); this._target.removeChild(this._screen);
@ -819,6 +867,51 @@ export default class RFB extends EventTargetMixin {
this.sendKey(keysym, code, down); this.sendKey(keysym, code, down);
} }
_handleMouse(ev) {
/*
* We don't check connection status or viewOnly here as the
* mouse events might be used to control the viewport
*/
if (ev.type === 'click') {
/*
* Note: This is only needed for the 'click' event as it fails
* to fire properly for the target element so we have
* to listen on the document element instead.
*/
if (ev.target !== this._canvas) {
return;
}
}
// FIXME: if we're in view-only and not dragging,
// should we stop events?
ev.stopPropagation();
ev.preventDefault();
if ((ev.type === 'click') || (ev.type === 'contextmenu')) {
return;
}
let pos = clientToElement(ev.clientX, ev.clientY,
this._canvas);
switch (ev.type) {
case 'mousedown':
setCapture(this._canvas);
this._handleMouseButton(pos.x, pos.y,
true, 1 << ev.button);
break;
case 'mouseup':
this._handleMouseButton(pos.x, pos.y,
false, 1 << ev.button);
break;
case 'mousemove':
this._handleMouseMove(pos.x, pos.y);
break;
}
}
_handleMouseButton(x, y, down, bmask) { _handleMouseButton(x, y, down, bmask) {
if (this.dragViewport) { if (this.dragViewport) {
if (down && !this._viewportDragging) { if (down && !this._viewportDragging) {
@ -910,6 +1003,217 @@ export default class RFB extends EventTargetMixin {
this._display.absY(y), mask); this._display.absY(y), mask);
} }
_handleWheel(ev) {
if (this._rfbConnectionState !== 'connected') { return; }
if (this._viewOnly) { return; } // View only, skip mouse events
ev.stopPropagation();
ev.preventDefault();
let pos = clientToElement(ev.clientX, ev.clientY,
this._canvas);
let dX = ev.deltaX;
let dY = ev.deltaY;
// Pixel units unless it's non-zero.
// Note that if deltamode is line or page won't matter since we aren't
// sending the mouse wheel delta to the server anyway.
// The difference between pixel and line can be important however since
// we have a threshold that can be smaller than the line height.
if (ev.deltaMode !== 0) {
dX *= WHEEL_LINE_HEIGHT;
dY *= WHEEL_LINE_HEIGHT;
}
// Mouse wheel events are sent in steps over VNC. This means that the VNC
// protocol can't handle a wheel event with specific distance or speed.
// Therefor, if we get a lot of small mouse wheel events we combine them.
this._accumulatedWheelDeltaX += dX;
this._accumulatedWheelDeltaY += dY;
// Generate a mouse wheel step event when the accumulated delta
// for one of the axes is large enough.
if (Math.abs(this._accumulatedWheelDeltaX) >= WHEEL_STEP) {
if (this._accumulatedWheelDeltaX < 0) {
this._handleMouseButton(pos.x, pos.y, true, 1 << 5);
this._handleMouseButton(pos.x, pos.y, false, 1 << 5);
} else if (this._accumulatedWheelDeltaX > 0) {
this._handleMouseButton(pos.x, pos.y, true, 1 << 6);
this._handleMouseButton(pos.x, pos.y, false, 1 << 6);
}
this._accumulatedWheelDeltaX = 0;
}
if (Math.abs(this._accumulatedWheelDeltaY) >= WHEEL_STEP) {
if (this._accumulatedWheelDeltaY < 0) {
this._handleMouseButton(pos.x, pos.y, true, 1 << 3);
this._handleMouseButton(pos.x, pos.y, false, 1 << 3);
} else if (this._accumulatedWheelDeltaY > 0) {
this._handleMouseButton(pos.x, pos.y, true, 1 << 4);
this._handleMouseButton(pos.x, pos.y, false, 1 << 4);
}
this._accumulatedWheelDeltaY = 0;
}
}
_fakeMouseMove(ev, elementX, elementY) {
this._handleMouseMove(elementX, elementY);
this._cursor.move(ev.detail.clientX, ev.detail.clientY);
}
_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._fakeMouseMove(this._gestureFirstDoubleTapEv, 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._fakeMouseMove(ev, pos.x, pos.y);
this._handleMouseButton(pos.x, pos.y, true, 0x1);
break;
case 'longpress':
this._fakeMouseMove(ev, 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._fakeMouseMove(ev, pos.x, pos.y);
break;
case 'pinch':
this._gestureLastMagnitudeX = Math.hypot(ev.detail.magnitudeX,
ev.detail.magnitudeY);
this._fakeMouseMove(ev, pos.x, pos.y);
break;
}
break;
case 'gesturemove':
switch (ev.detail.type) {
case 'onetap':
case 'twotap':
case 'threetap':
break;
case 'drag':
case 'longpress':
this._fakeMouseMove(ev, 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._fakeMouseMove(ev, 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._fakeMouseMove(ev, 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;
}
break;
case 'gestureend':
switch (ev.detail.type) {
case 'onetap':
case 'twotap':
case 'threetap':
case 'pinch':
case 'twodrag':
break;
case 'drag':
this._fakeMouseMove(ev, pos.x, pos.y);
this._handleMouseButton(pos.x, pos.y, false, 0x1);
break;
case 'longpress':
this._fakeMouseMove(ev, pos.x, pos.y);
this._handleMouseButton(pos.x, pos.y, false, 0x4);
break;
}
break;
}
}
// Message Handlers // Message Handlers
_negotiateProtocolVersion() { _negotiateProtocolVersion() {
@ -1433,7 +1737,6 @@ export default class RFB extends EventTargetMixin {
this._resize(width, height); this._resize(width, height);
if (!this._viewOnly) { this._keyboard.grab(); } if (!this._viewOnly) { this._keyboard.grab(); }
if (!this._viewOnly) { this._mouse.grab(); }
this._fbDepth = 24; this._fbDepth = 24;

View File

@ -30,9 +30,6 @@ export default class Cursor {
'mouseleave': this._handleMouseLeave.bind(this), 'mouseleave': this._handleMouseLeave.bind(this),
'mousemove': this._handleMouseMove.bind(this), 'mousemove': this._handleMouseMove.bind(this),
'mouseup': this._handleMouseUp.bind(this), 'mouseup': this._handleMouseUp.bind(this),
'touchstart': this._handleTouchStart.bind(this),
'touchmove': this._handleTouchMove.bind(this),
'touchend': this._handleTouchEnd.bind(this),
}; };
} }
@ -54,11 +51,6 @@ export default class Cursor {
this._target.addEventListener('mouseleave', this._eventHandlers.mouseleave, options); this._target.addEventListener('mouseleave', this._eventHandlers.mouseleave, options);
this._target.addEventListener('mousemove', this._eventHandlers.mousemove, options); this._target.addEventListener('mousemove', this._eventHandlers.mousemove, options);
this._target.addEventListener('mouseup', this._eventHandlers.mouseup, options); this._target.addEventListener('mouseup', this._eventHandlers.mouseup, options);
// There is no "touchleave" so we monitor touchstart globally
window.addEventListener('touchstart', this._eventHandlers.touchstart, options);
this._target.addEventListener('touchmove', this._eventHandlers.touchmove, options);
this._target.addEventListener('touchend', this._eventHandlers.touchend, options);
} }
this.clear(); this.clear();
@ -76,10 +68,6 @@ export default class Cursor {
this._target.removeEventListener('mousemove', this._eventHandlers.mousemove, options); this._target.removeEventListener('mousemove', this._eventHandlers.mousemove, options);
this._target.removeEventListener('mouseup', this._eventHandlers.mouseup, options); this._target.removeEventListener('mouseup', this._eventHandlers.mouseup, options);
window.removeEventListener('touchstart', this._eventHandlers.touchstart, options);
this._target.removeEventListener('touchmove', this._eventHandlers.touchmove, options);
this._target.removeEventListener('touchend', this._eventHandlers.touchend, options);
document.body.removeChild(this._canvas); document.body.removeChild(this._canvas);
} }
@ -131,6 +119,19 @@ export default class Cursor {
this._hotSpot.y = 0; this._hotSpot.y = 0;
} }
// Mouse events might be emulated, this allows
// moving the cursor in such cases
move(clientX, clientY) {
if (!useFallback) {
return;
}
this._position.x = clientX;
this._position.y = clientY;
this._updatePosition();
let target = document.elementFromPoint(clientX, clientY);
this._updateVisibility(target);
}
_handleMouseOver(event) { _handleMouseOver(event) {
// This event could be because we're entering the target, or // This event could be because we're entering the target, or
// moving around amongst its sub elements. Let the move handler // moving around amongst its sub elements. Let the move handler
@ -179,27 +180,6 @@ export default class Cursor {
} }
} }
_handleTouchStart(event) {
// Just as for mouseover, we let the move handler deal with it
this._handleTouchMove(event);
}
_handleTouchMove(event) {
this._updateVisibility(event.target);
this._position.x = event.changedTouches[0].clientX - this._hotSpot.x;
this._position.y = event.changedTouches[0].clientY - this._hotSpot.y;
this._updatePosition();
}
_handleTouchEnd(event) {
// Same principle as for mouseup
let target = document.elementFromPoint(event.changedTouches[0].clientX,
event.changedTouches[0].clientY);
this._updateVisibility(target);
}
_showCursor() { _showCursor() {
if (this._canvas.style.visibility === 'hidden') { if (this._canvas.style.visibility === 'hidden') {
this._canvas.style.visibility = ''; this._canvas.style.visibility = '';

32
core/util/element.js Normal file
View File

@ -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;
}

View File

@ -11,9 +11,6 @@ official external API.
## 1.1 Module List ## 1.1 Module List
* __Mouse__ (core/input/mouse.js): Mouse input event handler with
limited touch support.
* __Keyboard__ (core/input/keyboard.js): Keyboard input event handler with * __Keyboard__ (core/input/keyboard.js): Keyboard input event handler with
non-US keyboard support. Translates keyDown and keyUp events to X11 non-US keyboard support. Translates keyDown and keyUp events to X11
keysym values. keysym values.
@ -35,52 +32,29 @@ callback event name, and the callback function.
## 2. Modules ## 2. Modules
## 2.1 Mouse Module ## 2.1 Keyboard Module
### 2.1.1 Configuration Attributes ### 2.1.1 Configuration Attributes
| name | type | mode | default | description
| ----------- | ---- | ---- | -------- | ------------
| touchButton | int | RW | 1 | Button mask (1, 2, 4) for which click to send on touch devices. 0 means ignore clicks.
### 2.1.2 Methods
| name | parameters | description
| ------ | ---------- | ------------
| grab | () | Begin capturing mouse events
| ungrab | () | Stop capturing mouse events
### 2.1.2 Callbacks
| name | parameters | description
| ------------- | ------------------- | ------------
| onmousebutton | (x, y, down, bmask) | Handler for mouse button click/release
| onmousemove | (x, y) | Handler for mouse movement
## 2.2 Keyboard Module
### 2.2.1 Configuration Attributes
None None
### 2.2.2 Methods ### 2.1.2 Methods
| name | parameters | description | name | parameters | description
| ------ | ---------- | ------------ | ------ | ---------- | ------------
| grab | () | Begin capturing keyboard events | grab | () | Begin capturing keyboard events
| ungrab | () | Stop capturing keyboard events | ungrab | () | Stop capturing keyboard events
### 2.2.3 Callbacks ### 2.1.3 Callbacks
| name | parameters | description | name | parameters | description
| ---------- | -------------------- | ------------ | ---------- | -------------------- | ------------
| onkeypress | (keysym, code, down) | Handler for key press/release | onkeypress | (keysym, code, down) | Handler for key press/release
## 2.3 Display Module ## 2.2 Display Module
### 2.3.1 Configuration Attributes ### 2.2.1 Configuration Attributes
| name | type | mode | default | description | name | type | mode | default | description
| ------------ | ----- | ---- | ------- | ------------ | ------------ | ----- | ---- | ------- | ------------
@ -89,7 +63,7 @@ None
| width | int | RO | | Display area width | width | int | RO | | Display area width
| height | int | RO | | Display area height | height | int | RO | | Display area height
### 2.3.2 Methods ### 2.2.2 Methods
| name | parameters | description | name | parameters | description
| ------------------ | ------------------------------------------------------- | ------------ | ------------------ | ------------------------------------------------------- | ------------
@ -113,7 +87,7 @@ None
| drawImage | (img, x, y) | Draw image and track damage | drawImage | (img, x, y) | Draw image and track damage
| autoscale | (containerWidth, containerHeight) | Scale the display | autoscale | (containerWidth, containerHeight) | Scale the display
### 2.3.3 Callbacks ### 2.2.3 Callbacks
| name | parameters | description | name | parameters | description
| ------- | ---------- | ------------ | ------- | ---------- | ------------

View File

@ -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

1031
tests/test.gesturehandler.js Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,303 +0,0 @@
const expect = chai.expect;
import Mouse from '../core/input/mouse.js';
describe('Mouse Event Handling', function () {
"use strict";
let target;
beforeEach(function () {
// For these tests we can assume that the canvas is 100x100
// located at coordinates 10x10
target = document.createElement('canvas');
target.style.position = "absolute";
target.style.top = "10px";
target.style.left = "10px";
target.style.width = "100px";
target.style.height = "100px";
document.body.appendChild(target);
});
afterEach(function () {
document.body.removeChild(target);
target = null;
});
// The real constructors might not work everywhere we
// want to run these tests
const mouseevent = (typeArg, MouseEventInit) => {
const e = { type: typeArg };
for (let key in MouseEventInit) {
e[key] = MouseEventInit[key];
}
e.stopPropagation = sinon.spy();
e.preventDefault = sinon.spy();
return e;
};
const touchevent = mouseevent;
describe('Decode Mouse Events', function () {
it('should decode mousedown events', function (done) {
const mouse = new Mouse(target);
mouse.onmousebutton = (x, y, down, bmask) => {
expect(bmask).to.be.equal(0x01);
expect(down).to.be.equal(1);
done();
};
mouse._handleMouseDown(mouseevent('mousedown', { button: '0x01' }));
});
it('should decode mouseup events', function (done) {
let calls = 0;
const mouse = new Mouse(target);
mouse.onmousebutton = (x, y, down, bmask) => {
expect(bmask).to.be.equal(0x01);
if (calls++ === 1) {
expect(down).to.not.be.equal(1);
done();
}
};
mouse._handleMouseDown(mouseevent('mousedown', { button: '0x01' }));
mouse._handleMouseUp(mouseevent('mouseup', { button: '0x01' }));
});
it('should decode mousemove events', function (done) {
const mouse = new Mouse(target);
mouse.onmousemove = (x, y) => {
// Note that target relative coordinates are sent
expect(x).to.be.equal(40);
expect(y).to.be.equal(10);
done();
};
mouse._handleMouseMove(mouseevent('mousemove',
{ clientX: 50, clientY: 20 }));
});
it('should decode mousewheel events', function (done) {
let calls = 0;
const mouse = new Mouse(target);
mouse.onmousebutton = (x, y, down, bmask) => {
calls++;
expect(bmask).to.be.equal(1<<6);
if (calls === 1) {
expect(down).to.be.equal(1);
} else if (calls === 2) {
expect(down).to.not.be.equal(1);
done();
}
};
mouse._handleMouseWheel(mouseevent('mousewheel',
{ deltaX: 50, deltaY: 0,
deltaMode: 0}));
});
});
describe('Double-click for Touch', function () {
beforeEach(function () { this.clock = sinon.useFakeTimers(); });
afterEach(function () { this.clock.restore(); });
it('should use same pos for 2nd tap if close enough', function (done) {
let calls = 0;
const mouse = new Mouse(target);
mouse.onmousebutton = (x, y, down, bmask) => {
calls++;
if (calls === 1) {
expect(down).to.be.equal(1);
expect(x).to.be.equal(68);
expect(y).to.be.equal(36);
} else if (calls === 3) {
expect(down).to.be.equal(1);
expect(x).to.be.equal(68);
expect(y).to.be.equal(36);
done();
}
};
// touch events are sent in an array of events
// with one item for each touch point
mouse._handleMouseDown(touchevent(
'touchstart', { touches: [{ clientX: 78, clientY: 46 }]}));
this.clock.tick(10);
mouse._handleMouseUp(touchevent(
'touchend', { touches: [{ clientX: 79, clientY: 45 }]}));
this.clock.tick(200);
mouse._handleMouseDown(touchevent(
'touchstart', { touches: [{ clientX: 67, clientY: 35 }]}));
this.clock.tick(10);
mouse._handleMouseUp(touchevent(
'touchend', { touches: [{ clientX: 66, clientY: 36 }]}));
});
it('should not modify 2nd tap pos if far apart', function (done) {
let calls = 0;
const mouse = new Mouse(target);
mouse.onmousebutton = (x, y, down, bmask) => {
calls++;
if (calls === 1) {
expect(down).to.be.equal(1);
expect(x).to.be.equal(68);
expect(y).to.be.equal(36);
} else if (calls === 3) {
expect(down).to.be.equal(1);
expect(x).to.not.be.equal(68);
expect(y).to.not.be.equal(36);
done();
}
};
mouse._handleMouseDown(touchevent(
'touchstart', { touches: [{ clientX: 78, clientY: 46 }]}));
this.clock.tick(10);
mouse._handleMouseUp(touchevent(
'touchend', { touches: [{ clientX: 79, clientY: 45 }]}));
this.clock.tick(200);
mouse._handleMouseDown(touchevent(
'touchstart', { touches: [{ clientX: 57, clientY: 35 }]}));
this.clock.tick(10);
mouse._handleMouseUp(touchevent(
'touchend', { touches: [{ clientX: 56, clientY: 36 }]}));
});
it('should not modify 2nd tap pos if not soon enough', function (done) {
let calls = 0;
const mouse = new Mouse(target);
mouse.onmousebutton = (x, y, down, bmask) => {
calls++;
if (calls === 1) {
expect(down).to.be.equal(1);
expect(x).to.be.equal(68);
expect(y).to.be.equal(36);
} else if (calls === 3) {
expect(down).to.be.equal(1);
expect(x).to.not.be.equal(68);
expect(y).to.not.be.equal(36);
done();
}
};
mouse._handleMouseDown(touchevent(
'touchstart', { touches: [{ clientX: 78, clientY: 46 }]}));
this.clock.tick(10);
mouse._handleMouseUp(touchevent(
'touchend', { touches: [{ clientX: 79, clientY: 45 }]}));
this.clock.tick(500);
mouse._handleMouseDown(touchevent(
'touchstart', { touches: [{ clientX: 67, clientY: 35 }]}));
this.clock.tick(10);
mouse._handleMouseUp(touchevent(
'touchend', { touches: [{ clientX: 66, clientY: 36 }]}));
});
it('should not modify 2nd tap pos if not touch', function (done) {
let calls = 0;
const mouse = new Mouse(target);
mouse.onmousebutton = (x, y, down, bmask) => {
calls++;
if (calls === 1) {
expect(down).to.be.equal(1);
expect(x).to.be.equal(68);
expect(y).to.be.equal(36);
} else if (calls === 3) {
expect(down).to.be.equal(1);
expect(x).to.not.be.equal(68);
expect(y).to.not.be.equal(36);
done();
}
};
mouse._handleMouseDown(mouseevent(
'mousedown', { button: '0x01', clientX: 78, clientY: 46 }));
this.clock.tick(10);
mouse._handleMouseUp(mouseevent(
'mouseup', { button: '0x01', clientX: 79, clientY: 45 }));
this.clock.tick(200);
mouse._handleMouseDown(mouseevent(
'mousedown', { button: '0x01', clientX: 67, clientY: 35 }));
this.clock.tick(10);
mouse._handleMouseUp(mouseevent(
'mouseup', { button: '0x01', clientX: 66, clientY: 36 }));
});
});
describe('Accumulate mouse wheel events with small delta', function () {
beforeEach(function () { this.clock = sinon.useFakeTimers(); });
afterEach(function () { this.clock.restore(); });
it('should accumulate wheel events if small enough', function () {
const mouse = new Mouse(target);
mouse.onmousebutton = sinon.spy();
mouse._handleMouseWheel(mouseevent(
'mousewheel', { clientX: 18, clientY: 40,
deltaX: 4, deltaY: 0, deltaMode: 0 }));
this.clock.tick(10);
mouse._handleMouseWheel(mouseevent(
'mousewheel', { clientX: 18, clientY: 40,
deltaX: 4, deltaY: 0, deltaMode: 0 }));
// threshold is 10
expect(mouse._accumulatedWheelDeltaX).to.be.equal(8);
this.clock.tick(10);
mouse._handleMouseWheel(mouseevent(
'mousewheel', { clientX: 18, clientY: 40,
deltaX: 4, deltaY: 0, deltaMode: 0 }));
expect(mouse.onmousebutton).to.have.callCount(2); // mouse down and up
this.clock.tick(10);
mouse._handleMouseWheel(mouseevent(
'mousewheel', { clientX: 18, clientY: 40,
deltaX: 4, deltaY: 9, deltaMode: 0 }));
expect(mouse._accumulatedWheelDeltaX).to.be.equal(4);
expect(mouse._accumulatedWheelDeltaY).to.be.equal(9);
expect(mouse.onmousebutton).to.have.callCount(2); // still
});
it('should not accumulate large wheel events', function () {
const mouse = new Mouse(target);
mouse.onmousebutton = sinon.spy();
mouse._handleMouseWheel(mouseevent(
'mousewheel', { clientX: 18, clientY: 40,
deltaX: 11, deltaY: 0, deltaMode: 0 }));
this.clock.tick(10);
mouse._handleMouseWheel(mouseevent(
'mousewheel', { clientX: 18, clientY: 40,
deltaX: 0, deltaY: 70, deltaMode: 0 }));
this.clock.tick(10);
mouse._handleMouseWheel(mouseevent(
'mousewheel', { clientX: 18, clientY: 40,
deltaX: 400, deltaY: 400, deltaMode: 0 }));
expect(mouse.onmousebutton).to.have.callCount(8); // mouse down and up
});
it('should send even small wheel events after a timeout', function () {
const mouse = new Mouse(target);
mouse.onmousebutton = sinon.spy();
mouse._handleMouseWheel(mouseevent(
'mousewheel', { clientX: 18, clientY: 40,
deltaX: 1, deltaY: 0, deltaMode: 0 }));
this.clock.tick(51); // timeout on 50 ms
expect(mouse.onmousebutton).to.have.callCount(2); // mouse down and up
});
it('should account for non-zero deltaMode', function () {
const mouse = new Mouse(target);
mouse.onmousebutton = sinon.spy();
mouse._handleMouseWheel(mouseevent(
'mousewheel', { clientX: 18, clientY: 40,
deltaX: 0, deltaY: 2, deltaMode: 1 }));
this.clock.tick(10);
mouse._handleMouseWheel(mouseevent(
'mousewheel', { clientX: 18, clientY: 40,
deltaX: 1, deltaY: 0, deltaMode: 2 }));
expect(mouse.onmousebutton).to.have.callCount(4); // mouse down and up
});
});
});

File diff suppressed because it is too large Load Diff

View File

@ -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>