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 }],
"indent": ["error", 4, { "SwitchCase": 1,
"FunctionDeclaration": { "parameters": "first" },
"CallExpression": { "arguments": "first" },
"ArrayExpression": "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() {
document.getElementById("noVNC_mouse_button0")
.addEventListener('click', () => UI.setMouseButton(1));
document.getElementById("noVNC_mouse_button1")
.addEventListener('click', () => UI.setMouseButton(2));
document.getElementById("noVNC_mouse_button2")
.addEventListener('click', () => UI.setMouseButton(4));
document.getElementById("noVNC_mouse_button4")
.addEventListener('click', () => UI.setMouseButton(0));
document.getElementById("noVNC_keyboard_button")
.addEventListener('click', UI.toggleVirtualKeyboard);
@ -430,7 +422,6 @@ const UI = {
UI.disableSetting('port');
UI.disableSetting('path');
UI.disableSetting('repeaterID');
UI.setMouseButton(1);
// Hide the controlbar after 2 seconds
UI.closeControlbarTimeout = setTimeout(UI.closeControlbar, 2000);
@ -1633,24 +1624,6 @@ const UI = {
* MISC
* ------v------*/
setMouseButton(num) {
const viewOnly = UI.rfb.viewOnly;
if (UI.rfb && !viewOnly) {
UI.rfb.touchButton = num;
}
const blist = [0, 1, 2, 4];
for (let b = 0; b < blist.length; b++) {
const button = document.getElementById('noVNC_mouse_button' +
blist[b]);
if (blist[b] === num && !viewOnly) {
button.classList.remove("noVNC_hidden");
} else {
button.classList.add("noVNC_hidden");
}
}
},
updateViewOnly() {
if (!UI.rfb) return;
UI.rfb.viewOnly = UI.getSetting('view_only');
@ -1661,8 +1634,6 @@ const UI = {
.classList.add('noVNC_hidden');
document.getElementById('noVNC_toggle_extra_keys_button')
.classList.add('noVNC_hidden');
document.getElementById('noVNC_mouse_button' + UI.rfb.touchButton)
.classList.add('noVNC_hidden');
document.getElementById('noVNC_clipboard_button')
.classList.add('noVNC_hidden');
} else {
@ -1670,8 +1641,6 @@ const UI = {
.classList.remove('noVNC_hidden');
document.getElementById('noVNC_toggle_extra_keys_button')
.classList.remove('noVNC_hidden');
document.getElementById('noVNC_mouse_button' + UI.rfb.touchButton)
.classList.remove('noVNC_hidden');
document.getElementById('noVNC_clipboard_button')
.classList.remove('noVNC_hidden');
}

View File

@ -9,6 +9,7 @@
import * as Log from './util/logging.js';
import Base64 from "./base64.js";
import { supportsImageMetadata } from './util/browser.js';
import { toSigned32bit } from './util/int.js';
export default class Display {
constructor(target) {
@ -190,14 +191,14 @@ export default class Display {
if (this._scale === 0) {
return 0;
}
return x / this._scale + this._viewportLoc.x;
return toSigned32bit(x / this._scale + this._viewportLoc.x);
}
absY(y) {
if (this._scale === 0) {
return 0;
}
return y / this._scale + this._viewportLoc.y;
return toSigned32bit(y / this._scale + this._viewportLoc.y);
}
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 { encodeUTF8, decodeUTF8 } from './util/strings.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 Display from "./display.js";
import Inflator from "./inflator.js";
import Deflator from "./deflator.js";
import Keyboard from "./input/keyboard.js";
import Mouse from "./input/mouse.js";
import GestureHandler from "./input/gesturehandler.js";
import Cursor from "./util/cursor.js";
import Websock from "./websock.js";
import DES from "./des.js";
@ -39,6 +41,16 @@ const DEFAULT_BACKGROUND = 'rgb(40, 40, 40)';
// Minimum wait (ms) between two mouse moves
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
const extendedClipboardFormatText = 1;
/*eslint-disable no-unused-vars */
@ -117,7 +129,7 @@ export default class RFB extends EventTargetMixin {
this._display = null; // Display object
this._flushing = false; // Display flushing state
this._keyboard = null; // Keyboard input handler object
this._mouse = null; // Mouse input handler object
this._gestures = null; // Gesture input handler object
// Timers
this._disconnTimer = null; // disconnection timer
@ -143,11 +155,22 @@ export default class RFB extends EventTargetMixin {
this._viewportDragging = false;
this._viewportDragPos = {};
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
this._eventHandlers = {
focusCanvas: this._focusCanvas.bind(this),
windowResize: this._windowResize.bind(this),
handleMouse: this._handleMouse.bind(this),
handleWheel: this._handleWheel.bind(this),
handleGesture: this._handleGesture.bind(this),
};
// main setup
@ -206,9 +229,7 @@ export default class RFB extends EventTargetMixin {
this._keyboard = new Keyboard(this._canvas);
this._keyboard.onkeyevent = this._handleKeyEvent.bind(this);
this._mouse = new Mouse(this._canvas);
this._mouse.onmousebutton = this._handleMouseButton.bind(this);
this._mouse.onmousemove = this._handleMouseMove.bind(this);
this._gestures = new GestureHandler();
this._sock = new Websock();
this._sock.on('message', () => {
@ -296,18 +317,16 @@ export default class RFB extends EventTargetMixin {
this._rfbConnectionState === "connected") {
if (viewOnly) {
this._keyboard.ungrab();
this._mouse.ungrab();
} else {
this._keyboard.grab();
this._mouse.grab();
}
}
}
get capabilities() { return this._capabilities; }
get touchButton() { return this._mouse.touchButton; }
set touchButton(button) { this._mouse.touchButton = button; }
get touchButton() { return 0; }
set touchButton(button) { Log.Warn("Using old API!"); }
get clipViewport() { return this._clipViewport; }
set clipViewport(viewport) {
@ -501,6 +520,8 @@ export default class RFB extends EventTargetMixin {
// Make our elements part of the page
this._target.appendChild(this._screen);
this._gestures.attach(this._canvas);
this._cursor.attach(this._canvas);
this._refreshCursor();
@ -512,17 +533,44 @@ export default class RFB extends EventTargetMixin {
this._canvas.addEventListener("mousedown", 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");
}
_disconnect() {
Log.Debug(">> RFB.disconnect");
this._cursor.detach();
this._canvas.removeEventListener("gesturestart", this._eventHandlers.handleGesture);
this._canvas.removeEventListener("gesturemove", this._eventHandlers.handleGesture);
this._canvas.removeEventListener("gestureend", this._eventHandlers.handleGesture);
this._canvas.removeEventListener("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("touchstart", this._eventHandlers.focusCanvas);
window.removeEventListener('resize', this._eventHandlers.windowResize);
this._keyboard.ungrab();
this._mouse.ungrab();
this._gestures.detach();
this._sock.close();
try {
this._target.removeChild(this._screen);
@ -819,6 +867,51 @@ export default class RFB extends EventTargetMixin {
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) {
if (this.dragViewport) {
if (down && !this._viewportDragging) {
@ -910,6 +1003,217 @@ export default class RFB extends EventTargetMixin {
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
_negotiateProtocolVersion() {
@ -1433,7 +1737,6 @@ export default class RFB extends EventTargetMixin {
this._resize(width, height);
if (!this._viewOnly) { this._keyboard.grab(); }
if (!this._viewOnly) { this._mouse.grab(); }
this._fbDepth = 24;

View File

@ -30,9 +30,6 @@ export default class Cursor {
'mouseleave': this._handleMouseLeave.bind(this),
'mousemove': this._handleMouseMove.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('mousemove', this._eventHandlers.mousemove, 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();
@ -76,10 +68,6 @@ export default class Cursor {
this._target.removeEventListener('mousemove', this._eventHandlers.mousemove, 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);
}
@ -131,6 +119,19 @@ export default class Cursor {
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) {
// This event could be because we're entering the target, or
// 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() {
if (this._canvas.style.visibility === 'hidden') {
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
* __Mouse__ (core/input/mouse.js): Mouse input event handler with
limited touch support.
* __Keyboard__ (core/input/keyboard.js): Keyboard input event handler with
non-US keyboard support. Translates keyDown and keyUp events to X11
keysym values.
@ -35,52 +32,29 @@ callback event name, and the callback function.
## 2. Modules
## 2.1 Mouse Module
## 2.1 Keyboard Module
### 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
### 2.2.2 Methods
### 2.1.2 Methods
| name | parameters | description
| ------ | ---------- | ------------
| grab | () | Begin capturing keyboard events
| ungrab | () | Stop capturing keyboard events
### 2.2.3 Callbacks
### 2.1.3 Callbacks
| name | parameters | description
| ---------- | -------------------- | ------------
| 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
| ------------ | ----- | ---- | ------- | ------------
@ -89,7 +63,7 @@ None
| width | int | RO | | Display area width
| height | int | RO | | Display area height
### 2.3.2 Methods
### 2.2.2 Methods
| name | parameters | description
| ------------------ | ------------------------------------------------------- | ------------
@ -113,7 +87,7 @@ None
| drawImage | (img, x, y) | Draw image and track damage
| autoscale | (containerWidth, containerHeight) | Scale the display
### 2.3.3 Callbacks
### 2.2.3 Callbacks
| name | parameters | description
| ------- | ---------- | ------------

View File

@ -26,12 +26,6 @@ protocol stream.
moved to the remote session when a `mousedown` or `touchstart`
event is received. Enabled by default.
`touchButton`
- Is a `long` controlling the button mask that should be simulated
when a touch event is recieved. Uses the same values as
[`MouseEvent.button`](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button).
Is set to `1` by default.
`clipViewport`
- Is a `boolean` indicating if the remote session should be clipped
to its container. When disabled scrollbars will be shown to handle

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-->
<div id="noVNC_mobile_buttons">
<input type="image" alt="No mousebutton" src="app/images/mouse_none.svg"
id="noVNC_mouse_button0" class="noVNC_button"
title="Active Mouse Button">
<input type="image" alt="Left mousebutton" src="app/images/mouse_left.svg"
id="noVNC_mouse_button1" class="noVNC_button"
title="Active Mouse Button">
<input type="image" alt="Middle mousebutton" src="app/images/mouse_middle.svg"
id="noVNC_mouse_button2" class="noVNC_button"
title="Active Mouse Button">
<input type="image" alt="Right mousebutton" src="app/images/mouse_right.svg"
id="noVNC_mouse_button4" class="noVNC_button"
title="Active Mouse Button">
<input type="image" alt="Keyboard" src="app/images/keyboard.svg"
id="noVNC_keyboard_button" class="noVNC_button" title="Show Keyboard">
</div>