294 Commits

Author SHA1 Message Date
Samuel Mannehed 0f4a06ffcd noVNC 1.3.0 beta 2021-09-08 16:06:42 +02:00
Samuel Mannehed f69d55c02f Fix parsing of query string variables
This space that was added here was added to the parsed value of the
query variable. This broke any comparisons with the value, for example
"myvar=true" resulted in a value of "true ".

This was broken by f796b05e42

The commit also adds unit tests for webutil.getConfigVar() that will
detect problems like this in the future.
2021-09-08 15:37:42 +02:00
Samuel Mannehed 7841037618 Merge pull request #1365 from baleeds/feature/detect-parent-resize
feature: Detect parent resize
2021-09-03 17:51:18 +02:00
Samuel Mannehed 1afa18f09e Increase browser version requirements
Now that we use ResizeObserver we know that we require more modern
browsers. The most notable ones here are Firefox and Safari.

With regards to Firefox, while the desktop version has had support
since 69, the Android app requires 79. At the time of writing the
current ESR of Firefox is 78, but the concept of ESR doesn't seem to
exist for Android.

The Safari 13 requirement means we no longer support for example iPhone
5S or the 4th generation of the iPad. These are devices from 2013~2014.
2021-09-03 16:52:20 +02:00
Samuel Mannehed 375f36c575 Modify unit tests to work with ResizeObserver 2021-09-03 16:52:20 +02:00
Benjamin Leeds a9c2ff30b6 Replace window.onresize with ResizeObserver
Fixes an issue where if the screen div resizes for a reason other than
window resize, the canvas wouldn't redraw.
2021-09-01 23:45:55 +02:00
Samuel Mannehed fcb95821b7 Merge pull request #1573 from yatru/security-privacy-url-patch
Security privacy to url parameters
2021-09-01 16:00:19 +02:00
yatru f796b05e42 Add support for URL fragment parameters
Passing parameters as part of the fragment could be considered
benifical from a security or privacy standpoint when compared to query
string parameters. The URL fragment parameters are not sent to the
server.
2021-09-01 14:49:37 +02:00
Samuel Mannehed 0a8ced2cfe Update Swedish translation 2021-08-27 16:12:02 +02:00
Samuel Mannehed 7a76fbb767 Remove duplicate translation string from es.po 2021-08-27 16:04:27 +02:00
Samuel Mannehed cb56f35fab Update the translation template file for v1.3.0 2021-08-27 16:04:27 +02:00
Pierre Ossman 7485e82b72 Update playback test to use new API
Hooking in to the underlying WebSocket after it has been created no
longer works, so clean things up and use the new method of passing an
existing object to the RFB constructor.
2021-07-22 16:56:49 +02:00
Samuel Mannehed d44ddbe186 Merge pull request #1449 from JanZerebecki/manpage
add Man page for launch.sh and rename to novnc_proxy
2021-06-30 16:04:01 +02:00
Jan Zerebecki 89e206c146 add Man page and rename launch.sh to novnc_proxy.
Co-Authored-By: Adam Young <ayoung@redhat.com>
2021-06-25 13:54:04 +02:00
Jose (Ito) Matsuda dd20b17d49 feat: add French localization strings 2021-05-06 22:35:18 -04:00
Samuel Mannehed 8ab18125f9 Merge pull request #1545 from ascillato/patch-2
Update Spanish Translation
2021-04-25 01:20:02 +02:00
Adrian Scillato e283f08d04 Update Spanish Translation
Update Spanish Translation.

Added missing translations. Fixed some typos.
Also, some changes were made to use a more formal and international spanish.
2021-04-23 12:10:53 -03:00
Pierre Ossman dbd519558c Initiate connection from RFB constructor
We need to do this in order to safely attach to existing WebSocket
objects. There may be message events already pending so we must set up
our event handlers before returning.

This means we will now throw errors instead of generating "disconnect"
events on problems as the caller no longer has the opportunity to set up
event handlers.

This might have been the correct approach from the start as it mimics
how e.g. the WebSocket constructor works.
2021-04-18 14:27:57 +02:00
Pierre Ossman de9fc9508c Don't fake open events in Websock
We don't know if the caller is prepared to receive those events right
now as normally they would get them on a fresh new stack later. We also
can't delay delivery since then we might deliver the event after any
pending "message" events.

Better to push the problem one layer up to the caller, which now needs
to be more aware of the state of the WebSocket object it is trying to
use.
2021-04-18 14:26:30 +02:00
Pierre Ossman 9376191fc4 Refuse to use already closed WebSocket objects
We can't do anything useful with them anyway.
2021-04-18 14:26:28 +02:00
Pierre Ossman b7b7e4e26b Provide readyState for Websock objects
It mainly reports the state of the underlying object in consistent
manner.
2021-04-18 14:26:05 +02:00
Pierre Ossman 2244f53774 Move Websock event handlers to own methods
Avoid cluttering up the RFB constructor.
2021-04-18 14:26:02 +02:00
Pierre Ossman 42100e8233 Add unit tests for connect/attach errors 2021-04-18 14:25:59 +02:00
Pierre Ossman ae3c01f782 Add unit tests for passing WebSocket objects 2021-04-18 14:25:03 +02:00
Pierre Ossman f0e4908dec Stop explicitly testing connection states
These are internal and we should be testing the externally observable
behaviour.
2021-04-18 14:24:05 +02:00
Pierre Ossman ae5f3f6909 Revert "Fixed a race condition when attaching to an existing socket"
This reverts commit ef27628c6d. By
bypassing setTimeout() it creates other race conditions so this is not
the proper fix for the issue.
2021-04-16 13:28:47 +02:00
Samuel Mannehed 84f102d6a9 Merge pull request #1537 from TimSBSquare/bugfix/existing-channel-race-condition
Fixed a race condition when attaching to an existing socket
2021-03-30 15:59:37 +02:00
Tim Stableford ef27628c6d Fixed a race condition when attaching to an existing socket
This is an error that presents itself with RTCDataChannel's, I suspect this could not
happen with a pre-existing WebSocket.

If the remote connection creates a data channel then the local (VNC) side gets a channel
created callback. It may also be the case that in that very same tick the socket is also
opened and buffered data received. This meant that (in my tests) about 1/3 of the time
noVNC would fail to respond to the initial message from the server because it was received
and subsequently not handled during that initial tick.

Also made the documentation reflect this new behaviour and document the existing behaviour.
2021-03-30 14:32:04 +01:00
Liddack 89f9ac0016 Add Portuguese (Brazil) translation 2021-03-16 13:30:23 +01:00
lhchavez f9a8c4ccd5 Add VeNCrypt Plain authentication tests
This change adds tests for the VeNCrypt Plain authentication. In doing
that, this also fixes a typo that was introduced in a recent change.
2021-03-13 05:40:38 -08:00
Pierre Ossman 9ca337d3a8 Merge branch 'homogenize-credentials-testing' of https://github.com/lhchavez/noVNC 2021-03-11 16:47:35 +01:00
Pierre Ossman 4c96d4b7bd Merge branch 'feature/support-existing-rtcdatachannel-or-websocket-squashed' of https://github.com/TimSBSquare/noVNC 2021-03-11 16:30:22 +01:00
lhchavez 0c55c64757 Normalize the credentials presence check
Most places that check for the presence / absence of credentials compare
them against `undefined`, except the one for Plain authentication.

This change makes the very last place to use the same pattern (instead
of checking for falsiness) for consistency. Additionally, there are ways
to configure PAM to accept empty passwords, so it's possible for a user
to legitimately send an empty string as password.
2021-03-11 06:42:11 -08:00
Tim Stableford 44d384b99c Added support for RTCDataChannel
This work is originally by Ryan Castner <castner.rr@gmail.com> and
submitted as a PR here https://github.com/novnc/noVNC/pull/1362

Architecturally it is much the same except it doesn't rename a lot
of variables to make this more reviewable. It also avoids unrelated
changes such as replacing .onclose with an event listener, which
caused numerous test failures.

It also adds in ppoffice's fix to initialise the buffers.

Like the original author I don't have enough time available to
refactor this project to the new style event listeners.

Review cleanup for RTCDataChannel support (see below)

* More descriptive error when url or channel not set.
* Moved websocket property check to WebSock.
  This had unintended consequences in the tests that required some
  fixup. Mostly due to some tests not always passing FakeWebsocket.
  FakeWebsocket also needs to set the listeners to null to be compatible
  with what is in thw browser and expected by the property check code.
  The property check code now also takes into account class prototypes
  for test compatibility.
* Removed unreachable code.
* Reverted comment.
* Cleanup raw channel reference in rfb on websock close.
* Use readyState to check whether a socket is open rather than assuming.
* Updated RFB constructor documentation

Removed an unused boolean passed to attach
2021-03-04 18:55:06 +00:00
lhchavez 18593154d3 Allow longer passwords in Plain authentication
Some people have longer passwords than 256 characters (hooray for
password managers!). Server implementations also allow longer passwords:
TigerVNC allows up to 1024 characters.
2021-03-03 17:34:02 -08:00
Pierre Ossman 5a0cceb815 Change phrasing for discussion group link
It's listed with the issue templates, so make sure it follows the same
style in phrasing.
2021-01-15 16:16:38 +01:00
Pierre Ossman d56e042fee Add link to discussion group from issues
So that people can easily find it and not file bug reports for
things that are just questions.
2021-01-15 16:14:40 +01:00
Pierre Ossman babd665c03 Hide link to create blank issues
We want users to use the templates so we don't miss any relevant
information.
2021-01-15 16:08:13 +01:00
NNN1590 199910e63b Update Japanese translation 2021-01-15 14:04:49 +09:00
Pierre Ossman 4a319c414d Merge branch 'more_noie' of https://github.com/CendioOssman/noVNC 2020-12-30 16:13:52 +01:00
Pierre Ossman adfb99e7ec Update conversion documentation for Node.js
We now only support conversion to CommonJS, in order to support Node.js
older than version 15.
2020-12-30 15:57:02 +01:00
Pierre Ossman 32222304f4 Remove documentation about converting the app
This is no longer possible as we now require browser support for
modules.
2020-12-30 15:56:42 +01:00
Pierre Ossman 4a8efa6bc9 Use snap actions instead of the broken container
The container didn't work properly for our base snap anyway.
2020-12-30 15:46:03 +01:00
Pierre Ossman cd9f535eb3 Package files directly in snapcraft.yaml
We don't need to convert things anymore, so reference files directly in
the snap yaml file.
2020-12-30 15:46:03 +01:00
Pierre Ossman 23249c7263 Store result from NPM and snap builds
To ease debugging.
2020-12-30 15:46:03 +01:00
Pierre Ossman 76aa3d1256 Run NPM and snap builds on every push
To make sure these things still build. That means we need to make the
actual deploy parts optional.
2020-12-30 13:40:03 +01:00
Quentin Dreyer 6784bb312f chore: sync with W3C documentation
https://www.w3.org/TR/uievents-key/
2020-12-22 10:50:23 +01:00
Pierre Ossman 67ac9f9c0d Merge branch 'jp' of https://github.com/CendioOssman/noVNC 2020-12-10 10:23:10 +01:00
Pierre Ossman 4ae9d3e75a Remove some unnecessary use of done argument 2020-12-10 10:21:21 +01:00
Pierre Ossman 0cdf2962c0 Fake key releases for some Japanese IM keys
Windows behaves very oddly for some Japanese IM keys in that it won't
send a key release event when the key is released. In some keys it never
sends the event, and in some cases it sends the release as the key is
pressed the subsequent time.
2020-12-10 10:21:21 +01:00
Pierre Ossman 146258291a Send combination keysyms for some Japanese keys
Windows doesn't give us stable symbols for a bunch of Japanese IM keys,
instead alternating between two symbols. This state is not synchronised
with the IM running on the remote server so to have stable behaviour we
have to collapse these multiple symbols in to a single keysym.
2020-12-10 10:00:44 +01:00
Pierre Ossman 3e55d5d71a Fix typo for ZenkakuHankaku key 2020-12-10 09:43:08 +01:00
Pierre Ossman bd1bb2ed75 Use toggle keysym for Eisu key
This matches how the key behaves on a Linux system.
2020-12-10 09:42:19 +01:00
Pierre Ossman dc9da4a042 Merge branch 'noie' of https://github.com/CendioOssman/noVNC 2020-12-07 14:30:05 +01:00
Pierre Ossman 6a4c411976 Remove createEvent() fallbacks
We can now rely on proper constructors for our events.
2020-12-07 10:10:53 +01:00
Pierre Ossman 27496941a0 Remove createImageData() fallback
All our browsers should be new enough now that we can rely on the
ImageData constructor.
2020-12-07 10:10:53 +01:00
Pierre Ossman 5b5b747494 Remove many small, obsolete, old browser hacks
These are for browsers no longer supported anyway.
2020-12-07 10:10:53 +01:00
Pierre Ossman 6cd9bacf8b Use Fetch API for getting JSON data
We no longer need to support Internet Explorer so we can use a more
proper API here.
2020-12-04 16:43:44 +01:00
Pierre Ossman 273acf3e89 Remove unused injectParamIfMissing()
It should have been removed in 58fc267b2b
with the caller.
2020-12-04 16:43:04 +01:00
Pierre Ossman 60c7518f8c Update keycode mappings to latest version
This update fixes Korean layouts.
2020-11-16 13:44:27 +01:00
Pierre Ossman 90456dbeed Merge branches 'logs' and 'timeout' of https://github.com/jcpunk/noVNC 2020-11-10 13:21:15 +01:00
Pat Riehecky 5dbacc5e41 Make timeouts/heartbeats easy to setup 2020-11-09 13:25:29 -06:00
Pat Riehecky 18256baad0 Permit setting syslog on websockify 2020-10-29 13:47:07 -05:00
Pierre Ossman 1f7e1c7572 Remove keypress handling
We no longer support any browser that requires this legacy handling.
2020-10-15 18:53:51 +02:00
Pierre Ossman dccf6facdc Drop support for legacy Edge 2020-10-15 18:53:51 +02:00
Pierre Ossman 890cff921d Remove legacy conversion of modules
We no longer support Internet Explorer so we can now require that
browsers support modules.

Some conversion to commonjs still remains for nodejs.
2020-10-15 18:53:51 +02:00
Pierre Ossman c01eb5e74d Drop support for Internet Explorer 2020-10-15 18:53:51 +02:00
Pierre Ossman 499eee4d06 Include current websockify in noVNC snap
The one from the system is too old to support current versions of noVNC,
so we need to bundle our own.
2020-10-08 16:55:12 +02:00
Pierre Ossman c1281b136d Add workaround for Firefox PNG rounding bug 2020-09-28 12:24:56 +02:00
Pierre Ossman 20e4f1b3f8 Remove Firefox Alt workaround
The bug got fixed way back in Firefox 63, and it is also misbehaving
with modern Firefox as they no longer consider AltGr an Alt-key.
2020-09-28 11:12:16 +02:00
Pierre Ossman b91b1e8edc Handle empty rects in RAW decoder as well
It was overlooked in the previous commit because we couldn't feed
empty data messages through the test framework.
2020-09-07 12:58:52 +02:00
Samuel Mannehed 3037eb16f7 Ignore recording variables in our linter 2020-09-05 11:08:16 +02:00
Pierre Ossman 3762300399 Approximate comparison of JPEG result
The browsers have various rounding errors so we need to compare the
output data only approximately and not exactly.
2020-09-04 16:48:44 +02:00
Pierre Ossman 113fa27ebc Handle empty rects from the server
These are very pointless for the server to send, but not a violation of
the protocol so we need to be able to handle them. We've seen this
happen in real world scenarios a few times.
2020-09-04 16:16:44 +02:00
Pierre Ossman 0630352e19 Merge branch 'rgbx' of https://github.com/CendioOssman/noVNC 2020-09-04 13:40:23 +02:00
Samuel Mannehed 7ce1b071ec Fix call to _recvMessage()
Missed to rename this one in commit ea858bfa27
2020-09-04 10:40:42 +02:00
Samuel Mannehed ecdd075672 Fix names of recording variables
The name of these variables must match how they were set when the
recording was created.

Reverts part of 95632e413d
2020-09-04 10:40:37 +02:00
Pierre Ossman 9142f8f0f7 noVNC 1.2.0 2020-07-14 10:16:52 +02:00
Pierre Ossman b053b3f86d Update generated JS files for translations 2020-07-14 10:16:40 +02:00
Samuel Mannehed 2334a7a7c3 Update Swedish translation 2020-07-14 10:16:02 +02:00
Samuel Mannehed 4fa8051949 Merge pull request #1435 from nnn1590/fix-japanese-translation
Fix Japanese translation
2020-07-08 23:25:28 +02:00
Pierre Ossman 2654bcd354 Explicitly set NPM registry URL
Otherwise the action doesn't configure npm properly to authenticate and
our publish actions will fail with 404 errors.
2020-07-07 12:49:50 +02:00
Pierre Ossman e17de291b9 noVNC 1.2.0 beta 2020-07-07 10:48:24 +02:00
Pierre Ossman 81898d7cea Remove note about clipboard ISO 8859-1 restriction
We now support full Unicode, provided the server also supports the
proper extension.
2020-07-07 10:38:06 +02:00
Pierre Ossman dcdc17bf24 Update translation template file 2020-07-03 16:11:53 +02:00
Pierre Ossman 72ca470750 Merge branch 'deploy' of https://github.com/CendioOssman/noVNC 2020-07-03 16:06:08 +02:00
Pierre Ossman ef5db94a89 Publish pre-releases to beta channels
Extra important for NPM which doesn't allow replacing a release once it
is published.
2020-07-03 16:00:04 +02:00
Samuel Mannehed 90ead240c7 Mention gestures in README 2020-06-28 23:01:23 +02:00
Pierre Ossman 244c02c5ea Deploy new snap on release 2020-06-26 14:48:32 +02:00
Pierre Ossman fe2ad57077 Move snap specific script to snap directory 2020-06-26 14:46:52 +02:00
Pierre Ossman 9b3cea950a Remove redundant VERSION file
We want to avoid having this in multiple places.
2020-06-26 13:25:25 +02:00
Pierre Ossman 794b06b2bd Don't detach handler if it doesn't exist
If the beforeEach() step has been skipped then there won't be anything
here to detach.
2020-06-25 14:45:58 +02:00
Pierre Ossman 484a9551d1 Handle quick Cursor detach after mouse up
This timer might fire after the Cursor object has detached from a DOM
element, causing crashes. This will likely not happen in real scenarios,
but the tests are quick enough to trigger this.
2020-06-25 14:37:21 +02:00
Pierre Ossman 6c6776a7a0 Update build badges for GitHub actions 2020-06-24 13:02:03 +02:00
Pierre Ossman ffb9dfdc0a Merge branch 'actions' of https://github.com/CendioOssman/noVNC 2020-06-24 12:50:31 +02:00
Pierre Ossman 2835616b75 Use GitHub actions instead of Travis/Sauce Labs
The ability to use Sauce Labs for pull requests has now stopped working,
as Travis warned about several years ago. Instead run our tests directly
on GitHub on their various virtual machines.
2020-06-23 13:56:33 +02:00
Pierre Ossman 643442feac Remove default Karma options
No need to mention things where we already use the default value.
2020-06-23 13:54:46 +02:00
Pierre Ossman 57ba67f306 Alway focus on touchstart
The new gesture detection code will always prevent the default behaviour
of touchstart, so this check no longer works properly. We might want to
add something similar to GestureHandler in the future, but let's wait
and see what use cases are requested.
2020-06-16 15:32:38 +02:00
Pierre Ossman 48f15efa69 Compensate for visual viewport when moving cursor 2020-06-16 14:24:00 +02:00
NNN1590 35de121ac5 Fix Japanese translation 2020-06-15 17:46:28 +09:00
Samuel Mannehed bb09e766ba Merge pull request #1414 from CendioOssman/gesture
Add gesture handling
2020-06-12 16:11:30 +02:00
Samuel Mannehed 32ed7c6724 Fake cursor position when using touch
With the new gestures we will simulate the cursor being in a different
location than any of the touch points. This is a bit too complex for the
Cursor class, so let's just explicitly tell it where we want the cursor
rendered.
2020-06-12 14:36:10 +02:00
Pierre Ossman 50cde2faab Move mouse event handling to RFB class
Move the last remaining bits to the RFB class to keep things simple, as
the Mouse class no longer provides any real value.
2020-06-12 14:36:10 +02:00
Pierre Ossman 88589a44f7 Increase wheel step threshold
The previous value made the detection too sensitive and it was very
difficult to scroll precisely. A value of 50 pixels should give similar
behaviour to systems that don't do fine grained scrolling.
2020-06-12 09:18:46 +02:00
Pierre Ossman f84bc57bda Move wheel event handling to RFB class
The Mouse class does very little now so it mostly just obfuscate things.
Move everything directly in to the RFB class instead.
2020-06-12 09:18:46 +02:00
Pierre Ossman 4a87038080 Remove very legacy mouse event handling
This is only needed on such ancient versions of Internet Explorer that
it wouldn't satisfy our other requirements anyway.
2020-06-12 09:18:46 +02:00
Pierre Ossman 77c32d164d Remove delayed wheel timer
This isn't really expected behaviour from a user, i.e. that an extremely
small wheel movement still gives a large scroll event in the remote application.
2020-06-12 09:18:46 +02:00
Samuel Mannehed 07a69954b1 Add lint rule for function declaration indentation 2020-06-12 09:18:46 +02:00
Pierre Ossman 8be924c9d9 Add touch gestures for mouse emulation
Add several single and multitouch gestures to simulate various mouse
actions that would otherwise be impossible to perform.

This replaces the old system where you could select which mouse button
a single touch would generate.
2020-06-12 09:18:46 +02:00
Pierre Ossman 440ec8a0b6 Start fake test clock at real clock time
Some code relies on the clock having a somewhat sane value, so let's not
start at 0.
2020-06-11 16:50:08 +02:00
Pierre Ossman 97b86abc94 Avoid fractional pixel sizes from Display 2020-06-11 16:50:08 +02:00
Pierre Ossman 0a6aec3578 Avoid printing the Websock buffer in tests
It takes forever and just messes up the output.
2020-06-11 16:50:08 +02:00
Pierre Ossman 6a19390baa Switch to RGBx pixel format
This is what the browser wants so it avoids having to spend time
converting everything. Unfortunately it usually means the server instead
needs to convert it for us, but we assume it has more power than we do.
2020-06-08 07:57:17 +02:00
Pierre Ossman f5b5767c98 Standardise on a single blit function
Keep everything simpler by always blitting in the same pixel format.
It's up to the decoders to convert if they need to.
2020-06-08 07:53:41 +02:00
Pierre Ossman 34f52a8f41 Fix bad BasicCompression check in Tight decoder 2020-06-08 07:53:41 +02:00
Pierre Ossman 18a68dfac1 Test correct handling of alpha
The forth byte of a pixel is undefined in most encodings, so make sure
the decoders don't leak that through as an alpha channel.
2020-06-08 07:53:16 +02:00
Pierre Ossman 15cfa13563 Add tests for the Tight decoders 2020-06-08 07:48:20 +02:00
Pierre Ossman 111225fa41 Split decoder tests to separate files 2020-06-08 07:46:42 +02:00
Pierre Ossman 224f95f997 Move tile handling to Hextile decoder
It is only used there so no need for it to be in the general
Display class.
2020-06-06 13:23:05 +02:00
Pierre Ossman f694c32fd5 Merge branch 'camelcase' of https://github.com/samhed/noVNC 2020-06-05 09:41:25 +02:00
Pierre Ossman 0e37a3f83a Merge branch 'limitmouse' of https://github.com/novnc/noVNC 2020-06-03 13:55:33 +02:00
Samuel Mannehed cfb824ed03 Add camelCase rule to eslint 2020-05-31 23:37:29 +02:00
Samuel Mannehed 756af5b44c Standardize on camelCase in App 2020-05-31 23:37:29 +02:00
Samuel Mannehed f2fbaacc82 Standardize on camelCase in Base64 2020-05-31 23:37:29 +02:00
Samuel Mannehed 164bf50fda Standardize on camelCase in Decoders 2020-05-31 23:37:29 +02:00
Samuel Mannehed a7fe079f81 Standardize on camelCase in Logging 2020-05-31 23:37:29 +02:00
Samuel Mannehed ea858bfa27 Standardize on camelCase in Websock 2020-05-31 23:21:35 +02:00
Samuel Mannehed 5d570207f7 Standardize on camelCase in Display 2020-05-31 23:21:35 +02:00
Samuel Mannehed 95632e413d Standardize on camelCase in tests 2020-05-31 23:21:35 +02:00
Samuel Mannehed 8b0034ee84 Standardize on camelCase in utils 2020-05-31 23:21:35 +02:00
Samuel Mannehed 80187d158c Standardize on camelCase in RFB 2020-05-31 23:21:35 +02:00
Samuel Mannehed dff4fefa3c Remove unused properties and variables
The code that used these were removed in the following commits:

* 9ff86fb718 (RFB._mouse_arr)
* bb6965f2e6 (old_requestAnimationFrame)
* 490d471c53 (Display._c_forceCanvas)
2020-05-31 22:49:41 +02:00
Samuel Mannehed 150596be83 Properly limit mouse moves to once every 17 ms
Previous attempt in c958269 had a number of issues, this is a full
rewrite, complete with improved unit tests.

Fixes github issue #1402
2020-05-31 00:53:15 +02:00
Samuel Mannehed 11a22dbf0c Stop send mouse clicks while dragging in view only 2020-05-31 00:53:15 +02:00
Samuel Mannehed 006743857b Standardize on camelCase for functions in RFB 2020-05-31 00:53:15 +02:00
Samuel Mannehed e7dec5270e Standardize on camelCase for variables in RFB 2020-05-31 00:53:15 +02:00
Samuel Mannehed b5ff33a556 Remove unused mouse_arr variable 2020-05-08 22:30:20 +02:00
Pierre Ossman 42e3b03fa8 Consistently close dialogs on connect/disconnect
This was done a bit arbitrarily before which could easily miss things,
end up in the wrong state and not trigger animations correctly.

This reverts commit c12e5b2b54 and fixes
things in a different way.
2020-05-05 12:43:04 +02:00
Samuel Mannehed 776cda5dc4 Merge pull request #1398 from novnc/compressionlevel
Add ability to set Tight compression level
2020-05-01 20:49:49 +02:00
Samuel Mannehed 479d8cefd1 Add ability to set compression level
Fixes github issue #1382.
2020-05-01 20:47:36 +02:00
Samuel Mannehed a672168d4d Add unit tests for mouse move limit 2020-05-01 20:37:48 +02:00
Samuel Mannehed 0f81407c64 Shorten rows to max 80 chars in mouse.js 2020-05-01 20:37:48 +02:00
Samuel Mannehed f477469fb5 Fix wording in comment 2020-05-01 20:37:48 +02:00
Samuel Mannehed c9582690ac Merge pull request #1352 from uklatt/master
Limit mouse move events to one every 17 mS.
2020-05-01 20:36:18 +02:00
Uwe Klatt 44eb1fe59b Limit mouse move events to one every 17 ms 2020-05-01 20:28:33 +02:00
Samuel Mannehed e7fa686f32 Fix indentation for focus check 2020-05-01 15:34:14 +02:00
Samuel Mannehed 8df281cce6 Don't fade the control bar if it has focus
Fixes github issue #1369
2020-05-01 14:38:09 +02:00
Samuel Mannehed c12e5b2b54 Hide the clipboard when not connected
Fixes github issue #1367.
2020-05-01 13:35:34 +02:00
Samuel Mannehed 302895cdf3 Merge pull request #1396 from iblech/patch-1
Document default setting of `focusOnClick`
2020-04-29 18:58:04 +02:00
Ingo Blechschmidt 27a6978e30 Document default setting of focusOnClick 2020-04-29 09:36:32 +02:00
Filip Stedronsky a1015d8db5 rfb: VeNCrypt Plain SecurityType support
This allows using TigerVNC server with PAM authentication (e.g. agains
LDAP or other extensible authentication mechanisms)

Tested with TigerVNC server (Xvnc -SecurityTypes Plain -PlainUsers '*')

Should not break anything else, this method is tried last when all
other fail.

Tested in Firefox 74 and Chromium 80
2020-04-08 08:58:32 +02:00
Alex Tanskanen a040c402ed Fix focus problem after closing the toolbar
Closing the toolbar would make the focus remain on the toolbar and
not in the session. The only way to switch focus was to click in the
session. This commit will automatically switch back focus to the session
after closing the toolbar.
2020-03-12 13:17:51 +01:00
Pierre Ossman c4633ab333 Set a default value for the quality input 2020-02-28 14:56:57 +01:00
Pierre Ossman 5243cbf611 Add UI for quality setting 2020-02-28 14:54:09 +01:00
Pierre Ossman 71429d45d0 Merge branch 'quality-level' of https://github.com/eDrillingSolutions/noVNC 2020-02-28 13:54:36 +01:00
Andrey Trebler efd1f8a4f2 adds qualityLevel property to RFB class for updating JPEG quality level encoding on the fly 2020-02-28 13:14:19 +01:00
Niko Lehto 9253e178fc Hide clipboard side bar button when view only mode
The clipboard side bar button serves no purpose if user uses 'View Only'
mode, this commit hides this button in those instances.
2020-02-24 08:57:28 +01:00
Alex Tanskanen ceb8ef4ec1 Fix crash with too large clipboard data
If too much text is copied in the session, String.fromCharCode.apply()
would crash in Safari on macOS and Chrome on Linux. This commit fixes
this issue by avoiding apply() altogether. Also added test to cover this
issue.
2020-02-21 09:39:31 +01:00
Pierre Ossman e4e6a9b9b4 Style all input types for consistent UI
At least all that the browsers will let us.
2020-02-18 15:24:51 +01:00
Pierre Ossman 384232fb56 Merge branch 'clipboard_unicode' of https://github.com/CendioNiko/noVNC 2020-02-18 09:46:10 +01:00
Niko Lehto f73fdc3ed3 Add extended clipboard Pseudo-Encoding
Add extended clipboard pseudo-encoding to allow the use of unicode
characters in the clipboard.
2020-02-18 09:32:36 +01:00
Niko Lehto 9a31083a8a Export constants in inflate.js for easier usage 2020-02-17 11:29:41 +01:00
Niko Lehto 13be552d60 Fix bug where inflate would read too much data 2020-02-17 11:29:41 +01:00
Niko Lehto 2cee106eee Split api of inflate
Added ability to read data chunk wise.
2020-02-17 11:29:41 +01:00
Niko Lehto 3cf11004b4 Handle errors from zlib/pako 2020-02-17 11:29:41 +01:00
Niko Lehto f6669ff7b2 Move error handling to Inflate class
Every call wants this check so this should be done inside the class.
2020-02-17 11:29:41 +01:00
Niko Lehto fe5aa6408a Add missing copyright header for Inflator.js 2020-02-17 11:29:41 +01:00
Niko Lehto 183cab0eca Remove unused inflate argument
The value true was an invalid flush argument so it was in practice
unused.
2020-02-17 11:29:40 +01:00
Niko Lehto 9575ded8da Add util for unsigned and signed int. conversion
Will be used in later commit in extended clipboard handling.
2020-02-17 11:29:40 +01:00
Niko Lehto f52e979082 Add deflator helper class for deflating data
Wraps pako's deflate for easier usage.
2020-02-17 11:29:29 +01:00
Niko Lehto 3b562e8a0f Make clipBoardPasteFrom() test more specific
Don't rely on clientCutText() to test clipboardPasteFrom().
2020-02-17 09:34:44 +01:00
Samuel Mannehed 4ab5070548 Merge pull request #1361 from alvintownsend/master
Correcting path to package.json for running at a path other than root.
2020-02-12 11:25:04 +01:00
Alvin Townsend 546edcd4a0 Correcting path to package.json for running at a path other than root. 2020-01-31 11:34:53 +01:00
Samuel Mannehed 71bb3fdfa5 Fix color channels for VMware alpha cursors
The red and blue channels were incorrectly swapped.
2020-01-30 11:48:17 +01:00
Pierre Ossman eb05b45b70 Make afterEach() hooks work when skipping tests
Mocha will now run afterEach() hooks when tests are skipped, so we need
to make them more robust against things being partially set up.
2020-01-23 14:27:37 +01:00
Juanjo Diaz 8394462356 Remove generated HTML by Cursor when it detaches 2020-01-23 11:58:16 +02:00
Pierre Ossman 2d53a785d5 Merge branch 'abstraction_for_detection' of https://github.com/samhed/noVNC 2020-01-14 09:45:28 +01:00
Samuel Mannehed 64fdd336a0 Simplify encodeUTF8/decodeUTF8 unittests 2020-01-03 10:41:34 +01:00
Samuel Mannehed 80c72e92d2 Add unit tests for encodeUTF8 and decodeUTF8 2020-01-02 17:29:41 +01:00
Samuel Mannehed cbf090fe70 Remove unused python scripts 2020-01-02 13:56:07 +01:00
Samuel Mannehed 274652d119 Fix chinese translation for "Disconnect"
Thanks for @wavezhang, @litongjava, and @bhzhu203 for helping out.
2020-01-02 13:24:42 +01:00
Samuel Mannehed 208e34bc34 Update chinese translation strings
Thanks to @QQ2017 and @wavezhang for helping out with correcting these.
2020-01-02 13:18:24 +01:00
Samuel Mannehed 11ae8f0ef4 Add comment for browser and platform detection 2020-01-02 11:30:34 +01:00
Samuel Mannehed c32d4f3cd0 Add short description at the top of browser.js 2020-01-02 11:30:34 +01:00
Samuel Mannehed e52a278ed7 Properly detect scrollbar gutter
As a rule, instead of hard-coding a behavior on specific platforms we
should do dynamic detection.

This commit moves away from always hiding scrollbars on Android and iOS
and instead detects the rendered width of scrollbars in the browser.
2020-01-02 11:30:18 +01:00
Samuel Mannehed 8f230f45cc Remove Google Chrome Frame plugin compatability
Chrome Frame has been retired we so we can remove it from this
compatability tag.
2019-12-31 02:15:07 +01:00
Samuel Mannehed 78bbf6bad2 Restore X-UA-Compatible meta tag to vnc_lite
It's still required since without this IE's default setting for "Display intranet web pages in compatibility mode" will cause errors.
2019-12-31 02:07:31 +01:00
Samuel Mannehed 3a64043f28 Restore X-UA-Compatible meta tag to vnc.html
It's still required since without this IE's default setting for "Display intranet web pages in compatibility mode" will cause errors.
2019-12-31 02:06:02 +01:00
Pierre Ossman 49db41ea4b Allow cursor to be updated while connecting
We haven't got a server provided cursor at this point, but we might
have done something local, e.g. enabled the dot cursor.
2019-12-30 09:30:00 +01:00
Pierre Ossman d507d1415e Make sure "undefined" can be a default parameter value
Lower layers can consider null to be a valid value, when we'd rather
they treat the value as not set.
2019-12-25 12:10:21 +01:00
Pierre Ossman c4eb4ddcfe Handle slow loading of images
Internet Explorer seems to flag images as loaded prematurely, which
can result in rendering bugs. We can detect this by looking at the
dimensions though.
2019-12-23 15:52:54 +01:00
Pierre Ossman 4babdf33bd Validate decoded image dimensions
They are expected to be a certain size, so verify this so no server
tries to do something broken.
2019-12-23 15:42:02 +01:00
Pierre Ossman b8d1a8bb57 Avoid using Array.includes()
Internet Explorer doesn't have this method. Use the safer indexOf()
instead.
2019-12-23 15:42:02 +01:00
Samuel Mannehed 2cf82a5c8e Build in the behavior to ignore decodeUTF8 errors
Makes the code clearer and more explicit in intent.
2019-12-23 10:27:40 +01:00
Pierre Ossman 06a8f7d91a Use undefined as the default value for password
An empty password is techincally legal, and now supported by the
RFB core, so we cannot use that as a placeholder for "no password".
2019-12-23 09:35:51 +01:00
Samuel Mannehed dbbb676da9 Peter is no longer part of the noVNC team 2019-12-21 00:26:57 +01:00
Samuel Mannehed 84a8c1b0cc Merge pull request #1327 from vanym/decodeUTF8_try_catch
Fixes exception when desktop name contains non-utf8 character
2019-12-07 20:12:27 +01:00
VanyM ff1b10ca66 Add try catch in every place that uses decodeUTF8 2019-12-06 09:44:11 +03:00
Chris "Koying" Browet 1c9826140a Add support for Unix Tight auth 2019-12-05 15:46:31 +01:00
Chris Koying Browet 5b453ed4a8 Expand password dialog to work for usernames too
Some VNC authentication schemes use usernames, our UI should support
these.
2019-12-05 15:46:24 +01:00
Samuel Mannehed b39caa7469 Merge pull request #1318 from chrisjdev/emptyPassword
Allow connecting with empty string credentials
2019-12-05 12:15:43 +01:00
Chris J 01d4514dee Allow connecting with empty string credentials
Checking for undefined instead of falsy. That way an empty strings are allowed.
2019-12-05 12:13:11 +01:00
Samuel Mannehed 80b078c469 Add encodeUTF8 function to core/util/strings.js 2019-12-04 10:37:11 +01:00
Pierre Ossman 9f557f5280 Make Cursor.detach() safe to call when not attached
Avoids having checks in higher layers.
2019-11-29 10:08:15 +01:00
Samuel Mannehed 94c89284fc Fix chinese translation errors
Thanks to @QQ2017 for providing the translation strings
2019-11-28 12:51:32 +01:00
Samuel Mannehed b17f6c6929 Simplify logic for status hierarchy
Removes unnecessary variable
2019-11-25 17:03:55 +01:00
Samuel Mannehed afa1f8a2ab Ensure warning status timeouts are honored
When showing a new status popup we want to set a timer for how long to
show it. In cases where we show many statuses in a fast succession we
need to remove any running timeouts when showing a new one.

There are exceptions when new statuses won't be shown, and thats if a
more severe status is already showing, i.e and error or a warning.

Warnings can still have timeouts. There was a bug that occured when we
tried to show a normal status while a warning was showing. The bug
caused the warning status timeout to be removed even if the normal
status was never shown. We should only remove running timeouts if we're
actually going to show a new status.
2019-11-25 16:38:03 +01:00
Pierre Ossman 686c8d259a Merge branch 'babel' of https://github.com/CendioOssman/noVNC 2019-11-12 14:12:18 +01:00
Pierre Ossman d01ecc18d5 Don't use arrow functions in legacy loader
The browsers that need the legacy code do not support such fancy
modern things.
2019-11-12 14:05:55 +01:00
Pierre Ossman 66ab0d98d7 Load support scripts first
E.g. SystemJS requires the Promise polyfill, so make sure all our
support files are loaded first.
2019-11-12 14:05:55 +01:00
Pierre Ossman 0dd439a874 Upgrade to latest babel
There has been a lot of renaming and restructuring in babel, so we need
to modify our code to handle the latest version. We also need to adjust
the way we build our babel worker as babel itself no longer runs in older
browsers such as Internet Explorer.
2019-11-12 14:05:55 +01:00
Samuel Mannehed ae127d8a38 Merge pull request #1319 from wavezhang/patch-2
Update zh_CN.po
2019-11-11 16:46:36 +01:00
Pierre Ossman b88a92afe8 Always include Promise polyfill for legacy browsers
It is now used by our general code and not just by the conversion
routines, so we need to make sure it is always included for the
old browsers.
2019-11-11 13:36:30 +01:00
Pierre Ossman 6b20803401 Clean up handling of untransformed files
This control flow is difficult enough as it is to follow. Move the
handling of the untransformed files to a separate block to make it
slightly easier to understand.
2019-11-11 13:33:47 +01:00
Pierre Ossman 8cfa673d94 Remove redundant "no copy" check
We also check this list as a filter to walkDir(), so no need for the
extra check here.
2019-11-11 13:32:19 +01:00
Pierre Ossman c6e37040de Use proper backticks for generating legacy script tags 2019-11-11 10:01:43 +01:00
wavezhang 9653598af7 Update zh_CN.po
fix spell errors, improve translations
2019-11-08 14:08:23 +08:00
Alex Tanskanen a6304f91d0 Fix missing caps lock events on iOS
Caps Lock on iOS only trigged key release or key press events.
When it's clicked it would only send keydown, and next time
it would only send keyup and so on. It should send both a key press
and a key release.

Also added the unit tests for macOS since those were missing.

Co-Authored-By: Alex Tanskanen <aleta@cendio.se>
2019-11-07 16:44:26 +01:00
Samuel Mannehed c15502525e Add README to app/locale warning not to modify 2019-11-07 13:07:45 +01:00
Alex Tanskanen 175b843b66 Add "macOS shuffle" to iOS as well
Since iOS functions like macOS with regards to Alt behaving like AltGr,
we need the same workaround on iOS as well.
2019-11-04 14:22:46 +01:00
Pierre Ossman ccb511a527 Handle missing Shift events on Windows
This is a bug in the OS that leaks through to the browsers. We need
to fake a Shift release here to avoid Shift getting stuck in the remote
session.
2019-11-04 10:17:45 +01:00
Pierre Ossman 3388c92c7f Send NumLock on macOS, even though the key is Clear
There is no obvious choice what works best here, but this is what
TigerVNC has been doing for years without complaints. Let's follow
them until we get reports that this doesn't work well.
2019-11-01 13:13:35 +01:00
Pierre Ossman 1096555414 Fix typo for MailSend key 2019-11-01 10:49:19 +01:00
Pierre Ossman ebee9cddbf Update to latest UI Events key specification 2019-11-01 10:25:23 +01:00
Pierre Ossman 5736ea0bd5 Fix AltGr for a few more keys in IE and Edge
Some keys apparently send 'Unidentified' rather than an unshifted value.
Make sure those are also handled. Examples are \ and | on a Swedish
keyboard.
2019-11-01 09:59:02 +01:00
Samuel Mannehed 94a01b0ae0 Keep the virtual keyboard after using extra keys
If using the extra keys always gives focus to the screen then an
on-screen keyboard would be closed. When using on-screen keyboards we
instead want to give focus to our virtual keyboard input element.
2019-10-31 16:24:06 +01:00
Pierre Ossman 8c51e9a8a2 Revert iOS keyup workaround
It seems Apple has fixed their buggy keyup events, so remove the
workaround and allow keys to be kept pressed again.

This is a revert of 9e99ce126c.
2019-10-31 15:36:40 +01:00
Pierre Ossman 9d956e9198 Handle broken numpad delete key in Chrome 2019-10-31 14:51:36 +01:00
Pierre Ossman dd4341fe67 Explain why Clear maps to KP_Begin 2019-10-31 14:12:58 +01:00
Pierre Ossman 758399050d Try to handle Meta key properly
The standards have unfortunatly caused some confusion between the Windows
key and the original Meta key. Try to handle the common case sanely at least.
2019-10-31 14:12:58 +01:00
Samuel Mannehed 1dd1bf0306 Merge pull request #1312 from samhed/master
Fix so that you can use the keyboard after using the extra keys
2019-10-28 10:45:08 +01:00
Samuel Mannehed a5aa8e1282 Move focus to the screen when using extra keys
A regression from 2afda54 and friends was that you couldn't use the
extra keys and then directly use the keyboard, you would have to click
in the session first.

This commit restores the correct behavior and also adds a visual queue
to the fact that the screen got the focus by fading the controlbar.
2019-10-28 10:37:10 +01:00
Samuel Mannehed c568ad4c74 Add missing scancode for sendTab 2019-10-28 10:02:23 +01:00
Samuel Mannehed 0c4b3e802f Rename document.capturedElem to captureElement
To better fit most naming.
2019-10-23 15:59:43 +02:00
Pierre Ossman 65066326c5 Improve Windows key image
The previous one didn't have sharp lines, or follow pixel boundaries
properly.
2019-10-23 15:51:41 +02:00
Samuel Mannehed ffdd0dfeef Merge pull request #1309 from samhed/disappearing_cursor
Fix disappearing cursor
2019-10-21 14:16:48 +02:00
Samuel Mannehed c3a7524c9e Hide the emulated cursor when target is null
Makes it easier to understand what happens when a real element isn't
passed as a target to updateVisibility(). Also makes the code more
robust to future changes.

Co-authored-by: Alex Tanskanen <aleta@cendio.se>
Co-authored-by: Niko Lehto <nikle@cendio.se>
2019-10-21 13:51:42 +02:00
Samuel Mannehed 7a96fc3785 Fix disappearing cursor after click
In the cursor emulation when deciding if the cursor should be hidden -
Instead of checking what's under the cursor, we check the element that
has capture.

This introduced another bug in the cursor emulation. The cursor did not
always disappear properly when using our cursor emulation together with
our setCapture polyfill. More specifically, we saw a problem when a
capture ended on an element without cursor emulation.

We solved this by introducing another visibility check on a timer in
the cursor emulation. However this led to yet another problem where
this timer conflicted with the timer in the setCapture polyfill.

We removed the timeout in the setCapture polyfill and created a
variable to make sure that all the events remaining in the queue can be
completed.

Co-authored-by: Alex Tanskanen <aleta@cendio.se>
Co-authored-by: Niko Lehto <nikle@cendio.se>
2019-10-21 13:51:42 +02:00
Samuel Mannehed 938690375b Check next elem at mouseleave in cursor emulation
It's not obvious that we want to hide the cursor when we get a leave,
it depends on the element that we're leaving to. This makes the code
more robust.

Co-authored-by: Alex Tanskanen <aleta@cendio.se>
Co-authored-by: Niko Lehto <nikle@cendio.se>
2019-10-21 13:10:13 +02:00
Samuel Mannehed fcd99d04fb Rename variables in setCapture proxy
The names of many variables were too similar. To make the code easier
to follow we renamed:

* _captureElem to _capturedElem
* _captureElemChanged() to _capturedElemChanged()
* captureElem to proxyElem
* elem to target

Co-authored-by: Alex Tanskanen <aleta@cendio.se>
Co-authored-by: Niko Lehto <nikle@cendio.se>
2019-10-21 13:10:09 +02:00
Pierre Ossman 2b4c655405 Fix alt text for drag button
We had left an old placeholder text on this button.
2019-10-14 10:17:44 +02:00
Pierre Ossman f2d42dc357 Never show drag icon if clipping is disabled
Toggling the enabled state is a remnant from an earlier version
of the code where we could determine if the the session is actually
clipped, and not just that the setting is enabled.

Right now we only change things based on the setting, so let's
completely hide the button when clipping is disabled.
2019-10-14 10:15:19 +02:00
Samuel Mannehed 412d93060d Update copyright to 2019 for modified files 2019-09-30 15:35:33 +02:00
Samuel Mannehed e8614e20ef Code comments for how the receieve queue works 2019-09-25 21:04:46 +02:00
Samuel Mannehed c90d53565a Clarify why we ENABLE_COPYWITHIN is false 2019-09-25 21:03:37 +02:00
Samuel Mannehed 3aeaea50af Merge pull request #1298 from CendioNiko/edgeCursorURI
Fix url cursor detection on Edge
2019-09-25 14:46:58 +02:00
Samuel Mannehed 3055307d3d Merge pull request #1299 from CendioNiko/vmwarecursor
Add support for VMware cursor encoding
2019-09-25 14:45:29 +02:00
Samuel Mannehed 8dc47f3c06 Merge pull request #1289 from CendioNiko/master
Desktop name improvements
2019-09-25 14:43:55 +02:00
Samuel Mannehed c51a77c2eb Merge pull request #1281 from jalfd/optimize-receive-buffer
Optimize receive buffer
2019-09-25 13:26:02 +02:00
Samuel Mannehed d39e0d1244 Clarify comments for broken alt in FF on Windows 2019-09-24 16:00:05 +02:00
Samuel Mannehed ebb58c34da Merge pull request #1280 from jalfd/alt-key-recursion
Avoid recursion in Alt check on Firefox
2019-09-24 15:56:13 +02:00
Samuel Mannehed 35b78e95d2 Merge pull request #1279 from CendioOssman/nomodule
Use "nomodule" instead of manual check
2019-09-24 15:40:05 +02:00
Pierre Ossman 0b51419ca4 Use "nomodule" instead of manual check
Very few browsers are left in the wild that supports modules but not
"nomodule", so let's simplify our handling a bit.

Safari 10 supports modules but not 'nomodule', this means that this
particular version of Safari will be broken. Due to this we have to
bump up the required Safari version to 11.
2019-09-24 15:34:59 +02:00
Niko Lehto 296ba51f49 Add support for VMware cursor encoding
Supports both classic cursor type and alpha cursor type. In classic
mode the server can send 'inverted' pixels for the cursor, our code
does not support this but handles these pixels as opaque black.

Co-authored-by: Samuel Mannehed <samuel@cendio.se>
2019-09-24 10:26:30 +02:00
Niko Lehto a1afb2a215 Fix url cursor detection on Edge
_supportCursorURIs was set to true even when Edge didn't support
URIs because the fallback value "default" was used.
2019-09-23 13:50:17 +02:00
Niko Lehto c90245da25 Restore page name after disconnect 2019-09-04 15:05:14 +02:00
Niko Lehto 8d6f686b59 Test unicode desktop names 2019-09-04 14:47:40 +02:00
Niko Lehto ce66b46986 Add support for DesktopName extension
This extension allows session name to be changed during runtime.
2019-09-04 14:23:37 +02:00
Pierre Ossman 9886d5951d Set viewport size for autoscale tests
We were incorrectly relying on the viewport being indirectly set
for us. Make sure we are explicit in what we want for these tests.
2019-08-23 15:48:30 +02:00
Pierre Ossman 30ff15a35a Merge branch 'upgrade' of https://github.com/CendioOssman/noVNC 2019-08-23 15:08:56 +02:00
Pierre Ossman e5255fc246 Remove pointless Display.clear()
It served no meaningful purpose and it had bugs. So let's remove it
rather than try to fix it.
2019-08-23 15:05:58 +02:00
Pierre Ossman 3855a7bee4 Remove unused Display.logo attribute 2019-08-23 15:04:23 +02:00
Pierre Ossman 6aed0b4dd2 Deprecate showDotCursor option for RFB constructor
It is not relevant for the connection stage so it should not have
been a constructor argument to begin with. Ship with a warning for
a release before we remove it.
2019-08-23 14:00:20 +02:00
Pierre Ossman 1f2bb52850 Make sure showDotCursor can be modified before connecting
The cursor object is only attached to our canvas whilst connecting,
so we need to make sure we don't try to update anything when were
not connected or we'll get a crash.
2019-08-23 13:57:30 +02:00
Pierre Ossman 4222d72bfe Regenerate module loader after tool upgrades 2019-08-19 13:32:41 +02:00
Pierre Ossman e24b501c47 Use latest versions of development dependencies
Let's make sure we get the latest features and fixes for all the
tools we are using.
2019-08-19 13:32:41 +02:00
Pierre Ossman c47a3a3e09 Use latest NodeJS version in Travis
We want the latest and not be stuck on some old version. Otherwise
our scripts might not execute correctly.
2019-08-19 13:32:41 +02:00
Pierre Ossman 90d463f969 Make sure translation tools are lint checked
They do not have a .js suffix so eslint isn't finding them
automatically.
2019-08-19 13:32:29 +02:00
Jesper Alf Dam 08567b08ac When compacting the receive buffer don't copy unused buffer space
When compacting the receive buffer, we should only copy the bytes
between _rQi and _rQlen (the index of the first unread byte, and the
next write position).

Previously, we copied everything for _rQi up untill the end of the
buffer.
2019-08-16 19:34:09 +02:00
Jesper Alf Dam 7d755d10dc Don't compact the receive buffer unless we've actually run out of space
Previously, we would compact the buffer (moving unread data to the
start of the buffer) as follows:

- after processing a message, if there are zero unread bytes, just reset
  the indices for first and last unread byte to zero
- else, if at least 1/8th of the buffer is used, copy remaining data to the beginning of the buffer

The second option is never actually necessary, as before inserting new data
into the array, we already check if there's enough free space, and
compact the buffer first if necessary. So we've been doing a lot of
copies that weren't actually needed. Let's not do that any more.
2019-08-16 19:34:09 +02:00
Jesper Alf Dam e9f489a629 Avoid recursion in Alt check on Firefox
The Firefox workaround which checks for missing Alt key events may
synthesise new KeyboardEvents. On these events, checkAlt should not be
recursively triggered. Otherwise, we get "too much recursion" errors
whenever the Alt key is pressed.
2019-08-16 16:20:41 +02:00
Pierre Ossman 776024a008 Fix trivial lint issues in translation tools
Indentation, missing semicolon, etc.
2019-08-15 15:58:09 +02:00
Pierre Ossman 604edf07d2 Upgrade to latest rollup 2019-08-15 15:58:09 +02:00
Pierre Ossman ff7882c44c Remove unused import from module loader 2019-08-15 15:58:09 +02:00
Pierre Ossman c9765e5066 Upgrade to latest sinon and chai 2019-08-15 15:58:09 +02:00
Pierre Ossman b875486db8 Avoid deprecated called.once from sinon-chai
It's been removed in newer versions and will break eventually.
2019-08-15 15:58:09 +02:00
Tim Edwards e1d50c8c10 Added a Snap package for noVNC (#1231)
Creating an Ubuntu Snap package to make noVNC easier to deploy.

Checks for the websockify binary in both the PATH (using which) and in the location where the Snap package places the binary. This is necessary for noVNC to be usable in a Snap. It doesn't affect the original functionality of git cloning websockify if it's not found in PATH or the Snap location.
2019-07-25 22:22:48 +02:00
Samuel Mannehed 897b465b87 Add missing parentheses for arrow func arg
Our lint tests expect this when an arrow functino has a body with
curly braces.
2019-07-23 16:12:22 +02:00
Samuel Mannehed e14aa4d0fe Add documentation for the option showDotCursor
This is not only a property, it's also a parameter to the constructor.
2019-07-23 16:07:17 +02:00
Samuel Mannehed c912230309 Remove the default value of wsProtocols
Using the 'binary' protocol by default is very non-standard.
2019-07-23 16:03:49 +02:00
Samuel Mannehed 21387f9c24 Merge pull request #1262 from shiramax/sub_protocols
Add support in websocket sub-protocols
2019-07-23 15:24:10 +02:00
Shira Maximov 25b3d49d32 Add support in websocket sub-protocols 2019-07-23 13:44:18 +03:00
Samuel Mannehed 8f2bcfbe79 Merge pull request #1265 from juanjoDiaz/add_version_number_to_UI_2
Add version number to UI
2019-07-23 11:51:09 +02:00
Juanjo Diaz 15c7b7a619 Add version number to UI 2019-07-23 10:25:59 +03:00
Samuel Mannehed 21ac6ca0f2 Update link to websock.js API 2019-07-15 14:05:24 +02:00
Samuel Mannehed df4b7515a3 Merge pull request #1259 from lyarwood/launch.sh
launch.sh: Check for a local websockify directory
2019-07-14 23:31:10 +02:00
Lee Yarwood 188c9a591b launch.sh: Check for a local websockify directory
Previously launch.sh would check both for the existence of a local
websockify file and /websockify/run file.

This initial check should really be for a local websockify directory
as in packaged environments a file could very well be the actual
executable leading to launch.sh incorrectly attempting to use a local
version of websockify.
2019-07-09 12:27:26 +01:00
Pierre Ossman 23af6e142a Merge branch 'add-japanese-translation' of https://github.com/nnn1590/noVNC 2019-06-13 13:59:35 +02:00
Pierre Ossman 97924ebd5d Add support for separate key file in launch script 2019-06-13 13:51:56 +02:00
nnn1590 7ded517823 Add Japanese translation 2019-06-12 04:30:56 +09:00
Samuel Mannehed 32e081950c Revert "Fullscreen from iframe (#1236)" (#1247)
This reverts commit 19cdc15aa3.
2019-05-25 02:51:38 +02:00
Pierre Ossman 755d6eae99 Remove server pixel format warnings
These are harmless and really only for debugging. So remove them
as they tend to trick people in to thinking something is wrong.
We already print the entire server pixel format earlier anyway in
case we need the details.
2019-05-24 13:06:26 +02:00
Ján Jockusch 19cdc15aa3 Fullscreen from iframe (#1236)
* First attempt to make the fullscreen button work inside an iframe.

* Cleaner distinction between document element and document.

* Scoping corrections. Auto-detect correct iframe.

* Added comments to the relevant sections.

* IE issue fixed.

* Same source issue solved. fullscreenToggle now checks if it is permitted to inspect other iframes.
2019-05-13 15:06:32 +02:00
Pierre Ossman 2b2b6073dd Don't do cleanup on deploy
Travis wipes out node_modules otherwise, which we need for the prepare
stages of the deployment to npm.
2019-04-15 16:11:01 +02:00
116 changed files with 10263 additions and 62841 deletions
+2
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",
@@ -44,5 +45,6 @@
"named": "never",
"asyncArrow": "always" }],
"switch-colon-spacing": ["error"],
"camelcase": ["error", { allow: ["^XK_", "^XF86XK_"] }],
}
}
+5
View File
@@ -0,0 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: Question or discussion
url: https://groups.google.com/forum/?fromgroups#!forum/novnc
about: Ask a question or start a discussion
+56
View File
@@ -0,0 +1,56 @@
name: Publish
on:
push:
pull_request:
release:
types: [published]
jobs:
npm:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
# Needs to be explicitly specified for auth to work
registry-url: 'https://registry.npmjs.org'
- run: npm install
- uses: actions/upload-artifact@v2
with:
name: npm
path: lib
- run: npm publish --access public
env:
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
if: ${{ github.event_name == 'release' && !github.event.release.prerelease }}
- run: npm publish --access public --tag beta
env:
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
if: ${{ github.event_name == 'release' && github.event.release.prerelease }}
snap:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- run: |
VERSION=$(grep '"version"' package.json | cut -d '"' -f 4)
echo $VERSION
sed -i "s/@VERSION@/$VERSION/g" snap/snapcraft.yaml
- uses: snapcore/action-build@v1
id: snapcraft
- uses: actions/upload-artifact@v2
with:
name: snap
path: ${{ steps.snapcraft.outputs.snap }}
- uses: snapcore/action-publish@v1
with:
store_login: ${{ secrets.SNAPCRAFT_LOGIN }}
snap: ${{ steps.build.outputs.snap }}
release: stable
if: ${{ github.event_name == 'release' && !github.event.release.prerelease }}
- uses: snapcore/action-publish@v1
with:
store_login: ${{ secrets.SNAPCRAFT_LOGIN }}
snap: ${{ steps.build.outputs.snap }}
release: beta
if: ${{ github.event_name == 'release' && github.event.release.prerelease }}
+19
View File
@@ -0,0 +1,19 @@
name: Lint
on: [push, pull_request]
jobs:
eslint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
- run: npm install
- run: npm run lint
html:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
- run: npm install
- run: git ls-tree --name-only -r HEAD | grep -E "[.](html|css)$" | xargs ./utils/validate
+28
View File
@@ -0,0 +1,28 @@
name: Test
on: [push, pull_request]
jobs:
test:
strategy:
matrix:
os:
- ubuntu-latest
- windows-latest
browser:
- ChromeHeadless
- FirefoxHeadless
include:
- os: macos-latest
browser: Safari
- os: windows-latest
browser: EdgeHeadless
fail-fast: false
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
- run: npm install
- run: npm run test
env:
TEST_BROWSER_NAME: ${{ matrix.browser }}
-58
View File
@@ -1,58 +0,0 @@
language: node_js
sudo: false
cache:
directories:
- node_modules
node_js:
- 6
env:
matrix:
- TEST_BROWSER_NAME=chrome TEST_BROWSER_OS='Windows 10'
# FIXME Skip tests in Linux since Sauce Labs browser versions are ancient.
# - TEST_BROWSER_NAME=chrome TEST_BROWSER_OS='Linux'
- TEST_BROWSER_NAME=chrome TEST_BROWSER_OS='OS X 10.11'
- TEST_BROWSER_NAME=firefox TEST_BROWSER_OS='Windows 10'
# - TEST_BROWSER_NAME=firefox TEST_BROWSER_OS='Linux'
- TEST_BROWSER_NAME=firefox TEST_BROWSER_OS='OS X 10.11'
- TEST_BROWSER_NAME='internet explorer' TEST_BROWSER_OS='Windows 10'
- TEST_BROWSER_NAME='internet explorer' TEST_BROWSER_OS='Windows 7'
- TEST_BROWSER_NAME=microsoftedge TEST_BROWSER_OS='Windows 10'
- TEST_BROWSER_NAME=safari TEST_BROWSER_OS='OS X 10.13'
before_script: npm install -g karma-cli
addons:
sauce_connect:
username: "directxman12"
jwt:
secure: "d3ekMYslpn6R4f0ajtRMt9SUFmNGDiItHpqaXC5T4KI0KMEsxgvEOfJot5PiFFJWg1DSpJZH6oaW2UxGZ3duJLZrXIEd/JePY8a6NtT35BNgiDPgcp+eu2Bu3rhrSNg7/HEsD1ma+JeUTnv18Ai5oMFfCCQJx2J6osIxyl/ZVxA="
stages:
- lint
- test
- name: deploy
if: tag is PRESENT
jobs:
include:
- stage: lint
env:
addons:
before_script:
script: npm run lint
-
env:
addons:
before_script:
script: git ls-tree --name-only -r HEAD | grep -E "[.](html|css)$" | xargs ./utils/validate
- stage: deploy
env:
addons:
script: skip
before_script: skip
deploy:
provider: npm
email: ossman@cendio.se
api_key:
secure: "Qq2Mi9xQawO2zlAigzshzMu2QMHvu1IaN9l0ZIivE99wHJj7eS5f4miJ9wB+/mWRRgb3E8uj9ZRV24+Oc36drlBTU9sz+lHhH0uFMfAIseceK64wZV9sLAZm472fmPp2xdUeTCCqPaRy7g1XBqiJ0LyZvEFLsRijqcLjPBF+b8w="
on:
tags: true
repo: novnc/noVNC
+1 -7
View File
@@ -1,4 +1,4 @@
noVNC is Copyright (C) 2018 The noVNC Authors
noVNC is Copyright (C) 2019 The noVNC Authors
(./AUTHORS)
The noVNC core library files are licensed under the MPL 2.0 (Mozilla
@@ -42,12 +42,6 @@ licenses (all MPL 2.0 compatible):
vendor/pako/ : MIT
vendor/browser-es-module-loader/src/ : MIT
vendor/browser-es-module-loader/dist/ : Various BSD style licenses
vendor/promise.js : MIT
Any other files not mentioned above are typically marked with
a copyright/license header at the top of the file. The default noVNC
license is MPL-2.0.
+68 -6
View File
@@ -1,6 +1,7 @@
## noVNC: HTML VNC Client Library and Application
[![Build Status](https://travis-ci.org/novnc/noVNC.svg?branch=master)](https://travis-ci.org/novnc/noVNC)
[![Test Status](https://github.com/novnc/noVNC/workflows/Test/badge.svg)](https://github.com/novnc/noVNC/actions?query=workflow%3ATest)
[![Lint Status](https://github.com/novnc/noVNC/workflows/Lint/badge.svg)](https://github.com/novnc/noVNC/actions?query=workflow%3ALint)
### Description
@@ -24,6 +25,7 @@ for a more complete list with additional info and links.
- [Browser Requirements](#browser-requirements)
- [Server Requirements](#server-requirements)
- [Quick Start](#quick-start)
- [Installation from Snap Package](#installation-from-snap-package)
- [Integration and Deployment](#integration-and-deployment)
- [Authors/Contributors](#authorscontributors)
@@ -68,6 +70,7 @@ Please tweet [@noVNC](http://www.twitter.com/noVNC) if you do.
* Local cursor rendering
* Clipboard copy/paste
* Translations
* Touch gestures for emulating common mouse actions
* Licensed mainly under the [MPL 2.0](http://www.mozilla.org/MPL/2.0/), see
[the license document](LICENSE.txt) for details
@@ -88,7 +91,7 @@ noVNC uses many modern web technologies so a formal requirement list is
not available. However these are the minimum versions we are currently
aware of:
* Chrome 49, Firefox 44, Safari 10, Opera 36, IE 11, Edge 12
* Chrome 64, Firefox 79, Safari 13.4, Opera 51, Edge 79
### Server Requirements
@@ -105,16 +108,76 @@ proxy.
### Quick Start
* Use the launch script to automatically download and start websockify, which
* Use the `novnc_proxy` script to automatically download and start websockify, which
includes a mini-webserver and the WebSockets proxy. The `--vnc` option is
used to specify the location of a running VNC server:
`./utils/launch.sh --vnc localhost:5901`
`./utils/novnc_proxy --vnc localhost:5901`
* Point your browser to the cut-and-paste URL that is output by the launch
* Point your browser to the cut-and-paste URL that is output by the `novnc_procy`
script. Hit the Connect button, enter a password if the VNC server has one
configured, and enjoy!
### Installation from Snap Package
Running the command below will install the latest release of noVNC from Snap:
`sudo snap install novnc`
#### Running noVNC
You can run the Snap-package installed novnc directly with, for example:
`novnc --listen 6081 --vnc localhost:5901 # /snap/bin/novnc if /snap/bin is not in your PATH`
#### Running as a Service (Daemon)
The Snap package also has the capability to run a 'novnc' service which can be
configured to listen on multiple ports connecting to multiple VNC servers
(effectively a service runing multiple instances of novnc).
Instructions (with example values):
List current services (out-of-box this will be blank):
```
sudo snap get novnc services
Key Value
services.n6080 {...}
services.n6081 {...}
```
Create a new service that listens on port 6082 and connects to the VNC server
running on port 5902 on localhost:
`sudo snap set novnc services.n6082.listen=6082 services.n6082.vnc=localhost:5902`
(Any services you define with 'snap set' will be automatically started)
Note that the name of the service, 'n6082' in this example, can be anything
as long as it doesn't start with a number or contain spaces/special characters.
View the configuration of the service just created:
```
sudo snap get novnc services.n6082
Key Value
services.n6082.listen 6082
services.n6082.vnc localhost:5902
```
Disable a service (note that because of a limitation in Snap it's currently not
possible to unset config variables, setting them to blank values is the way
to disable a service):
`sudo snap set novnc services.n6082.listen='' services.n6082.vnc=''`
(Any services you set to blank with 'snap set' like this will be automatically stopped)
Verify that the service is disabled (blank values):
```
sudo snap get novnc services.n6082
Key Value
services.n6082.listen
services.n6082.vnc
```
### Integration and Deployment
@@ -133,7 +196,6 @@ that list and you think you should be, feel free to send a PR to fix that.
* Core team:
* [Joel Martin](https://github.com/kanaka)
* [Samuel Mannehed](https://github.com/samhed) (Cendio)
* [Peter Åstrand](https://github.com/astrand) (Cendio)
* [Solly Ross](https://github.com/DirectXMan12) (Red Hat / OpenStack)
* [Pierre Ossman](https://github.com/CendioOssman) (Cendio)
-1
View File
@@ -1 +0,0 @@
1.1.0
+8
View File
@@ -1,3 +1,11 @@
/*
* noVNC: HTML5 VNC client
* Copyright (C) 2019 The noVNC Authors
* Licensed under MPL 2.0 (see LICENSE.txt)
*
* See README.md for usage and integration instructions.
*/
// NB: this should *not* be included as a module until we have
// native support in the browsers, so that our error handler
// can catch script-loading errors.
-92
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

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

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

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

+25 -45
View File
@@ -15,18 +15,18 @@
inkscape:export-xdpi="90"
sodipodi:docname="windows.svg"
inkscape:export-filename="/home/ossman/devel/noVNC/images/drag.png"
inkscape:version="0.92.3 (2405546, 2018-03-11)"
inkscape:version="0.92.4 (unknown)"
x="0px"
y="0px"
viewBox="-293 384 25 23"
viewBox="-293 384 25 25"
xml:space="preserve"
width="25"
height="23"><metadata
height="25"><metadata
id="metadata21"><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><defs
id="defs19" /><sodipodi:namedview
pagecolor="#ffffff"
pagecolor="#959595"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
@@ -35,51 +35,31 @@
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1017"
inkscape:window-height="1136"
id="namedview17"
showgrid="false"
inkscape:pagecheckerboard="true"
inkscape:zoom="9.44"
inkscape:cx="-0.84745763"
inkscape:cy="12.5"
inkscape:window-x="2552"
inkscape:window-y="122"
showgrid="true"
inkscape:pagecheckerboard="false"
inkscape:zoom="32"
inkscape:cx="3.926913"
inkscape:cy="13.255959"
inkscape:window-x="1920"
inkscape:window-y="27"
inkscape:window-maximized="1"
inkscape:current-layer="svg2" />
inkscape:current-layer="svg2"><inkscape:grid
type="xygrid"
id="grid818" /></sodipodi:namedview>
<style
type="text/css"
id="style2">
.st0{fill:#FFFFFF;}
</style>
<g
id="g14"
transform="matrix(1.2624869,0,0,1.3601695,73.614445,-144.84322)">
<g
id="g12">
<path
class="st0"
d="m -277.4,396 c -0.7,0 -1.3,0 -2,0 -0.4,0 -0.5,-0.1 -0.5,-0.5 0,-1 0,-2 0,-3 0,-0.3 0.2,-0.5 0.5,-0.5 1.3,-0.1 2.6,-0.3 3.9,-0.4 0.4,0 0.7,0.1 0.7,0.6 0,1.1 0,2.2 0,3.3 0,0.4 -0.2,0.6 -0.6,0.6 -0.7,-0.1 -1.4,-0.1 -2,-0.1 z"
id="path4"
inkscape:connector-curvature="0"
style="fill:#ffffff" />
<path
class="st0"
d="m -274.9,399.3 c 0,0.6 0,1.1 0,1.7 0,0.4 -0.1,0.6 -0.6,0.6 -1.4,-0.1 -2.8,-0.3 -4.1,-0.4 -0.3,0 -0.4,-0.3 -0.4,-0.5 0,-1 0,-2 0,-3 0,-0.4 0.2,-0.5 0.6,-0.5 1.3,0 2.6,0 3.9,0 0.5,0 0.6,0.2 0.6,0.6 0,0.4 0,0.9 0,1.5 z"
id="path6"
inkscape:connector-curvature="0"
style="fill:#ffffff" />
<path
class="st0"
d="m -283.5,396 c -0.6,0 -1.3,0 -1.9,0 -0.4,0 -0.6,-0.1 -0.6,-0.6 0,-0.8 0,-1.5 0,-2.3 0,-0.4 0.2,-0.6 0.6,-0.7 1.3,-0.1 2.7,-0.3 4,-0.4 0.4,0 0.5,0.1 0.5,0.5 0,1 0,1.9 0,2.9 0,0.4 -0.2,0.5 -0.5,0.5 -0.8,0.1 -1.5,0.1 -2.1,0.1 z"
id="path8"
inkscape:connector-curvature="0"
style="fill:#ffffff" />
<path
class="st0"
d="m -283.5,397 c 0.6,0 1.3,0 1.9,0 0.4,0 0.6,0.1 0.6,0.5 0,1 0,1.9 0,2.9 0,0.4 -0.2,0.5 -0.5,0.5 -1.3,-0.1 -2.7,-0.3 -4,-0.4 -0.4,0 -0.6,-0.2 -0.6,-0.7 0,-0.7 0,-1.5 0,-2.2 0,-0.5 0.2,-0.7 0.7,-0.7 0.6,0.1 1.2,0.1 1.9,0.1 z"
id="path10"
inkscape:connector-curvature="0"
style="fill:#ffffff" />
</g>
</g>
</svg>
<path
style="fill:#ffffff;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;fill-opacity:1"
d="M 21 4 L 11 5.1757812 L 11 12 L 21 12 L 21 4 z M 10 5.2949219 L 4 6 L 4 12 L 10 12 L 10 5.2949219 z "
transform="translate(-293,384)"
id="path853" /><path
id="path858"
d="m -272,405 -10,-1.17578 V 397 h 10 z M -283,403.70508 -289,403 v -6 h 6 z"
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
inkscape:connector-curvature="0" /></svg>

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

+1
View File
@@ -0,0 +1 @@
DO NOT MODIFY THE FILES IN THIS FOLDER, THEY ARE AUTOMATICALLY GENERATED FROM THE PO-FILES.
+72
View File
@@ -0,0 +1,72 @@
{
"Connecting...": "接続しています...",
"Disconnecting...": "切断しています...",
"Reconnecting...": "再接続しています...",
"Internal error": "内部エラー",
"Must set host": "ホストを設定する必要があります",
"Connected (encrypted) to ": "接続しました (暗号化済み): ",
"Connected (unencrypted) to ": "接続しました (暗号化されていません): ",
"Something went wrong, connection is closed": "何らかの問題で、接続が閉じられました",
"Failed to connect to server": "サーバーへの接続に失敗しました",
"Disconnected": "切断しました",
"New connection has been rejected with reason: ": "新規接続は次の理由で拒否されました: ",
"New connection has been rejected": "新規接続は拒否されました",
"Credentials are required": "資格情報が必要です",
"noVNC encountered an error:": "noVNC でエラーが発生しました:",
"Hide/Show the control bar": "コントロールバーを隠す/表示する",
"Drag": "ドラッグ",
"Move/Drag Viewport": "ビューポートを移動/ドラッグ",
"Keyboard": "キーボード",
"Show Keyboard": "キーボードを表示",
"Extra keys": "追加キー",
"Show Extra Keys": "追加キーを表示",
"Ctrl": "Ctrl",
"Toggle Ctrl": "Ctrl キーを切り替え",
"Alt": "Alt",
"Toggle Alt": "Alt キーを切り替え",
"Toggle Windows": "Windows キーを切り替え",
"Windows": "Windows",
"Send Tab": "Tab キーを送信",
"Tab": "Tab",
"Esc": "Esc",
"Send Escape": "Escape キーを送信",
"Ctrl+Alt+Del": "Ctrl+Alt+Del",
"Send Ctrl-Alt-Del": "Ctrl-Alt-Del を送信",
"Shutdown/Reboot": "シャットダウン/再起動",
"Shutdown/Reboot...": "シャットダウン/再起動...",
"Power": "電源",
"Shutdown": "シャットダウン",
"Reboot": "再起動",
"Reset": "リセット",
"Clipboard": "クリップボード",
"Clear": "クリア",
"Fullscreen": "全画面表示",
"Settings": "設定",
"Shared Mode": "共有モード",
"View Only": "表示のみ",
"Clip to Window": "ウィンドウにクリップ",
"Scaling Mode:": "スケーリングモード:",
"None": "なし",
"Local Scaling": "ローカルスケーリング",
"Remote Resizing": "リモートでリサイズ",
"Advanced": "高度",
"Quality:": "品質:",
"Compression level:": "圧縮レベル:",
"Repeater ID:": "リピーター ID:",
"WebSocket": "WebSocket",
"Encrypt": "暗号化",
"Host:": "ホスト:",
"Port:": "ポート:",
"Path:": "パス:",
"Automatic Reconnect": "自動再接続",
"Reconnect Delay (ms):": "再接続する遅延 (ミリ秒):",
"Show Dot when No Cursor": "カーソルがないときにドットを表示",
"Logging:": "ロギング:",
"Version:": "バージョン:",
"Disconnect": "切断",
"Connect": "接続",
"Username:": "ユーザー名:",
"Password:": "パスワード:",
"Send Credentials": "資格情報を送信",
"Cancel": "キャンセル"
}
+72
View File
@@ -0,0 +1,72 @@
{
"Connecting...": "Conectando...",
"Disconnecting...": "Desconectando...",
"Reconnecting...": "Reconectando...",
"Internal error": "Erro interno",
"Must set host": "É necessário definir o host",
"Connected (encrypted) to ": "Conectado (com criptografia) a ",
"Connected (unencrypted) to ": "Conectado (sem criptografia) a ",
"Something went wrong, connection is closed": "Algo deu errado. A conexão foi encerrada.",
"Failed to connect to server": "Falha ao conectar-se ao servidor",
"Disconnected": "Desconectado",
"New connection has been rejected with reason: ": "A nova conexão foi rejeitada pelo motivo: ",
"New connection has been rejected": "A nova conexão foi rejeitada",
"Credentials are required": "Credenciais são obrigatórias",
"noVNC encountered an error:": "O noVNC encontrou um erro:",
"Hide/Show the control bar": "Esconder/mostrar a barra de controles",
"Drag": "Arrastar",
"Move/Drag Viewport": "Mover/arrastar a janela",
"Keyboard": "Teclado",
"Show Keyboard": "Mostrar teclado",
"Extra keys": "Teclas adicionais",
"Show Extra Keys": "Mostar teclas adicionais",
"Ctrl": "Ctrl",
"Toggle Ctrl": "Pressionar/soltar Ctrl",
"Alt": "Alt",
"Toggle Alt": "Pressionar/soltar Alt",
"Toggle Windows": "Pressionar/soltar Windows",
"Windows": "Windows",
"Send Tab": "Enviar Tab",
"Tab": "Tab",
"Esc": "Esc",
"Send Escape": "Enviar Esc",
"Ctrl+Alt+Del": "Ctrl+Alt+Del",
"Send Ctrl-Alt-Del": "Enviar Ctrl-Alt-Del",
"Shutdown/Reboot": "Desligar/reiniciar",
"Shutdown/Reboot...": "Desligar/reiniciar...",
"Power": "Ligar",
"Shutdown": "Desligar",
"Reboot": "Reiniciar",
"Reset": "Reiniciar (forçado)",
"Clipboard": "Área de transferência",
"Clear": "Limpar",
"Fullscreen": "Tela cheia",
"Settings": "Configurações",
"Shared Mode": "Modo compartilhado",
"View Only": "Apenas visualizar",
"Clip to Window": "Recortar à janela",
"Scaling Mode:": "Modo de dimensionamento:",
"None": "Nenhum",
"Local Scaling": "Local",
"Remote Resizing": "Remoto",
"Advanced": "Avançado",
"Quality:": "Qualidade:",
"Compression level:": "Nível de compressão:",
"Repeater ID:": "ID do repetidor:",
"WebSocket": "WebSocket",
"Encrypt": "Criptografar",
"Host:": "Host:",
"Port:": "Porta:",
"Path:": "Caminho:",
"Automatic Reconnect": "Reconexão automática",
"Reconnect Delay (ms):": "Atraso da reconexão (ms)",
"Show Dot when No Cursor": "Mostrar ponto quando não há cursor",
"Logging:": "Registros:",
"Version:": "Versão:",
"Disconnect": "Desconectar",
"Connect": "Conectar",
"Username:": "Nome de usuário:",
"Password:": "Senha:",
"Send Credentials": "Enviar credenciais",
"Cancel": "Cancelar"
}
+7 -8
View File
@@ -11,16 +11,11 @@
"Disconnected": "Frånkopplad",
"New connection has been rejected with reason: ": "Ny anslutning har blivit nekad med följande skäl: ",
"New connection has been rejected": "Ny anslutning har blivit nekad",
"Password is required": "Lösenord krävs",
"Credentials are required": "Användaruppgifter krävs",
"noVNC encountered an error:": "noVNC stötte på ett problem:",
"Hide/Show the control bar": "Göm/Visa kontrollbaren",
"Drag": "Dra",
"Move/Drag Viewport": "Flytta/Dra Vyn",
"viewport drag": "dra vy",
"Active Mouse Button": "Aktiv musknapp",
"No mousebutton": "Ingen musknapp",
"Left mousebutton": "Vänster musknapp",
"Middle mousebutton": "Mitten-musknapp",
"Right mousebutton": "Höger musknapp",
"Keyboard": "Tangentbord",
"Show Keyboard": "Visa Tangentbord",
"Extra keys": "Extraknappar",
@@ -55,6 +50,8 @@
"Local Scaling": "Lokal Skalning",
"Remote Resizing": "Ändra Storlek",
"Advanced": "Avancerat",
"Quality:": "Kvalitet:",
"Compression level:": "Kompressionsnivå:",
"Repeater ID:": "Repeater-ID:",
"WebSocket": "WebSocket",
"Encrypt": "Kryptera",
@@ -65,9 +62,11 @@
"Reconnect Delay (ms):": "Fördröjning (ms):",
"Show Dot when No Cursor": "Visa prick när ingen muspekare finns",
"Logging:": "Loggning:",
"Version:": "Version:",
"Disconnect": "Koppla från",
"Connect": "Anslut",
"Username:": "Användarnamn:",
"Password:": "Lösenord:",
"Send Password": "Skicka lösenord",
"Send Credentials": "Skicka Användaruppgifter",
"Cancel": "Avbryt"
}
+17 -17
View File
@@ -1,19 +1,19 @@
{
"Connecting...": "接中...",
"Disconnecting...": "正在断连接...",
"Reconnecting...": "重新接中...",
"Connecting...": "接中...",
"Disconnecting...": "正在断连接...",
"Reconnecting...": "重新接中...",
"Internal error": "内部错误",
"Must set host": "请提供主机名",
"Connected (encrypted) to ": "已加密链接到",
"Connected (unencrypted) to ": "未加密链接到",
"Something went wrong, connection is closed": "发生错误,接已关闭",
"Failed to connect to server": "无法接到服务器",
"Disconnected": "链接已中断",
"New connection has been rejected with reason: ": "接被拒绝,原因:",
"New connection has been rejected": "接被拒绝",
"Connected (encrypted) to ": "已连接到(加密)",
"Connected (unencrypted) to ": "已连接到(未加密)",
"Something went wrong, connection is closed": "发生错误,接已关闭",
"Failed to connect to server": "无法接到服务器",
"Disconnected": "已断开连接",
"New connection has been rejected with reason: ": "接被拒绝,原因:",
"New connection has been rejected": "接被拒绝",
"Password is required": "请提供密码",
"noVNC encountered an error:": "noVNC 遇到一个错误:",
"Hide/Show the control bar": "显示/隐藏控制",
"Hide/Show the control bar": "显示/隐藏控制",
"Move/Drag Viewport": "拖放显示范围",
"viewport drag": "显示范围拖放",
"Active Mouse Button": "启动鼠标按鍵",
@@ -43,10 +43,10 @@
"Reset": "重置",
"Clipboard": "剪贴板",
"Clear": "清除",
"Fullscreen": "全屏",
"Fullscreen": "全屏",
"Settings": "设置",
"Shared Mode": "分享模式",
"View Only": "仅检视",
"View Only": "仅查看",
"Clip to Window": "限制/裁切窗口大小",
"Scaling Mode:": "缩放模式:",
"None": "无",
@@ -59,11 +59,11 @@
"Host:": "主机:",
"Port:": "端口:",
"Path:": "路径:",
"Automatic Reconnect": "自动重新接",
"Reconnect Delay (ms):": "重新接间隔 (ms)",
"Automatic Reconnect": "自动重新接",
"Reconnect Delay (ms):": "重新接间隔 (ms)",
"Logging:": "日志级别:",
"Disconnect": "终端链接",
"Connect": "接",
"Disconnect": "中断连接",
"Connect": "接",
"Password:": "密码:",
"Cancel": "取消"
}
+87 -17
View File
@@ -1,6 +1,6 @@
/*
* noVNC base CSS
* Copyright (C) 2018 The noVNC Authors
* Copyright (C) 2019 The noVNC Authors
* noVNC is licensed under the MPL 2.0 (see LICENSE.txt)
* This file is licensed under the 2-Clause BSD license (see LICENSE.txt).
*/
@@ -83,8 +83,20 @@ html {
* ----------------------------------------
*/
input[type=input], input[type=password], input[type=number],
input:not([type]), textarea {
input:not([type]),
input[type=date],
input[type=datetime-local],
input[type=email],
input[type=month],
input[type=number],
input[type=password],
input[type=search],
input[type=tel],
input[type=text],
input[type=time],
input[type=url],
input[type=week],
textarea {
/* Disable default rendering */
-webkit-appearance: none;
-moz-appearance: none;
@@ -98,7 +110,11 @@ input:not([type]), textarea {
background: linear-gradient(to top, rgb(255, 255, 255) 80%, rgb(240, 240, 240));
}
input[type=button], input[type=submit], select {
input[type=button],
input[type=color],
input[type=reset],
input[type=submit],
select {
/* Disable default rendering */
-webkit-appearance: none;
-moz-appearance: none;
@@ -116,7 +132,10 @@ input[type=button], input[type=submit], select {
vertical-align: middle;
}
input[type=button], input[type=submit] {
input[type=button],
input[type=color],
input[type=reset],
input[type=submit] {
padding-left: 20px;
padding-right: 20px;
}
@@ -126,35 +145,72 @@ option {
background: white;
}
input[type=input]:focus, input[type=password]:focus,
input:not([type]):focus, input[type=button]:focus,
input:not([type]):focus,
input[type=button]:focus,
input[type=color]:focus,
input[type=date]:focus,
input[type=datetime-local]:focus,
input[type=email]:focus,
input[type=month]:focus,
input[type=number]:focus,
input[type=password]:focus,
input[type=reset]:focus,
input[type=search]:focus,
input[type=submit]:focus,
textarea:focus, select:focus {
input[type=tel]:focus,
input[type=text]:focus,
input[type=time]:focus,
input[type=url]:focus,
input[type=week]:focus,
select:focus,
textarea:focus {
box-shadow: 0px 0px 3px rgba(74, 144, 217, 0.5);
border-color: rgb(74, 144, 217);
outline: none;
}
input[type=button]::-moz-focus-inner,
input[type=color]::-moz-focus-inner,
input[type=reset]::-moz-focus-inner,
input[type=submit]::-moz-focus-inner {
border: none;
}
input[type=input]:disabled, input[type=password]:disabled,
input:not([type]):disabled, input[type=button]:disabled,
input[type=submit]:disabled, input[type=number]:disabled,
textarea:disabled, select:disabled {
input:not([type]):disabled,
input[type=button]:disabled,
input[type=color]:disabled,
input[type=date]:disabled,
input[type=datetime-local]:disabled,
input[type=email]:disabled,
input[type=month]:disabled,
input[type=number]:disabled,
input[type=password]:disabled,
input[type=reset]:disabled,
input[type=search]:disabled,
input[type=submit]:disabled,
input[type=tel]:disabled,
input[type=text]:disabled,
input[type=time]:disabled,
input[type=url]:disabled,
input[type=week]:disabled,
select:disabled,
textarea:disabled {
color: rgb(128, 128, 128);
background: rgb(240, 240, 240);
}
input[type=button]:active, input[type=submit]:active,
input[type=button]:active,
input[type=color]:active,
input[type=reset]:active,
input[type=submit]:active,
select:active {
border-bottom-width: 1px;
margin-top: 3px;
}
:root:not(.noVNC_touch) input[type=button]:hover:not(:disabled),
:root:not(.noVNC_touch) input[type=color]:hover:not(:disabled),
:root:not(.noVNC_touch) input[type=reset]:hover:not(:disabled),
:root:not(.noVNC_touch) input[type=submit]:hover:not(:disabled),
:root:not(.noVNC_touch) select:hover:not(:disabled) {
background: linear-gradient(to top, rgb(255, 255, 255), rgb(250, 250, 250));
@@ -579,7 +635,7 @@ select:active {
}
/* Extra manual keys */
:root:not(.noVNC_connected) #noVNC_extra_keys {
:root:not(.noVNC_connected) #noVNC_toggle_extra_keys_button {
display: none;
}
@@ -631,6 +687,16 @@ select:active {
width: 100px;
}
/* Version */
.noVNC_version_wrapper {
font-size: small;
}
.noVNC_version {
margin-left: 1rem;
}
/* Connection Controls */
:root:not(.noVNC_connected) #noVNC_disconnect_button {
display: none;
@@ -780,19 +846,23 @@ select:active {
* ----------------------------------------
*/
#noVNC_password_dlg {
#noVNC_credentials_dlg {
position: relative;
transform: translateY(-50px);
}
#noVNC_password_dlg.noVNC_open {
#noVNC_credentials_dlg.noVNC_open {
transform: translateY(0);
}
#noVNC_password_dlg ul {
#noVNC_credentials_dlg ul {
list-style: none;
margin: 0px;
padding: 0px;
}
.noVNC_hidden {
display: none;
}
/* ----------------------------------------
* Main Area
+180 -119
View File
@@ -1,6 +1,6 @@
/*
* noVNC: HTML5 VNC client
* Copyright (C) 2018 The noVNC Authors
* Copyright (C) 2019 The noVNC Authors
* Licensed under MPL 2.0 (see LICENSE.txt)
*
* See README.md for usage and integration instructions.
@@ -8,7 +8,7 @@
import * as Log from '../core/util/logging.js';
import _, { l10n } from './localization.js';
import { isTouchDevice, isSafari, isIOS, isAndroid, dragThreshold }
import { isTouchDevice, isSafari, hasScrollbarGutter, dragThreshold }
from '../core/util/browser.js';
import { setCapture, getPointerEvent } from '../core/util/events.js';
import KeyTable from "../core/input/keysym.js";
@@ -17,6 +17,8 @@ import Keyboard from "../core/input/keyboard.js";
import RFB from "../core/rfb.js";
import * as WebUtil from "./webutil.js";
const PAGE_TITLE = "noVNC";
const UI = {
connected: false,
@@ -35,9 +37,9 @@ const UI = {
lastKeyboardinput: null,
defaultKeyboardinputLen: 100,
inhibit_reconnect: true,
reconnect_callback: null,
reconnect_password: null,
inhibitReconnect: true,
reconnectCallback: null,
reconnectPassword: null,
prime() {
return WebUtil.initSettings().then(() => {
@@ -59,6 +61,23 @@ const UI = {
// Translate the DOM
l10n.translateDOM();
fetch('./package.json')
.then((response) => {
if (!response.ok) {
throw Error("" + response.status + " " + response.statusText);
}
return response.json();
})
.then((packageInfo) => {
Array.from(document.getElementsByClassName('noVNC_version')).forEach(el => el.innerText = packageInfo.version);
})
.catch((err) => {
Log.Error("Couldn't fetch package.json: " + err);
Array.from(document.getElementsByClassName('noVNC_version_wrapper'))
.concat(Array.from(document.getElementsByClassName('noVNC_version_separator')))
.forEach(el => el.style.display = 'none');
});
// Adapt the interface for touch screen devices
if (isTouchDevice) {
document.documentElement.classList.add("noVNC_touch");
@@ -148,6 +167,8 @@ const UI = {
UI.initSetting('encrypt', (window.location.protocol === "https:"));
UI.initSetting('view_clip', false);
UI.initSetting('resize', 'off');
UI.initSetting('quality', 6);
UI.initSetting('compression', 2);
UI.initSetting('shared', true);
UI.initSetting('view_only', false);
UI.initSetting('show_dot', false);
@@ -219,14 +240,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);
@@ -303,8 +316,8 @@ const UI = {
document.getElementById("noVNC_cancel_reconnect_button")
.addEventListener('click', UI.cancelReconnect);
document.getElementById("noVNC_password_button")
.addEventListener('click', UI.setPassword);
document.getElementById("noVNC_credentials_button")
.addEventListener('click', UI.setCredentials);
},
addClipboardHandlers() {
@@ -334,6 +347,10 @@ const UI = {
UI.addSettingChangeHandler('resize');
UI.addSettingChangeHandler('resize', UI.applyResizeMode);
UI.addSettingChangeHandler('resize', UI.updateViewClip);
UI.addSettingChangeHandler('quality');
UI.addSettingChangeHandler('quality', UI.updateQuality);
UI.addSettingChangeHandler('compression');
UI.addSettingChangeHandler('compression', UI.updateCompression);
UI.addSettingChangeHandler('view_clip');
UI.addSettingChangeHandler('view_clip', UI.updateViewClip);
UI.addSettingChangeHandler('shared');
@@ -375,25 +392,25 @@ const UI = {
document.documentElement.classList.remove("noVNC_disconnecting");
document.documentElement.classList.remove("noVNC_reconnecting");
const transition_elem = document.getElementById("noVNC_transition_text");
const transitionElem = document.getElementById("noVNC_transition_text");
switch (state) {
case 'init':
break;
case 'connecting':
transition_elem.textContent = _("Connecting...");
transitionElem.textContent = _("Connecting...");
document.documentElement.classList.add("noVNC_connecting");
break;
case 'connected':
document.documentElement.classList.add("noVNC_connected");
break;
case 'disconnecting':
transition_elem.textContent = _("Disconnecting...");
transitionElem.textContent = _("Disconnecting...");
document.documentElement.classList.add("noVNC_disconnecting");
break;
case 'disconnected':
break;
case 'reconnecting':
transition_elem.textContent = _("Reconnecting...");
transitionElem.textContent = _("Reconnecting...");
document.documentElement.classList.add("noVNC_reconnecting");
break;
default:
@@ -411,7 +428,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);
@@ -426,38 +442,35 @@ const UI = {
UI.keepControlbar();
}
// State change closes the password dialog
document.getElementById('noVNC_password_dlg')
// State change closes dialogs as they may not be relevant
// anymore
UI.closeAllPanels();
document.getElementById('noVNC_credentials_dlg')
.classList.remove('noVNC_open');
},
showStatus(text, status_type, time) {
showStatus(text, statusType, time) {
const statusElem = document.getElementById('noVNC_status');
clearTimeout(UI.statusTimeout);
if (typeof status_type === 'undefined') {
status_type = 'normal';
if (typeof statusType === 'undefined') {
statusType = 'normal';
}
// Don't overwrite more severe visible statuses and never
// errors. Only shows the first error.
let visible_status_type = 'none';
if (statusElem.classList.contains("noVNC_open")) {
if (statusElem.classList.contains("noVNC_status_error")) {
visible_status_type = 'error';
} else if (statusElem.classList.contains("noVNC_status_warn")) {
visible_status_type = 'warn';
} else {
visible_status_type = 'normal';
return;
}
if (statusElem.classList.contains("noVNC_status_warn") &&
statusType === 'normal') {
return;
}
}
if (visible_status_type === 'error' ||
(visible_status_type === 'warn' && status_type === 'normal')) {
return;
}
switch (status_type) {
clearTimeout(UI.statusTimeout);
switch (statusType) {
case 'error':
statusElem.classList.remove("noVNC_status_warn");
statusElem.classList.remove("noVNC_status_normal");
@@ -487,7 +500,7 @@ const UI = {
}
// Error messages do not timeout
if (status_type !== 'error') {
if (statusType !== 'error') {
UI.statusTimeout = window.setTimeout(UI.hideStatus, time);
}
},
@@ -507,6 +520,13 @@ const UI = {
},
idleControlbar() {
// Don't fade if a child of the control bar has focus
if (document.getElementById('noVNC_control_bar')
.contains(document.activeElement) && document.hasFocus()) {
UI.activateControlbar();
return;
}
document.getElementById('noVNC_control_bar_anchor')
.classList.add("noVNC_idle");
},
@@ -524,6 +544,7 @@ const UI = {
UI.closeAllPanels();
document.getElementById('noVNC_control_bar')
.classList.remove("noVNC_open");
UI.rfb.focus();
},
toggleControlbar() {
@@ -740,11 +761,6 @@ const UI = {
}
}
} else {
/*Weird IE9 error leads to 'null' appearring
in textboxes instead of ''.*/
if (value === null) {
value = "";
}
ctrl.value = value;
}
},
@@ -821,6 +837,8 @@ const UI = {
UI.updateSetting('encrypt');
UI.updateSetting('view_clip');
UI.updateSetting('resize');
UI.updateSetting('quality');
UI.updateSetting('compression');
UI.updateSetting('shared');
UI.updateSetting('view_only');
UI.updateSetting('path');
@@ -977,7 +995,7 @@ const UI = {
if (typeof password === 'undefined') {
password = WebUtil.getConfigVar('password');
UI.reconnect_password = password;
UI.reconnectPassword = password;
}
if (password === null) {
@@ -992,7 +1010,6 @@ const UI = {
return;
}
UI.closeAllPanels();
UI.closeConnectPanel();
UI.updateVisualState('connecting');
@@ -1009,7 +1026,6 @@ const UI = {
UI.rfb = new RFB(document.getElementById('noVNC_container'), url,
{ shared: UI.getSetting('shared'),
showDotCursor: UI.getSetting('show_dot'),
repeaterID: UI.getSetting('repeaterID'),
credentials: { password: password } });
UI.rfb.addEventListener("connect", UI.connectFinished);
@@ -1023,18 +1039,20 @@ const UI = {
UI.rfb.clipViewport = UI.getSetting('view_clip');
UI.rfb.scaleViewport = UI.getSetting('resize') === 'scale';
UI.rfb.resizeSession = UI.getSetting('resize') === 'remote';
UI.rfb.qualityLevel = parseInt(UI.getSetting('quality'));
UI.rfb.compressionLevel = parseInt(UI.getSetting('compression'));
UI.rfb.showDotCursor = UI.getSetting('show_dot');
UI.updateViewOnly(); // requires UI.rfb
},
disconnect() {
UI.closeAllPanels();
UI.rfb.disconnect();
UI.connected = false;
// Disable automatic reconnecting
UI.inhibit_reconnect = true;
UI.inhibitReconnect = true;
UI.updateVisualState('disconnecting');
@@ -1042,20 +1060,20 @@ const UI = {
},
reconnect() {
UI.reconnect_callback = null;
UI.reconnectCallback = null;
// if reconnect has been disabled in the meantime, do nothing.
if (UI.inhibit_reconnect) {
if (UI.inhibitReconnect) {
return;
}
UI.connect(null, UI.reconnect_password);
UI.connect(null, UI.reconnectPassword);
},
cancelReconnect() {
if (UI.reconnect_callback !== null) {
clearTimeout(UI.reconnect_callback);
UI.reconnect_callback = null;
if (UI.reconnectCallback !== null) {
clearTimeout(UI.reconnectCallback);
UI.reconnectCallback = null;
}
UI.updateVisualState('disconnected');
@@ -1066,7 +1084,7 @@ const UI = {
connectFinished(e) {
UI.connected = true;
UI.inhibit_reconnect = false;
UI.inhibitReconnect = false;
let msg;
if (UI.getSetting('encrypt')) {
@@ -1100,17 +1118,19 @@ const UI = {
} else {
UI.showStatus(_("Failed to connect to server"), 'error');
}
} else if (UI.getSetting('reconnect', false) === true && !UI.inhibit_reconnect) {
} else if (UI.getSetting('reconnect', false) === true && !UI.inhibitReconnect) {
UI.updateVisualState('reconnecting');
const delay = parseInt(UI.getSetting('reconnect_delay'));
UI.reconnect_callback = setTimeout(UI.reconnect, delay);
UI.reconnectCallback = setTimeout(UI.reconnect, delay);
return;
} else {
UI.updateVisualState('disconnected');
UI.showStatus(_("Disconnected"), 'normal');
}
document.title = PAGE_TITLE;
UI.openControlbar();
UI.openConnectPanel();
},
@@ -1137,27 +1157,46 @@ const UI = {
credentials(e) {
// FIXME: handle more types
document.getElementById('noVNC_password_dlg')
document.getElementById("noVNC_username_block").classList.remove("noVNC_hidden");
document.getElementById("noVNC_password_block").classList.remove("noVNC_hidden");
let inputFocus = "none";
if (e.detail.types.indexOf("username") === -1) {
document.getElementById("noVNC_username_block").classList.add("noVNC_hidden");
} else {
inputFocus = inputFocus === "none" ? "noVNC_username_input" : inputFocus;
}
if (e.detail.types.indexOf("password") === -1) {
document.getElementById("noVNC_password_block").classList.add("noVNC_hidden");
} else {
inputFocus = inputFocus === "none" ? "noVNC_password_input" : inputFocus;
}
document.getElementById('noVNC_credentials_dlg')
.classList.add('noVNC_open');
setTimeout(() => document
.getElementById('noVNC_password_input').focus(), 100);
.getElementById(inputFocus).focus(), 100);
Log.Warn("Server asked for a password");
UI.showStatus(_("Password is required"), "warning");
Log.Warn("Server asked for credentials");
UI.showStatus(_("Credentials are required"), "warning");
},
setPassword(e) {
setCredentials(e) {
// Prevent actually submitting the form
e.preventDefault();
const inputElem = document.getElementById('noVNC_password_input');
const password = inputElem.value;
let inputElemUsername = document.getElementById('noVNC_username_input');
const username = inputElemUsername.value;
let inputElemPassword = document.getElementById('noVNC_password_input');
const password = inputElemPassword.value;
// Clear the input after reading the password
inputElem.value = "";
UI.rfb.sendCredentials({ password: password });
UI.reconnect_password = password;
document.getElementById('noVNC_password_dlg')
inputElemPassword.value = "";
UI.rfb.sendCredentials({ username: username, password: password });
UI.reconnectPassword = password;
document.getElementById('noVNC_credentials_dlg')
.classList.remove('noVNC_open');
},
@@ -1240,8 +1279,9 @@ const UI = {
// Can't be clipping if viewport is scaled to fit
UI.forceSetting('view_clip', false);
UI.rfb.clipViewport = false;
} else if (isIOS() || isAndroid()) {
// iOS and Android usually have shit scrollbars
} else if (!hasScrollbarGutter) {
// Some platforms have scrollbars that are difficult
// to use in our case, so we always use our own panning
UI.forceSetting('view_clip', true);
UI.rfb.clipViewport = true;
} else {
@@ -1284,30 +1324,40 @@ const UI = {
viewDragButton.classList.remove("noVNC_selected");
}
// Different behaviour for touch vs non-touch
// The button is disabled instead of hidden on touch devices
if (isTouchDevice) {
if (UI.rfb.clipViewport) {
viewDragButton.classList.remove("noVNC_hidden");
if (UI.rfb.clipViewport) {
viewDragButton.disabled = false;
} else {
viewDragButton.disabled = true;
}
} else {
viewDragButton.disabled = false;
if (UI.rfb.clipViewport) {
viewDragButton.classList.remove("noVNC_hidden");
} else {
viewDragButton.classList.add("noVNC_hidden");
}
viewDragButton.classList.add("noVNC_hidden");
}
},
/* ------^-------
* /VIEWDRAG
* ==============
* QUALITY
* ------v------*/
updateQuality() {
if (!UI.rfb) return;
UI.rfb.qualityLevel = parseInt(UI.getSetting('quality'));
},
/* ------^-------
* /QUALITY
* ==============
* COMPRESSION
* ------v------*/
updateCompression() {
if (!UI.rfb) return;
UI.rfb.compressionLevel = parseInt(UI.getSetting('compression'));
},
/* ------^-------
* /COMPRESSION
* ==============
* KEYBOARD
* ------v------*/
@@ -1502,20 +1552,20 @@ const UI = {
},
sendEsc() {
UI.rfb.sendKey(KeyTable.XK_Escape, "Escape");
UI.sendKey(KeyTable.XK_Escape, "Escape");
},
sendTab() {
UI.rfb.sendKey(KeyTable.XK_Tab);
UI.sendKey(KeyTable.XK_Tab, "Tab");
},
toggleCtrl() {
const btn = document.getElementById('noVNC_toggle_ctrl_button');
if (btn.classList.contains("noVNC_selected")) {
UI.rfb.sendKey(KeyTable.XK_Control_L, "ControlLeft", false);
UI.sendKey(KeyTable.XK_Control_L, "ControlLeft", false);
btn.classList.remove("noVNC_selected");
} else {
UI.rfb.sendKey(KeyTable.XK_Control_L, "ControlLeft", true);
UI.sendKey(KeyTable.XK_Control_L, "ControlLeft", true);
btn.classList.add("noVNC_selected");
}
},
@@ -1523,10 +1573,10 @@ const UI = {
toggleWindows() {
const btn = document.getElementById('noVNC_toggle_windows_button');
if (btn.classList.contains("noVNC_selected")) {
UI.rfb.sendKey(KeyTable.XK_Super_L, "MetaLeft", false);
UI.sendKey(KeyTable.XK_Super_L, "MetaLeft", false);
btn.classList.remove("noVNC_selected");
} else {
UI.rfb.sendKey(KeyTable.XK_Super_L, "MetaLeft", true);
UI.sendKey(KeyTable.XK_Super_L, "MetaLeft", true);
btn.classList.add("noVNC_selected");
}
},
@@ -1534,16 +1584,39 @@ const UI = {
toggleAlt() {
const btn = document.getElementById('noVNC_toggle_alt_button');
if (btn.classList.contains("noVNC_selected")) {
UI.rfb.sendKey(KeyTable.XK_Alt_L, "AltLeft", false);
UI.sendKey(KeyTable.XK_Alt_L, "AltLeft", false);
btn.classList.remove("noVNC_selected");
} else {
UI.rfb.sendKey(KeyTable.XK_Alt_L, "AltLeft", true);
UI.sendKey(KeyTable.XK_Alt_L, "AltLeft", true);
btn.classList.add("noVNC_selected");
}
},
sendCtrlAltDel() {
UI.rfb.sendCtrlAltDel();
// See below
UI.rfb.focus();
UI.idleControlbar();
},
sendKey(keysym, code, down) {
UI.rfb.sendKey(keysym, code, down);
// Move focus to the screen in order to be able to use the
// keyboard right after these extra keys.
// The exception is when a virtual keyboard is used, because
// if we focus the screen the virtual keyboard would be closed.
// In this case we focus our special virtual keyboard input
// element instead.
if (document.getElementById('noVNC_keyboard_button')
.classList.contains("noVNC_selected")) {
document.getElementById('noVNC_keyboardinput').focus();
} else {
UI.rfb.focus();
}
// fade out the controlbar to highlight that
// the focus has been moved to the screen
UI.idleControlbar();
},
/* ------^-------
@@ -1552,24 +1625,6 @@ const UI = {
* MISC
* ------v------*/
setMouseButton(num) {
const view_only = UI.rfb.viewOnly;
if (UI.rfb && !view_only) {
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 && !view_only) {
button.classList.remove("noVNC_hidden");
} else {
button.classList.add("noVNC_hidden");
}
}
},
updateViewOnly() {
if (!UI.rfb) return;
UI.rfb.viewOnly = UI.getSetting('view_only');
@@ -1580,14 +1635,14 @@ 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)
document.getElementById('noVNC_clipboard_button')
.classList.add('noVNC_hidden');
} else {
document.getElementById('noVNC_keyboard_button')
.classList.remove('noVNC_hidden');
document.getElementById('noVNC_toggle_extra_keys_button')
.classList.remove('noVNC_hidden');
document.getElementById('noVNC_mouse_button' + UI.rfb.touchButton)
document.getElementById('noVNC_clipboard_button')
.classList.remove('noVNC_hidden');
}
},
@@ -1598,13 +1653,13 @@ const UI = {
},
updateLogging() {
WebUtil.init_logging(UI.getSetting('logging'));
WebUtil.initLogging(UI.getSetting('logging'));
},
updateDesktopName(e) {
UI.desktopName = e.detail.name;
// Display the desktop name in the document title
document.title = e.detail.name + " - noVNC";
document.title = e.detail.name + " - " + PAGE_TITLE;
},
bell(e) {
@@ -1640,12 +1695,18 @@ const UI = {
};
// Set up translations
const LINGUAS = ["cs", "de", "el", "es", "ko", "nl", "pl", "ru", "sv", "tr", "zh_CN", "zh_TW"];
const LINGUAS = ["cs", "de", "el", "es", "fr", "ja", "ko", "nl", "pl", "pt_BR", "ru", "sv", "tr", "zh_CN", "zh_TW"];
l10n.setup(LINGUAS);
if (l10n.language === "en" || l10n.dictionary !== undefined) {
UI.prime();
} else {
WebUtil.fetchJSON('app/locale/' + l10n.language + '.json')
fetch('app/locale/' + l10n.language + '.json')
.then((response) => {
if (!response.ok) {
throw Error("" + response.status + " " + response.statusText);
}
return response.json();
})
.then((translations) => { l10n.dictionary = translations; })
.catch(err => Log.Error("Failed to load translations: " + err))
.then(UI.prime);
+15 -68
View File
@@ -1,29 +1,38 @@
/*
* noVNC: HTML5 VNC client
* Copyright (C) 2018 The noVNC Authors
* Copyright (C) 2019 The noVNC Authors
* Licensed under MPL 2.0 (see LICENSE.txt)
*
* See README.md for usage and integration instructions.
*/
import { init_logging as main_init_logging } from '../core/util/logging.js';
import { initLogging as mainInitLogging } from '../core/util/logging.js';
// init log level reading the logging HTTP param
export function init_logging(level) {
export function initLogging(level) {
"use strict";
if (typeof level !== "undefined") {
main_init_logging(level);
mainInitLogging(level);
} else {
const param = document.location.href.match(/logging=([A-Za-z0-9._-]*)/);
main_init_logging(param || undefined);
mainInitLogging(param || undefined);
}
}
// Read a query string variable
// A URL with a query parameter can look like this (But will most probably get logged on the http server):
// https://www.example.com?myqueryparam=myvalue
//
// For privacy (Using a hastag #, the parameters will not be sent to the server)
// the url can be requested in the following way:
// https://www.example.com#myqueryparam=myvalue&password=secreatvalue
//
// Even Mixing public and non public parameters will work:
// https://www.example.com?nonsecretparam=example.com#password=secreatvalue
export function getQueryVar(name, defVal) {
"use strict";
const re = new RegExp('.*[?&]' + name + '=([^&#]*)'),
match = document.location.href.match(re);
match = ''.concat(document.location.href, window.location.hash).match(re);
if (typeof defVal === 'undefined') { defVal = null; }
if (match) {
@@ -175,65 +184,3 @@ export function eraseSetting(name) {
localStorage.removeItem(name);
}
}
export function injectParamIfMissing(path, param, value) {
// force pretend that we're dealing with a relative path
// (assume that we wanted an extra if we pass one in)
path = "/" + path;
const elem = document.createElement('a');
elem.href = path;
const param_eq = encodeURIComponent(param) + "=";
let query;
if (elem.search) {
query = elem.search.slice(1).split('&');
} else {
query = [];
}
if (!query.some(v => v.startsWith(param_eq))) {
query.push(param_eq + encodeURIComponent(value));
elem.search = "?" + query.join("&");
}
// some browsers (e.g. IE11) may occasionally omit the leading slash
// in the elem.pathname string. Handle that case gracefully.
if (elem.pathname.charAt(0) == "/") {
return elem.pathname.slice(1) + elem.search + elem.hash;
}
return elem.pathname + elem.search + elem.hash;
}
// sadly, we can't use the Fetch API until we decide to drop
// IE11 support or polyfill promises and fetch in IE11.
// resolve will receive an object on success, while reject
// will receive either an event or an error on failure.
export function fetchJSON(path) {
return new Promise((resolve, reject) => {
// NB: IE11 doesn't support JSON as a responseType
const req = new XMLHttpRequest();
req.open('GET', path);
req.onload = () => {
if (req.status === 200) {
let resObj;
try {
resObj = JSON.parse(req.responseText);
} catch (err) {
reject(err);
}
resolve(resObj);
} else {
reject(new Error("XHR got non-200 status while trying to load '" + path + "': " + req.status));
}
};
req.onerror = evt => reject(new Error("XHR encountered an error while trying to load '" + path + "': " + evt.message));
req.ontimeout = evt => reject(new Error("XHR timed out while trying to load '" + path + "'"));
req.send();
});
}
+4 -4
View File
@@ -57,12 +57,12 @@ export default {
/* eslint-enable comma-spacing */
decode(data, offset = 0) {
let data_length = data.indexOf('=') - offset;
if (data_length < 0) { data_length = data.length - offset; }
let dataLength = data.indexOf('=') - offset;
if (dataLength < 0) { dataLength = data.length - offset; }
/* Every four characters is 3 resulting numbers */
const result_length = (data_length >> 2) * 3 + Math.floor((data_length % 4) / 1.5);
const result = new Array(result_length);
const resultLength = (dataLength >> 2) * 3 + Math.floor((dataLength % 4) / 1.5);
const result = new Array(resultLength);
// Convert one by one.
+6 -3
View File
@@ -1,8 +1,6 @@
/*
* noVNC: HTML5 VNC client
* Copyright (C) 2012 Joel Martin
* Copyright (C) 2018 Samuel Mannehed for Cendio AB
* Copyright (C) 2018 Pierre Ossman for Cendio AB
* Copyright (C) 2019 The noVNC Authors
* Licensed under MPL 2.0 (see LICENSE.txt)
*
* See README.md for usage and integration instructions.
@@ -17,6 +15,11 @@ export default class CopyRectDecoder {
let deltaX = sock.rQshift16();
let deltaY = sock.rQshift16();
if ((width === 0) || (height === 0)) {
return true;
}
display.copyImage(deltaX, deltaY, x, y, width, height);
return true;
+67 -15
View File
@@ -1,8 +1,6 @@
/*
* noVNC: HTML5 VNC client
* Copyright (C) 2012 Joel Martin
* Copyright (C) 2018 Samuel Mannehed for Cendio AB
* Copyright (C) 2018 Pierre Ossman for Cendio AB
* Copyright (C) 2019 The noVNC Authors
* Licensed under MPL 2.0 (see LICENSE.txt)
*
* See README.md for usage and integration instructions.
@@ -15,14 +13,15 @@ export default class HextileDecoder {
constructor() {
this._tiles = 0;
this._lastsubencoding = 0;
this._tileBuffer = new Uint8Array(16 * 16 * 4);
}
decodeRect(x, y, width, height, sock, display, depth) {
if (this._tiles === 0) {
this._tiles_x = Math.ceil(width / 16);
this._tiles_y = Math.ceil(height / 16);
this._total_tiles = this._tiles_x * this._tiles_y;
this._tiles = this._total_tiles;
this._tilesX = Math.ceil(width / 16);
this._tilesY = Math.ceil(height / 16);
this._totalTiles = this._tilesX * this._tilesY;
this._tiles = this._totalTiles;
}
while (this._tiles > 0) {
@@ -41,11 +40,11 @@ export default class HextileDecoder {
subencoding + ")");
}
const curr_tile = this._total_tiles - this._tiles;
const tile_x = curr_tile % this._tiles_x;
const tile_y = Math.floor(curr_tile / this._tiles_x);
const tx = x + tile_x * 16;
const ty = y + tile_y * 16;
const currTile = this._totalTiles - this._tiles;
const tileX = currTile % this._tilesX;
const tileY = Math.floor(currTile / this._tilesX);
const tx = x + tileX * 16;
const ty = y + tileY * 16;
const tw = Math.min(16, (x + width) - tx);
const th = Math.min(16, (y + height) - ty);
@@ -89,6 +88,11 @@ export default class HextileDecoder {
display.fillRect(tx, ty, tw, th, this._background);
}
} else if (subencoding & 0x01) { // Raw
let pixels = tw * th;
// Max sure the image is fully opaque
for (let i = 0;i < pixels;i++) {
rQ[rQi + i * 4 + 3] = 255;
}
display.blitImage(tx, ty, tw, th, rQ, rQi);
rQi += bytes - 1;
} else {
@@ -101,7 +105,7 @@ export default class HextileDecoder {
rQi += 4;
}
display.startTile(tx, ty, tw, th, this._background);
this._startTile(tx, ty, tw, th, this._background);
if (subencoding & 0x08) { // AnySubrects
let subrects = rQ[rQi];
rQi++;
@@ -124,10 +128,10 @@ export default class HextileDecoder {
const sw = (wh >> 4) + 1;
const sh = (wh & 0x0f) + 1;
display.subTile(sx, sy, sw, sh, color);
this._subTile(sx, sy, sw, sh, color);
}
}
display.finishTile();
this._finishTile(display);
}
sock.rQi = rQi;
this._lastsubencoding = subencoding;
@@ -136,4 +140,52 @@ export default class HextileDecoder {
return true;
}
// start updating a tile
_startTile(x, y, width, height, color) {
this._tileX = x;
this._tileY = y;
this._tileW = width;
this._tileH = height;
const red = color[0];
const green = color[1];
const blue = color[2];
const data = this._tileBuffer;
for (let i = 0; i < width * height * 4; i += 4) {
data[i] = red;
data[i + 1] = green;
data[i + 2] = blue;
data[i + 3] = 255;
}
}
// update sub-rectangle of the current tile
_subTile(x, y, w, h, color) {
const red = color[0];
const green = color[1];
const blue = color[2];
const xend = x + w;
const yend = y + h;
const data = this._tileBuffer;
const width = this._tileW;
for (let j = y; j < yend; j++) {
for (let i = x; i < xend; i++) {
const p = (i + (j * width)) * 4;
data[p] = red;
data[p + 1] = green;
data[p + 2] = blue;
data[p + 3] = 255;
}
}
}
// draw the current tile to the screen
_finishTile(display) {
display.blitImage(this._tileX, this._tileY,
this._tileW, this._tileH,
this._tileBuffer, 0);
}
}
+19 -11
View File
@@ -1,8 +1,6 @@
/*
* noVNC: HTML5 VNC client
* Copyright (C) 2012 Joel Martin
* Copyright (C) 2018 Samuel Mannehed for Cendio AB
* Copyright (C) 2018 Pierre Ossman for Cendio AB
* Copyright (C) 2019 The noVNC Authors
* Licensed under MPL 2.0 (see LICENSE.txt)
*
* See README.md for usage and integration instructions.
@@ -15,6 +13,10 @@ export default class RawDecoder {
}
decodeRect(x, y, width, height, sock, display, depth) {
if ((width === 0) || (height === 0)) {
return true;
}
if (this._lines === 0) {
this._lines = height;
}
@@ -26,29 +28,35 @@ export default class RawDecoder {
return false;
}
const cur_y = y + (height - this._lines);
const curr_height = Math.min(this._lines,
Math.floor(sock.rQlen / bytesPerLine));
const curY = y + (height - this._lines);
const currHeight = Math.min(this._lines,
Math.floor(sock.rQlen / bytesPerLine));
const pixels = width * currHeight;
let data = sock.rQ;
let index = sock.rQi;
// Convert data if needed
if (depth == 8) {
const pixels = width * curr_height;
const newdata = new Uint8Array(pixels * 4);
for (let i = 0; i < pixels; i++) {
newdata[i * 4 + 0] = ((data[index + i] >> 0) & 0x3) * 255 / 3;
newdata[i * 4 + 1] = ((data[index + i] >> 2) & 0x3) * 255 / 3;
newdata[i * 4 + 2] = ((data[index + i] >> 4) & 0x3) * 255 / 3;
newdata[i * 4 + 4] = 0;
newdata[i * 4 + 3] = 255;
}
data = newdata;
index = 0;
}
display.blitImage(x, cur_y, width, curr_height, data, index);
sock.rQskipBytes(curr_height * bytesPerLine);
this._lines -= curr_height;
// Max sure the image is fully opaque
for (let i = 0; i < pixels; i++) {
data[i * 4 + 3] = 255;
}
display.blitImage(x, curY, width, currHeight, data, index);
sock.rQskipBytes(currHeight * bytesPerLine);
this._lines -= currHeight;
if (this._lines > 0) {
return false;
}
+1 -3
View File
@@ -1,8 +1,6 @@
/*
* noVNC: HTML5 VNC client
* Copyright (C) 2012 Joel Martin
* Copyright (C) 2018 Samuel Mannehed for Cendio AB
* Copyright (C) 2018 Pierre Ossman for Cendio AB
* Copyright (C) 2019 The noVNC Authors
* Licensed under MPL 2.0 (see LICENSE.txt)
*
* See README.md for usage and integration instructions.
+32 -20
View File
@@ -1,9 +1,7 @@
/*
* noVNC: HTML5 VNC client
* Copyright (C) 2012 Joel Martin
* Copyright (C) 2019 The noVNC Authors
* (c) 2012 Michael Tinglof, Joe Balaz, Les Piech (Mercuri.ca)
* Copyright (C) 2018 Samuel Mannehed for Cendio AB
* Copyright (C) 2018 Pierre Ossman for Cendio AB
* Licensed under MPL 2.0 (see LICENSE.txt)
*
* See README.md for usage and integration instructions.
@@ -58,7 +56,7 @@ export default class TightDecoder {
} else if (this._ctl === 0x0A) {
ret = this._pngRect(x, y, width, height,
sock, display, depth);
} else if ((this._ctl & 0x80) == 0) {
} else if ((this._ctl & 0x08) == 0) {
ret = this._basicRect(this._ctl, x, y, width, height,
sock, display, depth);
} else {
@@ -82,7 +80,7 @@ export default class TightDecoder {
const rQ = sock.rQ;
display.fillRect(x, y, width, height,
[rQ[rQi + 2], rQ[rQi + 1], rQ[rQi]], false);
[rQ[rQi], rQ[rQi + 1], rQ[rQi + 2]], false);
sock.rQskipBytes(3);
return true;
@@ -94,7 +92,7 @@ export default class TightDecoder {
return false;
}
display.imageRect(x, y, "image/jpeg", data);
display.imageRect(x, y, width, height, "image/jpeg", data);
return true;
}
@@ -150,6 +148,10 @@ export default class TightDecoder {
const uncompressedSize = width * height * 3;
let data;
if (uncompressedSize === 0) {
return true;
}
if (uncompressedSize < 12) {
if (sock.rQwait("TIGHT", uncompressedSize)) {
return false;
@@ -162,13 +164,20 @@ export default class TightDecoder {
return false;
}
data = this._zlibs[streamId].inflate(data, true, uncompressedSize);
if (data.length != uncompressedSize) {
throw new Error("Incomplete zlib block");
}
this._zlibs[streamId].setInput(data);
data = this._zlibs[streamId].inflate(uncompressedSize);
this._zlibs[streamId].setInput(null);
}
display.blitRgbImage(x, y, width, height, data, 0, false);
let rgbx = new Uint8Array(width * height * 4);
for (let i = 0, j = 0; i < width * height * 4; i += 4, j += 3) {
rgbx[i] = data[j];
rgbx[i + 1] = data[j + 1];
rgbx[i + 2] = data[j + 2];
rgbx[i + 3] = 255; // Alpha
}
display.blitImage(x, y, width, height, rgbx, 0, false);
return true;
}
@@ -198,6 +207,10 @@ export default class TightDecoder {
let data;
if (uncompressedSize === 0) {
return true;
}
if (uncompressedSize < 12) {
if (sock.rQwait("TIGHT", uncompressedSize)) {
return false;
@@ -210,10 +223,9 @@ export default class TightDecoder {
return false;
}
data = this._zlibs[streamId].inflate(data, true, uncompressedSize);
if (data.length != uncompressedSize) {
throw new Error("Incomplete zlib block");
}
this._zlibs[streamId].setInput(data);
data = this._zlibs[streamId].inflate(uncompressedSize);
this._zlibs[streamId].setInput(null);
}
// Convert indexed (palette based) image data to RGB
@@ -241,7 +253,7 @@ export default class TightDecoder {
for (let b = 7; b >= 0; b--) {
dp = (y * width + x * 8 + 7 - b) * 4;
sp = (data[y * w + x] >> b & 1) * 3;
dest[dp] = palette[sp];
dest[dp] = palette[sp];
dest[dp + 1] = palette[sp + 1];
dest[dp + 2] = palette[sp + 2];
dest[dp + 3] = 255;
@@ -251,14 +263,14 @@ export default class TightDecoder {
for (let b = 7; b >= 8 - width % 8; b--) {
dp = (y * width + x * 8 + 7 - b) * 4;
sp = (data[y * w + x] >> b & 1) * 3;
dest[dp] = palette[sp];
dest[dp] = palette[sp];
dest[dp + 1] = palette[sp + 1];
dest[dp + 2] = palette[sp + 2];
dest[dp + 3] = 255;
}
}
display.blitRgbxImage(x, y, width, height, dest, 0, false);
display.blitImage(x, y, width, height, dest, 0, false);
}
_paletteRect(x, y, width, height, data, palette, display) {
@@ -267,13 +279,13 @@ export default class TightDecoder {
const total = width * height * 4;
for (let i = 0, j = 0; i < total; i += 4, j++) {
const sp = data[j] * 3;
dest[i] = palette[sp];
dest[i] = palette[sp];
dest[i + 1] = palette[sp + 1];
dest[i + 2] = palette[sp + 2];
dest[i + 3] = 255;
}
display.blitRgbxImage(x, y, width, height, dest, 0, false);
display.blitImage(x, y, width, height, dest, 0, false);
}
_gradientFilter(streamId, x, y, width, height, sock, display, depth) {
+2 -4
View File
@@ -1,8 +1,6 @@
/*
* noVNC: HTML5 VNC client
* Copyright (C) 2012 Joel Martin
* Copyright (C) 2018 Samuel Mannehed for Cendio AB
* Copyright (C) 2018 Pierre Ossman for Cendio AB
* Copyright (C) 2019 The noVNC Authors
* Licensed under MPL 2.0 (see LICENSE.txt)
*
* See README.md for usage and integration instructions.
@@ -18,7 +16,7 @@ export default class TightPNGDecoder extends TightDecoder {
return false;
}
display.imageRect(x, y, "image/png", data);
display.imageRect(x, y, width, height, "image/png", data);
return true;
}
+85
View File
@@ -0,0 +1,85 @@
/*
* 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.
*/
import { deflateInit, deflate } from "../vendor/pako/lib/zlib/deflate.js";
import { Z_FULL_FLUSH } from "../vendor/pako/lib/zlib/deflate.js";
import ZStream from "../vendor/pako/lib/zlib/zstream.js";
export default class Deflator {
constructor() {
this.strm = new ZStream();
this.chunkSize = 1024 * 10 * 10;
this.outputBuffer = new Uint8Array(this.chunkSize);
this.windowBits = 5;
deflateInit(this.strm, this.windowBits);
}
deflate(inData) {
/* eslint-disable camelcase */
this.strm.input = inData;
this.strm.avail_in = this.strm.input.length;
this.strm.next_in = 0;
this.strm.output = this.outputBuffer;
this.strm.avail_out = this.chunkSize;
this.strm.next_out = 0;
/* eslint-enable camelcase */
let lastRet = deflate(this.strm, Z_FULL_FLUSH);
let outData = new Uint8Array(this.strm.output.buffer, 0, this.strm.next_out);
if (lastRet < 0) {
throw new Error("zlib deflate failed");
}
if (this.strm.avail_in > 0) {
// Read chunks until done
let chunks = [outData];
let totalLen = outData.length;
do {
/* eslint-disable camelcase */
this.strm.output = new Uint8Array(this.chunkSize);
this.strm.next_out = 0;
this.strm.avail_out = this.chunkSize;
/* eslint-enable camelcase */
lastRet = deflate(this.strm, Z_FULL_FLUSH);
if (lastRet < 0) {
throw new Error("zlib deflate failed");
}
let chunk = new Uint8Array(this.strm.output.buffer, 0, this.strm.next_out);
totalLen += chunk.length;
chunks.push(chunk);
} while (this.strm.avail_in > 0);
// Combine chunks into a single data
let newData = new Uint8Array(totalLen);
let offset = 0;
for (let i = 0; i < chunks.length; i++) {
newData.set(chunks[i], offset);
offset += chunks[i].length;
}
outData = newData;
}
/* eslint-disable camelcase */
this.strm.input = null;
this.strm.avail_in = 0;
this.strm.next_in = 0;
/* eslint-enable camelcase */
return outData;
}
}
+76 -217
View File
@@ -1,6 +1,6 @@
/*
* noVNC: HTML5 VNC client
* Copyright (C) 2018 The noVNC Authors
* Copyright (C) 2019 The noVNC Authors
* Licensed under MPL 2.0 (see LICENSE.txt)
*
* See README.md for usage and integration instructions.
@@ -8,25 +8,20 @@
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) {
this._drawCtx = null;
this._c_forceCanvas = false;
this._renderQ = []; // queue drawing actions for in-oder rendering
this._flushing = false;
// the full frame buffer (logical canvas) size
this._fb_width = 0;
this._fb_height = 0;
this._fbWidth = 0;
this._fbHeight = 0;
this._prevDrawStyle = "";
this._tile = null;
this._tile16x16 = null;
this._tile_x = 0;
this._tile_y = 0;
Log.Debug(">> Display.constructor");
@@ -60,21 +55,12 @@ export default class Display {
Log.Debug("User Agent: " + navigator.userAgent);
this.clear();
// Check canvas features
if (!('createImageData' in this._drawCtx)) {
throw new Error("Canvas does not support createImageData");
}
this._tile16x16 = this._drawCtx.createImageData(16, 16);
Log.Debug("<< Display.constructor");
// ===== PROPERTIES =====
this._scale = 1.0;
this._clipViewport = false;
this.logo = null;
// ===== EVENT HANDLERS =====
@@ -98,11 +84,11 @@ export default class Display {
}
get width() {
return this._fb_width;
return this._fbWidth;
}
get height() {
return this._fb_height;
return this._fbHeight;
}
// ===== PUBLIC METHODS =====
@@ -125,15 +111,15 @@ export default class Display {
if (deltaX < 0 && vp.x + deltaX < 0) {
deltaX = -vp.x;
}
if (vx2 + deltaX >= this._fb_width) {
deltaX -= vx2 + deltaX - this._fb_width + 1;
if (vx2 + deltaX >= this._fbWidth) {
deltaX -= vx2 + deltaX - this._fbWidth + 1;
}
if (vp.y + deltaY < 0) {
deltaY = -vp.y;
}
if (vy2 + deltaY >= this._fb_height) {
deltaY -= (vy2 + deltaY - this._fb_height + 1);
if (vy2 + deltaY >= this._fbHeight) {
deltaY -= (vy2 + deltaY - this._fbHeight + 1);
}
if (deltaX === 0 && deltaY === 0) {
@@ -156,18 +142,18 @@ export default class Display {
typeof(height) === "undefined") {
Log.Debug("Setting viewport to full display region");
width = this._fb_width;
height = this._fb_height;
width = this._fbWidth;
height = this._fbHeight;
}
width = Math.floor(width);
height = Math.floor(height);
if (width > this._fb_width) {
width = this._fb_width;
if (width > this._fbWidth) {
width = this._fbWidth;
}
if (height > this._fb_height) {
height = this._fb_height;
if (height > this._fbHeight) {
height = this._fbHeight;
}
const vp = this._viewportLoc;
@@ -194,21 +180,21 @@ 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) {
this._prevDrawStyle = "";
this._fb_width = width;
this._fb_height = height;
this._fbWidth = width;
this._fbHeight = height;
const canvas = this._backbuffer;
if (canvas.width !== width || canvas.height !== height) {
@@ -256,9 +242,9 @@ export default class Display {
// Update the visible canvas with the contents of the
// rendering canvas
flip(from_queue) {
if (this._renderQ.length !== 0 && !from_queue) {
this._renderQ_push({
flip(fromQueue) {
if (this._renderQ.length !== 0 && !fromQueue) {
this._renderQPush({
'type': 'flip'
});
} else {
@@ -302,17 +288,6 @@ export default class Display {
}
}
clear() {
if (this._logo) {
this.resize(this._logo.width, this._logo.height);
this.imageRect(0, 0, this._logo.type, this._logo.data);
} else {
this.resize(240, 20);
this._drawCtx.clearRect(0, 0, this._fb_width, this._fb_height);
}
this.flip();
}
pending() {
return this._renderQ.length > 0;
}
@@ -325,9 +300,9 @@ export default class Display {
}
}
fillRect(x, y, width, height, color, from_queue) {
if (this._renderQ.length !== 0 && !from_queue) {
this._renderQ_push({
fillRect(x, y, width, height, color, fromQueue) {
if (this._renderQ.length !== 0 && !fromQueue) {
this._renderQPush({
'type': 'fill',
'x': x,
'y': y,
@@ -342,14 +317,14 @@ export default class Display {
}
}
copyImage(old_x, old_y, new_x, new_y, w, h, from_queue) {
if (this._renderQ.length !== 0 && !from_queue) {
this._renderQ_push({
copyImage(oldX, oldY, newX, newY, w, h, fromQueue) {
if (this._renderQ.length !== 0 && !fromQueue) {
this._renderQPush({
'type': 'copy',
'old_x': old_x,
'old_y': old_y,
'x': new_x,
'y': new_y,
'oldX': oldX,
'oldY': oldY,
'x': newX,
'y': newY,
'width': w,
'height': h,
});
@@ -367,131 +342,54 @@ export default class Display {
this._drawCtx.imageSmoothingEnabled = false;
this._drawCtx.drawImage(this._backbuffer,
old_x, old_y, w, h,
new_x, new_y, w, h);
this._damage(new_x, new_y, w, h);
oldX, oldY, w, h,
newX, newY, w, h);
this._damage(newX, newY, w, h);
}
}
imageRect(x, y, mime, arr) {
imageRect(x, y, width, height, mime, arr) {
/* The internal logic cannot handle empty images, so bail early */
if ((width === 0) || (height === 0)) {
return;
}
const img = new Image();
img.src = "data: " + mime + ";base64," + Base64.encode(arr);
this._renderQ_push({
this._renderQPush({
'type': 'img',
'img': img,
'x': x,
'y': y
'y': y,
'width': width,
'height': height
});
}
// start updating a tile
startTile(x, y, width, height, color) {
this._tile_x = x;
this._tile_y = y;
if (width === 16 && height === 16) {
this._tile = this._tile16x16;
} else {
this._tile = this._drawCtx.createImageData(width, height);
}
const red = color[2];
const green = color[1];
const blue = color[0];
const data = this._tile.data;
for (let i = 0; i < width * height * 4; i += 4) {
data[i] = red;
data[i + 1] = green;
data[i + 2] = blue;
data[i + 3] = 255;
}
}
// update sub-rectangle of the current tile
subTile(x, y, w, h, color) {
const red = color[2];
const green = color[1];
const blue = color[0];
const xend = x + w;
const yend = y + h;
const data = this._tile.data;
const width = this._tile.width;
for (let j = y; j < yend; j++) {
for (let i = x; i < xend; i++) {
const p = (i + (j * width)) * 4;
data[p] = red;
data[p + 1] = green;
data[p + 2] = blue;
data[p + 3] = 255;
}
}
}
// draw the current tile to the screen
finishTile() {
this._drawCtx.putImageData(this._tile, this._tile_x, this._tile_y);
this._damage(this._tile_x, this._tile_y,
this._tile.width, this._tile.height);
}
blitImage(x, y, width, height, arr, offset, from_queue) {
if (this._renderQ.length !== 0 && !from_queue) {
blitImage(x, y, width, height, arr, offset, fromQueue) {
if (this._renderQ.length !== 0 && !fromQueue) {
// NB(directxman12): it's technically more performant here to use preallocated arrays,
// but it's a lot of extra work for not a lot of payoff -- if we're using the render queue,
// this probably isn't getting called *nearly* as much
const new_arr = new Uint8Array(width * height * 4);
new_arr.set(new Uint8Array(arr.buffer, 0, new_arr.length));
this._renderQ_push({
const newArr = new Uint8Array(width * height * 4);
newArr.set(new Uint8Array(arr.buffer, 0, newArr.length));
this._renderQPush({
'type': 'blit',
'data': new_arr,
'data': newArr,
'x': x,
'y': y,
'width': width,
'height': height,
});
} else {
this._bgrxImageData(x, y, width, height, arr, offset);
}
}
blitRgbImage(x, y, width, height, arr, offset, from_queue) {
if (this._renderQ.length !== 0 && !from_queue) {
// NB(directxman12): it's technically more performant here to use preallocated arrays,
// but it's a lot of extra work for not a lot of payoff -- if we're using the render queue,
// this probably isn't getting called *nearly* as much
const new_arr = new Uint8Array(width * height * 3);
new_arr.set(new Uint8Array(arr.buffer, 0, new_arr.length));
this._renderQ_push({
'type': 'blitRgb',
'data': new_arr,
'x': x,
'y': y,
'width': width,
'height': height,
});
} else {
this._rgbImageData(x, y, width, height, arr, offset);
}
}
blitRgbxImage(x, y, width, height, arr, offset, from_queue) {
if (this._renderQ.length !== 0 && !from_queue) {
// NB(directxman12): it's technically more performant here to use preallocated arrays,
// but it's a lot of extra work for not a lot of payoff -- if we're using the render queue,
// this probably isn't getting called *nearly* as much
const new_arr = new Uint8Array(width * height * 4);
new_arr.set(new Uint8Array(arr.buffer, 0, new_arr.length));
this._renderQ_push({
'type': 'blitRgbx',
'data': new_arr,
'x': x,
'y': y,
'width': width,
'height': height,
});
} else {
this._rgbxImageData(x, y, width, height, arr, offset);
// NB(directxman12): arr must be an Type Array view
let data = new Uint8ClampedArray(arr.buffer,
arr.byteOffset + offset,
width * height * 4);
let img = new ImageData(data, width, height);
this._drawCtx.putImageData(img, x, y);
this._damage(x, y, width, height);
}
}
@@ -543,69 +441,30 @@ export default class Display {
}
_setFillColor(color) {
const newStyle = 'rgb(' + color[2] + ',' + color[1] + ',' + color[0] + ')';
const newStyle = 'rgb(' + color[0] + ',' + color[1] + ',' + color[2] + ')';
if (newStyle !== this._prevDrawStyle) {
this._drawCtx.fillStyle = newStyle;
this._prevDrawStyle = newStyle;
}
}
_rgbImageData(x, y, width, height, arr, offset) {
const img = this._drawCtx.createImageData(width, height);
const data = img.data;
for (let i = 0, j = offset; i < width * height * 4; i += 4, j += 3) {
data[i] = arr[j];
data[i + 1] = arr[j + 1];
data[i + 2] = arr[j + 2];
data[i + 3] = 255; // Alpha
}
this._drawCtx.putImageData(img, x, y);
this._damage(x, y, img.width, img.height);
}
_bgrxImageData(x, y, width, height, arr, offset) {
const img = this._drawCtx.createImageData(width, height);
const data = img.data;
for (let i = 0, j = offset; i < width * height * 4; i += 4, j += 4) {
data[i] = arr[j + 2];
data[i + 1] = arr[j + 1];
data[i + 2] = arr[j];
data[i + 3] = 255; // Alpha
}
this._drawCtx.putImageData(img, x, y);
this._damage(x, y, img.width, img.height);
}
_rgbxImageData(x, y, width, height, arr, offset) {
// NB(directxman12): arr must be an Type Array view
let img;
if (supportsImageMetadata) {
img = new ImageData(new Uint8ClampedArray(arr.buffer, arr.byteOffset, width * height * 4), width, height);
} else {
img = this._drawCtx.createImageData(width, height);
img.data.set(new Uint8ClampedArray(arr.buffer, arr.byteOffset, width * height * 4));
}
this._drawCtx.putImageData(img, x, y);
this._damage(x, y, img.width, img.height);
}
_renderQ_push(action) {
_renderQPush(action) {
this._renderQ.push(action);
if (this._renderQ.length === 1) {
// If this can be rendered immediately it will be, otherwise
// the scanner will wait for the relevant event
this._scan_renderQ();
this._scanRenderQ();
}
}
_resume_renderQ() {
_resumeRenderQ() {
// "this" is the object that is ready, not the
// display object
this.removeEventListener('load', this._noVNC_display._resume_renderQ);
this._noVNC_display._scan_renderQ();
this.removeEventListener('load', this._noVNCDisplay._resumeRenderQ);
this._noVNCDisplay._scanRenderQ();
}
_scan_renderQ() {
_scanRenderQ() {
let ready = true;
while (ready && this._renderQ.length > 0) {
const a = this._renderQ[0];
@@ -614,7 +473,7 @@ export default class Display {
this.flip(true);
break;
case 'copy':
this.copyImage(a.old_x, a.old_y, a.x, a.y, a.width, a.height, true);
this.copyImage(a.oldX, a.oldY, a.x, a.y, a.width, a.height, true);
break;
case 'fill':
this.fillRect(a.x, a.y, a.width, a.height, a.color, true);
@@ -622,18 +481,18 @@ export default class Display {
case 'blit':
this.blitImage(a.x, a.y, a.width, a.height, a.data, 0, true);
break;
case 'blitRgb':
this.blitRgbImage(a.x, a.y, a.width, a.height, a.data, 0, true);
break;
case 'blitRgbx':
this.blitRgbxImage(a.x, a.y, a.width, a.height, a.data, 0, true);
break;
case 'img':
if (a.img.complete) {
if (a.img.width !== a.width || a.img.height !== a.height) {
Log.Error("Decoded image has incorrect dimensions. Got " +
a.img.width + "x" + a.img.height + ". Expected " +
a.width + "x" + a.height + ".");
return;
}
this.drawImage(a.img, a.x, a.y);
} else {
a.img._noVNC_display = this;
a.img.addEventListener('load', this._resume_renderQ);
a.img._noVNCDisplay = this;
a.img.addEventListener('load', this._resumeRenderQ);
// We need to wait for this image to 'load'
// to keep things in-order
ready = false;
+4 -1
View File
@@ -1,6 +1,6 @@
/*
* noVNC: HTML5 VNC client
* Copyright (C) 2018 The noVNC Authors
* Copyright (C) 2019 The noVNC Authors
* Licensed under MPL 2.0 (see LICENSE.txt)
*
* See README.md for usage and integration instructions.
@@ -20,12 +20,15 @@ export const encodings = {
pseudoEncodingLastRect: -224,
pseudoEncodingCursor: -239,
pseudoEncodingQEMUExtendedKeyEvent: -258,
pseudoEncodingDesktopName: -307,
pseudoEncodingExtendedDesktopSize: -308,
pseudoEncodingXvp: -309,
pseudoEncodingFence: -312,
pseudoEncodingContinuousUpdates: -313,
pseudoEncodingCompressLevel9: -247,
pseudoEncodingCompressLevel0: -256,
pseudoEncodingVMwareCursor: 0x574d5664,
pseudoEncodingExtendedClipboard: 0xc0a1e5ce
};
export function encodingName(num) {
+35 -7
View File
@@ -1,3 +1,11 @@
/*
* 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.
*/
import { inflateInit, inflate, inflateReset } from "../vendor/pako/lib/zlib/inflate.js";
import ZStream from "../vendor/pako/lib/zlib/zstream.js";
@@ -11,12 +19,22 @@ export default class Inflate {
inflateInit(this.strm, this.windowBits);
}
inflate(data, flush, expected) {
this.strm.input = data;
this.strm.avail_in = this.strm.input.length;
this.strm.next_in = 0;
this.strm.next_out = 0;
setInput(data) {
if (!data) {
//FIXME: flush remaining data.
/* eslint-disable camelcase */
this.strm.input = null;
this.strm.avail_in = 0;
this.strm.next_in = 0;
} else {
this.strm.input = data;
this.strm.avail_in = this.strm.input.length;
this.strm.next_in = 0;
/* eslint-enable camelcase */
}
}
inflate(expected) {
// resize our output buffer if it's too small
// (we could just use multiple chunks, but that would cause an extra
// allocation each time to flatten the chunks)
@@ -25,9 +43,19 @@ export default class Inflate {
this.strm.output = new Uint8Array(this.chunkSize);
}
this.strm.avail_out = this.chunkSize;
/* eslint-disable camelcase */
this.strm.next_out = 0;
this.strm.avail_out = expected;
/* eslint-enable camelcase */
inflate(this.strm, flush);
let ret = inflate(this.strm, 0); // Flush argument not used.
if (ret < 0) {
throw new Error("zlib inflate failed");
}
if (this.strm.next_out != expected) {
throw new Error("Incomplete zlib block");
}
return new Uint8Array(this.strm.output.buffer, 0, this.strm.next_out);
}
+32 -28
View File
@@ -35,7 +35,7 @@ function addNumpad(key, standard, numpad) {
DOMKeyTable[key] = [standard, standard, standard, numpad];
}
// 2.2. Modifier Keys
// 3.2. Modifier Keys
addLeftRight("Alt", KeyTable.XK_Alt_L, KeyTable.XK_Alt_R);
addStandard("AltGraph", KeyTable.XK_ISO_Level3_Shift);
@@ -43,35 +43,38 @@ addStandard("CapsLock", KeyTable.XK_Caps_Lock);
addLeftRight("Control", KeyTable.XK_Control_L, KeyTable.XK_Control_R);
// - Fn
// - FnLock
addLeftRight("Hyper", KeyTable.XK_Super_L, KeyTable.XK_Super_R);
addLeftRight("Meta", KeyTable.XK_Super_L, KeyTable.XK_Super_R);
addStandard("NumLock", KeyTable.XK_Num_Lock);
addStandard("ScrollLock", KeyTable.XK_Scroll_Lock);
addLeftRight("Shift", KeyTable.XK_Shift_L, KeyTable.XK_Shift_R);
addLeftRight("Super", KeyTable.XK_Super_L, KeyTable.XK_Super_R);
// - Symbol
// - SymbolLock
// - Hyper
// - Super
// 2.3. Whitespace Keys
// 3.3. Whitespace Keys
addNumpad("Enter", KeyTable.XK_Return, KeyTable.XK_KP_Enter);
addStandard("Tab", KeyTable.XK_Tab);
addNumpad(" ", KeyTable.XK_space, KeyTable.XK_KP_Space);
// 2.4. Navigation Keys
// 3.4. Navigation Keys
addNumpad("ArrowDown", KeyTable.XK_Down, KeyTable.XK_KP_Down);
addNumpad("ArrowUp", KeyTable.XK_Up, KeyTable.XK_KP_Up);
addNumpad("ArrowLeft", KeyTable.XK_Left, KeyTable.XK_KP_Left);
addNumpad("ArrowRight", KeyTable.XK_Right, KeyTable.XK_KP_Right);
addNumpad("ArrowUp", KeyTable.XK_Up, KeyTable.XK_KP_Up);
addNumpad("End", KeyTable.XK_End, KeyTable.XK_KP_End);
addNumpad("Home", KeyTable.XK_Home, KeyTable.XK_KP_Home);
addNumpad("PageDown", KeyTable.XK_Next, KeyTable.XK_KP_Next);
addNumpad("PageUp", KeyTable.XK_Prior, KeyTable.XK_KP_Prior);
// 2.5. Editing Keys
// 3.5. Editing Keys
addStandard("Backspace", KeyTable.XK_BackSpace);
// Browsers send "Clear" for the numpad 5 without NumLock because
// Windows uses VK_Clear for that key. But Unix expects KP_Begin for
// that scenario.
addNumpad("Clear", KeyTable.XK_Clear, KeyTable.XK_KP_Begin);
addStandard("Copy", KeyTable.XF86XK_Copy);
// - CrSel
@@ -84,7 +87,7 @@ addStandard("Paste", KeyTable.XF86XK_Paste);
addStandard("Redo", KeyTable.XK_Redo);
addStandard("Undo", KeyTable.XK_Undo);
// 2.6. UI Keys
// 3.6. UI Keys
// - Accept
// - Again (could just be XK_Redo)
@@ -102,7 +105,7 @@ addStandard("Select", KeyTable.XK_Select);
addStandard("ZoomIn", KeyTable.XF86XK_ZoomIn);
addStandard("ZoomOut", KeyTable.XF86XK_ZoomOut);
// 2.7. Device Keys
// 3.7. Device Keys
addStandard("BrightnessDown", KeyTable.XF86XK_MonBrightnessDown);
addStandard("BrightnessUp", KeyTable.XF86XK_MonBrightnessUp);
@@ -115,10 +118,10 @@ addStandard("Hibernate", KeyTable.XF86XK_Hibernate);
addStandard("Standby", KeyTable.XF86XK_Standby);
addStandard("WakeUp", KeyTable.XF86XK_WakeUp);
// 2.8. IME and Composition Keys
// 3.8. IME and Composition Keys
addStandard("AllCandidates", KeyTable.XK_MultipleCandidate);
addStandard("Alphanumeric", KeyTable.XK_Eisu_Shift); // could also be _Eisu_Toggle
addStandard("Alphanumeric", KeyTable.XK_Eisu_toggle);
addStandard("CodeInput", KeyTable.XK_Codeinput);
addStandard("Compose", KeyTable.XK_Multi_key);
addStandard("Convert", KeyTable.XK_Henkan);
@@ -136,7 +139,7 @@ addStandard("PreviousCandidate", KeyTable.XK_PreviousCandidate);
addStandard("SingleCandidate", KeyTable.XK_SingleCandidate);
addStandard("HangulMode", KeyTable.XK_Hangul);
addStandard("HanjaMode", KeyTable.XK_Hangul_Hanja);
addStandard("JunjuaMode", KeyTable.XK_Hangul_Jeonja);
addStandard("JunjaMode", KeyTable.XK_Hangul_Jeonja);
addStandard("Eisu", KeyTable.XK_Eisu_toggle);
addStandard("Hankaku", KeyTable.XK_Hankaku);
addStandard("Hiragana", KeyTable.XK_Hiragana);
@@ -146,9 +149,9 @@ addStandard("KanjiMode", KeyTable.XK_Kanji);
addStandard("Katakana", KeyTable.XK_Katakana);
addStandard("Romaji", KeyTable.XK_Romaji);
addStandard("Zenkaku", KeyTable.XK_Zenkaku);
addStandard("ZenkakuHanaku", KeyTable.XK_Zenkaku_Hankaku);
addStandard("ZenkakuHankaku", KeyTable.XK_Zenkaku_Hankaku);
// 2.9. General-Purpose Function Keys
// 3.9. General-Purpose Function Keys
addStandard("F1", KeyTable.XK_F1);
addStandard("F2", KeyTable.XK_F2);
@@ -187,17 +190,19 @@ addStandard("F34", KeyTable.XK_F34);
addStandard("F35", KeyTable.XK_F35);
// - Soft1...
// 2.10. Multimedia Keys
// 3.10. Multimedia Keys
// - ChannelDown
// - ChannelUp
addStandard("Close", KeyTable.XF86XK_Close);
addStandard("MailForward", KeyTable.XF86XK_MailForward);
addStandard("MailReply", KeyTable.XF86XK_Reply);
addStandard("MainSend", KeyTable.XF86XK_Send);
addStandard("MailSend", KeyTable.XF86XK_Send);
// - MediaClose
addStandard("MediaFastForward", KeyTable.XF86XK_AudioForward);
addStandard("MediaPause", KeyTable.XF86XK_AudioPause);
addStandard("MediaPlay", KeyTable.XF86XK_AudioPlay);
// - MediaPlayPause
addStandard("MediaRecord", KeyTable.XF86XK_AudioRecord);
addStandard("MediaRewind", KeyTable.XF86XK_AudioRewind);
addStandard("MediaStop", KeyTable.XF86XK_AudioStop);
@@ -209,20 +214,18 @@ addStandard("Print", KeyTable.XK_Print);
addStandard("Save", KeyTable.XF86XK_Save);
addStandard("SpellCheck", KeyTable.XF86XK_Spell);
// 2.11. Multimedia Numpad Keys
// 3.11. Multimedia Numpad Keys
// - Key11
// - Key12
// 2.12. Audio Keys
// 3.12. Audio Keys
// - AudioBalanceLeft
// - AudioBalanceRight
// - AudioBassDown
// - AudioBassBoostDown
// - AudioBassBoostToggle
// - AudioBassBoostUp
// - AudioBassUp
// - AudioFaderFront
// - AudioFaderRear
// - AudioSurroundModeNext
@@ -236,19 +239,20 @@ addStandard("AudioVolumeMute", KeyTable.XF86XK_AudioMute);
// - MicrophoneVolumeUp
addStandard("MicrophoneVolumeMute", KeyTable.XF86XK_AudioMicMute);
// 2.13. Speech Keys
// 3.13. Speech Keys
// - SpeechCorrectionList
// - SpeechInputToggle
// 2.14. Application Keys
// 3.14. Application Keys
addStandard("LaunchCalculator", KeyTable.XF86XK_Calculator);
addStandard("LaunchApplication1", KeyTable.XF86XK_MyComputer);
addStandard("LaunchApplication2", KeyTable.XF86XK_Calculator);
addStandard("LaunchCalendar", KeyTable.XF86XK_Calendar);
// - LaunchContacts
addStandard("LaunchMail", KeyTable.XF86XK_Mail);
addStandard("LaunchMediaPlayer", KeyTable.XF86XK_AudioMedia);
addStandard("LaunchMusicPlayer", KeyTable.XF86XK_Music);
addStandard("LaunchMyComputer", KeyTable.XF86XK_MyComputer);
addStandard("LaunchPhone", KeyTable.XF86XK_Phone);
addStandard("LaunchScreenSaver", KeyTable.XF86XK_ScreenSaver);
addStandard("LaunchSpreadsheet", KeyTable.XF86XK_Excel);
@@ -256,7 +260,7 @@ addStandard("LaunchWebBrowser", KeyTable.XF86XK_WWW);
addStandard("LaunchWebCam", KeyTable.XF86XK_WebCam);
addStandard("LaunchWordProcessor", KeyTable.XF86XK_Word);
// 2.15. Browser Keys
// 3.15. Browser Keys
addStandard("BrowserBack", KeyTable.XF86XK_Back);
addStandard("BrowserFavorites", KeyTable.XF86XK_Favorites);
@@ -266,15 +270,15 @@ addStandard("BrowserRefresh", KeyTable.XF86XK_Refresh);
addStandard("BrowserSearch", KeyTable.XF86XK_Search);
addStandard("BrowserStop", KeyTable.XF86XK_Stop);
// 2.16. Mobile Phone Keys
// 3.16. Mobile Phone Keys
// - A whole bunch...
// 2.17. TV Keys
// 3.17. TV Keys
// - A whole bunch...
// 2.18. Media Controller Keys
// 3.18. Media Controller Keys
// - A whole bunch...
addStandard("Dimmer", KeyTable.XF86XK_BrightnessAdjust);
+567
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 } };
}
}
+32 -129
View File
@@ -1,6 +1,6 @@
/*
* noVNC: HTML5 VNC client
* Copyright (C) 2018 The noVNC Authors
* Copyright (C) 2019 The noVNC Authors
* Licensed under MPL 2.0 or any later version (see LICENSE.txt)
*/
@@ -20,16 +20,13 @@ export default class Keyboard {
this._keyDownList = {}; // List of depressed keys
// (even if they are happy)
this._pendingKey = null; // Key waiting for keypress
this._altGrArmed = false; // Windows AltGr detection
// keep these here so we can refer to them later
this._eventHandlers = {
'keyup': this._handleKeyUp.bind(this),
'keydown': this._handleKeyDown.bind(this),
'keypress': this._handleKeyPress.bind(this),
'blur': this._allKeysUp.bind(this),
'checkalt': this._checkAlt.bind(this),
};
// ===== EVENT HANDLERS =====
@@ -62,9 +59,7 @@ export default class Keyboard {
}
// Unstable, but we don't have anything else to go on
// (don't use it for 'keypress' events thought since
// WebKit sets it to the same as charCode)
if (e.keyCode && (e.type !== 'keypress')) {
if (e.keyCode) {
// 229 is used for composition events
if (e.keyCode !== 229) {
return 'Platform' + e.keyCode;
@@ -118,9 +113,7 @@ export default class Keyboard {
// We cannot handle keys we cannot track, but we also need
// to deal with virtual keyboards which omit key info
// (iOS omits tracking info on keyup events, which forces us to
// special treat that platform here)
if ((code === 'Unidentified') || browser.isIOS()) {
if (code === 'Unidentified') {
if (keysym) {
// If it's a virtual keyboard then it should be
// sufficient to just send press and release right
@@ -137,7 +130,7 @@ export default class Keyboard {
// keys around a bit to make things more sane for the remote
// server. This method is used by RealVNC and TigerVNC (and
// possibly others).
if (browser.isMac()) {
if (browser.isMac() || browser.isIOS()) {
switch (keysym) {
case KeyTable.XK_Super_L:
keysym = KeyTable.XK_Alt_L;
@@ -164,27 +157,27 @@ export default class Keyboard {
// state change events. That gets extra confusing for CapsLock
// which toggles on each press, but not on release. So pretend
// it was a quick press and release of the button.
if (browser.isMac() && (code === 'CapsLock')) {
if ((browser.isMac() || browser.isIOS()) && (code === 'CapsLock')) {
this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', true);
this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', false);
stopEvent(e);
return;
}
// If this is a legacy browser then we'll need to wait for
// a keypress event as well
// (IE and Edge has a broken KeyboardEvent.key, so we can't
// just check for the presence of that field)
if (!keysym && (!e.key || browser.isIE() || browser.isEdge())) {
this._pendingKey = code;
// However we might not get a keypress event if the key
// is non-printable, which needs some special fallback
// handling
setTimeout(this._handleKeyPressTimeout.bind(this), 10, e);
// Windows doesn't send proper key releases for a bunch of
// Japanese IM keys so we have to fake the release right away
const jpBadKeys = [ KeyTable.XK_Zenkaku_Hankaku,
KeyTable.XK_Eisu_toggle,
KeyTable.XK_Katakana,
KeyTable.XK_Hiragana,
KeyTable.XK_Romaji ];
if (browser.isWindows() && jpBadKeys.includes(keysym)) {
this._sendKeyEvent(keysym, code, true);
this._sendKeyEvent(keysym, code, false);
stopEvent(e);
return;
}
this._pendingKey = null;
stopEvent(e);
// Possible start of AltGr sequence? (see above)
@@ -199,69 +192,6 @@ export default class Keyboard {
this._sendKeyEvent(keysym, code, true);
}
// Legacy event for browsers without code/key
_handleKeyPress(e) {
stopEvent(e);
// Are we expecting a keypress?
if (this._pendingKey === null) {
return;
}
let code = this._getKeyCode(e);
const keysym = KeyboardUtil.getKeysym(e);
// The key we were waiting for?
if ((code !== 'Unidentified') && (code != this._pendingKey)) {
return;
}
code = this._pendingKey;
this._pendingKey = null;
if (!keysym) {
Log.Info('keypress with no keysym:', e);
return;
}
this._sendKeyEvent(keysym, code, true);
}
_handleKeyPressTimeout(e) {
// Did someone manage to sort out the key already?
if (this._pendingKey === null) {
return;
}
let keysym;
const code = this._pendingKey;
this._pendingKey = null;
// We have no way of knowing the proper keysym with the
// information given, but the following are true for most
// layouts
if ((e.keyCode >= 0x30) && (e.keyCode <= 0x39)) {
// Digit
keysym = e.keyCode;
} else if ((e.keyCode >= 0x41) && (e.keyCode <= 0x5a)) {
// Character (A-Z)
let char = String.fromCharCode(e.keyCode);
// A feeble attempt at the correct case
if (e.shiftKey) {
char = char.toUpperCase();
} else {
char = char.toLowerCase();
}
keysym = char.charCodeAt();
} else {
// Unknown, give up
keysym = 0;
}
this._sendKeyEvent(keysym, code, true);
}
_handleKeyUp(e) {
stopEvent(e);
@@ -276,13 +206,28 @@ export default class Keyboard {
}
// See comment in _handleKeyDown()
if (browser.isMac() && (code === 'CapsLock')) {
if ((browser.isMac() || browser.isIOS()) && (code === 'CapsLock')) {
this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', true);
this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', false);
return;
}
this._sendKeyEvent(this._keyDownList[code], code, false);
// Windows has a rather nasty bug where it won't send key
// release events for a Shift button if the other Shift is still
// pressed
if (browser.isWindows() && ((code === 'ShiftLeft') ||
(code === 'ShiftRight'))) {
if ('ShiftRight' in this._keyDownList) {
this._sendKeyEvent(this._keyDownList['ShiftRight'],
'ShiftRight', false);
}
if ('ShiftLeft' in this._keyDownList) {
this._sendKeyEvent(this._keyDownList['ShiftLeft'],
'ShiftLeft', false);
}
}
}
_handleAltGrTimeout() {
@@ -299,26 +244,6 @@ export default class Keyboard {
Log.Debug("<< Keyboard.allKeysUp");
}
// Firefox Alt workaround, see below
_checkAlt(e) {
if (e.altKey) {
return;
}
const target = this._target;
const downList = this._keyDownList;
['AltLeft', 'AltRight'].forEach((code) => {
if (!(code in downList)) {
return;
}
const event = new KeyboardEvent('keyup',
{ key: downList[code],
code: code });
target.dispatchEvent(event);
});
}
// ===== PUBLIC METHODS =====
grab() {
@@ -326,40 +251,18 @@ export default class Keyboard {
this._target.addEventListener('keydown', this._eventHandlers.keydown);
this._target.addEventListener('keyup', this._eventHandlers.keyup);
this._target.addEventListener('keypress', this._eventHandlers.keypress);
// Release (key up) if window loses focus
window.addEventListener('blur', this._eventHandlers.blur);
// Firefox has broken handling of Alt, so we need to poll as
// best we can for releases (still doesn't prevent the menu
// from popping up though as we can't call preventDefault())
if (browser.isWindows() && browser.isFirefox()) {
const handler = this._eventHandlers.checkalt;
['mousedown', 'mouseup', 'mousemove', 'wheel',
'touchstart', 'touchend', 'touchmove',
'keydown', 'keyup'].forEach(type =>
document.addEventListener(type, handler,
{ capture: true,
passive: true }));
}
//Log.Debug("<< Keyboard.grab");
}
ungrab() {
//Log.Debug(">> Keyboard.ungrab");
if (browser.isWindows() && browser.isFirefox()) {
const handler = this._eventHandlers.checkalt;
['mousedown', 'mouseup', 'mousemove', 'wheel',
'touchstart', 'touchend', 'touchmove',
'keydown', 'keyup'].forEach(type => document.removeEventListener(type, handler));
}
this._target.removeEventListener('keydown', this._eventHandlers.keydown);
this._target.removeEventListener('keyup', this._eventHandlers.keyup);
this._target.removeEventListener('keypress', this._eventHandlers.keypress);
window.removeEventListener('blur', this._eventHandlers.blur);
// Release (key up) all keys that are in a down state
-276
View File
@@ -1,276 +0,0 @@
/*
* noVNC: HTML5 VNC client
* Copyright (C) 2018 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 click/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() {
if (isTouchDevice) {
this._target.addEventListener('touchstart', this._eventHandlers.mousedown);
this._target.addEventListener('touchend', this._eventHandlers.mouseup);
this._target.addEventListener('touchmove', this._eventHandlers.mousemove);
}
this._target.addEventListener('mousedown', this._eventHandlers.mousedown);
this._target.addEventListener('mouseup', this._eventHandlers.mouseup);
this._target.addEventListener('mousemove', this._eventHandlers.mousemove);
this._target.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 */
this._target.addEventListener('contextmenu', this._eventHandlers.mousedisable);
}
ungrab() {
this._resetWheelStepTimers();
if (isTouchDevice) {
this._target.removeEventListener('touchstart', this._eventHandlers.mousedown);
this._target.removeEventListener('touchend', this._eventHandlers.mouseup);
this._target.removeEventListener('touchmove', this._eventHandlers.mousemove);
}
this._target.removeEventListener('mousedown', this._eventHandlers.mousedown);
this._target.removeEventListener('mouseup', this._eventHandlers.mouseup);
this._target.removeEventListener('mousemove', this._eventHandlers.mousemove);
this._target.removeEventListener('wheel', this._eventHandlers.mousewheel);
document.removeEventListener('click', this._eventHandlers.mousedisable);
this._target.removeEventListener('contextmenu', this._eventHandlers.mousedisable);
}
}
+54 -27
View File
@@ -1,3 +1,4 @@
import KeyTable from "./keysym.js";
import keysyms from "./keysymdef.js";
import vkeys from "./vkeys.js";
import fixedkeys from "./fixedkeys.js";
@@ -21,9 +22,8 @@ export function getKeycode(evt) {
}
// The de-facto standard is to use Windows Virtual-Key codes
// in the 'keyCode' field for non-printable characters. However
// Webkit sets it to the same as charCode in 'keypress' events.
if ((evt.type !== 'keypress') && (evt.keyCode in vkeys)) {
// in the 'keyCode' field for non-printable characters
if (evt.keyCode in vkeys) {
let code = vkeys[evt.keyCode];
// macOS has messed up this code for some reason
@@ -68,29 +68,11 @@ export function getKeycode(evt) {
export function getKey(evt) {
// Are we getting a proper key value?
if (evt.key !== undefined) {
// IE and Edge use some ancient version of the spec
// https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/8860571/
switch (evt.key) {
case 'Spacebar': return ' ';
case 'Esc': return 'Escape';
case 'Scroll': return 'ScrollLock';
case 'Win': return 'Meta';
case 'Apps': return 'ContextMenu';
case 'Up': return 'ArrowUp';
case 'Left': return 'ArrowLeft';
case 'Right': return 'ArrowRight';
case 'Down': return 'ArrowDown';
case 'Del': return 'Delete';
case 'Divide': return '/';
case 'Multiply': return '*';
case 'Subtract': return '-';
case 'Add': return '+';
case 'Decimal': return evt.char;
}
// Mozilla isn't fully in sync with the spec yet
switch (evt.key) {
case 'OS': return 'Meta';
case 'LaunchMyComputer': return 'LaunchApplication1';
case 'LaunchCalculator': return 'LaunchApplication2';
}
// iOS leaks some OS names
@@ -102,11 +84,12 @@ export function getKey(evt) {
case 'UIKeyInputEscape': return 'Escape';
}
// IE and Edge have broken handling of AltGraph so we cannot
// trust them for printable characters
if ((evt.key.length !== 1) || (!browser.isIE() && !browser.isEdge())) {
return evt.key;
// Broken behaviour in Chrome
if ((evt.key === '\x00') && (evt.code === 'NumpadDecimal')) {
return 'Delete';
}
return evt.key;
}
// Try to deduce it based on the physical key
@@ -141,10 +124,54 @@ export function getKeysym(evt) {
location = 2;
}
// And for Clear
if ((key === 'Clear') && (location === 3)) {
let code = getKeycode(evt);
if (code === 'NumLock') {
location = 0;
}
}
if ((location === undefined) || (location > 3)) {
location = 0;
}
// The original Meta key now gets confused with the Windows key
// https://bugs.chromium.org/p/chromium/issues/detail?id=1020141
// https://bugzilla.mozilla.org/show_bug.cgi?id=1232918
if (key === 'Meta') {
let code = getKeycode(evt);
if (code === 'AltLeft') {
return KeyTable.XK_Meta_L;
} else if (code === 'AltRight') {
return KeyTable.XK_Meta_R;
}
}
// macOS has Clear instead of NumLock, but the remote system is
// probably not macOS, so lying here is probably best...
if (key === 'Clear') {
let code = getKeycode(evt);
if (code === 'NumLock') {
return KeyTable.XK_Num_Lock;
}
}
// Windows sends alternating symbols for some keys when using a
// Japanese layout. We have no way of synchronising with the IM
// running on the remote system, so we send some combined keysym
// instead and hope for the best.
if (browser.isWindows()) {
switch (key) {
case 'Zenkaku':
case 'Hankaku':
return KeyTable.XK_Zenkaku_Hankaku;
case 'Romaji':
case 'KanaMode':
return KeyTable.XK_Romaji;
}
}
return DOMKeyTable[key][location];
}
-1
View File
@@ -13,7 +13,6 @@ export default {
0x08: 'Backspace',
0x09: 'Tab',
0x0a: 'NumpadClear',
0x0c: 'Numpad5', // IE11 sends evt.keyCode: 12 when numlock is off
0x0d: 'Enter',
0x10: 'ShiftLeft',
0x11: 'ControlLeft',
+5 -3
View File
@@ -1,8 +1,8 @@
/*
* This file is auto-generated from keymaps.csv on 2017-05-31 16:20
* Database checksum sha256(92fd165507f2a3b8c5b3fa56e425d45788dbcb98cf067a307527d91ce22cab94)
* This file is auto-generated from keymaps.csv
* Database checksum sha256(76d68c10e97d37fe2ea459e210125ae41796253fb217e900bf2983ade13a7920)
* To re-generate, run:
* keymap-gen --lang=js code-map keymaps.csv html atset1
* keymap-gen code-map --lang=js keymaps.csv html atset1
*/
export default {
"Again": 0xe005, /* html:Again (Again) -> linux:129 (KEY_AGAIN) -> atset1:57349 */
@@ -111,6 +111,8 @@ export default {
"KeyX": 0x2d, /* html:KeyX (KeyX) -> linux:45 (KEY_X) -> atset1:45 */
"KeyY": 0x15, /* html:KeyY (KeyY) -> linux:21 (KEY_Y) -> atset1:21 */
"KeyZ": 0x2c, /* html:KeyZ (KeyZ) -> linux:44 (KEY_Z) -> atset1:44 */
"Lang1": 0x72, /* html:Lang1 (Lang1) -> linux:122 (KEY_HANGEUL) -> atset1:114 */
"Lang2": 0x71, /* html:Lang2 (Lang2) -> linux:123 (KEY_HANJA) -> atset1:113 */
"Lang3": 0x78, /* html:Lang3 (Lang3) -> linux:90 (KEY_KATAKANA) -> atset1:120 */
"Lang4": 0x77, /* html:Lang4 (Lang4) -> linux:91 (KEY_HIRAGANA) -> atset1:119 */
"Lang5": 0x76, /* html:Lang5 (Lang5) -> linux:85 (KEY_ZENKAKUHANKAKU) -> atset1:118 */
+1314 -381
View File
File diff suppressed because it is too large Load Diff
+33 -20
View File
@@ -1,9 +1,11 @@
/*
* noVNC: HTML5 VNC client
* Copyright (C) 2018 The noVNC Authors
* Copyright (C) 2019 The noVNC Authors
* Licensed under MPL 2.0 (see LICENSE.txt)
*
* See README.md for usage and integration instructions.
*
* Browser feature support detection
*/
import * as Log from './logging.js';
@@ -31,7 +33,7 @@ try {
const target = document.createElement('canvas');
target.style.cursor = 'url("data:image/x-icon;base64,AAACAAEACAgAAAIAAgA4AQAAFgAAACgAAAAIAAAAEAAAAAEAIAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAD/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////AAAAAAAAAAAAAAAAAAAAAA==") 2 2, default';
if (target.style.cursor) {
if (target.style.cursor.indexOf("url") === 0) {
Log.Info("Data URI scheme cursor supported");
_supportsCursorURIs = true;
} else {
@@ -43,14 +45,37 @@ try {
export const supportsCursorURIs = _supportsCursorURIs;
let _supportsImageMetadata = false;
let _hasScrollbarGutter = true;
try {
new ImageData(new Uint8ClampedArray(4), 1, 1);
_supportsImageMetadata = true;
} catch (ex) {
// ignore failure
// Create invisible container
const container = document.createElement('div');
container.style.visibility = 'hidden';
container.style.overflow = 'scroll'; // forcing scrollbars
document.body.appendChild(container);
// Create a div and place it in the container
const child = document.createElement('div');
container.appendChild(child);
// Calculate the difference between the container's full width
// and the child's width - the difference is the scrollbars
const scrollbarWidth = (container.offsetWidth - child.offsetWidth);
// Clean up
container.parentNode.removeChild(container);
_hasScrollbarGutter = scrollbarWidth != 0;
} catch (exc) {
Log.Error("Scrollbar test exception: " + exc);
}
export const supportsImageMetadata = _supportsImageMetadata;
export const hasScrollbarGutter = _hasScrollbarGutter;
/*
* The functions for detection of platforms and browsers below are exported
* but the use of these should be minimized as much as possible.
*
* It's better to use feature detection than platform detection.
*/
export function isMac() {
return navigator && !!(/mac/i).exec(navigator.platform);
@@ -67,23 +92,11 @@ export function isIOS() {
!!(/ipod/i).exec(navigator.platform));
}
export function isAndroid() {
return navigator && !!(/android/i).exec(navigator.userAgent);
}
export function isSafari() {
return navigator && (navigator.userAgent.indexOf('Safari') !== -1 &&
navigator.userAgent.indexOf('Chrome') === -1);
}
export function isIE() {
return navigator && !!(/trident/i).exec(navigator.userAgent);
}
export function isEdge() {
return navigator && !!(/edge/i).exec(navigator.userAgent);
}
export function isFirefox() {
return navigator && !!(/firefox/i).exec(navigator.userAgent);
}
+67 -45
View File
@@ -1,6 +1,6 @@
/*
* noVNC: HTML5 VNC client
* Copyright (C) 2018 The noVNC Authors
* Copyright (C) 2019 The noVNC Authors
* Licensed under MPL 2.0 or any later version (see LICENSE.txt)
*/
@@ -20,7 +20,6 @@ export default class Cursor {
this._canvas.style.pointerEvents = 'none';
// Can't use "display" because of Firefox bug #1445997
this._canvas.style.visibility = 'hidden';
document.body.appendChild(this._canvas);
}
this._position = { x: 0, y: 0 };
@@ -31,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),
};
}
@@ -45,25 +41,23 @@ export default class Cursor {
this._target = target;
if (useFallback) {
// FIXME: These don't fire properly except for mouse
/// movement in IE. We want to also capture element
// movement, size changes, visibility, etc.
document.body.appendChild(this._canvas);
const options = { capture: true, passive: true };
this._target.addEventListener('mouseover', this._eventHandlers.mouseover, options);
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();
}
detach() {
if (!this._target) {
return;
}
if (useFallback) {
const options = { capture: true, passive: true };
this._target.removeEventListener('mouseover', this._eventHandlers.mouseover, options);
@@ -71,9 +65,7 @@ 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);
}
this._target = null;
@@ -95,14 +87,7 @@ export default class Cursor {
this._canvas.width = w;
this._canvas.height = h;
let img;
try {
// IE doesn't support this
img = new ImageData(new Uint8ClampedArray(rgba), w, h);
} catch (ex) {
img = ctx.createImageData(w, h);
img.data.set(new Uint8ClampedArray(rgba));
}
let img = new ImageData(new Uint8ClampedArray(rgba), w, h);
ctx.clearRect(0, 0, w, h);
ctx.putImageData(img, 0, 0);
@@ -124,6 +109,27 @@ 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;
}
// clientX/clientY are relative the _visual viewport_,
// but our position is relative the _layout viewport_,
// so try to compensate when we can
if (window.visualViewport) {
this._position.x = clientX + window.visualViewport.offsetLeft;
this._position.y = clientY + window.visualViewport.offsetTop;
} else {
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
@@ -132,7 +138,8 @@ export default class Cursor {
}
_handleMouseLeave(event) {
this._hideCursor();
// Check if we should show the cursor on the element we are leaving to
this._updateVisibility(event.relatedTarget);
}
_handleMouseMove(event) {
@@ -150,27 +157,29 @@ export default class Cursor {
// now and adjust visibility based on that.
let target = document.elementFromPoint(event.clientX, event.clientY);
this._updateVisibility(target);
}
_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);
// Captures end with a mouseup but we can't know the event order of
// mouseup vs releaseCapture.
//
// In the cases when releaseCapture comes first, the code above is
// enough.
//
// In the cases when the mouseup comes first, we need wait for the
// browser to flush all events and then check again if the cursor
// should be visible.
if (this._captureIsActive()) {
window.setTimeout(() => {
// We might have detached at this point
if (!this._target) {
return;
}
// Refresh the target from elementFromPoint since queued events
// might have altered the DOM
target = document.elementFromPoint(event.clientX,
event.clientY);
this._updateVisibility(target);
}, 0);
}
}
_showCursor() {
@@ -189,6 +198,9 @@ export default class Cursor {
// (i.e. are we over the target, or a child of the target without a
// different cursor set)
_shouldShowCursor(target) {
if (!target) {
return false;
}
// Easy case
if (target === this._target) {
return true;
@@ -207,6 +219,11 @@ export default class Cursor {
}
_updateVisibility(target) {
// When the cursor target has capture we want to show the cursor.
// So, if a capture is active - look at the captured element instead.
if (this._captureIsActive()) {
target = document.captureElement;
}
if (this._shouldShowCursor(target)) {
this._showCursor();
} else {
@@ -218,4 +235,9 @@ export default class Cursor {
this._canvas.style.left = this._position.x + "px";
this._canvas.style.top = this._position.y + "px";
}
_captureIsActive() {
return document.captureElement &&
document.documentElement.contains(document.captureElement);
}
}
+32
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;
}
+46 -47
View File
@@ -21,7 +21,8 @@ export function stopEvent(e) {
// Emulate Element.setCapture() when not supported
let _captureRecursion = false;
let _captureElem = null;
let _elementForUnflushedEvents = null;
document.captureElement = null;
function _captureProxy(e) {
// Recursion protection as we'll see our own event
if (_captureRecursion) return;
@@ -30,7 +31,11 @@ function _captureProxy(e) {
const newEv = new e.constructor(e.type, e);
_captureRecursion = true;
_captureElem.dispatchEvent(newEv);
if (document.captureElement) {
document.captureElement.dispatchEvent(newEv);
} else {
_elementForUnflushedEvents.dispatchEvent(newEv);
}
_captureRecursion = false;
// Avoid double events
@@ -48,58 +53,52 @@ function _captureProxy(e) {
}
// Follow cursor style of target element
function _captureElemChanged() {
const captureElem = document.getElementById("noVNC_mouse_capture_elem");
captureElem.style.cursor = window.getComputedStyle(_captureElem).cursor;
function _capturedElemChanged() {
const proxyElem = document.getElementById("noVNC_mouse_capture_elem");
proxyElem.style.cursor = window.getComputedStyle(document.captureElement).cursor;
}
const _captureObserver = new MutationObserver(_captureElemChanged);
const _captureObserver = new MutationObserver(_capturedElemChanged);
let _captureIndex = 0;
export function setCapture(elem) {
if (elem.setCapture) {
elem.setCapture();
// IE releases capture on 'click' events which might not trigger
elem.addEventListener('mouseup', releaseCapture);
export function setCapture(target) {
if (target.setCapture) {
target.setCapture();
document.captureElement = target;
} else {
// Release any existing capture in case this method is
// called multiple times without coordination
releaseCapture();
let captureElem = document.getElementById("noVNC_mouse_capture_elem");
let proxyElem = document.getElementById("noVNC_mouse_capture_elem");
if (captureElem === null) {
captureElem = document.createElement("div");
captureElem.id = "noVNC_mouse_capture_elem";
captureElem.style.position = "fixed";
captureElem.style.top = "0px";
captureElem.style.left = "0px";
captureElem.style.width = "100%";
captureElem.style.height = "100%";
captureElem.style.zIndex = 10000;
captureElem.style.display = "none";
document.body.appendChild(captureElem);
if (proxyElem === null) {
proxyElem = document.createElement("div");
proxyElem.id = "noVNC_mouse_capture_elem";
proxyElem.style.position = "fixed";
proxyElem.style.top = "0px";
proxyElem.style.left = "0px";
proxyElem.style.width = "100%";
proxyElem.style.height = "100%";
proxyElem.style.zIndex = 10000;
proxyElem.style.display = "none";
document.body.appendChild(proxyElem);
// This is to make sure callers don't get confused by having
// our blocking element as the target
captureElem.addEventListener('contextmenu', _captureProxy);
proxyElem.addEventListener('contextmenu', _captureProxy);
captureElem.addEventListener('mousemove', _captureProxy);
captureElem.addEventListener('mouseup', _captureProxy);
proxyElem.addEventListener('mousemove', _captureProxy);
proxyElem.addEventListener('mouseup', _captureProxy);
}
_captureElem = elem;
_captureIndex++;
document.captureElement = target;
// Track cursor and get initial cursor
_captureObserver.observe(elem, {attributes: true});
_captureElemChanged();
_captureObserver.observe(target, {attributes: true});
_capturedElemChanged();
captureElem.style.display = "";
proxyElem.style.display = "";
// We listen to events on window in order to keep tracking if it
// happens to leave the viewport
@@ -112,26 +111,26 @@ export function releaseCapture() {
if (document.releaseCapture) {
document.releaseCapture();
document.captureElement = null;
} else {
if (!_captureElem) {
if (!document.captureElement) {
return;
}
// There might be events already queued, so we need to wait for
// them to flush. E.g. contextmenu in Microsoft Edge
window.setTimeout((expected) => {
// Only clear it if it's the expected grab (i.e. no one
// else has initiated a new grab)
if (_captureIndex === expected) {
_captureElem = null;
}
}, 0, _captureIndex);
// There might be events already queued. The event proxy needs
// access to the captured element for these queued events.
// E.g. contextmenu (right-click) in Microsoft Edge
//
// Before removing the capturedElem pointer we save it to a
// temporary variable that the unflushed events can use.
_elementForUnflushedEvents = document.captureElement;
document.captureElement = null;
_captureObserver.disconnect();
const captureElem = document.getElementById("noVNC_mouse_capture_elem");
captureElem.style.display = "none";
const proxyElem = document.getElementById("noVNC_mouse_capture_elem");
proxyElem.style.display = "none";
window.removeEventListener('mousemove', _captureProxy);
window.removeEventListener('mouseup', _captureProxy);
+1 -1
View File
@@ -1,6 +1,6 @@
/*
* noVNC: HTML5 VNC client
* Copyright (C) 2018 The noVNC Authors
* Copyright (C) 2019 The noVNC Authors
* Licensed under MPL 2.0 (see LICENSE.txt)
*
* See README.md for usage and integration instructions.
+15
View File
@@ -0,0 +1,15 @@
/*
* 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.
*/
export function toUnsigned32bit(toConvert) {
return toConvert >>> 0;
}
export function toSigned32bit(toConvert) {
return toConvert | 0;
}
+8 -8
View File
@@ -1,6 +1,6 @@
/*
* noVNC: HTML5 VNC client
* Copyright (C) 2018 The noVNC Authors
* Copyright (C) 2019 The noVNC Authors
* Licensed under MPL 2.0 (see LICENSE.txt)
*
* See README.md for usage and integration instructions.
@@ -10,18 +10,18 @@
* Logging/debug routines
*/
let _log_level = 'warn';
let _logLevel = 'warn';
let Debug = () => {};
let Info = () => {};
let Warn = () => {};
let Error = () => {};
export function init_logging(level) {
export function initLogging(level) {
if (typeof level === 'undefined') {
level = _log_level;
level = _logLevel;
} else {
_log_level = level;
_logLevel = level;
}
Debug = Info = Warn = Error = () => {};
@@ -46,11 +46,11 @@ export function init_logging(level) {
}
}
export function get_logging() {
return _log_level;
export function getLogging() {
return _logLevel;
}
export { Debug, Info, Warn, Error };
// Initialize logging level
init_logging();
initLogging();
-54
View File
@@ -1,54 +0,0 @@
/*
* noVNC: HTML5 VNC client
* Copyright (C) 2018 The noVNC Authors
* Licensed under MPL 2.0 or any later version (see LICENSE.txt)
*/
/* Polyfills to provide new APIs in old browsers */
/* Object.assign() (taken from MDN) */
if (typeof Object.assign != 'function') {
// Must be writable: true, enumerable: false, configurable: true
Object.defineProperty(Object, "assign", {
value: function assign(target, varArgs) { // .length of function is 2
'use strict';
if (target == null) { // TypeError if undefined or null
throw new TypeError('Cannot convert undefined or null to object');
}
const to = Object(target);
for (let index = 1; index < arguments.length; index++) {
const nextSource = arguments[index];
if (nextSource != null) { // Skip over if undefined or null
for (let nextKey in nextSource) {
// Avoid bugs when hasOwnProperty is shadowed
if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
to[nextKey] = nextSource[nextKey];
}
}
}
}
return to;
},
writable: true,
configurable: true
});
}
/* CustomEvent constructor (taken from MDN) */
(() => {
function CustomEvent(event, params) {
params = params || { bubbles: false, cancelable: false, detail: undefined };
const evt = document.createEvent( 'CustomEvent' );
evt.initCustomEvent( event, params.bubbles, params.cancelable, params.detail );
return evt;
}
CustomEvent.prototype = window.Event.prototype;
if (typeof window.CustomEvent !== "function") {
window.CustomEvent = CustomEvent;
}
})();
+20 -6
View File
@@ -1,14 +1,28 @@
/*
* noVNC: HTML5 VNC client
* Copyright (C) 2018 The noVNC Authors
* Copyright (C) 2019 The noVNC Authors
* Licensed under MPL 2.0 (see LICENSE.txt)
*
* See README.md for usage and integration instructions.
*/
/*
* Decode from UTF-8
*/
export function decodeUTF8(utf8string) {
return decodeURIComponent(escape(utf8string));
// Decode from UTF-8
export function decodeUTF8(utf8string, allowLatin1=false) {
try {
return decodeURIComponent(escape(utf8string));
} catch (e) {
if (e instanceof URIError) {
if (allowLatin1) {
// If we allow Latin1 we can ignore any decoding fails
// and in these cases return the original string
return utf8string;
}
}
throw e;
}
}
// Encode to UTF-8
export function encodeUTF8(DOMString) {
return unescape(encodeURIComponent(DOMString));
}
+107 -44
View File
@@ -1,10 +1,10 @@
/*
* Websock: high-performance binary WebSockets
* Copyright (C) 2018 The noVNC Authors
* Websock: high-performance buffering wrapper
* Copyright (C) 2019 The noVNC Authors
* Licensed under MPL 2.0 (see LICENSE.txt)
*
* Websock is similar to the standard WebSocket object but with extra
* buffer handling.
* Websock is similar to the standard WebSocket / RTCDataChannel object
* but with extra buffer handling.
*
* Websock has built-in receive queue buffering; the message event
* does not contain actual data but is simply a notification that
@@ -17,17 +17,43 @@ import * as Log from './util/logging.js';
// this has performance issues in some versions Chromium, and
// doesn't gain a tremendous amount of performance increase in Firefox
// at the moment. It may be valuable to turn it on in the future.
const ENABLE_COPYWITHIN = false;
const MAX_RQ_GROW_SIZE = 40 * 1024 * 1024; // 40 MiB
// Constants pulled from RTCDataChannelState enum
// https://developer.mozilla.org/en-US/docs/Web/API/RTCDataChannel/readyState#RTCDataChannelState_enum
const DataChannel = {
CONNECTING: "connecting",
OPEN: "open",
CLOSING: "closing",
CLOSED: "closed"
};
const ReadyStates = {
CONNECTING: [WebSocket.CONNECTING, DataChannel.CONNECTING],
OPEN: [WebSocket.OPEN, DataChannel.OPEN],
CLOSING: [WebSocket.CLOSING, DataChannel.CLOSING],
CLOSED: [WebSocket.CLOSED, DataChannel.CLOSED],
};
// Properties a raw channel must have, WebSocket and RTCDataChannel are two examples
const rawChannelProps = [
"send",
"close",
"binaryType",
"onerror",
"onmessage",
"onopen",
"protocol",
"readyState",
];
export default class Websock {
constructor() {
this._websocket = null; // WebSocket object
this._websocket = null; // WebSocket or RTCDataChannel object
this._rQi = 0; // Receive queue index
this._rQlen = 0; // Next write position in the receive queue
this._rQbufferSize = 1024 * 1024 * 4; // Receive queue buffer size (4 MiB)
this._rQmax = this._rQbufferSize / 8;
// called in init: this._rQ = new Uint8Array(this._rQbufferSize);
this._rQ = null; // Receive queue
@@ -45,6 +71,29 @@ export default class Websock {
}
// Getters and Setters
get readyState() {
let subState;
if (this._websocket === null) {
return "unused";
}
subState = this._websocket.readyState;
if (ReadyStates.CONNECTING.includes(subState)) {
return "connecting";
} else if (ReadyStates.OPEN.includes(subState)) {
return "open";
} else if (ReadyStates.CLOSING.includes(subState)) {
return "closing";
} else if (ReadyStates.CLOSED.includes(subState)) {
return "closed";
}
return "unknown";
}
get sQ() {
return this._sQ;
}
@@ -142,8 +191,8 @@ export default class Websock {
// Send Queue
flush() {
if (this._sQlen > 0 && this._websocket.readyState === WebSocket.OPEN) {
this._websocket.send(this._encode_message());
if (this._sQlen > 0 && this.readyState === 'open') {
this._websocket.send(this._encodeMessage());
this._sQlen = 0;
}
}
@@ -154,7 +203,7 @@ export default class Websock {
this.flush();
}
send_string(str) {
sendString(str) {
this.send(str.split('').map(chr => chr.charCodeAt(0)));
}
@@ -167,24 +216,37 @@ export default class Websock {
this._eventHandlers[evt] = handler;
}
_allocate_buffers() {
_allocateBuffers() {
this._rQ = new Uint8Array(this._rQbufferSize);
this._sQ = new Uint8Array(this._sQbufferSize);
}
init() {
this._allocate_buffers();
this._allocateBuffers();
this._rQi = 0;
this._websocket = null;
}
open(uri, protocols) {
this.attach(new WebSocket(uri, protocols));
}
attach(rawChannel) {
this.init();
this._websocket = new WebSocket(uri, protocols);
this._websocket.binaryType = 'arraybuffer';
// Must get object and class methods to be compatible with the tests.
const channelProps = [...Object.keys(rawChannel), ...Object.getOwnPropertyNames(Object.getPrototypeOf(rawChannel))];
for (let i = 0; i < rawChannelProps.length; i++) {
const prop = rawChannelProps[i];
if (channelProps.indexOf(prop) < 0) {
throw new Error('Raw channel missing property: ' + prop);
}
}
this._websocket = rawChannel;
this._websocket.binaryType = "arraybuffer";
this._websocket.onmessage = this._recvMessage.bind(this);
this._websocket.onmessage = this._recv_message.bind(this);
this._websocket.onopen = () => {
Log.Debug('>> WebSock.onopen');
if (this._websocket.protocol) {
@@ -194,11 +256,13 @@ export default class Websock {
this._eventHandlers.open();
Log.Debug("<< WebSock.onopen");
};
this._websocket.onclose = (e) => {
Log.Debug(">> WebSock.onclose");
this._eventHandlers.close(e);
Log.Debug("<< WebSock.onclose");
};
this._websocket.onerror = (e) => {
Log.Debug(">> WebSock.onerror: " + e);
this._eventHandlers.error(e);
@@ -208,8 +272,8 @@ export default class Websock {
close() {
if (this._websocket) {
if ((this._websocket.readyState === WebSocket.OPEN) ||
(this._websocket.readyState === WebSocket.CONNECTING)) {
if (this.readyState === 'connecting' ||
this.readyState === 'open') {
Log.Info("Closing WebSocket connection");
this._websocket.close();
}
@@ -219,69 +283,68 @@ export default class Websock {
}
// private methods
_encode_message() {
_encodeMessage() {
// Put in a binary arraybuffer
// according to the spec, you can send ArrayBufferViews with the send method
return new Uint8Array(this._sQ.buffer, 0, this._sQlen);
}
_expand_compact_rQ(min_fit) {
const resizeNeeded = min_fit || this.rQlen > this._rQbufferSize / 2;
// We want to move all the unread data to the start of the queue,
// e.g. compacting.
// The function also expands the receive que if needed, and for
// performance reasons we combine these two actions to avoid
// unneccessary copying.
_expandCompactRQ(minFit) {
// if we're using less than 1/8th of the buffer even with the incoming bytes, compact in place
// instead of resizing
const requiredBufferSize = (this._rQlen - this._rQi + minFit) * 8;
const resizeNeeded = this._rQbufferSize < requiredBufferSize;
if (resizeNeeded) {
if (!min_fit) {
// just double the size if we need to do compaction
this._rQbufferSize *= 2;
} else {
// otherwise, make sure we satisy rQlen - rQi + min_fit < rQbufferSize / 8
this._rQbufferSize = (this.rQlen + min_fit) * 8;
}
// Make sure we always *at least* double the buffer size, and have at least space for 8x
// the current amount of data
this._rQbufferSize = Math.max(this._rQbufferSize * 2, requiredBufferSize);
}
// we don't want to grow unboundedly
if (this._rQbufferSize > MAX_RQ_GROW_SIZE) {
this._rQbufferSize = MAX_RQ_GROW_SIZE;
if (this._rQbufferSize - this.rQlen < min_fit) {
if (this._rQbufferSize - this.rQlen < minFit) {
throw new Error("Receive Queue buffer exceeded " + MAX_RQ_GROW_SIZE + " bytes, and the new message could not fit");
}
}
if (resizeNeeded) {
const old_rQbuffer = this._rQ.buffer;
this._rQmax = this._rQbufferSize / 8;
const oldRQbuffer = this._rQ.buffer;
this._rQ = new Uint8Array(this._rQbufferSize);
this._rQ.set(new Uint8Array(old_rQbuffer, this._rQi));
this._rQ.set(new Uint8Array(oldRQbuffer, this._rQi, this._rQlen - this._rQi));
} else {
if (ENABLE_COPYWITHIN) {
this._rQ.copyWithin(0, this._rQi);
} else {
this._rQ.set(new Uint8Array(this._rQ.buffer, this._rQi));
}
this._rQ.copyWithin(0, this._rQi, this._rQlen);
}
this._rQlen = this._rQlen - this._rQi;
this._rQi = 0;
}
_decode_message(data) {
// push arraybuffer values onto the end
// push arraybuffer values onto the end of the receive que
_DecodeMessage(data) {
const u8 = new Uint8Array(data);
if (u8.length > this._rQbufferSize - this._rQlen) {
this._expand_compact_rQ(u8.length);
this._expandCompactRQ(u8.length);
}
this._rQ.set(u8, this._rQlen);
this._rQlen += u8.length;
}
_recv_message(e) {
this._decode_message(e.data);
_recvMessage(e) {
this._DecodeMessage(e.data);
if (this.rQlen > 0) {
this._eventHandlers.message();
// Compact the receive queue
if (this._rQlen == this._rQi) {
// All data has now been processed, this means we
// can reset the receive queue.
this._rQlen = 0;
this._rQi = 0;
} else if (this._rQlen > this._rQmax) {
this._expand_compact_rQ();
}
} else {
Log.Debug("Ignoring empty message");
+9 -42
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.
@@ -23,7 +20,7 @@ layered on the HTML5 canvas element.
* __Websock__ (core/websock.js): Websock client from websockify
with transparent binary data support.
[Websock API](https://github.com/novnc/websockify/wiki/websock.js) wiki page.
[Websock API](https://github.com/novnc/websockify-js/wiki/websock.js) wiki page.
## 1.2 Callbacks
@@ -35,62 +32,38 @@ 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
| ------------ | ----- | ---- | ------- | ------------
| logo | raw | RW | | Logo to display when cleared: {"width": width, "height": height, "type": mime-type, "data": data}
| scale | float | RW | 1.0 | Display area scale factor 0.0 - 1.0
| clipViewport | bool | RW | false | Use viewport clipping
| width | int | RO | | Display area width
| height | int | RO | | Display area height
### 2.3.2 Methods
### 2.2.2 Methods
| name | parameters | description
| ------------------ | ------------------------------------------------------- | ------------
@@ -100,22 +73,16 @@ None
| absY | (y) | Return Y relative to the remote display
| resize | (width, height) | Set width and height
| flip | (from_queue) | Update the visible canvas with the contents of the rendering canvas
| clear | () | Clear the display (show logo if set)
| pending | () | Check if there are waiting items in the render queue
| flush | () | Resume processing the render queue unless it's empty
| fillRect | (x, y, width, height, color, from_queue) | Draw a filled in rectangle
| copyImage | (old_x, old_y, new_x, new_y, width, height, from_queue) | Copy a rectangular area
| imageRect | (x, y, mime, arr) | Draw a rectangle with an image
| startTile | (x, y, width, height, color) | Begin updating a tile
| subTile | (tile, x, y, w, h, color) | Update a sub-rectangle within the given tile
| finishTile | () | Draw the current tile to the display
| imageRect | (x, y, width, height, mime, arr) | Draw a rectangle with an image
| blitImage | (x, y, width, height, arr, offset, from_queue) | Blit pixels (of R,G,B,A) to the display
| blitRgbImage | (x, y, width, height, arr, offset, from_queue) | Blit RGB encoded image to display
| blitRgbxImage | (x, y, width, height, arr, offset, from_queue) | Blit RGBX encoded image to display
| 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
| ------- | ---------- | ------------
+21 -11
View File
@@ -24,13 +24,7 @@ protocol stream.
`focusOnClick`
- Is a `boolean` indicating if keyboard focus should automatically be
moved to the remote session when a `mousedown` or `touchstart`
event is received.
`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.
event is received. Enabled by default.
`clipViewport`
- Is a `boolean` indicating if the remote session should be clipped
@@ -64,6 +58,19 @@ protocol stream.
to the element containing the remote session screen. The default value is `rgb(40, 40, 40)`
(solid gray color).
`qualityLevel`
- Is an `int` in range `[0-9]` controlling the desired JPEG quality.
Value `0` implies low quality and `9` implies high quality.
Default value is `6`.
`compressionLevel`
- Is an `int` in range `[0-9]` controlling the desired compression
level. Value `0` means no compression. Level 1 uses a minimum of CPU
resources and achieves weak compression ratios, while level 9 offers
best compression but is slow in terms of CPU consumption on the server
side. Use high levels with very slow network connections.
Default value is `2`.
`capabilities` *Read only*
- Is an `Object` indicating which optional extensions are available
on the server. Some methods may only be called if the corresponding
@@ -158,9 +165,9 @@ connection to a specified VNC server.
existing contents of the `HTMLElement` will be untouched, but new
elements will be added during the lifetime of the `RFB` object.
**`url`**
**`urlOrDataChannel`**
- A `DOMString` specifying the VNC server to connect to. This must be
a valid WebSocket URL.
a valid WebSocket URL. This can also be a `WebSocket` or `RTCDataChannel`.
**`options`** *Optional*
- An `Object` specifying extra details about how the connection
@@ -187,6 +194,10 @@ connection to a specified VNC server.
- A `DOMString` specifying the ID to provide to any VNC repeater
encountered.
`wsProtocols`
- An `Array` of `DOMString`s specifying the sub-protocols to use
in the WebSocket connection. Empty by default.
#### connect
The `connect` event is fired after all the handshaking with the server
@@ -371,5 +382,4 @@ to the remote server.
###### Parameters
**`text`**
- A `DOMString` specifying the clipboard data to send. Currently only
characters from ISO 8859-1 are supported.
- A `DOMString` specifying the clipboard data to send.
+4 -18
View File
@@ -61,30 +61,16 @@ query string. Currently the following options are available:
* `resize` - How to resize the remote session if it is not the same size as
the browser window. Can be one of `off`, `scale` and `remote`.
* `quality` - The session JPEG quality level. Can be `0` to `9`.
* `compression` - The session compression level. Can be `0` to `9`.
* `show_dot` - If a dot cursor should be shown when the remote server provides
no local cursor, or provides a fully-transparent (invisible) cursor.
* `logging` - The console log level. Can be one of `error`, `warn`, `info` or
`debug`.
## Pre-conversion of Modules
noVNC is written using ECMAScript 6 modules. Many of the major browsers support
these modules natively, but not all. By default the noVNC application includes
a script that can convert these modules to an older format as they are being
loaded. However this process can be slow and severely increases the load time
for the application.
It is possible to perform this conversion ahead of time, avoiding the extra
load times. To do this please follow these steps:
1. Install Node.js
2. Run `npm install` in the noVNC directory
3. Run `./utils/use_require.js --with-app --as commonjs`
This will produce a `build/` directory that includes everything needed to run
the noVNC application.
## HTTP Serving Considerations
### Browser Cache Issue
+3 -7
View File
@@ -18,18 +18,14 @@ do things.
## Conversion of Modules
noVNC is written using ECMAScript 6 modules. Many of the major browsers support
these modules natively, but not all. They are also not supported by Node.js. To
use noVNC in these places the library must first be converted.
noVNC is written using ECMAScript 6 modules. This is not supported by older
versions of Node.js. To use noVNC with those older versions of Node.js the
library must first be converted.
Fortunately noVNC includes a script to handle this conversion. Please follow
the following steps:
1. Install Node.js
2. Run `npm install` in the noVNC directory
3. Run `./utils/use_require.js --as <module format>`
Several module formats are available. Please run
`./utils/use_require.js --help` to see them all.
The result of the conversion is available in the `lib/` directory.
+37
View File
@@ -0,0 +1,37 @@
.TH novnc_proxy 1 "June 25, 2020" "version 1.2.0" "USER COMMANDS"
.SH NAME
novnc_proxy - noVNC proxy server
.SH SYNOPSIS
.B novnc_proxy [--listen PORT] [--vnc VNC_HOST:PORT] [--cert CERT] [--ssl-only]
Starts the WebSockets proxy and a mini-webserver and
provides a cut-and-paste URL to go to.
--listen PORT Port for proxy/webserver to listen on
Default: 6080
--vnc VNC_HOST:PORT VNC server host:port proxy target
Default: localhost:5900
--cert CERT Path to combined cert/key file, or just
the cert file if used with --key
Default: self.pem
--key KEY Path to key file, when not combined with cert
--web WEB Path to web files (e.g. vnc.html)
Default: ./
--ssl-only Disable non-https connections.
--record FILE Record traffic to FILE.session.js
--syslog SERVER Can be local socket such as /dev/log, or a UDP host:port pair.
--heartbeat SEC send a ping to the client every SEC seconds
--timeout SEC after SEC seconds exit when not connected
--idle-timeout SEC server exits after SEC seconds if there are no
active connections
.SH AUTHOR
The noVNC Authors
https://github.com/novnc/noVNC
.SH SEE ALSO
websockify(1), nova-novncproxy(1)
+27 -76
View File
@@ -1,48 +1,23 @@
// Karma configuration
// The Safari launcher is broken, so construct our own
function SafariBrowser(id, baseBrowserDecorator, args) {
baseBrowserDecorator(this);
this._start = function(url) {
this._execCommand('/usr/bin/open', ['-W', '-n', '-a', 'Safari', url]);
}
}
SafariBrowser.prototype = {
name: 'Safari'
}
module.exports = (config) => {
const customLaunchers = {};
let browsers = [];
let useSauce = false;
// use Sauce when running on Travis
if (process.env.TRAVIS_JOB_NUMBER) {
useSauce = true;
}
if (useSauce && process.env.TEST_BROWSER_NAME && process.env.TEST_BROWSER_NAME != 'PhantomJS') {
const names = process.env.TEST_BROWSER_NAME.split(',');
const platforms = process.env.TEST_BROWSER_OS.split(',');
const versions = process.env.TEST_BROWSER_VERSION
? process.env.TEST_BROWSER_VERSION.split(',')
: [null];
for (let i = 0; i < names.length; i++) {
for (let j = 0; j < platforms.length; j++) {
for (let k = 0; k < versions.length; k++) {
let launcher_name = 'sl_' + platforms[j].replace(/[^a-zA-Z0-9]/g, '') + '_' + names[i];
if (versions[k]) {
launcher_name += '_' + versions[k];
}
customLaunchers[launcher_name] = {
base: 'SauceLabs',
browserName: names[i],
platform: platforms[j],
};
if (versions[i]) {
customLaunchers[launcher_name].version = versions[k];
}
}
}
}
browsers = Object.keys(customLaunchers);
} else {
useSauce = false;
//browsers = ['PhantomJS'];
browsers = [];
if (process.env.TEST_BROWSER_NAME) {
browsers = process.env.TEST_BROWSER_NAME.split(',');
}
const my_conf = {
@@ -56,16 +31,13 @@ module.exports = (config) => {
// list of files / patterns to load in the browser (loaded in order)
files: [
{ pattern: 'app/localization.js', included: false },
{ pattern: 'app/webutil.js', included: false },
{ pattern: 'core/**/*.js', included: false },
{ pattern: 'vendor/pako/**/*.js', included: false },
{ pattern: 'vendor/browser-es-module-loader/dist/*.js*', included: false },
{ pattern: 'tests/test.*.js', included: false },
{ pattern: 'tests/fake.*.js', included: false },
{ pattern: 'tests/assertions.js', included: false },
'vendor/promise.js',
'tests/karma-test-main.js',
{ pattern: 'app/localization.js', included: false, type: 'module' },
{ pattern: 'app/webutil.js', included: false, type: 'module' },
{ pattern: 'core/**/*.js', included: false, type: 'module' },
{ pattern: 'vendor/pako/**/*.js', included: false, type: 'module' },
{ pattern: 'tests/test.*.js', type: 'module' },
{ pattern: 'tests/fake.*.js', included: false, type: 'module' },
{ pattern: 'tests/assertions.js', type: 'module' },
],
client: {
@@ -80,7 +52,11 @@ module.exports = (config) => {
exclude: [
],
customLaunchers: customLaunchers,
plugins: [
'karma-*',
'@chiragrupani/karma-chromium-edge-launcher',
{ 'launcher:Safari': [ 'type', SafariBrowser ] },
],
// start these browsers
// available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
@@ -92,14 +68,6 @@ module.exports = (config) => {
reporters: ['mocha'],
// web server port
port: 9876,
// enable / disable colors in the output (reporters and logs)
colors: true,
// level of logging
// possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
logLevel: config.LOG_INFO,
@@ -111,24 +79,7 @@ module.exports = (config) => {
// Continuous Integration mode
// if true, Karma captures browsers, runs the tests and exits
singleRun: true,
// Increase timeout in case connection is slow/we run more browsers than possible
// (we currently get 3 for free, and we try to run 7, so it can take a while)
captureTimeout: 240000,
// similarly to above
browserNoActivityTimeout: 100000,
};
if (useSauce) {
my_conf.reporters.push('saucelabs');
my_conf.captureTimeout = 0; // use SL timeout
my_conf.sauceLabs = {
testName: 'noVNC Tests (all)',
startConnect: false,
tunnelIdentifier: process.env.TRAVIS_JOB_NUMBER
};
}
config.set(my_conf);
};
+32 -29
View File
@@ -1,6 +1,6 @@
{
"name": "@novnc/novnc",
"version": "1.1.0",
"version": "1.3.0-beta",
"description": "An HTML5 VNC client",
"browser": "lib/rfb",
"directories": {
@@ -19,9 +19,9 @@
"vendor/pako"
],
"scripts": {
"lint": "eslint app core po tests utils",
"lint": "eslint app core po/po2js po/xgettext-html tests utils",
"test": "karma start karma.conf.js",
"prepublish": "node ./utils/use_require.js --as commonjs --clean"
"prepublish": "node ./utils/use_require.js --clean"
},
"repository": {
"type": "git",
@@ -40,36 +40,39 @@
},
"homepage": "https://github.com/novnc/noVNC",
"devDependencies": {
"babel-core": "^6.22.1",
"babel-plugin-add-module-exports": "^0.2.1",
"@babel/core": "*",
"@babel/plugin-syntax-dynamic-import": "*",
"@babel/plugin-transform-modules-commonjs": "*",
"@babel/preset-env": "*",
"@babel/cli": "*",
"babel-plugin-import-redirect": "*",
"babel-plugin-syntax-dynamic-import": "^6.18.0",
"babel-plugin-transform-es2015-modules-amd": "^6.22.0",
"babel-plugin-transform-es2015-modules-commonjs": "^6.18.0",
"babel-plugin-transform-es2015-modules-systemjs": "^6.22.0",
"babel-plugin-transform-es2015-modules-umd": "^6.22.0",
"babel-preset-es2015": "^6.24.1",
"babelify": "^7.3.0",
"browserify": "^13.1.0",
"chai": "^3.5.0",
"commander": "^2.9.0",
"es-module-loader": "^2.1.0",
"eslint": "^4.16.0",
"fs-extra": "^1.0.0",
"browserify": "*",
"babelify": "*",
"core-js": "*",
"chai": "*",
"commander": "*",
"es-module-loader": "*",
"eslint": "*",
"fs-extra": "*",
"jsdom": "*",
"karma": "^1.3.0",
"karma-mocha": "^1.3.0",
"karma-mocha-reporter": "^2.2.0",
"karma-sauce-launcher": "^1.0.0",
"karma-sinon-chai": "^2.0.0",
"mocha": "^3.1.2",
"karma": "*",
"karma-mocha": "*",
"karma-chrome-launcher": "*",
"@chiragrupani/karma-chromium-edge-launcher": "*",
"karma-firefox-launcher": "*",
"karma-ie-launcher": "*",
"karma-mocha-reporter": "*",
"karma-safari-launcher": "*",
"karma-script-launcher": "*",
"karma-sinon-chai": "*",
"mocha": "*",
"node-getopt": "*",
"po2json": "*",
"requirejs": "^2.3.2",
"rollup": "^0.41.4",
"rollup-plugin-node-resolve": "^2.0.0",
"sinon": "^4.0.0",
"sinon-chai": "^2.8.0"
"requirejs": "*",
"rollup": "*",
"rollup-plugin-node-resolve": "*",
"sinon": "*",
"sinon-chai": "*"
},
"dependencies": {},
"keywords": [
+5
View File
@@ -0,0 +1,5 @@
{
"env": {
"node": true,
},
}
+1 -1
View File
@@ -1,7 +1,7 @@
all:
.PHONY: update-po update-js update-pot
LINGUAS := cs de el es ko nl pl ru sv tr zh_CN zh_TW
LINGUAS := cs de el es fr ja ko nl pl pt_BR ru sv tr zh_CN zh_TW
VERSION := $(shell grep '"version"' ../package.json | cut -d '"' -f 4)
+17 -16
View File
@@ -3,14 +3,15 @@
# Copyright (C) 2018 The noVNC Authors
# This file is distributed under the same license as the noVNC package.
# Juanjo Diaz <juanjo.diazmo@gmail.com>, 2018.
# Adrian Scillato <ascillato@gmail.com>, 2021.
#
msgid ""
msgstr ""
"Project-Id-Version: noVNC 1.0.0-testing.2\n"
"Report-Msgid-Bugs-To: novnc@googlegroups.com\n"
"POT-Creation-Date: 2017-10-06 10:07+0200\n"
"PO-Revision-Date: 2018-01-30 19:14-0800\n"
"Last-Translator: Juanjo Diaz <juanjo.diazmo@gmail.com>\n"
"PO-Revision-Date: 2021-04-23 12:00-0300\n"
"Last-Translator: Adrian Scillato <ascillato@gmail.com>\n"
"Language-Team: Spanish\n"
"Language: es\n"
"MIME-Version: 1.0\n"
@@ -40,7 +41,7 @@ msgstr "Desconectado"
#: ../app/ui.js:1052 ../core/rfb.js:248
msgid "Must set host"
msgstr "Debes configurar el host"
msgstr "Se debe configurar el host"
#: ../app/ui.js:1101
msgid "Reconnecting..."
@@ -48,7 +49,7 @@ msgstr "Reconectando..."
#: ../app/ui.js:1140
msgid "Password is required"
msgstr "Contraseña es obligatoria"
msgstr "La contraseña es obligatoria"
#: ../core/rfb.js:548
msgid "Disconnect timeout"
@@ -186,6 +187,10 @@ msgstr "Pantalla Completa"
msgid "Settings"
msgstr "Configuraciones"
#: ../vnc.html:200
msgid "Encrypt"
msgstr "Encriptar"
#: ../vnc.html:202
msgid "Shared Mode"
msgstr "Modo Compartido"
@@ -228,27 +233,23 @@ msgstr "Cursor Local"
#: ../vnc.html:229
msgid "Repeater ID:"
msgstr "ID del Repetidor"
msgstr "ID del Repetidor:"
#: ../vnc.html:233
msgid "WebSocket"
msgstr "WebSocket"
#: ../vnc.html:236
msgid "Encrypt"
msgstr ""
#: ../vnc.html:239
msgid "Host:"
msgstr "Host"
msgstr "Host:"
#: ../vnc.html:243
msgid "Port:"
msgstr "Puesto"
msgstr "Puerto:"
#: ../vnc.html:247
msgid "Path:"
msgstr "Ruta"
msgstr "Ruta:"
#: ../vnc.html:254
msgid "Automatic Reconnect"
@@ -256,11 +257,11 @@ msgstr "Reconexión automática"
#: ../vnc.html:257
msgid "Reconnect Delay (ms):"
msgstr "Retraso en la reconexión (ms)"
msgstr "Retraso en la reconexión (ms):"
#: ../vnc.html:263
msgid "Logging:"
msgstr "Logging"
msgstr "Registrando:"
#: ../vnc.html:275
msgid "Disconnect"
@@ -272,7 +273,7 @@ msgstr "Conectar"
#: ../vnc.html:304
msgid "Password:"
msgstr "Contraseña"
msgstr "Contraseña:"
#: ../vnc.html:318
msgid "Cancel"
@@ -280,4 +281,4 @@ msgstr "Cancelar"
#: ../vnc.html:334
msgid "Canvas not supported."
msgstr "Canvas no está soportado"
msgstr "Canvas no soportado."
+299
View File
@@ -0,0 +1,299 @@
# French translations for noVNC package
# Traductions françaises du paquet noVNC.
# Copyright (C) 2021 The noVNC Authors
# This file is distributed under the same license as the noVNC package.
# Jose <jose.matsuda@canada.ca>, 2021.
#
msgid ""
msgstr ""
"Project-Id-Version: noVNC 1.2.0\n"
"Report-Msgid-Bugs-To: novnc@googlegroups.com\n"
"POT-Creation-Date: 2020-07-03 16:11+0200\n"
"PO-Revision-Date: 2021-05-05 20:19-0400\n"
"Last-Translator: Jose <jose.matsuda@canada.ca>\n"
"Language-Team: French\n"
"Language: fr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: ../app/ui.js:394
msgid "Connecting..."
msgstr "En cours de connexion..."
#: ../app/ui.js:401
msgid "Disconnecting..."
msgstr "Déconnexion en cours..."
#: ../app/ui.js:407
msgid "Reconnecting..."
msgstr "Reconnexion en cours..."
#: ../app/ui.js:412
msgid "Internal error"
msgstr "Erreur interne"
#: ../app/ui.js:1008
msgid "Must set host"
msgstr "Doit définir l'hôte"
#: ../app/ui.js:1090
msgid "Connected (encrypted) to "
msgstr "Connecté (crypté) à "
#: ../app/ui.js:1092
msgid "Connected (unencrypted) to "
msgstr "Connecté (non crypté) à "
#: ../app/ui.js:1115
msgid "Something went wrong, connection is closed"
msgstr "Quelque chose est arrivé, la connexion est fermée"
#: ../app/ui.js:1118
msgid "Failed to connect to server"
msgstr "Échec de connexion au serveur"
#: ../app/ui.js:1128
msgid "Disconnected"
msgstr "Déconnecté"
#: ../app/ui.js:1143
msgid "New connection has been rejected with reason: "
msgstr "Une nouvelle connexion a été rejetée avec raison: "
#: ../app/ui.js:1146
msgid "New connection has been rejected"
msgstr "Une nouvelle connexion a été rejetée"
#: ../app/ui.js:1181
msgid "Credentials are required"
msgstr "Les identifiants sont requis"
#: ../vnc.html:74
msgid "noVNC encountered an error:"
msgstr "noVNC a rencontré une erreur:"
#: ../vnc.html:84
msgid "Hide/Show the control bar"
msgstr "Masquer/Afficher la barre de contrôle"
#: ../vnc.html:91
msgid "Drag"
msgstr "Faire glisser"
#: ../vnc.html:91
msgid "Move/Drag Viewport"
msgstr "Déplacer/faire glisser Viewport"
#: ../vnc.html:97
msgid "Keyboard"
msgstr "Clavier"
#: ../vnc.html:97
msgid "Show Keyboard"
msgstr "Afficher le clavier"
#: ../vnc.html:102
msgid "Extra keys"
msgstr "Touches supplémentaires"
#: ../vnc.html:102
msgid "Show Extra Keys"
msgstr "Afficher les touches supplémentaires"
#: ../vnc.html:107
msgid "Ctrl"
msgstr "Ctrl"
#: ../vnc.html:107
msgid "Toggle Ctrl"
msgstr "Basculer Ctrl"
#: ../vnc.html:110
msgid "Alt"
msgstr "Alt"
#: ../vnc.html:110
msgid "Toggle Alt"
msgstr "Basculer Alt"
#: ../vnc.html:113
msgid "Toggle Windows"
msgstr "Basculer Windows"
#: ../vnc.html:113
msgid "Windows"
msgstr "Windows"
#: ../vnc.html:116
msgid "Send Tab"
msgstr "Envoyer l'onglet"
#: ../vnc.html:116
msgid "Tab"
msgstr "l'onglet"
#: ../vnc.html:119
msgid "Esc"
msgstr "Esc"
#: ../vnc.html:119
msgid "Send Escape"
msgstr "Envoyer Escape"
#: ../vnc.html:122
msgid "Ctrl+Alt+Del"
msgstr "Ctrl+Alt+Del"
#: ../vnc.html:122
msgid "Send Ctrl-Alt-Del"
msgstr "Envoyer Ctrl-Alt-Del"
#: ../vnc.html:129
msgid "Shutdown/Reboot"
msgstr "Arrêter/Redémarrer"
#: ../vnc.html:129
msgid "Shutdown/Reboot..."
msgstr "Arrêter/Redémarrer..."
#: ../vnc.html:135
msgid "Power"
msgstr "Alimentation"
#: ../vnc.html:137
msgid "Shutdown"
msgstr "Arrêter"
#: ../vnc.html:138
msgid "Reboot"
msgstr "Redémarrer"
#: ../vnc.html:139
msgid "Reset"
msgstr "Réinitialiser"
#: ../vnc.html:144 ../vnc.html:150
msgid "Clipboard"
msgstr "Presse-papiers"
#: ../vnc.html:154
msgid "Clear"
msgstr "Effacer"
#: ../vnc.html:160
msgid "Fullscreen"
msgstr "Plein écran"
#: ../vnc.html:165 ../vnc.html:172
msgid "Settings"
msgstr "Paramètres"
#: ../vnc.html:175
msgid "Shared Mode"
msgstr "Mode partagé"
#: ../vnc.html:178
msgid "View Only"
msgstr "Afficher uniquement"
#: ../vnc.html:182
msgid "Clip to Window"
msgstr "Clip à fenêtre"
#: ../vnc.html:185
msgid "Scaling Mode:"
msgstr "Mode mise à l'échelle:"
#: ../vnc.html:187
msgid "None"
msgstr "Aucun"
#: ../vnc.html:188
msgid "Local Scaling"
msgstr "Mise à l'échelle locale"
#: ../vnc.html:189
msgid "Remote Resizing"
msgstr "Redimensionnement à distance"
#: ../vnc.html:194
msgid "Advanced"
msgstr "Avancé"
#: ../vnc.html:197
msgid "Quality:"
msgstr "Qualité:"
#: ../vnc.html:201
msgid "Compression level:"
msgstr "Niveau de compression:"
#: ../vnc.html:206
msgid "Repeater ID:"
msgstr "ID Répéteur:"
#: ../vnc.html:210
msgid "WebSocket"
msgstr "WebSocket"
#: ../vnc.html:213
msgid "Encrypt"
msgstr "Crypter"
#: ../vnc.html:216
msgid "Host:"
msgstr "Hôte:"
#: ../vnc.html:220
msgid "Port:"
msgstr "Port:"
#: ../vnc.html:224
msgid "Path:"
msgstr "Chemin:"
#: ../vnc.html:231
msgid "Automatic Reconnect"
msgstr "Reconnecter automatiquemen"
#: ../vnc.html:234
msgid "Reconnect Delay (ms):"
msgstr "Délai de reconnexion (ms):"
#: ../vnc.html:239
msgid "Show Dot when No Cursor"
msgstr "Afficher le point lorsqu'il n'y a pas de curseur"
#: ../vnc.html:244
msgid "Logging:"
msgstr "Se connecter:"
#: ../vnc.html:253
msgid "Version:"
msgstr "Version:"
#: ../vnc.html:261
msgid "Disconnect"
msgstr "Déconnecter"
#: ../vnc.html:280
msgid "Connect"
msgstr "Connecter"
#: ../vnc.html:290
msgid "Username:"
msgstr "Nom d'utilisateur:"
#: ../vnc.html:294
msgid "Password:"
msgstr "Mot de passe:"
#: ../vnc.html:298
msgid "Send Credentials"
msgstr "Envoyer les identifiants"
#: ../vnc.html:308
msgid "Cancel"
msgstr "Annuler"
+324
View File
@@ -0,0 +1,324 @@
# Japanese translations for noVNC package
# noVNC パッケージに対する日訳
# Copyright (C) 2019 The noVNC Authors
# This file is distributed under the same license as the noVNC package.
# nnn1590 <nnn1590@nnn1590.org>, 2019-2020.
#
msgid ""
msgstr ""
"Project-Id-Version: noVNC 1.1.0\n"
"Report-Msgid-Bugs-To: novnc@googlegroups.com\n"
"POT-Creation-Date: 2020-07-03 16:11+0200\n"
"PO-Revision-Date: 2021-01-15 12:37+0900\n"
"Last-Translator: nnn1590 <nnn1590@nnn1590.org>\n"
"Language-Team: Japanese\n"
"Language: ja\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
"X-Generator: Poedit 2.3\n"
#: ../app/ui.js:394
msgid "Connecting..."
msgstr "接続しています..."
#: ../app/ui.js:401
msgid "Disconnecting..."
msgstr "切断しています..."
#: ../app/ui.js:407
msgid "Reconnecting..."
msgstr "再接続しています..."
#: ../app/ui.js:412
msgid "Internal error"
msgstr "内部エラー"
#: ../app/ui.js:1008
msgid "Must set host"
msgstr "ホストを設定する必要があります"
#: ../app/ui.js:1090
msgid "Connected (encrypted) to "
msgstr "接続しました (暗号化済み): "
#: ../app/ui.js:1092
msgid "Connected (unencrypted) to "
msgstr "接続しました (暗号化されていません): "
#: ../app/ui.js:1115
msgid "Something went wrong, connection is closed"
msgstr "何らかの問題で、接続が閉じられました"
#: ../app/ui.js:1118
msgid "Failed to connect to server"
msgstr "サーバーへの接続に失敗しました"
#: ../app/ui.js:1128
msgid "Disconnected"
msgstr "切断しました"
#: ../app/ui.js:1143
msgid "New connection has been rejected with reason: "
msgstr "新規接続は次の理由で拒否されました: "
#: ../app/ui.js:1146
msgid "New connection has been rejected"
msgstr "新規接続は拒否されました"
#: ../app/ui.js:1181
msgid "Credentials are required"
msgstr "資格情報が必要です"
#: ../vnc.html:74
msgid "noVNC encountered an error:"
msgstr "noVNC でエラーが発生しました:"
#: ../vnc.html:84
msgid "Hide/Show the control bar"
msgstr "コントロールバーを隠す/表示する"
#: ../vnc.html:91
msgid "Drag"
msgstr "ドラッグ"
#: ../vnc.html:91
msgid "Move/Drag Viewport"
msgstr "ビューポートを移動/ドラッグ"
#: ../vnc.html:97
msgid "Keyboard"
msgstr "キーボード"
#: ../vnc.html:97
msgid "Show Keyboard"
msgstr "キーボードを表示"
#: ../vnc.html:102
msgid "Extra keys"
msgstr "追加キー"
#: ../vnc.html:102
msgid "Show Extra Keys"
msgstr "追加キーを表示"
#: ../vnc.html:107
msgid "Ctrl"
msgstr "Ctrl"
#: ../vnc.html:107
msgid "Toggle Ctrl"
msgstr "Ctrl キーを切り替え"
#: ../vnc.html:110
msgid "Alt"
msgstr "Alt"
#: ../vnc.html:110
msgid "Toggle Alt"
msgstr "Alt キーを切り替え"
#: ../vnc.html:113
msgid "Toggle Windows"
msgstr "Windows キーを切り替え"
#: ../vnc.html:113
msgid "Windows"
msgstr "Windows"
#: ../vnc.html:116
msgid "Send Tab"
msgstr "Tab キーを送信"
#: ../vnc.html:116
msgid "Tab"
msgstr "Tab"
#: ../vnc.html:119
msgid "Esc"
msgstr "Esc"
#: ../vnc.html:119
msgid "Send Escape"
msgstr "Escape キーを送信"
#: ../vnc.html:122
msgid "Ctrl+Alt+Del"
msgstr "Ctrl+Alt+Del"
#: ../vnc.html:122
msgid "Send Ctrl-Alt-Del"
msgstr "Ctrl-Alt-Del を送信"
#: ../vnc.html:129
msgid "Shutdown/Reboot"
msgstr "シャットダウン/再起動"
#: ../vnc.html:129
msgid "Shutdown/Reboot..."
msgstr "シャットダウン/再起動..."
#: ../vnc.html:135
msgid "Power"
msgstr "電源"
#: ../vnc.html:137
msgid "Shutdown"
msgstr "シャットダウン"
#: ../vnc.html:138
msgid "Reboot"
msgstr "再起動"
#: ../vnc.html:139
msgid "Reset"
msgstr "リセット"
#: ../vnc.html:144 ../vnc.html:150
msgid "Clipboard"
msgstr "クリップボード"
#: ../vnc.html:154
msgid "Clear"
msgstr "クリア"
#: ../vnc.html:160
msgid "Fullscreen"
msgstr "全画面表示"
#: ../vnc.html:165 ../vnc.html:172
msgid "Settings"
msgstr "設定"
#: ../vnc.html:175
msgid "Shared Mode"
msgstr "共有モード"
#: ../vnc.html:178
msgid "View Only"
msgstr "表示のみ"
#: ../vnc.html:182
msgid "Clip to Window"
msgstr "ウィンドウにクリップ"
#: ../vnc.html:185
msgid "Scaling Mode:"
msgstr "スケーリングモード:"
#: ../vnc.html:187
msgid "None"
msgstr "なし"
#: ../vnc.html:188
msgid "Local Scaling"
msgstr "ローカルスケーリング"
#: ../vnc.html:189
msgid "Remote Resizing"
msgstr "リモートでリサイズ"
#: ../vnc.html:194
msgid "Advanced"
msgstr "高度"
#: ../vnc.html:197
msgid "Quality:"
msgstr "品質:"
#: ../vnc.html:201
msgid "Compression level:"
msgstr "圧縮レベル:"
#: ../vnc.html:206
msgid "Repeater ID:"
msgstr "リピーター ID:"
#: ../vnc.html:210
msgid "WebSocket"
msgstr "WebSocket"
#: ../vnc.html:213
msgid "Encrypt"
msgstr "暗号化"
#: ../vnc.html:216
msgid "Host:"
msgstr "ホスト:"
#: ../vnc.html:220
msgid "Port:"
msgstr "ポート:"
#: ../vnc.html:224
msgid "Path:"
msgstr "パス:"
#: ../vnc.html:231
msgid "Automatic Reconnect"
msgstr "自動再接続"
#: ../vnc.html:234
msgid "Reconnect Delay (ms):"
msgstr "再接続する遅延 (ミリ秒):"
#: ../vnc.html:239
msgid "Show Dot when No Cursor"
msgstr "カーソルがないときにドットを表示"
#: ../vnc.html:244
msgid "Logging:"
msgstr "ロギング:"
#: ../vnc.html:253
msgid "Version:"
msgstr "バージョン:"
#: ../vnc.html:261
msgid "Disconnect"
msgstr "切断"
#: ../vnc.html:280
msgid "Connect"
msgstr "接続"
#: ../vnc.html:290
msgid "Username:"
msgstr "ユーザー名:"
#: ../vnc.html:294
msgid "Password:"
msgstr "パスワード:"
#: ../vnc.html:298
msgid "Send Credentials"
msgstr "資格情報を送信"
#: ../vnc.html:308
msgid "Cancel"
msgstr "キャンセル"
#~ msgid "Password is required"
#~ msgstr "パスワードが必要です"
#~ msgid "viewport drag"
#~ msgstr "ビューポートをドラッグ"
#~ msgid "Active Mouse Button"
#~ msgstr "アクティブなマウスボタン"
#~ msgid "No mousebutton"
#~ msgstr "マウスボタンなし"
#~ msgid "Left mousebutton"
#~ msgstr "左マウスボタン"
#~ msgid "Middle mousebutton"
#~ msgstr "中マウスボタン"
#~ msgid "Right mousebutton"
#~ msgstr "右マウスボタン"
#~ msgid "Send Password"
#~ msgstr "パスワードを送信"
+89 -93
View File
@@ -6,9 +6,9 @@
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: noVNC 1.1.0\n"
"Project-Id-Version: noVNC 1.3.0\n"
"Report-Msgid-Bugs-To: novnc@googlegroups.com\n"
"POT-Creation-Date: 2019-01-16 11:06+0100\n"
"POT-Creation-Date: 2021-08-27 16:03+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -17,286 +17,282 @@ msgstr ""
"Content-Type: text/plain; charset=CHARSET\n"
"Content-Transfer-Encoding: 8bit\n"
#: ../app/ui.js:387
#: ../app/ui.js:400
msgid "Connecting..."
msgstr ""
#: ../app/ui.js:394
#: ../app/ui.js:407
msgid "Disconnecting..."
msgstr ""
#: ../app/ui.js:400
#: ../app/ui.js:413
msgid "Reconnecting..."
msgstr ""
#: ../app/ui.js:405
#: ../app/ui.js:418
msgid "Internal error"
msgstr ""
#: ../app/ui.js:995
#: ../app/ui.js:1009
msgid "Must set host"
msgstr ""
#: ../app/ui.js:1077
#: ../app/ui.js:1091
msgid "Connected (encrypted) to "
msgstr ""
#: ../app/ui.js:1079
#: ../app/ui.js:1093
msgid "Connected (unencrypted) to "
msgstr ""
#: ../app/ui.js:1102
#: ../app/ui.js:1116
msgid "Something went wrong, connection is closed"
msgstr ""
#: ../app/ui.js:1105
#: ../app/ui.js:1119
msgid "Failed to connect to server"
msgstr ""
#: ../app/ui.js:1115
#: ../app/ui.js:1129
msgid "Disconnected"
msgstr ""
#: ../app/ui.js:1128
#: ../app/ui.js:1144
msgid "New connection has been rejected with reason: "
msgstr ""
#: ../app/ui.js:1131
#: ../app/ui.js:1147
msgid "New connection has been rejected"
msgstr ""
#: ../app/ui.js:1151
msgid "Password is required"
#: ../app/ui.js:1182
msgid "Credentials are required"
msgstr ""
#: ../vnc.html:84
#: ../vnc.html:61
msgid "noVNC encountered an error:"
msgstr ""
#: ../vnc.html:94
#: ../vnc.html:71
msgid "Hide/Show the control bar"
msgstr ""
#: ../vnc.html:101
#: ../vnc.html:78
msgid "Drag"
msgstr ""
#: ../vnc.html:78
msgid "Move/Drag Viewport"
msgstr ""
#: ../vnc.html:101
msgid "viewport drag"
msgstr ""
#: ../vnc.html:107 ../vnc.html:110 ../vnc.html:113 ../vnc.html:116
msgid "Active Mouse Button"
msgstr ""
#: ../vnc.html:107
msgid "No mousebutton"
msgstr ""
#: ../vnc.html:110
msgid "Left mousebutton"
msgstr ""
#: ../vnc.html:113
msgid "Middle mousebutton"
msgstr ""
#: ../vnc.html:116
msgid "Right mousebutton"
msgstr ""
#: ../vnc.html:119
#: ../vnc.html:84
msgid "Keyboard"
msgstr ""
#: ../vnc.html:119
#: ../vnc.html:84
msgid "Show Keyboard"
msgstr ""
#: ../vnc.html:126
#: ../vnc.html:89
msgid "Extra keys"
msgstr ""
#: ../vnc.html:126
#: ../vnc.html:89
msgid "Show Extra Keys"
msgstr ""
#: ../vnc.html:131
#: ../vnc.html:94
msgid "Ctrl"
msgstr ""
#: ../vnc.html:131
#: ../vnc.html:94
msgid "Toggle Ctrl"
msgstr ""
#: ../vnc.html:134
#: ../vnc.html:97
msgid "Alt"
msgstr ""
#: ../vnc.html:134
#: ../vnc.html:97
msgid "Toggle Alt"
msgstr ""
#: ../vnc.html:137
#: ../vnc.html:100
msgid "Toggle Windows"
msgstr ""
#: ../vnc.html:137
#: ../vnc.html:100
msgid "Windows"
msgstr ""
#: ../vnc.html:140
#: ../vnc.html:103
msgid "Send Tab"
msgstr ""
#: ../vnc.html:140
#: ../vnc.html:103
msgid "Tab"
msgstr ""
#: ../vnc.html:143
#: ../vnc.html:106
msgid "Esc"
msgstr ""
#: ../vnc.html:143
#: ../vnc.html:106
msgid "Send Escape"
msgstr ""
#: ../vnc.html:146
#: ../vnc.html:109
msgid "Ctrl+Alt+Del"
msgstr ""
#: ../vnc.html:146
#: ../vnc.html:109
msgid "Send Ctrl-Alt-Del"
msgstr ""
#: ../vnc.html:154
#: ../vnc.html:116
msgid "Shutdown/Reboot"
msgstr ""
#: ../vnc.html:154
#: ../vnc.html:116
msgid "Shutdown/Reboot..."
msgstr ""
#: ../vnc.html:160
#: ../vnc.html:122
msgid "Power"
msgstr ""
#: ../vnc.html:162
#: ../vnc.html:124
msgid "Shutdown"
msgstr ""
#: ../vnc.html:163
#: ../vnc.html:125
msgid "Reboot"
msgstr ""
#: ../vnc.html:164
#: ../vnc.html:126
msgid "Reset"
msgstr ""
#: ../vnc.html:169 ../vnc.html:175
#: ../vnc.html:131 ../vnc.html:137
msgid "Clipboard"
msgstr ""
#: ../vnc.html:179
#: ../vnc.html:141
msgid "Clear"
msgstr ""
#: ../vnc.html:185
#: ../vnc.html:147
msgid "Fullscreen"
msgstr ""
#: ../vnc.html:190 ../vnc.html:197
#: ../vnc.html:152 ../vnc.html:159
msgid "Settings"
msgstr ""
#: ../vnc.html:200
#: ../vnc.html:162
msgid "Shared Mode"
msgstr ""
#: ../vnc.html:203
#: ../vnc.html:165
msgid "View Only"
msgstr ""
#: ../vnc.html:207
#: ../vnc.html:169
msgid "Clip to Window"
msgstr ""
#: ../vnc.html:210
#: ../vnc.html:172
msgid "Scaling Mode:"
msgstr ""
#: ../vnc.html:212
#: ../vnc.html:174
msgid "None"
msgstr ""
#: ../vnc.html:213
#: ../vnc.html:175
msgid "Local Scaling"
msgstr ""
#: ../vnc.html:214
#: ../vnc.html:176
msgid "Remote Resizing"
msgstr ""
#: ../vnc.html:219
#: ../vnc.html:181
msgid "Advanced"
msgstr ""
#: ../vnc.html:222
#: ../vnc.html:184
msgid "Quality:"
msgstr ""
#: ../vnc.html:188
msgid "Compression level:"
msgstr ""
#: ../vnc.html:193
msgid "Repeater ID:"
msgstr ""
#: ../vnc.html:226
#: ../vnc.html:197
msgid "WebSocket"
msgstr ""
#: ../vnc.html:229
#: ../vnc.html:200
msgid "Encrypt"
msgstr ""
#: ../vnc.html:232
#: ../vnc.html:203
msgid "Host:"
msgstr ""
#: ../vnc.html:236
#: ../vnc.html:207
msgid "Port:"
msgstr ""
#: ../vnc.html:240
#: ../vnc.html:211
msgid "Path:"
msgstr ""
#: ../vnc.html:247
#: ../vnc.html:218
msgid "Automatic Reconnect"
msgstr ""
#: ../vnc.html:250
#: ../vnc.html:221
msgid "Reconnect Delay (ms):"
msgstr ""
#: ../vnc.html:255
#: ../vnc.html:226
msgid "Show Dot when No Cursor"
msgstr ""
#: ../vnc.html:260
#: ../vnc.html:231
msgid "Logging:"
msgstr ""
#: ../vnc.html:272
#: ../vnc.html:240
msgid "Version:"
msgstr ""
#: ../vnc.html:248
msgid "Disconnect"
msgstr ""
#: ../vnc.html:291
#: ../vnc.html:267
msgid "Connect"
msgstr ""
#: ../vnc.html:301
#: ../vnc.html:277
msgid "Username:"
msgstr ""
#: ../vnc.html:281
msgid "Password:"
msgstr ""
#: ../vnc.html:305
msgid "Send Password"
#: ../vnc.html:285
msgid "Send Credentials"
msgstr ""
#: ../vnc.html:315
#: ../vnc.html:295
msgid "Cancel"
msgstr ""
+4 -4
View File
@@ -22,17 +22,17 @@ const fs = require('fs');
const po2json = require("po2json");
const opt = getopt.create([
['h' , 'help' , 'display this help'],
['h', 'help', 'display this help'],
]).bindHelp().parseSystem();
if (opt.argv.length != 2) {
console.error("Incorrect number of arguments given");
process.exit(1);
console.error("Incorrect number of arguments given");
process.exit(1);
}
const data = po2json.parseFileSync(opt.argv[0]);
const bodyPart = Object.keys(data).filter((msgid) => msgid !== "").map((msgid) => {
const bodyPart = Object.keys(data).filter(msgid => msgid !== "").map((msgid) => {
if (msgid === "") return;
const msgstr = data[msgid][1];
return " " + JSON.stringify(msgid) + ": " + JSON.stringify(msgstr);
+299
View File
@@ -0,0 +1,299 @@
# Portuguese translations for noVNC package.
# Copyright (C) 2021 The noVNC Authors
# This file is distributed under the same license as the noVNC package.
# <liddack@outlook.com>, 2021.
#
msgid ""
msgstr ""
"Project-Id-Version: noVNC 1.2.0\n"
"Report-Msgid-Bugs-To: novnc@googlegroups.com\n"
"POT-Creation-Date: 2021-03-15 21:55-0300\n"
"PO-Revision-Date: 2021-03-15 22:09-0300\n"
"Last-Translator: <liddack@outlook.com>\n"
"Language-Team: Brazilian Portuguese\n"
"Language: pt_BR\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
"X-Generator: Poedit 2.4.1\n"
#: ../app/ui.js:400
msgid "Connecting..."
msgstr "Conectando..."
#: ../app/ui.js:407
msgid "Disconnecting..."
msgstr "Desconectando..."
#: ../app/ui.js:413
msgid "Reconnecting..."
msgstr "Reconectando..."
#: ../app/ui.js:418
msgid "Internal error"
msgstr "Erro interno"
#: ../app/ui.js:1009
msgid "Must set host"
msgstr "É necessário definir o host"
#: ../app/ui.js:1091
msgid "Connected (encrypted) to "
msgstr "Conectado (com criptografia) a "
#: ../app/ui.js:1093
msgid "Connected (unencrypted) to "
msgstr "Conectado (sem criptografia) a "
#: ../app/ui.js:1116
msgid "Something went wrong, connection is closed"
msgstr "Algo deu errado. A conexão foi encerrada."
#: ../app/ui.js:1119
msgid "Failed to connect to server"
msgstr "Falha ao conectar-se ao servidor"
#: ../app/ui.js:1129
msgid "Disconnected"
msgstr "Desconectado"
#: ../app/ui.js:1144
msgid "New connection has been rejected with reason: "
msgstr "A nova conexão foi rejeitada pelo motivo: "
#: ../app/ui.js:1147
msgid "New connection has been rejected"
msgstr "A nova conexão foi rejeitada"
#: ../app/ui.js:1182
msgid "Credentials are required"
msgstr "Credenciais são obrigatórias"
#: ../vnc.html:61
msgid "noVNC encountered an error:"
msgstr "O noVNC encontrou um erro:"
#: ../vnc.html:71
msgid "Hide/Show the control bar"
msgstr "Esconder/mostrar a barra de controles"
#: ../vnc.html:78
msgid "Drag"
msgstr "Arrastar"
#: ../vnc.html:78
msgid "Move/Drag Viewport"
msgstr "Mover/arrastar a janela"
#: ../vnc.html:84
msgid "Keyboard"
msgstr "Teclado"
#: ../vnc.html:84
msgid "Show Keyboard"
msgstr "Mostrar teclado"
#: ../vnc.html:89
msgid "Extra keys"
msgstr "Teclas adicionais"
#: ../vnc.html:89
msgid "Show Extra Keys"
msgstr "Mostar teclas adicionais"
#: ../vnc.html:94
msgid "Ctrl"
msgstr "Ctrl"
#: ../vnc.html:94
msgid "Toggle Ctrl"
msgstr "Pressionar/soltar Ctrl"
#: ../vnc.html:97
msgid "Alt"
msgstr "Alt"
#: ../vnc.html:97
msgid "Toggle Alt"
msgstr "Pressionar/soltar Alt"
#: ../vnc.html:100
msgid "Toggle Windows"
msgstr "Pressionar/soltar Windows"
#: ../vnc.html:100
msgid "Windows"
msgstr "Windows"
#: ../vnc.html:103
msgid "Send Tab"
msgstr "Enviar Tab"
#: ../vnc.html:103
msgid "Tab"
msgstr "Tab"
#: ../vnc.html:106
msgid "Esc"
msgstr "Esc"
#: ../vnc.html:106
msgid "Send Escape"
msgstr "Enviar Esc"
#: ../vnc.html:109
msgid "Ctrl+Alt+Del"
msgstr "Ctrl+Alt+Del"
#: ../vnc.html:109
msgid "Send Ctrl-Alt-Del"
msgstr "Enviar Ctrl-Alt-Del"
#: ../vnc.html:116
msgid "Shutdown/Reboot"
msgstr "Desligar/reiniciar"
#: ../vnc.html:116
msgid "Shutdown/Reboot..."
msgstr "Desligar/reiniciar..."
#: ../vnc.html:122
msgid "Power"
msgstr "Ligar"
#: ../vnc.html:124
msgid "Shutdown"
msgstr "Desligar"
#: ../vnc.html:125
msgid "Reboot"
msgstr "Reiniciar"
#: ../vnc.html:126
msgid "Reset"
msgstr "Reiniciar (forçado)"
#: ../vnc.html:131 ../vnc.html:137
msgid "Clipboard"
msgstr "Área de transferência"
#: ../vnc.html:141
msgid "Clear"
msgstr "Limpar"
#: ../vnc.html:147
msgid "Fullscreen"
msgstr "Tela cheia"
#: ../vnc.html:152 ../vnc.html:159
msgid "Settings"
msgstr "Configurações"
#: ../vnc.html:162
msgid "Shared Mode"
msgstr "Modo compartilhado"
#: ../vnc.html:165
msgid "View Only"
msgstr "Apenas visualizar"
#: ../vnc.html:169
msgid "Clip to Window"
msgstr "Recortar à janela"
#: ../vnc.html:172
msgid "Scaling Mode:"
msgstr "Modo de dimensionamento:"
#: ../vnc.html:174
msgid "None"
msgstr "Nenhum"
#: ../vnc.html:175
msgid "Local Scaling"
msgstr "Local"
#: ../vnc.html:176
msgid "Remote Resizing"
msgstr "Remoto"
#: ../vnc.html:181
msgid "Advanced"
msgstr "Avançado"
#: ../vnc.html:184
msgid "Quality:"
msgstr "Qualidade:"
#: ../vnc.html:188
msgid "Compression level:"
msgstr "Nível de compressão:"
#: ../vnc.html:193
msgid "Repeater ID:"
msgstr "ID do repetidor:"
#: ../vnc.html:197
msgid "WebSocket"
msgstr "WebSocket"
#: ../vnc.html:200
msgid "Encrypt"
msgstr "Criptografar"
#: ../vnc.html:203
msgid "Host:"
msgstr "Host:"
#: ../vnc.html:207
msgid "Port:"
msgstr "Porta:"
#: ../vnc.html:211
msgid "Path:"
msgstr "Caminho:"
#: ../vnc.html:218
msgid "Automatic Reconnect"
msgstr "Reconexão automática"
#: ../vnc.html:221
msgid "Reconnect Delay (ms):"
msgstr "Atraso da reconexão (ms)"
#: ../vnc.html:226
msgid "Show Dot when No Cursor"
msgstr "Mostrar ponto quando não há cursor"
#: ../vnc.html:231
msgid "Logging:"
msgstr "Registros:"
#: ../vnc.html:240
msgid "Version:"
msgstr "Versão:"
#: ../vnc.html:248
msgid "Disconnect"
msgstr "Desconectar"
#: ../vnc.html:267
msgid "Connect"
msgstr "Conectar"
#: ../vnc.html:277
msgid "Username:"
msgstr "Nome de usuário:"
#: ../vnc.html:281
msgid "Password:"
msgstr "Senha:"
#: ../vnc.html:285
msgid "Send Credentials"
msgstr "Enviar credenciais"
#: ../vnc.html:295
msgid "Cancel"
msgstr "Cancelar"
+95 -111
View File
@@ -1,15 +1,15 @@
# Swedish translations for noVNC package
# Svenska översättningar för paket noVNC.
# Copyright (C) 2018 The noVNC Authors
# Svenska översättningar för paketet noVNC.
# Copyright (C) 2020 The noVNC Authors
# This file is distributed under the same license as the noVNC package.
# Samuel Mannehed <samuel@cendio.se>, 2019.
# Samuel Mannehed <samuel@cendio.se>, 2020.
#
msgid ""
msgstr ""
"Project-Id-Version: noVNC 1.1.0\n"
"Project-Id-Version: noVNC 1.3.0\n"
"Report-Msgid-Bugs-To: novnc@googlegroups.com\n"
"POT-Creation-Date: 2019-01-16 11:06+0100\n"
"PO-Revision-Date: 2019-04-08 10:18+0200\n"
"POT-Creation-Date: 2021-08-27 16:03+0200\n"
"PO-Revision-Date: 2021-08-27 16:18+0200\n"
"Last-Translator: Samuel Mannehed <samuel@cendio.se>\n"
"Language-Team: none\n"
"Language: sv\n"
@@ -19,298 +19,282 @@ msgstr ""
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Poedit 2.0.3\n"
#: ../app/ui.js:387
#: ../app/ui.js:400
msgid "Connecting..."
msgstr "Ansluter..."
#: ../app/ui.js:394
#: ../app/ui.js:407
msgid "Disconnecting..."
msgstr "Kopplar ner..."
#: ../app/ui.js:400
#: ../app/ui.js:413
msgid "Reconnecting..."
msgstr "Återansluter..."
#: ../app/ui.js:405
#: ../app/ui.js:418
msgid "Internal error"
msgstr "Internt fel"
#: ../app/ui.js:995
#: ../app/ui.js:1009
msgid "Must set host"
msgstr "Du måste specifiera en värd"
#: ../app/ui.js:1077
#: ../app/ui.js:1091
msgid "Connected (encrypted) to "
msgstr "Ansluten (krypterat) till "
#: ../app/ui.js:1079
#: ../app/ui.js:1093
msgid "Connected (unencrypted) to "
msgstr "Ansluten (okrypterat) till "
#: ../app/ui.js:1102
#: ../app/ui.js:1116
msgid "Something went wrong, connection is closed"
msgstr "Något gick fel, anslutningen avslutades"
#: ../app/ui.js:1105
#: ../app/ui.js:1119
msgid "Failed to connect to server"
msgstr "Misslyckades att ansluta till servern"
#: ../app/ui.js:1115
#: ../app/ui.js:1129
msgid "Disconnected"
msgstr "Frånkopplad"
#: ../app/ui.js:1128
#: ../app/ui.js:1144
msgid "New connection has been rejected with reason: "
msgstr "Ny anslutning har blivit nekad med följande skäl: "
#: ../app/ui.js:1131
#: ../app/ui.js:1147
msgid "New connection has been rejected"
msgstr "Ny anslutning har blivit nekad"
#: ../app/ui.js:1151
msgid "Password is required"
msgstr "Lösenord krävs"
#: ../app/ui.js:1182
msgid "Credentials are required"
msgstr "Användaruppgifter krävs"
#: ../vnc.html:84
#: ../vnc.html:61
msgid "noVNC encountered an error:"
msgstr "noVNC stötte på ett problem:"
#: ../vnc.html:94
#: ../vnc.html:71
msgid "Hide/Show the control bar"
msgstr "Göm/Visa kontrollbaren"
#: ../vnc.html:101
#: ../vnc.html:78
msgid "Drag"
msgstr "Dra"
#: ../vnc.html:78
msgid "Move/Drag Viewport"
msgstr "Flytta/Dra Vyn"
#: ../vnc.html:101
msgid "viewport drag"
msgstr "dra vy"
#: ../vnc.html:107 ../vnc.html:110 ../vnc.html:113 ../vnc.html:116
msgid "Active Mouse Button"
msgstr "Aktiv musknapp"
#: ../vnc.html:107
msgid "No mousebutton"
msgstr "Ingen musknapp"
#: ../vnc.html:110
msgid "Left mousebutton"
msgstr "Vänster musknapp"
#: ../vnc.html:113
msgid "Middle mousebutton"
msgstr "Mitten-musknapp"
#: ../vnc.html:116
msgid "Right mousebutton"
msgstr "Höger musknapp"
#: ../vnc.html:119
#: ../vnc.html:84
msgid "Keyboard"
msgstr "Tangentbord"
#: ../vnc.html:119
#: ../vnc.html:84
msgid "Show Keyboard"
msgstr "Visa Tangentbord"
#: ../vnc.html:126
#: ../vnc.html:89
msgid "Extra keys"
msgstr "Extraknappar"
#: ../vnc.html:126
#: ../vnc.html:89
msgid "Show Extra Keys"
msgstr "Visa Extraknappar"
#: ../vnc.html:131
#: ../vnc.html:94
msgid "Ctrl"
msgstr "Ctrl"
#: ../vnc.html:131
#: ../vnc.html:94
msgid "Toggle Ctrl"
msgstr "Växla Ctrl"
#: ../vnc.html:134
#: ../vnc.html:97
msgid "Alt"
msgstr "Alt"
#: ../vnc.html:134
#: ../vnc.html:97
msgid "Toggle Alt"
msgstr "Växla Alt"
#: ../vnc.html:137
#: ../vnc.html:100
msgid "Toggle Windows"
msgstr "Växla Windows"
#: ../vnc.html:137
#: ../vnc.html:100
msgid "Windows"
msgstr "Windows"
#: ../vnc.html:140
#: ../vnc.html:103
msgid "Send Tab"
msgstr "Skicka Tab"
#: ../vnc.html:140
#: ../vnc.html:103
msgid "Tab"
msgstr "Tab"
#: ../vnc.html:143
#: ../vnc.html:106
msgid "Esc"
msgstr "Esc"
#: ../vnc.html:143
#: ../vnc.html:106
msgid "Send Escape"
msgstr "Skicka Escape"
#: ../vnc.html:146
#: ../vnc.html:109
msgid "Ctrl+Alt+Del"
msgstr "Ctrl+Alt+Del"
#: ../vnc.html:146
#: ../vnc.html:109
msgid "Send Ctrl-Alt-Del"
msgstr "Skicka Ctrl-Alt-Del"
#: ../vnc.html:154
#: ../vnc.html:116
msgid "Shutdown/Reboot"
msgstr "Stäng av/Boota om"
#: ../vnc.html:154
#: ../vnc.html:116
msgid "Shutdown/Reboot..."
msgstr "Stäng av/Boota om..."
#: ../vnc.html:160
#: ../vnc.html:122
msgid "Power"
msgstr "Ström"
#: ../vnc.html:162
#: ../vnc.html:124
msgid "Shutdown"
msgstr "Stäng av"
#: ../vnc.html:163
#: ../vnc.html:125
msgid "Reboot"
msgstr "Boota om"
#: ../vnc.html:164
#: ../vnc.html:126
msgid "Reset"
msgstr "Återställ"
#: ../vnc.html:169 ../vnc.html:175
#: ../vnc.html:131 ../vnc.html:137
msgid "Clipboard"
msgstr "Urklipp"
#: ../vnc.html:179
#: ../vnc.html:141
msgid "Clear"
msgstr "Rensa"
#: ../vnc.html:185
#: ../vnc.html:147
msgid "Fullscreen"
msgstr "Fullskärm"
#: ../vnc.html:190 ../vnc.html:197
#: ../vnc.html:152 ../vnc.html:159
msgid "Settings"
msgstr "Inställningar"
#: ../vnc.html:200
#: ../vnc.html:162
msgid "Shared Mode"
msgstr "Delat Läge"
#: ../vnc.html:203
#: ../vnc.html:165
msgid "View Only"
msgstr "Endast Visning"
#: ../vnc.html:207
#: ../vnc.html:169
msgid "Clip to Window"
msgstr "Begränsa till Fönster"
#: ../vnc.html:210
#: ../vnc.html:172
msgid "Scaling Mode:"
msgstr "Skalningsläge:"
#: ../vnc.html:212
#: ../vnc.html:174
msgid "None"
msgstr "Ingen"
#: ../vnc.html:213
#: ../vnc.html:175
msgid "Local Scaling"
msgstr "Lokal Skalning"
#: ../vnc.html:214
#: ../vnc.html:176
msgid "Remote Resizing"
msgstr "Ändra Storlek"
#: ../vnc.html:219
#: ../vnc.html:181
msgid "Advanced"
msgstr "Avancerat"
#: ../vnc.html:222
#: ../vnc.html:184
msgid "Quality:"
msgstr "Kvalitet:"
#: ../vnc.html:188
msgid "Compression level:"
msgstr "Kompressionsnivå:"
#: ../vnc.html:193
msgid "Repeater ID:"
msgstr "Repeater-ID:"
#: ../vnc.html:226
#: ../vnc.html:197
msgid "WebSocket"
msgstr "WebSocket"
#: ../vnc.html:229
#: ../vnc.html:200
msgid "Encrypt"
msgstr "Kryptera"
#: ../vnc.html:232
#: ../vnc.html:203
msgid "Host:"
msgstr "Värd:"
#: ../vnc.html:236
#: ../vnc.html:207
msgid "Port:"
msgstr "Port:"
#: ../vnc.html:240
#: ../vnc.html:211
msgid "Path:"
msgstr "Sökväg:"
#: ../vnc.html:247
#: ../vnc.html:218
msgid "Automatic Reconnect"
msgstr "Automatisk Återanslutning"
#: ../vnc.html:250
#: ../vnc.html:221
msgid "Reconnect Delay (ms):"
msgstr "Fördröjning (ms):"
#: ../vnc.html:255
#: ../vnc.html:226
msgid "Show Dot when No Cursor"
msgstr "Visa prick när ingen muspekare finns"
#: ../vnc.html:260
#: ../vnc.html:231
msgid "Logging:"
msgstr "Loggning:"
#: ../vnc.html:272
#: ../vnc.html:240
msgid "Version:"
msgstr "Version:"
#: ../vnc.html:248
msgid "Disconnect"
msgstr "Koppla från"
#: ../vnc.html:291
#: ../vnc.html:267
msgid "Connect"
msgstr "Anslut"
#: ../vnc.html:301
#: ../vnc.html:277
msgid "Username:"
msgstr "Användarnamn:"
#: ../vnc.html:281
msgid "Password:"
msgstr "Lösenord:"
#: ../vnc.html:305
msgid "Send Password"
msgstr "Skicka lösenord"
#: ../vnc.html:285
msgid "Send Credentials"
msgstr "Skicka Användaruppgifter"
#: ../vnc.html:315
#: ../vnc.html:295
msgid "Cancel"
msgstr "Avbryt"
#~ msgid "Disconnect timeout"
#~ msgstr "Det tog för lång tid att koppla ner"
#~ msgid "Local Downscaling"
#~ msgstr "Lokal Nedskalning"
#~ msgid "Local Cursor"
#~ msgstr "Lokal Muspekare"
#~ msgid "Canvas not supported."
#~ msgstr "Canvas stöds ej"
+7 -7
View File
@@ -10,8 +10,8 @@ const jsdom = require("jsdom");
const fs = require("fs");
const opt = getopt.create([
['o' , 'output=FILE' , 'write output to specified file'],
['h' , 'help' , 'display this help'],
['o', 'output=FILE', 'write output to specified file'],
['h', 'help', 'display this help'],
]).bindHelp().parseSystem();
const strings = {};
@@ -22,7 +22,7 @@ function addString(str, location) {
}
if (strings[str] === undefined) {
strings[str] = {}
strings[str] = {};
}
strings[str][location] = null;
}
@@ -74,7 +74,7 @@ function process(elem, locator, enabled) {
}
for (let i = 0; i < elem.childNodes.length; i++) {
node = elem.childNodes[i];
let node = elem.childNodes[i];
if (node.nodeType === node.ELEMENT_NODE) {
process(node, locator, enabled);
} else if (node.nodeType === node.TEXT_NODE && enabled) {
@@ -89,18 +89,18 @@ for (let i = 0; i < opt.argv.length; i++) {
const dom = new jsdom.JSDOM(file, { includeNodeLocations: true });
const body = dom.window.document.body;
function locator(elem) {
let locator = (elem) => {
const offset = dom.nodeLocation(elem).startOffset;
const line = file.slice(0, offset).split("\n").length;
return fn + ":" + line;
}
};
process(body, locator, true);
}
let output = "";
for (str in strings) {
for (let str in strings) {
output += "#:";
for (location in strings[str]) {
output += " " + location;
+20 -20
View File
@@ -1,14 +1,14 @@
# Simplified Chinese translations for noVNC package.
# Copyright (C) 2018 The noVNC Authors
# Copyright (C) 2020 The noVNC Authors
# This file is distributed under the same license as the noVNC package.
# Peter Dave Hello <hsu@peterdavehello.org>, 2018.
#
msgid ""
msgstr ""
"Project-Id-Version: noVNC 1.0.0-testing.2\n"
"Project-Id-Version: noVNC 1.1.0\n"
"Report-Msgid-Bugs-To: novnc@googlegroups.com\n"
"POT-Creation-Date: 2018-01-10 00:53+0800\n"
"PO-Revision-Date: 2018-04-06 21:33+0800\n"
"PO-Revision-Date: 2020-01-02 13:19+0800\n"
"Last-Translator: CUI Wei <ghostplant@qq.com>\n"
"Language: zh_CN\n"
"MIME-Version: 1.0\n"
@@ -17,15 +17,15 @@ msgstr ""
#: ../app/ui.js:395
msgid "Connecting..."
msgstr "接中..."
msgstr "接中..."
#: ../app/ui.js:402
msgid "Disconnecting..."
msgstr "正在断连接..."
msgstr "正在断连接..."
#: ../app/ui.js:408
msgid "Reconnecting..."
msgstr "重新接中..."
msgstr "重新接中..."
#: ../app/ui.js:413
msgid "Internal error"
@@ -37,31 +37,31 @@ msgstr "请提供主机名"
#: ../app/ui.js:1097
msgid "Connected (encrypted) to "
msgstr "已加密链接到"
msgstr "已连接到(加密)"
#: ../app/ui.js:1099
msgid "Connected (unencrypted) to "
msgstr "未加密链接到"
msgstr "已连接到(未加密)"
#: ../app/ui.js:1120
msgid "Something went wrong, connection is closed"
msgstr "发生错误,接已关闭"
msgstr "发生错误,接已关闭"
#: ../app/ui.js:1123
msgid "Failed to connect to server"
msgstr "无法接到服务器"
msgstr "无法接到服务器"
#: ../app/ui.js:1133
msgid "Disconnected"
msgstr "链接已中断"
msgstr "已断开连接"
#: ../app/ui.js:1146
msgid "New connection has been rejected with reason: "
msgstr "接被拒绝,原因:"
msgstr "接被拒绝,原因:"
#: ../app/ui.js:1149
msgid "New connection has been rejected"
msgstr "接被拒绝"
msgstr "接被拒绝"
#: ../app/ui.js:1170
msgid "Password is required"
@@ -73,7 +73,7 @@ msgstr "noVNC 遇到一个错误:"
#: ../vnc.html:99
msgid "Hide/Show the control bar"
msgstr "显示/隐藏控制"
msgstr "显示/隐藏控制"
#: ../vnc.html:106
msgid "Move/Drag Viewport"
@@ -193,7 +193,7 @@ msgstr "清除"
#: ../vnc.html:187
msgid "Fullscreen"
msgstr "全屏"
msgstr "全屏"
#: ../vnc.html:192 ../vnc.html:199
msgid "Settings"
@@ -205,7 +205,7 @@ msgstr "分享模式"
#: ../vnc.html:205
msgid "View Only"
msgstr "仅检视"
msgstr "仅查看"
#: ../vnc.html:209
msgid "Clip to Window"
@@ -257,11 +257,11 @@ msgstr "路径:"
#: ../vnc.html:249
msgid "Automatic Reconnect"
msgstr "自动重新接"
msgstr "自动重新接"
#: ../vnc.html:252
msgid "Reconnect Delay (ms):"
msgstr "重新接间隔 (ms)"
msgstr "重新接间隔 (ms)"
#: ../vnc.html:258
msgid "Logging:"
@@ -269,11 +269,11 @@ msgstr "日志级别:"
#: ../vnc.html:270
msgid "Disconnect"
msgstr "终端链接"
msgstr "中断连接"
#: ../vnc.html:289
msgid "Connect"
msgstr "接"
msgstr "接"
#: ../vnc.html:299
msgid "Password:"
+3
View File
@@ -0,0 +1,3 @@
#!/bin/sh -e
snapctl restart novnc.novncsvc
+29
View File
@@ -0,0 +1,29 @@
#!/bin/bash
# `snapctl get services` returns a JSON array, example:
#{
#"n6801": {
# "listen": 6801,
# "vnc": "localhost:5901"
#},
#"n6802": {
# "listen": 6802,
# "vnc": "localhost:5902"
#}
#}
snapctl get services | jq -c '.[]' | while read service; do # for each service the user sepcified..
# get the important data for the service (listen port, VNC host:port)
listen_port="$(echo $service | jq --raw-output '.listen')"
vnc_host_port="$(echo $service | jq --raw-output '.vnc')" # --raw-output removes any quotation marks from the output
# check whether those values are valid
expr "$listen_port" : '^[0-9]\+$' > /dev/null
listen_port_valid=$?
if [ ! $listen_port_valid ] || [ -z "$vnc_host_port" ]; then
# invalid values mean the service is disabled, do nothing except for printing a message (logged in /var/log/system or systemd journal)
echo "novnc: not starting service ${service} with listen_port ${listen_port} and vnc_host_port ${vnc_host_port}"
else
# start (and fork with '&') the service using the specified listen port and VNC host:port
$SNAP/novnc_proxy --listen $listen_port --vnc $vnc_host_port &
fi
done
+55
View File
@@ -0,0 +1,55 @@
name: novnc
base: core18 # the base snap is the execution environment for this snap
version: '@VERSION@'
summary: Open Source VNC client using HTML5 (WebSockets, Canvas)
description: |
Open Source VNC client using HTML5 (WebSockets, Canvas).
noVNC is both a VNC client JavaScript library as well as an
application built on top of that library. noVNC runs well in any
modern browser including mobile browsers (iOS and Android).
grade: stable
confinement: strict
parts:
novnc:
source: .
plugin: dump
organize:
utils/novnc_proxy: /
stage:
- vnc.html
- app
- core/**/*.js
- vendor/**/*.js
- novnc_proxy
stage-packages:
- bash
svc-script:
source: snap/local
plugin: dump
stage:
- svc_wrapper.sh
stage-packages:
- bash
- jq
websockify:
source: https://github.com/novnc/websockify/archive/v0.9.0.tar.gz
plugin: python
stage-packages:
- python3-numpy
hooks:
configure:
plugs: [network, network-bind]
apps:
novnc:
command: ./novnc_proxy
plugs: [network, network-bind]
novncsvc:
command: ./svc_wrapper.sh
daemon: forking
plugs: [network, network-bind]
+16 -15
View File
@@ -1,32 +1,33 @@
// noVNC specific assertions
chai.use(function (_chai, utils) {
_chai.Assertion.addMethod('displayed', function (target_data) {
function _equal(a, b) {
return a === b;
}
_chai.Assertion.addMethod('displayed', function (targetData, cmp=_equal) {
const obj = this._obj;
const ctx = obj._target.getContext('2d');
const data_cl = ctx.getImageData(0, 0, obj._target.width, obj._target.height).data;
// NB(directxman12): PhantomJS 1.x doesn't implement Uint8ClampedArray, so work around that
const data = new Uint8Array(data_cl);
const len = data_cl.length;
new chai.Assertion(len).to.be.equal(target_data.length, "unexpected display size");
const data = ctx.getImageData(0, 0, obj._target.width, obj._target.height).data;
const len = data.length;
new chai.Assertion(len).to.be.equal(targetData.length, "unexpected display size");
let same = true;
for (let i = 0; i < len; i++) {
if (data[i] != target_data[i]) {
if (!cmp(data[i], targetData[i])) {
same = false;
break;
}
}
if (!same) {
// eslint-disable-next-line no-console
console.log("expected data: %o, actual data: %o", target_data, data);
console.log("expected data: %o, actual data: %o", targetData, data);
}
this.assert(same,
"expected #{this} to have displayed the image #{exp}, but instead it displayed #{act}",
"expected #{this} not to have displayed the image #{act}",
target_data,
targetData,
data);
});
_chai.Assertion.addMethod('sent', function (target_data) {
_chai.Assertion.addMethod('sent', function (targetData) {
const obj = this._obj;
obj.inspect = () => {
const res = { _websocket: obj._websocket, rQi: obj._rQi, _rQ: new Uint8Array(obj._rQ.buffer, 0, obj._rQlen),
@@ -34,13 +35,13 @@ chai.use(function (_chai, utils) {
res.prototype = obj;
return res;
};
const data = obj._websocket._get_sent_data();
const data = obj._websocket._getSentData();
let same = true;
if (data.length != target_data.length) {
if (data.length != targetData.length) {
same = false;
} else {
for (let i = 0; i < data.length; i++) {
if (data[i] != target_data[i]) {
if (data[i] != targetData[i]) {
same = false;
break;
}
@@ -48,12 +49,12 @@ chai.use(function (_chai, utils) {
}
if (!same) {
// eslint-disable-next-line no-console
console.log("expected data: %o, actual data: %o", target_data, data);
console.log("expected data: %o, actual data: %o", targetData, data);
}
this.assert(same,
"expected #{this} to have sent the data #{exp}, but it actually sent #{act}",
"expected #{this} not to have sent the data #{act}",
Array.prototype.slice.call(target_data),
Array.prototype.slice.call(targetData),
Array.prototype.slice.call(data));
});
+19 -27
View File
@@ -1,41 +1,33 @@
import Base64 from '../core/base64.js';
// PhantomJS can't create Event objects directly, so we need to use this
function make_event(name, props) {
const evt = document.createEvent('Event');
evt.initEvent(name, true, true);
if (props) {
for (let prop in props) {
evt[prop] = props[prop];
}
}
return evt;
}
export default class FakeWebSocket {
constructor(uri, protocols) {
this.url = uri;
this.binaryType = "arraybuffer";
this.extensions = "";
this.onerror = null;
this.onmessage = null;
this.onopen = null;
if (!protocols || typeof protocols === 'string') {
this.protocol = protocols;
} else {
this.protocol = protocols[0];
}
this._send_queue = new Uint8Array(20000);
this._sendQueue = new Uint8Array(20000);
this.readyState = FakeWebSocket.CONNECTING;
this.bufferedAmount = 0;
this.__is_fake = true;
this._isFake = true;
}
close(code, reason) {
this.readyState = FakeWebSocket.CLOSED;
if (this.onclose) {
this.onclose(make_event("close", { 'code': code, 'reason': reason, 'wasClean': true }));
this.onclose(new CloseEvent("close", { 'code': code, 'reason': reason, 'wasClean': true }));
}
}
@@ -45,12 +37,12 @@ export default class FakeWebSocket {
} else {
data = new Uint8Array(data);
}
this._send_queue.set(data, this.bufferedAmount);
this._sendQueue.set(data, this.bufferedAmount);
this.bufferedAmount += data.length;
}
_get_sent_data() {
const res = new Uint8Array(this._send_queue.buffer, 0, this.bufferedAmount);
_getSentData() {
const res = new Uint8Array(this._sendQueue.buffer, 0, this.bufferedAmount);
this.bufferedAmount = 0;
return res;
}
@@ -58,16 +50,16 @@ export default class FakeWebSocket {
_open() {
this.readyState = FakeWebSocket.OPEN;
if (this.onopen) {
this.onopen(make_event('open'));
this.onopen(new Event('open'));
}
}
_receive_data(data) {
_receiveData(data) {
// Break apart the data to expose bugs where we assume data is
// neatly packaged
for (let i = 0;i < data.length;i++) {
let buf = data.subarray(i, i+1);
this.onmessage(make_event("message", { 'data': buf }));
this.onmessage(new MessageEvent("message", { 'data': buf }));
}
}
}
@@ -77,20 +69,20 @@ FakeWebSocket.CONNECTING = WebSocket.CONNECTING;
FakeWebSocket.CLOSING = WebSocket.CLOSING;
FakeWebSocket.CLOSED = WebSocket.CLOSED;
FakeWebSocket.__is_fake = true;
FakeWebSocket._isFake = true;
FakeWebSocket.replace = () => {
if (!WebSocket.__is_fake) {
const real_version = WebSocket;
if (!WebSocket._isFake) {
const realVersion = WebSocket;
// eslint-disable-next-line no-global-assign
WebSocket = FakeWebSocket;
FakeWebSocket.__real_version = real_version;
FakeWebSocket._realVersion = realVersion;
}
};
FakeWebSocket.restore = () => {
if (WebSocket.__is_fake) {
if (WebSocket._isFake) {
// eslint-disable-next-line no-global-assign
WebSocket = WebSocket.__real_version;
WebSocket = WebSocket._realVersion;
}
};
-48
View File
@@ -1,48 +0,0 @@
const TEST_REGEXP = /test\..*\.js/;
const allTestFiles = [];
const extraFiles = ['/base/tests/assertions.js'];
Object.keys(window.__karma__.files).forEach(function (file) {
if (TEST_REGEXP.test(file)) {
// TODO: normalize?
allTestFiles.push(file);
}
});
// Stub out mocha's start function so we can run it once we're done loading
mocha.origRun = mocha.run;
mocha.run = function () {};
let script;
// Script to import all our tests
script = document.createElement("script");
script.type = "module";
script.text = "";
let allModules = allTestFiles.concat(extraFiles);
allModules.forEach(function (file) {
script.text += "import \"" + file + "\";\n";
});
script.text += "\nmocha.origRun();\n";
document.body.appendChild(script);
// Fallback code for browsers that don't support modules (IE)
script = document.createElement("script");
script.type = "module";
script.text = "window._noVNC_has_module_support = true;\n";
document.body.appendChild(script);
function fallback() {
if (!window._noVNC_has_module_support) {
/* eslint-disable no-console */
if (console) {
console.log("No module support detected. Loading fallback...");
}
/* eslint-enable no-console */
let loader = document.createElement("script");
loader.src = "base/vendor/browser-es-module-loader/dist/browser-es-module-loader.js";
document.body.appendChild(loader);
}
}
setTimeout(fallback, 500);
+9 -4
View File
@@ -41,6 +41,7 @@ function enableUI() {
document.getElementById('mode1').checked = true;
}
/* eslint-disable-next-line camelcase */
message("Loaded " + VNC_frame_data.length + " frames");
const startButton = document.getElementById('startButton');
@@ -49,12 +50,16 @@ function enableUI() {
message("Converting...");
/* eslint-disable-next-line camelcase */
frames = VNC_frame_data;
let encoding;
// Only present in older recordings
/* eslint-disable camelcase */
if (window.VNC_frame_encoding) {
// Only present in older recordings
encoding = VNC_frame_encoding;
/* eslint-enable camelcase */
} else {
let frame = frames[0];
let start = frame.indexOf('{', 1) + 1;
@@ -102,7 +107,7 @@ class IterationPlayer {
this._iteration = undefined;
this._player = undefined;
this._start_time = undefined;
this._startTime = undefined;
this._frames = frames;
@@ -115,7 +120,7 @@ class IterationPlayer {
start(realtime) {
this._iteration = 0;
this._start_time = (new Date()).getTime();
this._startTime = (new Date()).getTime();
this._realtime = realtime;
@@ -139,7 +144,7 @@ class IterationPlayer {
_finish() {
const endTime = (new Date()).getTime();
const totalDuration = endTime - this._start_time;
const totalDuration = endTime - this._startTime;
const evt = new CustomEvent('finish',
{ detail:
+38 -32
View File
@@ -42,6 +42,24 @@ if (window.setImmediate === undefined) {
});
}
class FakeWebSocket {
constructor() {
this.binaryType = "arraybuffer";
this.protocol = "";
this.readyState = "open";
this.onerror = () => {};
this.onmessage = () => {};
this.onopen = () => {};
}
send() {
}
close() {
}
}
export default class RecordingPlayer {
constructor(frames, disconnected) {
this._frames = frames;
@@ -49,10 +67,10 @@ export default class RecordingPlayer {
this._disconnected = disconnected;
this._rfb = undefined;
this._frame_length = this._frames.length;
this._frameLength = this._frames.length;
this._frame_index = 0;
this._start_time = undefined;
this._frameIndex = 0;
this._startTime = undefined;
this._realtime = true;
this._trafficManagement = true;
@@ -63,56 +81,44 @@ export default class RecordingPlayer {
run(realtime, trafficManagement) {
// initialize a new RFB
this._rfb = new RFB(document.getElementById('VNC_screen'), 'wss://test');
this._ws = new FakeWebSocket();
this._rfb = new RFB(document.getElementById('VNC_screen'), this._ws);
this._rfb.viewOnly = true;
this._rfb.addEventListener("disconnect",
this._handleDisconnect.bind(this));
this._rfb.addEventListener("credentialsrequired",
this._handleCredentials.bind(this));
this._enablePlaybackMode();
// reset the frame index and timer
this._frame_index = 0;
this._start_time = (new Date()).getTime();
this._frameIndex = 0;
this._startTime = (new Date()).getTime();
this._realtime = realtime;
this._trafficManagement = (trafficManagement === undefined) ? !realtime : trafficManagement;
this._running = true;
}
// _enablePlaybackMode mocks out things not required for running playback
_enablePlaybackMode() {
const self = this;
this._rfb._sock.send = () => {};
this._rfb._sock.close = () => {};
this._rfb._sock.flush = () => {};
this._rfb._sock.open = function () {
this.init();
this._eventHandlers.open();
self._queueNextPacket();
};
this._queueNextPacket();
}
_queueNextPacket() {
if (!this._running) { return; }
let frame = this._frames[this._frame_index];
let frame = this._frames[this._frameIndex];
// skip send frames
while (this._frame_index < this._frame_length && frame.fromClient) {
this._frame_index++;
frame = this._frames[this._frame_index];
while (this._frameIndex < this._frameLength && frame.fromClient) {
this._frameIndex++;
frame = this._frames[this._frameIndex];
}
if (this._frame_index >= this._frame_length) {
if (this._frameIndex >= this._frameLength) {
Log.Debug('Finished, no more frames');
this._finish();
return;
}
if (this._realtime) {
const toffset = (new Date()).getTime() - this._start_time;
const toffset = (new Date()).getTime() - this._startTime;
let delay = frame.timestamp - toffset;
if (delay < 1) delay = 1;
@@ -134,10 +140,10 @@ export default class RecordingPlayer {
return;
}
const frame = this._frames[this._frame_index];
const frame = this._frames[this._frameIndex];
this._rfb._sock._recv_message({'data': frame.data});
this._frame_index++;
this._ws.onmessage({'data': frame.data});
this._frameIndex++;
this._queueNextPacket();
}
@@ -153,15 +159,15 @@ export default class RecordingPlayer {
this._rfb._display.flush();
} else {
this._running = false;
this._rfb._sock._eventHandlers.close({code: 1000, reason: ""});
this._ws.onclose({code: 1000, reason: ""});
delete this._rfb;
this.onfinish((new Date()).getTime() - this._start_time);
this.onfinish((new Date()).getTime() - this._startTime);
}
}
_handleDisconnect(evt) {
this._running = false;
this._disconnected(evt.detail.clean, this._frame_index);
this._disconnected(evt.detail.clean, this._frameIndex);
}
_handleCredentials(evt) {
+83
View File
@@ -0,0 +1,83 @@
const expect = chai.expect;
import Websock from '../core/websock.js';
import Display from '../core/display.js';
import CopyRectDecoder from '../core/decoders/copyrect.js';
import FakeWebSocket from './fake.websocket.js';
function testDecodeRect(decoder, x, y, width, height, data, display, depth) {
let sock;
sock = new Websock;
sock.open("ws://example.com");
sock.on('message', () => {
decoder.decodeRect(x, y, width, height, sock, display, depth);
});
// Empty messages are filtered at multiple layers, so we need to
// do a direct call
if (data.length === 0) {
decoder.decodeRect(x, y, width, height, sock, display, depth);
} else {
sock._websocket._receiveData(new Uint8Array(data));
}
display.flip();
}
describe('CopyRect Decoder', function () {
let decoder;
let display;
before(FakeWebSocket.replace);
after(FakeWebSocket.restore);
beforeEach(function () {
decoder = new CopyRectDecoder();
display = new Display(document.createElement('canvas'));
display.resize(4, 4);
});
it('should handle the CopyRect encoding', function () {
// seed some initial data to copy
display.fillRect(0, 0, 4, 4, [ 0x11, 0x22, 0x33 ]);
display.fillRect(0, 0, 2, 2, [ 0x00, 0x00, 0xff ]);
display.fillRect(2, 0, 2, 2, [ 0x00, 0xff, 0x00 ]);
testDecodeRect(decoder, 0, 2, 2, 2,
[0x00, 0x02, 0x00, 0x00],
display, 24);
testDecodeRect(decoder, 2, 2, 2, 2,
[0x00, 0x00, 0x00, 0x00],
display, 24);
let targetData = new Uint8Array([
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,
0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255,
0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255
]);
expect(display).to.have.displayed(targetData);
});
it('should handle empty rects', function () {
display.fillRect(0, 0, 4, 4, [ 0x00, 0x00, 0xff ]);
display.fillRect(2, 0, 2, 2, [ 0x00, 0xff, 0x00 ]);
display.fillRect(0, 2, 2, 2, [ 0x00, 0xff, 0x00 ]);
testDecodeRect(decoder, 1, 2, 0, 0, [0x00, 0x00, 0x00, 0x00], display, 24);
let targetData = new Uint8Array([
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,
0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255,
0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255
]);
expect(display).to.have.displayed(targetData);
});
});
+82
View File
@@ -0,0 +1,82 @@
/* eslint-disable no-console */
const expect = chai.expect;
import { inflateInit, inflate } from "../vendor/pako/lib/zlib/inflate.js";
import ZStream from "../vendor/pako/lib/zlib/zstream.js";
import Deflator from "../core/deflator.js";
function _inflator(compText, expected) {
let strm = new ZStream();
let chunkSize = 1024 * 10 * 10;
strm.output = new Uint8Array(chunkSize);
inflateInit(strm, 5);
if (expected > chunkSize) {
chunkSize = expected;
strm.output = new Uint8Array(chunkSize);
}
/* eslint-disable camelcase */
strm.input = compText;
strm.avail_in = strm.input.length;
strm.next_in = 0;
strm.next_out = 0;
strm.avail_out = expected.length;
/* eslint-enable camelcase */
let ret = inflate(strm, 0);
// Check that return code is not an error
expect(ret).to.be.greaterThan(-1);
return new Uint8Array(strm.output.buffer, 0, strm.next_out);
}
describe('Deflate data', function () {
it('should be able to deflate messages', function () {
let deflator = new Deflator();
let text = "123asdf";
let preText = new Uint8Array(text.length);
for (let i = 0; i < preText.length; i++) {
preText[i] = text.charCodeAt(i);
}
let compText = deflator.deflate(preText);
let inflatedText = _inflator(compText, text.length);
expect(inflatedText).to.array.equal(preText);
});
it('should be able to deflate large messages', function () {
let deflator = new Deflator();
/* Generate a big string with random characters. Used because
repetition of letters might be deflated more effectively than
random ones. */
let text = "";
let characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
for (let i = 0; i < 300000; i++) {
text += characters.charAt(Math.floor(Math.random() * characters.length));
}
let preText = new Uint8Array(text.length);
for (let i = 0; i < preText.length; i++) {
preText[i] = text.charCodeAt(i);
}
let compText = deflator.deflate(preText);
//Check that the compressed size is expected size
expect(compText.length).to.be.greaterThan((1024 * 10 * 10) * 2);
let inflatedText = _inflator(compText, text.length);
expect(inflatedText).to.array.equal(preText);
});
});
+41 -128
View File
@@ -4,28 +4,27 @@ import Base64 from '../core/base64.js';
import Display from '../core/display.js';
describe('Display/Canvas Helper', function () {
const checked_data = new Uint8Array([
const checkedData = new Uint8ClampedArray([
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,
0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255,
0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255
]);
const basic_data = new Uint8Array([0xff, 0x00, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0xff, 0xff, 0xff, 255]);
const basicData = new Uint8ClampedArray([0xff, 0x00, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0xff, 0xff, 0xff, 255]);
function make_image_canvas(input_data) {
function makeImageCanvas(inputData, width, height) {
const canvas = document.createElement('canvas');
canvas.width = 4;
canvas.height = 4;
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
const data = ctx.createImageData(4, 4);
for (let i = 0; i < checked_data.length; i++) { data.data[i] = input_data[i]; }
const data = new ImageData(inputData, width, height);
ctx.putImageData(data, 0, 0);
return canvas;
}
function make_image_png(input_data) {
const canvas = make_image_canvas(input_data);
function makeImagePng(inputData, width, height) {
const canvas = makeImageCanvas(inputData, width, height);
const url = canvas.toDataURL();
const data = url.split(",")[1];
return Base64.decode(data);
@@ -44,11 +43,11 @@ describe('Display/Canvas Helper', function () {
it('should take viewport location into consideration when drawing images', function () {
display.resize(4, 4);
display.viewportChangeSize(2, 2);
display.drawImage(make_image_canvas(basic_data), 1, 1);
display.drawImage(makeImageCanvas(basicData, 4, 1), 1, 1);
display.flip();
const expected = new Uint8Array(16);
for (let i = 0; i < 8; i++) { expected[i] = basic_data[i]; }
for (let i = 0; i < 8; i++) { expected[i] = basicData[i]; }
for (let i = 8; i < 16; i++) { expected[i] = 0; }
expect(display).to.have.displayed(expected);
});
@@ -123,12 +122,12 @@ describe('Display/Canvas Helper', function () {
it('should change the size of the logical canvas', function () {
display.resize(5, 7);
expect(display._fb_width).to.equal(5);
expect(display._fb_height).to.equal(7);
expect(display._fbWidth).to.equal(5);
expect(display._fbHeight).to.equal(7);
});
it('should keep the framebuffer data', function () {
display.fillRect(0, 0, 4, 4, [0, 0, 0xff]);
display.fillRect(0, 0, 4, 4, [0xff, 0, 0]);
display.resize(2, 2);
display.flip();
const expected = [];
@@ -223,6 +222,7 @@ describe('Display/Canvas Helper', function () {
display = new Display(canvas);
display.clipViewport = true;
display.resize(4, 3);
display.viewportChangeSize(4, 3);
document.body.appendChild(canvas);
});
@@ -269,34 +269,12 @@ describe('Display/Canvas Helper', function () {
display.resize(4, 4);
});
it('should clear the screen on #clear without a logo set', function () {
display.fillRect(0, 0, 4, 4, [0x00, 0x00, 0xff]);
display._logo = null;
display.clear();
display.resize(4, 4);
const empty = [];
for (let i = 0; i < 4 * display._fb_width * display._fb_height; i++) { empty[i] = 0; }
expect(display).to.have.displayed(new Uint8Array(empty));
});
it('should draw the logo on #clear with a logo set', function (done) {
display._logo = { width: 4, height: 4, type: "image/png", data: make_image_png(checked_data) };
display.clear();
display.onflush = () => {
expect(display).to.have.displayed(checked_data);
expect(display._fb_width).to.equal(4);
expect(display._fb_height).to.equal(4);
done();
};
display.flush();
});
it('should not draw directly on the target canvas', function () {
display.fillRect(0, 0, 4, 4, [0, 0, 0xff]);
display.fillRect(0, 0, 4, 4, [0xff, 0, 0]);
display.flip();
display.fillRect(0, 0, 4, 4, [0, 0xff, 0]);
const expected = [];
for (let i = 0; i < 4 * display._fb_width * display._fb_height; i += 4) {
for (let i = 0; i < 4 * display._fbWidth * display._fbHeight; i += 4) {
expected[i] = 0xff;
expected[i+1] = expected[i+2] = 0;
expected[i+3] = 0xff;
@@ -306,95 +284,41 @@ describe('Display/Canvas Helper', function () {
it('should support filling a rectangle with particular color via #fillRect', function () {
display.fillRect(0, 0, 4, 4, [0, 0xff, 0]);
display.fillRect(0, 0, 2, 2, [0xff, 0, 0]);
display.fillRect(2, 2, 2, 2, [0xff, 0, 0]);
display.fillRect(0, 0, 2, 2, [0, 0, 0xff]);
display.fillRect(2, 2, 2, 2, [0, 0, 0xff]);
display.flip();
expect(display).to.have.displayed(checked_data);
expect(display).to.have.displayed(checkedData);
});
it('should support copying an portion of the canvas via #copyImage', function () {
display.fillRect(0, 0, 4, 4, [0, 0xff, 0]);
display.fillRect(0, 0, 2, 2, [0xff, 0, 0x00]);
display.fillRect(0, 0, 2, 2, [0, 0, 0xff]);
display.copyImage(0, 0, 2, 2, 2, 2);
display.flip();
expect(display).to.have.displayed(checked_data);
expect(display).to.have.displayed(checkedData);
});
it('should support drawing images via #imageRect', function (done) {
display.imageRect(0, 0, "image/png", make_image_png(checked_data));
display.imageRect(0, 0, 4, 4, "image/png", makeImagePng(checkedData, 4, 4));
display.flip();
display.onflush = () => {
expect(display).to.have.displayed(checked_data);
expect(display).to.have.displayed(checkedData);
done();
};
display.flush();
});
it('should support drawing tile data with a background color and sub tiles', function () {
display.startTile(0, 0, 4, 4, [0, 0xff, 0]);
display.subTile(0, 0, 2, 2, [0xff, 0, 0]);
display.subTile(2, 2, 2, 2, [0xff, 0, 0]);
display.finishTile();
it('should support blit images with true color via #blitImage', function () {
display.blitImage(0, 0, 4, 4, checkedData, 0);
display.flip();
expect(display).to.have.displayed(checked_data);
});
// We have a special cache for 16x16 tiles that we need to test
it('should support drawing a 16x16 tile', function () {
const large_checked_data = new Uint8Array(16*16*4);
display.resize(16, 16);
for (let y = 0;y < 16;y++) {
for (let x = 0;x < 16;x++) {
let pixel;
if ((x < 4) && (y < 4)) {
// NB: of course IE11 doesn't support #slice on ArrayBufferViews...
pixel = Array.prototype.slice.call(checked_data, (y*4+x)*4, (y*4+x+1)*4);
} else {
pixel = [0, 0xff, 0, 255];
}
large_checked_data.set(pixel, (y*16+x)*4);
}
}
display.startTile(0, 0, 16, 16, [0, 0xff, 0]);
display.subTile(0, 0, 2, 2, [0xff, 0, 0]);
display.subTile(2, 2, 2, 2, [0xff, 0, 0]);
display.finishTile();
display.flip();
expect(display).to.have.displayed(large_checked_data);
});
it('should support drawing BGRX blit images with true color via #blitImage', function () {
const data = [];
for (let i = 0; i < 16; i++) {
data[i * 4] = checked_data[i * 4 + 2];
data[i * 4 + 1] = checked_data[i * 4 + 1];
data[i * 4 + 2] = checked_data[i * 4];
data[i * 4 + 3] = checked_data[i * 4 + 3];
}
display.blitImage(0, 0, 4, 4, data, 0);
display.flip();
expect(display).to.have.displayed(checked_data);
});
it('should support drawing RGB blit images with true color via #blitRgbImage', function () {
const data = [];
for (let i = 0; i < 16; i++) {
data[i * 3] = checked_data[i * 4];
data[i * 3 + 1] = checked_data[i * 4 + 1];
data[i * 3 + 2] = checked_data[i * 4 + 2];
}
display.blitRgbImage(0, 0, 4, 4, data, 0);
display.flip();
expect(display).to.have.displayed(checked_data);
expect(display).to.have.displayed(checkedData);
});
it('should support drawing an image object via #drawImage', function () {
const img = make_image_canvas(checked_data);
const img = makeImageCanvas(checkedData, 4, 4);
display.drawImage(img, 0, 0);
display.flip();
expect(display).to.have.displayed(checked_data);
expect(display).to.have.displayed(checkedData);
});
});
@@ -403,38 +327,34 @@ describe('Display/Canvas Helper', function () {
beforeEach(function () {
display = new Display(document.createElement('canvas'));
display.resize(4, 4);
sinon.spy(display, '_scan_renderQ');
});
afterEach(function () {
window.requestAnimationFrame = this.old_requestAnimationFrame;
sinon.spy(display, '_scanRenderQ');
});
it('should try to process an item when it is pushed on, if nothing else is on the queue', function () {
display._renderQ_push({ type: 'noop' }); // does nothing
expect(display._scan_renderQ).to.have.been.calledOnce;
display._renderQPush({ type: 'noop' }); // does nothing
expect(display._scanRenderQ).to.have.been.calledOnce;
});
it('should not try to process an item when it is pushed on if we are waiting for other items', function () {
display._renderQ.length = 2;
display._renderQ_push({ type: 'noop' });
expect(display._scan_renderQ).to.not.have.been.called;
display._renderQPush({ type: 'noop' });
expect(display._scanRenderQ).to.not.have.been.called;
});
it('should wait until an image is loaded to attempt to draw it and the rest of the queue', function () {
const img = { complete: false, addEventListener: sinon.spy() };
display._renderQ = [{ type: 'img', x: 3, y: 4, img: img },
const img = { complete: false, width: 4, height: 4, addEventListener: sinon.spy() };
display._renderQ = [{ type: 'img', x: 3, y: 4, width: 4, height: 4, img: img },
{ type: 'fill', x: 1, y: 2, width: 3, height: 4, color: 5 }];
display.drawImage = sinon.spy();
display.fillRect = sinon.spy();
display._scan_renderQ();
display._scanRenderQ();
expect(display.drawImage).to.not.have.been.called;
expect(display.fillRect).to.not.have.been.called;
expect(img.addEventListener).to.have.been.calledOnce;
display._renderQ[0].img.complete = true;
display._scan_renderQ();
display._scanRenderQ();
expect(display.drawImage).to.have.been.calledOnce;
expect(display.fillRect).to.have.been.calledOnce;
expect(img.addEventListener).to.have.been.calledOnce;
@@ -450,35 +370,28 @@ describe('Display/Canvas Helper', function () {
it('should draw a blit image on type "blit"', function () {
display.blitImage = sinon.spy();
display._renderQ_push({ type: 'blit', x: 3, y: 4, width: 5, height: 6, data: [7, 8, 9] });
display._renderQPush({ type: 'blit', x: 3, y: 4, width: 5, height: 6, data: [7, 8, 9] });
expect(display.blitImage).to.have.been.calledOnce;
expect(display.blitImage).to.have.been.calledWith(3, 4, 5, 6, [7, 8, 9], 0);
});
it('should draw a blit RGB image on type "blitRgb"', function () {
display.blitRgbImage = sinon.spy();
display._renderQ_push({ type: 'blitRgb', x: 3, y: 4, width: 5, height: 6, data: [7, 8, 9] });
expect(display.blitRgbImage).to.have.been.calledOnce;
expect(display.blitRgbImage).to.have.been.calledWith(3, 4, 5, 6, [7, 8, 9], 0);
});
it('should copy a region on type "copy"', function () {
display.copyImage = sinon.spy();
display._renderQ_push({ type: 'copy', x: 3, y: 4, width: 5, height: 6, old_x: 7, old_y: 8 });
display._renderQPush({ type: 'copy', x: 3, y: 4, width: 5, height: 6, oldX: 7, oldY: 8 });
expect(display.copyImage).to.have.been.calledOnce;
expect(display.copyImage).to.have.been.calledWith(7, 8, 3, 4, 5, 6);
});
it('should fill a rect with a given color on type "fill"', function () {
display.fillRect = sinon.spy();
display._renderQ_push({ type: 'fill', x: 3, y: 4, width: 5, height: 6, color: [7, 8, 9]});
display._renderQPush({ type: 'fill', x: 3, y: 4, width: 5, height: 6, color: [7, 8, 9]});
expect(display.fillRect).to.have.been.calledOnce;
expect(display.fillRect).to.have.been.calledWith(3, 4, 5, 6, [7, 8, 9]);
});
it('should draw an image from an image object on type "img" (if complete)', function () {
display.drawImage = sinon.spy();
display._renderQ_push({ type: 'img', x: 3, y: 4, img: { complete: true } });
display._renderQPush({ type: 'img', x: 3, y: 4, img: { complete: true } });
expect(display.drawImage).to.have.been.calledOnce;
expect(display.drawImage).to.have.been.calledWith({ complete: true }, 3, 4);
});
File diff suppressed because it is too large Load Diff
+55 -55
View File
@@ -2,7 +2,6 @@
import keysyms from '../core/input/keysymdef.js';
import * as KeyboardUtil from "../core/input/util.js";
import * as browser from '../core/util/browser.js';
describe('Helpers', function () {
"use strict";
@@ -70,11 +69,6 @@ describe('Helpers', function () {
// environments, so we need to redefine it whilst running these
// tests.
origNavigator = Object.getOwnPropertyDescriptor(window, "navigator");
if (origNavigator === undefined) {
// Object.getOwnPropertyDescriptor() doesn't work
// properly in any version of IE
this.skip();
}
Object.defineProperty(window, "navigator", {value: {}});
if (window.navigator.platform !== undefined) {
@@ -86,7 +80,9 @@ describe('Helpers', function () {
window.navigator.platform = "Mac x86_64";
});
afterEach(function () {
Object.defineProperty(window, "navigator", origNavigator);
if (origNavigator !== undefined) {
Object.defineProperty(window, "navigator", origNavigator);
}
});
it('should respect ContextMenu on modern browser', function () {
@@ -100,16 +96,15 @@ describe('Helpers', function () {
describe('getKey', function () {
it('should prefer key', function () {
if (browser.isIE() || browser.isEdge()) this.skip();
expect(KeyboardUtil.getKey({key: 'a', charCode: 'Š'.charCodeAt(), keyCode: 0x42, which: 0x43})).to.be.equal('a');
});
it('should map legacy values', function () {
expect(KeyboardUtil.getKey({key: 'Spacebar'})).to.be.equal(' ');
expect(KeyboardUtil.getKey({key: 'Left'})).to.be.equal('ArrowLeft');
expect(KeyboardUtil.getKey({key: 'OS'})).to.be.equal('Meta');
expect(KeyboardUtil.getKey({key: 'Win'})).to.be.equal('Meta');
expect(KeyboardUtil.getKey({key: 'UIKeyInputLeftArrow'})).to.be.equal('ArrowLeft');
});
it('should handle broken Delete', function () {
expect(KeyboardUtil.getKey({key: '\x00', code: 'NumpadDecimal'})).to.be.equal('Delete');
});
it('should use code if no key', function () {
expect(KeyboardUtil.getKey({code: 'NumpadBackspace'})).to.be.equal('Backspace');
});
@@ -125,48 +120,6 @@ describe('Helpers', function () {
it('should return Unidentified when it cannot map the key', function () {
expect(KeyboardUtil.getKey({keycode: 0x42})).to.be.equal('Unidentified');
});
describe('Broken key AltGraph on IE/Edge', function () {
let origNavigator;
beforeEach(function () {
// window.navigator is a protected read-only property in many
// environments, so we need to redefine it whilst running these
// tests.
origNavigator = Object.getOwnPropertyDescriptor(window, "navigator");
if (origNavigator === undefined) {
// Object.getOwnPropertyDescriptor() doesn't work
// properly in any version of IE
this.skip();
}
Object.defineProperty(window, "navigator", {value: {}});
if (window.navigator.platform !== undefined) {
// Object.defineProperty() doesn't work properly in old
// versions of Chrome
this.skip();
}
});
afterEach(function () {
Object.defineProperty(window, "navigator", origNavigator);
});
it('should ignore printable character key on IE', function () {
window.navigator.userAgent = "Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko";
expect(KeyboardUtil.getKey({key: 'a'})).to.be.equal('Unidentified');
});
it('should ignore printable character key on Edge', function () {
window.navigator.userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.79 Safari/537.36 Edge/14.14393";
expect(KeyboardUtil.getKey({key: 'a'})).to.be.equal('Unidentified');
});
it('should allow non-printable character key on IE', function () {
window.navigator.userAgent = "Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko";
expect(KeyboardUtil.getKey({key: 'Shift'})).to.be.equal('Shift');
});
it('should allow non-printable character key on Edge', function () {
window.navigator.userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.79 Safari/537.36 Edge/14.14393";
expect(KeyboardUtil.getKey({key: 'Shift'})).to.be.equal('Shift');
});
});
});
describe('getKeysym', function () {
@@ -192,6 +145,22 @@ describe('Helpers', function () {
expect(KeyboardUtil.getKeysym({code: 'AltRight', key: 'Alt', location: 2})).to.be.equal(0xFFEA);
expect(KeyboardUtil.getKeysym({code: 'AltRight', key: 'AltGraph', location: 2})).to.be.equal(0xFE03);
});
it('should handle Windows key with incorrect location', function () {
expect(KeyboardUtil.getKeysym({key: 'Meta', location: 0})).to.be.equal(0xFFEC);
});
it('should handle Clear/NumLock key with incorrect location', function () {
this.skip(); // Broken because of Clear/NumLock override
expect(KeyboardUtil.getKeysym({key: 'Clear', code: 'NumLock', location: 3})).to.be.equal(0xFF0B);
});
it('should handle Meta/Windows distinction', function () {
expect(KeyboardUtil.getKeysym({code: 'AltLeft', key: 'Meta', location: 1})).to.be.equal(0xFFE7);
expect(KeyboardUtil.getKeysym({code: 'AltRight', key: 'Meta', location: 2})).to.be.equal(0xFFE8);
expect(KeyboardUtil.getKeysym({code: 'MetaLeft', key: 'Meta', location: 1})).to.be.equal(0xFFEB);
expect(KeyboardUtil.getKeysym({code: 'MetaRight', key: 'Meta', location: 2})).to.be.equal(0xFFEC);
});
it('should send NumLock even if key is Clear', function () {
expect(KeyboardUtil.getKeysym({key: 'Clear', code: 'NumLock'})).to.be.equal(0xFF7F);
});
it('should return null for unknown keys', function () {
expect(KeyboardUtil.getKeysym({key: 'Semicolon'})).to.be.null;
expect(KeyboardUtil.getKeysym({key: 'BracketRight'})).to.be.null;
@@ -203,7 +172,6 @@ describe('Helpers', function () {
describe('Numpad', function () {
it('should handle Numpad numbers', function () {
if (browser.isIE() || browser.isEdge()) this.skip();
expect(KeyboardUtil.getKeysym({code: 'Digit5', key: '5', location: 0})).to.be.equal(0x0035);
expect(KeyboardUtil.getKeysym({code: 'Numpad5', key: '5', location: 3})).to.be.equal(0xFFB5);
});
@@ -214,10 +182,42 @@ describe('Helpers', function () {
expect(KeyboardUtil.getKeysym({code: 'NumpadDecimal', key: 'Delete', location: 3})).to.be.equal(0xFF9F);
});
it('should handle Numpad Decimal key', function () {
if (browser.isIE() || browser.isEdge()) this.skip();
expect(KeyboardUtil.getKeysym({code: 'NumpadDecimal', key: '.', location: 3})).to.be.equal(0xFFAE);
expect(KeyboardUtil.getKeysym({code: 'NumpadDecimal', key: ',', location: 3})).to.be.equal(0xFFAC);
});
});
describe('Japanese IM keys on Windows', function () {
let origNavigator;
beforeEach(function () {
// window.navigator is a protected read-only property in many
// environments, so we need to redefine it whilst running these
// tests.
origNavigator = Object.getOwnPropertyDescriptor(window, "navigator");
Object.defineProperty(window, "navigator", {value: {}});
if (window.navigator.platform !== undefined) {
// Object.defineProperty() doesn't work properly in old
// versions of Chrome
this.skip();
}
window.navigator.platform = "Windows";
});
afterEach(function () {
if (origNavigator !== undefined) {
Object.defineProperty(window, "navigator", origNavigator);
}
});
const keys = { 'Zenkaku': 0xff2a, 'Hankaku': 0xff2a,
'Romaji': 0xff24, 'KanaMode': 0xff24 };
for (let [key, keysym] of Object.entries(keys)) {
it(`should fake combined key for ${key} on Windows`, function () {
expect(KeyboardUtil.getKeysym({code: 'FakeIM', key: key})).to.be.equal(keysym);
});
}
});
});
});
+232
View File
@@ -0,0 +1,232 @@
const expect = chai.expect;
import Websock from '../core/websock.js';
import Display from '../core/display.js';
import HextileDecoder from '../core/decoders/hextile.js';
import FakeWebSocket from './fake.websocket.js';
function testDecodeRect(decoder, x, y, width, height, data, display, depth) {
let sock;
sock = new Websock;
sock.open("ws://example.com");
sock.on('message', () => {
decoder.decodeRect(x, y, width, height, sock, display, depth);
});
// Empty messages are filtered at multiple layers, so we need to
// do a direct call
if (data.length === 0) {
decoder.decodeRect(x, y, width, height, sock, display, depth);
} else {
sock._websocket._receiveData(new Uint8Array(data));
}
display.flip();
}
function push32(arr, num) {
arr.push((num >> 24) & 0xFF,
(num >> 16) & 0xFF,
(num >> 8) & 0xFF,
num & 0xFF);
}
describe('Hextile Decoder', function () {
let decoder;
let display;
before(FakeWebSocket.replace);
after(FakeWebSocket.restore);
beforeEach(function () {
decoder = new HextileDecoder();
display = new Display(document.createElement('canvas'));
display.resize(4, 4);
});
it('should handle a tile with fg, bg specified, normal subrects', function () {
let data = [];
data.push(0x02 | 0x04 | 0x08); // bg spec, fg spec, anysubrects
push32(data, 0x00ff0000); // becomes 00ff0000 --> #00FF00 bg color
data.push(0x00); // becomes 0000ff00 --> #0000FF fg color
data.push(0x00);
data.push(0xff);
data.push(0x00);
data.push(2); // 2 subrects
data.push(0); // x: 0, y: 0
data.push(1 | (1 << 4)); // width: 2, height: 2
data.push(2 | (2 << 4)); // x: 2, y: 2
data.push(1 | (1 << 4)); // width: 2, height: 2
testDecodeRect(decoder, 0, 0, 4, 4, data, display, 24);
let targetData = new Uint8Array([
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,
0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255,
0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255
]);
expect(display).to.have.displayed(targetData);
});
it('should handle a raw tile', function () {
let targetData = new Uint8Array([
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,
0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255,
0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255
]);
let data = [];
data.push(0x01); // raw
for (let i = 0; i < targetData.length; i += 4) {
data.push(targetData[i]);
data.push(targetData[i + 1]);
data.push(targetData[i + 2]);
// Last byte zero to test correct alpha handling
data.push(0);
}
testDecodeRect(decoder, 0, 0, 4, 4, data, display, 24);
expect(display).to.have.displayed(targetData);
});
it('should handle a tile with only bg specified (solid bg)', function () {
let data = [];
data.push(0x02);
push32(data, 0x00ff0000); // becomes 00ff0000 --> #00FF00 bg color
testDecodeRect(decoder, 0, 0, 4, 4, data, display, 24);
let expected = [];
for (let i = 0; i < 16; i++) {
push32(expected, 0x00ff00ff);
}
expect(display).to.have.displayed(new Uint8Array(expected));
});
it('should handle a tile with only bg specified and an empty frame afterwards', function () {
// set the width so we can have two tiles
display.resize(8, 4);
let data = [];
// send a bg frame
data.push(0x02);
push32(data, 0x00ff0000); // becomes 00ff0000 --> #00FF00 bg color
// send an empty frame
data.push(0x00);
testDecodeRect(decoder, 0, 0, 32, 4, data, display, 24);
let expected = [];
for (let i = 0; i < 16; i++) {
push32(expected, 0x00ff00ff); // rect 1: solid
}
for (let i = 0; i < 16; i++) {
push32(expected, 0x00ff00ff); // rect 2: same bkground color
}
expect(display).to.have.displayed(new Uint8Array(expected));
});
it('should handle a tile with bg and coloured subrects', function () {
let data = [];
data.push(0x02 | 0x08 | 0x10); // bg spec, anysubrects, colouredsubrects
push32(data, 0x00ff0000); // becomes 00ff0000 --> #00FF00 bg color
data.push(2); // 2 subrects
data.push(0x00); // becomes 0000ff00 --> #0000FF fg color
data.push(0x00);
data.push(0xff);
data.push(0x00);
data.push(0); // x: 0, y: 0
data.push(1 | (1 << 4)); // width: 2, height: 2
data.push(0x00); // becomes 0000ff00 --> #0000FF fg color
data.push(0x00);
data.push(0xff);
data.push(0x00);
data.push(2 | (2 << 4)); // x: 2, y: 2
data.push(1 | (1 << 4)); // width: 2, height: 2
testDecodeRect(decoder, 0, 0, 4, 4, data, display, 24);
let targetData = new Uint8Array([
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,
0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255,
0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255
]);
expect(display).to.have.displayed(targetData);
});
it('should carry over fg and bg colors from the previous tile if not specified', function () {
display.resize(4, 17);
let data = [];
data.push(0x02 | 0x04 | 0x08); // bg spec, fg spec, anysubrects
push32(data, 0xff00ff); // becomes 00ff00ff --> #00FF00 bg color
data.push(0x00); // becomes 0000ffff --> #0000FF fg color
data.push(0x00);
data.push(0xff);
data.push(0xff);
data.push(8); // 8 subrects
for (let i = 0; i < 4; i++) {
data.push((0 << 4) | (i * 4)); // x: 0, y: i*4
data.push(1 | (1 << 4)); // width: 2, height: 2
data.push((2 << 4) | (i * 4 + 2)); // x: 2, y: i * 4 + 2
data.push(1 | (1 << 4)); // width: 2, height: 2
}
data.push(0x08); // anysubrects
data.push(1); // 1 subrect
data.push(0); // x: 0, y: 0
data.push(1 | (1 << 4)); // width: 2, height: 2
testDecodeRect(decoder, 0, 0, 4, 17, data, display, 24);
let targetData = [
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,
0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255,
0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255
];
let expected = [];
for (let i = 0; i < 4; i++) {
expected = expected.concat(targetData);
}
expected = expected.concat(targetData.slice(0, 16));
expect(display).to.have.displayed(new Uint8Array(expected));
});
it('should fail on an invalid subencoding', function () {
let data = [45]; // an invalid subencoding
expect(() => testDecodeRect(decoder, 0, 0, 4, 4, data, display, 24)).to.throw();
});
it('should handle empty rects', function () {
display.fillRect(0, 0, 4, 4, [ 0x00, 0x00, 0xff ]);
display.fillRect(2, 0, 2, 2, [ 0x00, 0xff, 0x00 ]);
display.fillRect(0, 2, 2, 2, [ 0x00, 0xff, 0x00 ]);
testDecodeRect(decoder, 1, 2, 0, 0, [], display, 24);
let targetData = new Uint8Array([
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,
0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255,
0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255
]);
expect(display).to.have.displayed(targetData);
});
});
+16
View File
@@ -0,0 +1,16 @@
/* eslint-disable no-console */
const expect = chai.expect;
import { toUnsigned32bit, toSigned32bit } from '../core/util/int.js';
describe('Integer casting', function () {
it('should cast unsigned to signed', function () {
let expected = 4294967286;
expect(toUnsigned32bit(-10)).to.equal(expected);
});
it('should cast signed to unsigned', function () {
let expected = -10;
expect(toSigned32bit(4294967286)).to.equal(expected);
});
});
+190 -179
View File
@@ -1,7 +1,6 @@
const expect = chai.expect;
import Keyboard from '../core/input/keyboard.js';
import * as browser from '../core/util/browser.js';
describe('Key Event Handling', function () {
"use strict";
@@ -20,7 +19,6 @@ describe('Key Event Handling', function () {
describe('Decode Keyboard Events', function () {
it('should decode keydown events', function (done) {
if (browser.isIE() || browser.isEdge()) this.skip();
const kbd = new Keyboard(document);
kbd.onkeyevent = (keysym, code, down) => {
expect(keysym).to.be.equal(0x61);
@@ -31,7 +29,6 @@ describe('Key Event Handling', function () {
kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', key: 'a'}));
});
it('should decode keyup events', function (done) {
if (browser.isIE() || browser.isEdge()) this.skip();
let calls = 0;
const kbd = new Keyboard(document);
kbd.onkeyevent = (keysym, code, down) => {
@@ -45,118 +42,10 @@ describe('Key Event Handling', function () {
kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', key: 'a'}));
kbd._handleKeyUp(keyevent('keyup', {code: 'KeyA', key: 'a'}));
});
describe('Legacy keypress Events', function () {
it('should wait for keypress when needed', function () {
const kbd = new Keyboard(document);
kbd.onkeyevent = sinon.spy();
kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', keyCode: 0x41}));
expect(kbd.onkeyevent).to.not.have.been.called;
});
it('should decode keypress events', function (done) {
const kbd = new Keyboard(document);
kbd.onkeyevent = (keysym, code, down) => {
expect(keysym).to.be.equal(0x61);
expect(code).to.be.equal('KeyA');
expect(down).to.be.equal(true);
done();
};
kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', keyCode: 0x41}));
kbd._handleKeyPress(keyevent('keypress', {code: 'KeyA', charCode: 0x61}));
});
it('should ignore keypress with different code', function () {
const kbd = new Keyboard(document);
kbd.onkeyevent = sinon.spy();
kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', keyCode: 0x41}));
kbd._handleKeyPress(keyevent('keypress', {code: 'KeyB', charCode: 0x61}));
expect(kbd.onkeyevent).to.not.have.been.called;
});
it('should handle keypress with missing code', function (done) {
const kbd = new Keyboard(document);
kbd.onkeyevent = (keysym, code, down) => {
expect(keysym).to.be.equal(0x61);
expect(code).to.be.equal('KeyA');
expect(down).to.be.equal(true);
done();
};
kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', keyCode: 0x41}));
kbd._handleKeyPress(keyevent('keypress', {charCode: 0x61}));
});
it('should guess key if no keypress and numeric key', function (done) {
const kbd = new Keyboard(document);
kbd.onkeyevent = (keysym, code, down) => {
expect(keysym).to.be.equal(0x32);
expect(code).to.be.equal('Digit2');
expect(down).to.be.equal(true);
done();
};
kbd._handleKeyDown(keyevent('keydown', {code: 'Digit2', keyCode: 0x32}));
});
it('should guess key if no keypress and alpha key', function (done) {
const kbd = new Keyboard(document);
kbd.onkeyevent = (keysym, code, down) => {
expect(keysym).to.be.equal(0x61);
expect(code).to.be.equal('KeyA');
expect(down).to.be.equal(true);
done();
};
kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', keyCode: 0x41, shiftKey: false}));
});
it('should guess key if no keypress and alpha key (with shift)', function (done) {
const kbd = new Keyboard(document);
kbd.onkeyevent = (keysym, code, down) => {
expect(keysym).to.be.equal(0x41);
expect(code).to.be.equal('KeyA');
expect(down).to.be.equal(true);
done();
};
kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', keyCode: 0x41, shiftKey: true}));
});
it('should not guess key if no keypress and unknown key', function (done) {
const kbd = new Keyboard(document);
kbd.onkeyevent = (keysym, code, down) => {
expect(keysym).to.be.equal(0);
expect(code).to.be.equal('KeyA');
expect(down).to.be.equal(true);
done();
};
kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', keyCode: 0x09}));
});
});
describe('suppress the right events at the right time', function () {
beforeEach(function () {
if (browser.isIE() || browser.isEdge()) this.skip();
});
it('should suppress anything with a valid key', function () {
const kbd = new Keyboard(document, {});
const evt1 = keyevent('keydown', {code: 'KeyA', key: 'a'});
kbd._handleKeyDown(evt1);
expect(evt1.preventDefault).to.have.been.called;
const evt2 = keyevent('keyup', {code: 'KeyA', key: 'a'});
kbd._handleKeyUp(evt2);
expect(evt2.preventDefault).to.have.been.called;
});
it('should not suppress keys without key', function () {
const kbd = new Keyboard(document, {});
const evt = keyevent('keydown', {code: 'KeyA', keyCode: 0x41});
kbd._handleKeyDown(evt);
expect(evt.preventDefault).to.not.have.been.called;
});
it('should suppress the following keypress event', function () {
const kbd = new Keyboard(document, {});
const evt1 = keyevent('keydown', {code: 'KeyA', keyCode: 0x41});
kbd._handleKeyDown(evt1);
const evt2 = keyevent('keypress', {code: 'KeyA', charCode: 0x41});
kbd._handleKeyPress(evt2);
expect(evt2.preventDefault).to.have.been.called;
});
});
});
describe('Fake keyup', function () {
it('should fake keyup events for virtual keyboards', function (done) {
if (browser.isIE() || browser.isEdge()) this.skip();
let count = 0;
const kbd = new Keyboard(document);
kbd.onkeyevent = (keysym, code, down) => {
@@ -175,60 +64,9 @@ describe('Key Event Handling', function () {
};
kbd._handleKeyDown(keyevent('keydown', {code: 'Unidentified', key: 'a'}));
});
describe('iOS', function () {
let origNavigator;
beforeEach(function () {
// window.navigator is a protected read-only property in many
// environments, so we need to redefine it whilst running these
// tests.
origNavigator = Object.getOwnPropertyDescriptor(window, "navigator");
if (origNavigator === undefined) {
// Object.getOwnPropertyDescriptor() doesn't work
// properly in any version of IE
this.skip();
}
Object.defineProperty(window, "navigator", {value: {}});
if (window.navigator.platform !== undefined) {
// Object.defineProperty() doesn't work properly in old
// versions of Chrome
this.skip();
}
window.navigator.platform = "iPhone 9.0";
});
afterEach(function () {
Object.defineProperty(window, "navigator", origNavigator);
});
it('should fake keyup events on iOS', function (done) {
if (browser.isIE() || browser.isEdge()) this.skip();
let count = 0;
const kbd = new Keyboard(document);
kbd.onkeyevent = (keysym, code, down) => {
switch (count++) {
case 0:
expect(keysym).to.be.equal(0x61);
expect(code).to.be.equal('KeyA');
expect(down).to.be.equal(true);
break;
case 1:
expect(keysym).to.be.equal(0x61);
expect(code).to.be.equal('KeyA');
expect(down).to.be.equal(false);
done();
}
};
kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', key: 'a'}));
});
});
});
describe('Track Key State', function () {
beforeEach(function () {
if (browser.isIE() || browser.isEdge()) this.skip();
});
it('should send release using the same keysym as the press', function (done) {
const kbd = new Keyboard(document);
kbd.onkeyevent = (keysym, code, down) => {
@@ -304,11 +142,6 @@ describe('Key Event Handling', function () {
// environments, so we need to redefine it whilst running these
// tests.
origNavigator = Object.getOwnPropertyDescriptor(window, "navigator");
if (origNavigator === undefined) {
// Object.getOwnPropertyDescriptor() doesn't work
// properly in any version of IE
this.skip();
}
Object.defineProperty(window, "navigator", {value: {}});
if (window.navigator.platform !== undefined) {
@@ -320,7 +153,9 @@ describe('Key Event Handling', function () {
window.navigator.platform = "Mac x86_64";
});
afterEach(function () {
Object.defineProperty(window, "navigator", origNavigator);
if (origNavigator !== undefined) {
Object.defineProperty(window, "navigator", origNavigator);
}
});
it('should change Alt to AltGraph', function () {
@@ -362,6 +197,114 @@ describe('Key Event Handling', function () {
});
});
describe('Caps Lock on iOS and macOS', function () {
let origNavigator;
beforeEach(function () {
// window.navigator is a protected read-only property in many
// environments, so we need to redefine it whilst running these
// tests.
origNavigator = Object.getOwnPropertyDescriptor(window, "navigator");
Object.defineProperty(window, "navigator", {value: {}});
if (window.navigator.platform !== undefined) {
// Object.defineProperty() doesn't work properly in old
// versions of Chrome
this.skip();
}
});
afterEach(function () {
if (origNavigator !== undefined) {
Object.defineProperty(window, "navigator", origNavigator);
}
});
it('should toggle caps lock on key press on iOS', function () {
window.navigator.platform = "iPad";
const kbd = new Keyboard(document);
kbd.onkeyevent = sinon.spy();
kbd._handleKeyDown(keyevent('keydown', {code: 'CapsLock', key: 'CapsLock'}));
expect(kbd.onkeyevent).to.have.been.calledTwice;
expect(kbd.onkeyevent.firstCall).to.have.been.calledWith(0xFFE5, "CapsLock", true);
expect(kbd.onkeyevent.secondCall).to.have.been.calledWith(0xFFE5, "CapsLock", false);
});
it('should toggle caps lock on key press on mac', function () {
window.navigator.platform = "Mac";
const kbd = new Keyboard(document);
kbd.onkeyevent = sinon.spy();
kbd._handleKeyDown(keyevent('keydown', {code: 'CapsLock', key: 'CapsLock'}));
expect(kbd.onkeyevent).to.have.been.calledTwice;
expect(kbd.onkeyevent.firstCall).to.have.been.calledWith(0xFFE5, "CapsLock", true);
expect(kbd.onkeyevent.secondCall).to.have.been.calledWith(0xFFE5, "CapsLock", false);
});
it('should toggle caps lock on key release on iOS', function () {
window.navigator.platform = "iPad";
const kbd = new Keyboard(document);
kbd.onkeyevent = sinon.spy();
kbd._handleKeyUp(keyevent('keyup', {code: 'CapsLock', key: 'CapsLock'}));
expect(kbd.onkeyevent).to.have.been.calledTwice;
expect(kbd.onkeyevent.firstCall).to.have.been.calledWith(0xFFE5, "CapsLock", true);
expect(kbd.onkeyevent.secondCall).to.have.been.calledWith(0xFFE5, "CapsLock", false);
});
it('should toggle caps lock on key release on mac', function () {
window.navigator.platform = "Mac";
const kbd = new Keyboard(document);
kbd.onkeyevent = sinon.spy();
kbd._handleKeyUp(keyevent('keyup', {code: 'CapsLock', key: 'CapsLock'}));
expect(kbd.onkeyevent).to.have.been.calledTwice;
expect(kbd.onkeyevent.firstCall).to.have.been.calledWith(0xFFE5, "CapsLock", true);
expect(kbd.onkeyevent.secondCall).to.have.been.calledWith(0xFFE5, "CapsLock", false);
});
});
describe('Japanese IM keys on Windows', function () {
let origNavigator;
beforeEach(function () {
// window.navigator is a protected read-only property in many
// environments, so we need to redefine it whilst running these
// tests.
origNavigator = Object.getOwnPropertyDescriptor(window, "navigator");
Object.defineProperty(window, "navigator", {value: {}});
if (window.navigator.platform !== undefined) {
// Object.defineProperty() doesn't work properly in old
// versions of Chrome
this.skip();
}
window.navigator.platform = "Windows";
});
afterEach(function () {
if (origNavigator !== undefined) {
Object.defineProperty(window, "navigator", origNavigator);
}
});
const keys = { 'Zenkaku': 0xff2a, 'Hankaku': 0xff2a,
'Alphanumeric': 0xff30, 'Katakana': 0xff26,
'Hiragana': 0xff25, 'Romaji': 0xff24,
'KanaMode': 0xff24 };
for (let [key, keysym] of Object.entries(keys)) {
it(`should fake key release for ${key} on Windows`, function () {
let kbd = new Keyboard(document);
kbd.onkeyevent = sinon.spy();
kbd._handleKeyDown(keyevent('keydown', {code: 'FakeIM', key: key}));
expect(kbd.onkeyevent).to.have.been.calledTwice;
expect(kbd.onkeyevent.firstCall).to.have.been.calledWith(keysym, "FakeIM", true);
expect(kbd.onkeyevent.secondCall).to.have.been.calledWith(keysym, "FakeIM", false);
});
}
});
describe('Escape AltGraph on Windows', function () {
let origNavigator;
beforeEach(function () {
@@ -369,11 +312,6 @@ describe('Key Event Handling', function () {
// environments, so we need to redefine it whilst running these
// tests.
origNavigator = Object.getOwnPropertyDescriptor(window, "navigator");
if (origNavigator === undefined) {
// Object.getOwnPropertyDescriptor() doesn't work
// properly in any version of IE
this.skip();
}
Object.defineProperty(window, "navigator", {value: {}});
if (window.navigator.platform !== undefined) {
@@ -387,8 +325,12 @@ describe('Key Event Handling', function () {
this.clock = sinon.useFakeTimers();
});
afterEach(function () {
Object.defineProperty(window, "navigator", origNavigator);
this.clock.restore();
if (origNavigator !== undefined) {
Object.defineProperty(window, "navigator", origNavigator);
}
if (this.clock !== undefined) {
this.clock.restore();
}
});
it('should supress ControlLeft until it knows if it is AltGr', function () {
@@ -437,7 +379,7 @@ describe('Key Event Handling', function () {
expect(kbd.onkeyevent.secondCall).to.have.been.calledWith(0x61, "KeyA", true);
// Check that the timer is properly dead
kbd.onkeyevent.reset();
kbd.onkeyevent.resetHistory();
this.clock.tick(100);
expect(kbd.onkeyevent).to.not.have.been.called;
});
@@ -455,7 +397,7 @@ describe('Key Event Handling', function () {
expect(kbd.onkeyevent.thirdCall).to.have.been.calledWith(0x61, "KeyA", false);
// Check that the timer is properly dead
kbd.onkeyevent.reset();
kbd.onkeyevent.resetHistory();
this.clock.tick(100);
expect(kbd.onkeyevent).to.not.have.been.called;
});
@@ -470,7 +412,7 @@ describe('Key Event Handling', function () {
expect(kbd.onkeyevent).to.have.been.calledWith(0xfe03, 'AltRight', true);
// Check that the timer is properly dead
kbd.onkeyevent.reset();
kbd.onkeyevent.resetHistory();
this.clock.tick(100);
expect(kbd.onkeyevent).to.not.have.been.called;
});
@@ -486,7 +428,7 @@ describe('Key Event Handling', function () {
expect(kbd.onkeyevent.secondCall).to.have.been.calledWith(0xffea, "AltRight", true);
// Check that the timer is properly dead
kbd.onkeyevent.reset();
kbd.onkeyevent.resetHistory();
this.clock.tick(100);
expect(kbd.onkeyevent).to.not.have.been.called;
});
@@ -507,4 +449,73 @@ describe('Key Event Handling', function () {
expect(kbd.onkeyevent).to.have.been.calledWith(0xfe03, 'AltRight', true);
});
});
describe('Missing Shift keyup on Windows', function () {
let origNavigator;
beforeEach(function () {
// window.navigator is a protected read-only property in many
// environments, so we need to redefine it whilst running these
// tests.
origNavigator = Object.getOwnPropertyDescriptor(window, "navigator");
Object.defineProperty(window, "navigator", {value: {}});
if (window.navigator.platform !== undefined) {
// Object.defineProperty() doesn't work properly in old
// versions of Chrome
this.skip();
}
window.navigator.platform = "Windows x86_64";
this.clock = sinon.useFakeTimers();
});
afterEach(function () {
if (origNavigator !== undefined) {
Object.defineProperty(window, "navigator", origNavigator);
}
if (this.clock !== undefined) {
this.clock.restore();
}
});
it('should fake a left Shift keyup', function () {
const kbd = new Keyboard(document);
kbd.onkeyevent = sinon.spy();
kbd._handleKeyDown(keyevent('keydown', {code: 'ShiftLeft', key: 'Shift', location: 1}));
expect(kbd.onkeyevent).to.have.been.calledOnce;
expect(kbd.onkeyevent).to.have.been.calledWith(0xffe1, 'ShiftLeft', true);
kbd.onkeyevent.resetHistory();
kbd._handleKeyDown(keyevent('keydown', {code: 'ShiftRight', key: 'Shift', location: 2}));
expect(kbd.onkeyevent).to.have.been.calledOnce;
expect(kbd.onkeyevent).to.have.been.calledWith(0xffe2, 'ShiftRight', true);
kbd.onkeyevent.resetHistory();
kbd._handleKeyUp(keyevent('keyup', {code: 'ShiftLeft', key: 'Shift', location: 1}));
expect(kbd.onkeyevent).to.have.been.calledTwice;
expect(kbd.onkeyevent).to.have.been.calledWith(0xffe2, 'ShiftRight', false);
expect(kbd.onkeyevent).to.have.been.calledWith(0xffe1, 'ShiftLeft', false);
});
it('should fake a right Shift keyup', function () {
const kbd = new Keyboard(document);
kbd.onkeyevent = sinon.spy();
kbd._handleKeyDown(keyevent('keydown', {code: 'ShiftLeft', key: 'Shift', location: 1}));
expect(kbd.onkeyevent).to.have.been.calledOnce;
expect(kbd.onkeyevent).to.have.been.calledWith(0xffe1, 'ShiftLeft', true);
kbd.onkeyevent.resetHistory();
kbd._handleKeyDown(keyevent('keydown', {code: 'ShiftRight', key: 'Shift', location: 2}));
expect(kbd.onkeyevent).to.have.been.calledOnce;
expect(kbd.onkeyevent).to.have.been.calledWith(0xffe2, 'ShiftRight', true);
kbd.onkeyevent.resetHistory();
kbd._handleKeyUp(keyevent('keyup', {code: 'ShiftRight', key: 'Shift', location: 2}));
expect(kbd.onkeyevent).to.have.been.calledTwice;
expect(kbd.onkeyevent).to.have.been.calledWith(0xffe2, 'ShiftRight', false);
expect(kbd.onkeyevent).to.have.been.calledWith(0xffe1, 'ShiftLeft', false);
});
});
});
+3 -6
View File
@@ -11,11 +11,6 @@ describe('Localization', function () {
// environments, so we need to redefine it whilst running these
// tests.
origNavigator = Object.getOwnPropertyDescriptor(window, "navigator");
if (origNavigator === undefined) {
// Object.getOwnPropertyDescriptor() doesn't work
// properly in any version of IE
this.skip();
}
Object.defineProperty(window, "navigator", {value: {}});
if (window.navigator.languages !== undefined) {
@@ -27,7 +22,9 @@ describe('Localization', function () {
window.navigator.languages = [];
});
afterEach(function () {
Object.defineProperty(window, "navigator", origNavigator);
if (origNavigator !== undefined) {
Object.defineProperty(window, "navigator", origNavigator);
}
});
it('should use English by default', function () {
-304
View File
@@ -1,304 +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
});
});
});
+129
View File
@@ -0,0 +1,129 @@
const expect = chai.expect;
import Websock from '../core/websock.js';
import Display from '../core/display.js';
import RawDecoder from '../core/decoders/raw.js';
import FakeWebSocket from './fake.websocket.js';
function testDecodeRect(decoder, x, y, width, height, data, display, depth) {
let sock;
sock = new Websock;
sock.open("ws://example.com");
sock.on('message', () => {
decoder.decodeRect(x, y, width, height, sock, display, depth);
});
// Empty messages are filtered at multiple layers, so we need to
// do a direct call
if (data.length === 0) {
decoder.decodeRect(x, y, width, height, sock, display, depth);
} else {
sock._websocket._receiveData(new Uint8Array(data));
}
display.flip();
}
describe('Raw Decoder', function () {
let decoder;
let display;
before(FakeWebSocket.replace);
after(FakeWebSocket.restore);
beforeEach(function () {
decoder = new RawDecoder();
display = new Display(document.createElement('canvas'));
display.resize(4, 4);
});
it('should handle the Raw encoding', function () {
testDecodeRect(decoder, 0, 0, 2, 2,
[0xff, 0x00, 0x00, 0, 0x00, 0xff, 0x00, 0,
0x00, 0xff, 0x00, 0, 0xff, 0x00, 0x00, 0],
display, 24);
testDecodeRect(decoder, 2, 0, 2, 2,
[0x00, 0x00, 0xff, 0, 0x00, 0x00, 0xff, 0,
0x00, 0x00, 0xff, 0, 0x00, 0x00, 0xff, 0],
display, 24);
testDecodeRect(decoder, 0, 2, 4, 1,
[0xee, 0x00, 0xff, 0, 0x00, 0xee, 0xff, 0,
0xaa, 0xee, 0xff, 0, 0xab, 0xee, 0xff, 0],
display, 24);
testDecodeRect(decoder, 0, 3, 4, 1,
[0xee, 0x00, 0xff, 0, 0x00, 0xee, 0xff, 0,
0xaa, 0xee, 0xff, 0, 0xab, 0xee, 0xff, 0],
display, 24);
let targetData = new Uint8Array([
0xff, 0x00, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255,
0x00, 0xff, 0x00, 255, 0xff, 0x00, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255,
0xee, 0x00, 0xff, 255, 0x00, 0xee, 0xff, 255, 0xaa, 0xee, 0xff, 255, 0xab, 0xee, 0xff, 255,
0xee, 0x00, 0xff, 255, 0x00, 0xee, 0xff, 255, 0xaa, 0xee, 0xff, 255, 0xab, 0xee, 0xff, 255
]);
expect(display).to.have.displayed(targetData);
});
it('should handle the Raw encoding in low colour mode', function () {
testDecodeRect(decoder, 0, 0, 2, 2,
[0x30, 0x30, 0x30, 0x30],
display, 8);
testDecodeRect(decoder, 2, 0, 2, 2,
[0x0c, 0x0c, 0x0c, 0x0c],
display, 8);
testDecodeRect(decoder, 0, 2, 4, 1,
[0x0c, 0x0c, 0x30, 0x30],
display, 8);
testDecodeRect(decoder, 0, 3, 4, 1,
[0x0c, 0x0c, 0x30, 0x30],
display, 8);
let targetData = new Uint8Array([
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,
0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255,
0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255
]);
expect(display).to.have.displayed(targetData);
});
it('should handle empty rects', function () {
display.fillRect(0, 0, 4, 4, [ 0x00, 0x00, 0xff ]);
display.fillRect(2, 0, 2, 2, [ 0x00, 0xff, 0x00 ]);
display.fillRect(0, 2, 2, 2, [ 0x00, 0xff, 0x00 ]);
testDecodeRect(decoder, 1, 2, 0, 0, [], display, 24);
let targetData = new Uint8Array([
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,
0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255,
0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255
]);
expect(display).to.have.displayed(targetData);
});
it('should handle empty rects in low colour mode', function () {
display.fillRect(0, 0, 4, 4, [ 0x00, 0x00, 0xff ]);
display.fillRect(2, 0, 2, 2, [ 0x00, 0xff, 0x00 ]);
display.fillRect(0, 2, 2, 2, [ 0x00, 0xff, 0x00 ]);
testDecodeRect(decoder, 1, 2, 0, 0, [], display, 8);
let targetData = new Uint8Array([
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,
0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255,
0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255
]);
expect(display).to.have.displayed(targetData);
});
});
+2551 -899
View File
File diff suppressed because it is too large Load Diff
+107
View File
@@ -0,0 +1,107 @@
const expect = chai.expect;
import Websock from '../core/websock.js';
import Display from '../core/display.js';
import RREDecoder from '../core/decoders/rre.js';
import FakeWebSocket from './fake.websocket.js';
function testDecodeRect(decoder, x, y, width, height, data, display, depth) {
let sock;
sock = new Websock;
sock.open("ws://example.com");
sock.on('message', () => {
decoder.decodeRect(x, y, width, height, sock, display, depth);
});
// Empty messages are filtered at multiple layers, so we need to
// do a direct call
if (data.length === 0) {
decoder.decodeRect(x, y, width, height, sock, display, depth);
} else {
sock._websocket._receiveData(new Uint8Array(data));
}
display.flip();
}
function push16(arr, num) {
arr.push((num >> 8) & 0xFF,
num & 0xFF);
}
function push32(arr, num) {
arr.push((num >> 24) & 0xFF,
(num >> 16) & 0xFF,
(num >> 8) & 0xFF,
num & 0xFF);
}
describe('RRE Decoder', function () {
let decoder;
let display;
before(FakeWebSocket.replace);
after(FakeWebSocket.restore);
beforeEach(function () {
decoder = new RREDecoder();
display = new Display(document.createElement('canvas'));
display.resize(4, 4);
});
// TODO(directxman12): test rre_chunk_sz?
it('should handle the RRE encoding', function () {
let data = [];
push32(data, 2); // 2 subrects
push32(data, 0x00ff0000); // becomes 00ff0000 --> #00FF00 bg color
data.push(0x00); // becomes 0000ff00 --> #0000FF fg color
data.push(0x00);
data.push(0xff);
data.push(0x00);
push16(data, 0); // x: 0
push16(data, 0); // y: 0
push16(data, 2); // width: 2
push16(data, 2); // height: 2
data.push(0x00); // becomes 0000ff00 --> #0000FF fg color
data.push(0x00);
data.push(0xff);
data.push(0x00);
push16(data, 2); // x: 2
push16(data, 2); // y: 2
push16(data, 2); // width: 2
push16(data, 2); // height: 2
testDecodeRect(decoder, 0, 0, 4, 4, data, display, 24);
let targetData = new Uint8Array([
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,
0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255,
0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255
]);
expect(display).to.have.displayed(targetData);
});
it('should handle empty rects', function () {
display.fillRect(0, 0, 4, 4, [ 0x00, 0x00, 0xff ]);
display.fillRect(2, 0, 2, 2, [ 0x00, 0xff, 0x00 ]);
display.fillRect(0, 2, 2, 2, [ 0x00, 0xff, 0x00 ]);
testDecodeRect(decoder, 1, 2, 0, 0, [ 0x00, 0xff, 0xff, 0xff, 0xff ], display, 24);
let targetData = new Uint8Array([
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,
0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255,
0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255
]);
expect(display).to.have.displayed(targetData);
});
});
+394
View File
@@ -0,0 +1,394 @@
const expect = chai.expect;
import Websock from '../core/websock.js';
import Display from '../core/display.js';
import TightDecoder from '../core/decoders/tight.js';
import FakeWebSocket from './fake.websocket.js';
function testDecodeRect(decoder, x, y, width, height, data, display, depth) {
let sock;
sock = new Websock;
sock.open("ws://example.com");
sock.on('message', () => {
decoder.decodeRect(x, y, width, height, sock, display, depth);
});
// Empty messages are filtered at multiple layers, so we need to
// do a direct call
if (data.length === 0) {
decoder.decodeRect(x, y, width, height, sock, display, depth);
} else {
sock._websocket._receiveData(new Uint8Array(data));
}
display.flip();
}
describe('Tight Decoder', function () {
let decoder;
let display;
before(FakeWebSocket.replace);
after(FakeWebSocket.restore);
beforeEach(function () {
decoder = new TightDecoder();
display = new Display(document.createElement('canvas'));
display.resize(4, 4);
});
it('should handle fill rects', function () {
testDecodeRect(decoder, 0, 0, 4, 4,
[0x80, 0xff, 0x88, 0x44],
display, 24);
let targetData = new Uint8Array([
0xff, 0x88, 0x44, 255, 0xff, 0x88, 0x44, 255, 0xff, 0x88, 0x44, 255, 0xff, 0x88, 0x44, 255,
0xff, 0x88, 0x44, 255, 0xff, 0x88, 0x44, 255, 0xff, 0x88, 0x44, 255, 0xff, 0x88, 0x44, 255,
0xff, 0x88, 0x44, 255, 0xff, 0x88, 0x44, 255, 0xff, 0x88, 0x44, 255, 0xff, 0x88, 0x44, 255,
0xff, 0x88, 0x44, 255, 0xff, 0x88, 0x44, 255, 0xff, 0x88, 0x44, 255, 0xff, 0x88, 0x44, 255,
]);
expect(display).to.have.displayed(targetData);
});
it('should handle uncompressed copy rects', function () {
let blueData = [ 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0xff ];
let greenData = [ 0x00, 0x00, 0xff, 0x00, 0x00, 0xff, 0x00 ];
testDecodeRect(decoder, 0, 0, 2, 1, blueData, display, 24);
testDecodeRect(decoder, 0, 1, 2, 1, blueData, display, 24);
testDecodeRect(decoder, 2, 0, 2, 1, greenData, display, 24);
testDecodeRect(decoder, 2, 1, 2, 1, greenData, display, 24);
testDecodeRect(decoder, 0, 2, 2, 1, greenData, display, 24);
testDecodeRect(decoder, 0, 3, 2, 1, greenData, display, 24);
testDecodeRect(decoder, 2, 2, 2, 1, blueData, display, 24);
testDecodeRect(decoder, 2, 3, 2, 1, blueData, display, 24);
let targetData = new Uint8Array([
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,
0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255,
0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255
]);
expect(display).to.have.displayed(targetData);
});
it('should handle compressed copy rects', function () {
let data = [
// Control byte
0x00,
// Pixels (compressed)
0x15,
0x78, 0x9c, 0x63, 0x60, 0xf8, 0xcf, 0x00, 0x44,
0x60, 0x82, 0x01, 0x99, 0x8d, 0x29, 0x02, 0xa6,
0x00, 0x7e, 0xbf, 0x0f, 0xf1 ];
testDecodeRect(decoder, 0, 0, 4, 4, data, display, 24);
let targetData = new Uint8Array([
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,
0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255,
0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255
]);
expect(display).to.have.displayed(targetData);
});
it('should handle uncompressed mono rects', function () {
let data = [
// Control bytes
0x40, 0x01,
// Palette
0x01, 0x00, 0x00, 0xff, 0x00, 0xff, 0x00,
// Pixels
0x30, 0x30, 0xc0, 0xc0 ];
testDecodeRect(decoder, 0, 0, 4, 4, data, display, 24);
let targetData = new Uint8Array([
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,
0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255,
0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255
]);
expect(display).to.have.displayed(targetData);
});
it('should handle compressed mono rects', function () {
display.resize(4, 12);
let data = [
// Control bytes
0x40, 0x01,
// Palette
0x01, 0x00, 0x00, 0xff, 0x00, 0xff, 0x00,
// Pixels (compressed)
0x0e,
0x78, 0x9c, 0x33, 0x30, 0x38, 0x70, 0xc0, 0x00,
0x8a, 0x01, 0x21, 0x3c, 0x05, 0xa1 ];
testDecodeRect(decoder, 0, 0, 4, 12, data, display, 24);
let targetData = new Uint8Array([
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,
0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255,
0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255,
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,
0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255,
0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255,
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,
0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255,
0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255
]);
expect(display).to.have.displayed(targetData);
});
it('should handle uncompressed palette rects', function () {
let data1 = [
// Control bytes
0x40, 0x01,
// Palette
0x02, 0x00, 0x00, 0xff, 0x00, 0xff, 0x00, 0x00, 0x00, 0x00,
// Pixels
0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x01 ];
let data2 = [
// Control bytes
0x40, 0x01,
// Palette
0x02, 0x00, 0x00, 0xff, 0x00, 0xff, 0x00, 0x00, 0x00, 0x00,
// Pixels
0x01, 0x01, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00 ];
testDecodeRect(decoder, 0, 0, 4, 2, data1, display, 24);
testDecodeRect(decoder, 0, 2, 4, 2, data2, display, 24);
let targetData = new Uint8Array([
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,
0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255,
0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255
]);
expect(display).to.have.displayed(targetData);
});
it('should handle compressed palette rects', function () {
let data = [
// Control bytes
0x40, 0x01,
// Palette
0x02, 0x00, 0x00, 0xff, 0x00, 0xff, 0x00, 0x00, 0x00, 0x00,
// Pixels (compressed)
0x12,
0x78, 0x9c, 0x63, 0x60, 0x60, 0x64, 0x64, 0x00,
0x62, 0x08, 0xc9, 0xc0, 0x00, 0x00, 0x00, 0x54,
0x00, 0x09 ];
testDecodeRect(decoder, 0, 0, 4, 4, data, display, 24);
let targetData = new Uint8Array([
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,
0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255,
0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255
]);
expect(display).to.have.displayed(targetData);
});
it.skip('should handle uncompressed gradient rects', function () {
// Not implemented yet
});
it.skip('should handle compressed gradient rects', function () {
// Not implemented yet
});
it('should handle empty copy rects', function () {
display.fillRect(0, 0, 4, 4, [ 0x00, 0x00, 0xff ]);
display.fillRect(2, 0, 2, 2, [ 0x00, 0xff, 0x00 ]);
display.fillRect(0, 2, 2, 2, [ 0x00, 0xff, 0x00 ]);
testDecodeRect(decoder, 1, 2, 0, 0, [ 0x00 ], display, 24);
let targetData = new Uint8Array([
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,
0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255,
0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255
]);
expect(display).to.have.displayed(targetData);
});
it('should handle empty palette rects', function () {
display.fillRect(0, 0, 4, 4, [ 0x00, 0x00, 0xff ]);
display.fillRect(2, 0, 2, 2, [ 0x00, 0xff, 0x00 ]);
display.fillRect(0, 2, 2, 2, [ 0x00, 0xff, 0x00 ]);
testDecodeRect(decoder, 1, 2, 0, 0,
[ 0x40, 0x01, 0x01,
0xff, 0xff, 0xff,
0xff, 0xff, 0xff ], display, 24);
let targetData = new Uint8Array([
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,
0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255,
0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255
]);
expect(display).to.have.displayed(targetData);
});
it('should handle empty fill rects', function () {
display.fillRect(0, 0, 4, 4, [ 0x00, 0x00, 0xff ]);
display.fillRect(2, 0, 2, 2, [ 0x00, 0xff, 0x00 ]);
display.fillRect(0, 2, 2, 2, [ 0x00, 0xff, 0x00 ]);
testDecodeRect(decoder, 1, 2, 0, 0,
[ 0x80, 0xff, 0xff, 0xff ], display, 24);
let targetData = new Uint8Array([
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,
0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255,
0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255
]);
expect(display).to.have.displayed(targetData);
});
it('should handle JPEG rects', function (done) {
let data = [
// Control bytes
0x90, 0xd6, 0x05,
// JPEG data
0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46,
0x49, 0x46, 0x00, 0x01, 0x01, 0x01, 0x00, 0x48,
0x00, 0x48, 0x00, 0x00, 0xff, 0xfe, 0x00, 0x13,
0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x20,
0x77, 0x69, 0x74, 0x68, 0x20, 0x47, 0x49, 0x4d,
0x50, 0xff, 0xdb, 0x00, 0x43, 0x00, 0x01, 0x01,
0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0xff, 0xdb,
0x00, 0x43, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
0x01, 0x01, 0x01, 0xff, 0xc2, 0x00, 0x11, 0x08,
0x00, 0x04, 0x00, 0x04, 0x03, 0x01, 0x11, 0x00,
0x02, 0x11, 0x01, 0x03, 0x11, 0x01, 0xff, 0xc4,
0x00, 0x14, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x07, 0xff, 0xc4, 0x00, 0x14,
0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x08, 0xff, 0xda, 0x00, 0x0c, 0x03, 0x01,
0x00, 0x02, 0x10, 0x03, 0x10, 0x00, 0x00, 0x01,
0x1e, 0x0a, 0xa7, 0x7f, 0xff, 0xc4, 0x00, 0x14,
0x10, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x05, 0xff, 0xda, 0x00, 0x08, 0x01, 0x01,
0x00, 0x01, 0x05, 0x02, 0x5d, 0x74, 0x41, 0x47,
0xff, 0xc4, 0x00, 0x1f, 0x11, 0x00, 0x01, 0x04,
0x02, 0x02, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x04, 0x05,
0x07, 0x08, 0x14, 0x16, 0x03, 0x15, 0x17, 0x25,
0x26, 0xff, 0xda, 0x00, 0x08, 0x01, 0x03, 0x01,
0x01, 0x3f, 0x01, 0xad, 0x35, 0xa6, 0x13, 0xb8,
0x10, 0x98, 0x5d, 0x8a, 0xb1, 0x41, 0x7e, 0x43,
0x99, 0x24, 0x3d, 0x8f, 0x70, 0x30, 0xd8, 0xcb,
0x44, 0xbb, 0x7d, 0x48, 0xb5, 0xf8, 0x18, 0x7f,
0xe7, 0xc1, 0x9f, 0x86, 0x45, 0x9b, 0xfa, 0xf1,
0x61, 0x96, 0x46, 0xbf, 0x56, 0xc8, 0x8b, 0x2b,
0x0b, 0x35, 0x6e, 0x4b, 0x8a, 0x95, 0x6a, 0xf9,
0xff, 0x00, 0xff, 0xc4, 0x00, 0x1f, 0x11, 0x00,
0x01, 0x04, 0x02, 0x02, 0x03, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03,
0x02, 0x04, 0x05, 0x12, 0x13, 0x14, 0x01, 0x06,
0x11, 0x22, 0x23, 0xff, 0xda, 0x00, 0x08, 0x01,
0x02, 0x01, 0x01, 0x3f, 0x01, 0x85, 0x85, 0x8c,
0xec, 0x31, 0x8d, 0xa6, 0x26, 0x1b, 0x6e, 0x48,
0xbc, 0xcd, 0xb0, 0xe3, 0x33, 0x86, 0xf9, 0x35,
0xdc, 0x15, 0xa8, 0xbe, 0x4d, 0x4a, 0x10, 0x22,
0x80, 0x00, 0x91, 0xe8, 0x24, 0xda, 0xb6, 0x57,
0x95, 0xf2, 0xa5, 0x73, 0xff, 0xc4, 0x00, 0x1e,
0x10, 0x00, 0x01, 0x04, 0x03, 0x00, 0x03, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x03, 0x01, 0x02, 0x04, 0x12, 0x05, 0x11,
0x13, 0x14, 0x22, 0x23, 0xff, 0xda, 0x00, 0x08,
0x01, 0x01, 0x00, 0x06, 0x3f, 0x02, 0x91, 0x89,
0xc4, 0xc8, 0xf1, 0x60, 0x45, 0xe5, 0xc0, 0x1c,
0x80, 0x7a, 0x77, 0x00, 0xe4, 0x97, 0xeb, 0x24,
0x66, 0x33, 0xac, 0x63, 0x11, 0xfe, 0xe4, 0x76,
0xad, 0x56, 0xe9, 0xa8, 0x88, 0x9f, 0xff, 0xc4,
0x00, 0x14, 0x10, 0x01, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0xff, 0xda, 0x00, 0x08,
0x01, 0x01, 0x00, 0x01, 0x3f, 0x21, 0x68, 0x3f,
0x92, 0x17, 0x81, 0x1f, 0x7f, 0xff, 0xda, 0x00,
0x0c, 0x03, 0x01, 0x00, 0x02, 0x00, 0x03, 0x00,
0x00, 0x00, 0x10, 0x5f, 0xff, 0xc4, 0x00, 0x14,
0x11, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0xff, 0xda, 0x00, 0x08, 0x01, 0x03,
0x01, 0x01, 0x3f, 0x10, 0x03, 0xeb, 0x11, 0xe4,
0xa7, 0xe3, 0xff, 0x00, 0xff, 0xc4, 0x00, 0x14,
0x11, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0xff, 0xda, 0x00, 0x08, 0x01, 0x02,
0x01, 0x01, 0x3f, 0x10, 0x6b, 0xd3, 0x02, 0xdc,
0x9a, 0xf4, 0xff, 0x00, 0xff, 0xc4, 0x00, 0x14,
0x10, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0xff, 0xda, 0x00, 0x08, 0x01, 0x01,
0x00, 0x01, 0x3f, 0x10, 0x62, 0x7b, 0x3a, 0xd0,
0x3f, 0xeb, 0xff, 0x00, 0xff, 0xd9,
];
testDecodeRect(decoder, 0, 0, 4, 4, data, display, 24);
let targetData = new Uint8Array([
0xff, 0x00, 0x00, 255, 0xff, 0x00, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,
0xff, 0x00, 0x00, 255, 0xff, 0x00, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,
0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0xff, 0x00, 0x00, 255, 0xff, 0x00, 0x00, 255,
0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0xff, 0x00, 0x00, 255, 0xff, 0x00, 0x00, 255
]);
// Browsers have rounding errors, so we need an approximate
// comparing function
function almost(a, b) {
let diff = Math.abs(a - b);
return diff < 5;
}
display.onflush = () => {
expect(display).to.have.displayed(targetData, almost);
done();
};
display.flush();
});
});
+144
View File
@@ -0,0 +1,144 @@
const expect = chai.expect;
import Websock from '../core/websock.js';
import Display from '../core/display.js';
import TightPngDecoder from '../core/decoders/tightpng.js';
import FakeWebSocket from './fake.websocket.js';
function testDecodeRect(decoder, x, y, width, height, data, display, depth) {
let sock;
sock = new Websock;
sock.open("ws://example.com");
sock.on('message', () => {
decoder.decodeRect(x, y, width, height, sock, display, depth);
});
// Empty messages are filtered at multiple layers, so we need to
// do a direct call
if (data.length === 0) {
decoder.decodeRect(x, y, width, height, sock, display, depth);
} else {
sock._websocket._receiveData(new Uint8Array(data));
}
display.flip();
}
describe('TightPng Decoder', function () {
let decoder;
let display;
before(FakeWebSocket.replace);
after(FakeWebSocket.restore);
beforeEach(function () {
decoder = new TightPngDecoder();
display = new Display(document.createElement('canvas'));
display.resize(4, 4);
});
it('should handle the TightPng encoding', function (done) {
let data = [
// Control bytes
0xa0, 0xb4, 0x04,
// PNG data
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52,
0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04,
0x08, 0x02, 0x00, 0x00, 0x00, 0x26, 0x93, 0x09,
0x29, 0x00, 0x00, 0x01, 0x84, 0x69, 0x43, 0x43,
0x50, 0x49, 0x43, 0x43, 0x20, 0x70, 0x72, 0x6f,
0x66, 0x69, 0x6c, 0x65, 0x00, 0x00, 0x28, 0x91,
0x7d, 0x91, 0x3d, 0x48, 0xc3, 0x40, 0x18, 0x86,
0xdf, 0xa6, 0x6a, 0x45, 0x2a, 0x0e, 0x76, 0x10,
0x71, 0x08, 0x52, 0x9d, 0x2c, 0x88, 0x8a, 0x38,
0x6a, 0x15, 0x8a, 0x50, 0x21, 0xd4, 0x0a, 0xad,
0x3a, 0x98, 0x5c, 0xfa, 0x07, 0x4d, 0x1a, 0x92,
0x14, 0x17, 0x47, 0xc1, 0xb5, 0xe0, 0xe0, 0xcf,
0x62, 0xd5, 0xc1, 0xc5, 0x59, 0x57, 0x07, 0x57,
0x41, 0x10, 0xfc, 0x01, 0x71, 0x72, 0x74, 0x52,
0x74, 0x91, 0x12, 0xbf, 0x4b, 0x0a, 0x2d, 0x62,
0xbc, 0xe3, 0xb8, 0x87, 0xf7, 0xbe, 0xf7, 0xe5,
0xee, 0x3b, 0x40, 0xa8, 0x97, 0x99, 0x66, 0x75,
0x8c, 0x03, 0x9a, 0x6e, 0x9b, 0xa9, 0x44, 0x5c,
0xcc, 0x64, 0x57, 0xc5, 0xd0, 0x2b, 0xba, 0x68,
0x86, 0x31, 0x8c, 0x2e, 0x99, 0x59, 0xc6, 0x9c,
0x24, 0x25, 0xe1, 0x3b, 0xbe, 0xee, 0x11, 0xe0,
0xfb, 0x5d, 0x8c, 0x67, 0xf9, 0xd7, 0xfd, 0x39,
0x7a, 0xd5, 0x9c, 0xc5, 0x80, 0x80, 0x48, 0x3c,
0xcb, 0x0c, 0xd3, 0x26, 0xde, 0x20, 0x9e, 0xde,
0xb4, 0x0d, 0xce, 0xfb, 0xc4, 0x11, 0x56, 0x94,
0x55, 0xe2, 0x73, 0xe2, 0x31, 0x93, 0x2e, 0x48,
0xfc, 0xc8, 0x75, 0xc5, 0xe3, 0x37, 0xce, 0x05,
0x97, 0x05, 0x9e, 0x19, 0x31, 0xd3, 0xa9, 0x79,
0xe2, 0x08, 0xb1, 0x58, 0x68, 0x63, 0xa5, 0x8d,
0x59, 0xd1, 0xd4, 0x88, 0xa7, 0x88, 0xa3, 0xaa,
0xa6, 0x53, 0xbe, 0x90, 0xf1, 0x58, 0xe5, 0xbc,
0xc5, 0x59, 0x2b, 0x57, 0x59, 0xf3, 0x9e, 0xfc,
0x85, 0xe1, 0x9c, 0xbe, 0xb2, 0xcc, 0x75, 0x5a,
0x43, 0x48, 0x60, 0x11, 0x4b, 0x90, 0x20, 0x42,
0x41, 0x15, 0x25, 0x94, 0x61, 0x23, 0x46, 0xbb,
0x4e, 0x8a, 0x85, 0x14, 0x9d, 0xc7, 0x7d, 0xfc,
0x83, 0xae, 0x5f, 0x22, 0x97, 0x42, 0xae, 0x12,
0x18, 0x39, 0x16, 0x50, 0x81, 0x06, 0xd9, 0xf5,
0x83, 0xff, 0xc1, 0xef, 0xde, 0x5a, 0xf9, 0xc9,
0x09, 0x2f, 0x29, 0x1c, 0x07, 0x3a, 0x5f, 0x1c,
0xe7, 0x63, 0x04, 0x08, 0xed, 0x02, 0x8d, 0x9a,
0xe3, 0x7c, 0x1f, 0x3b, 0x4e, 0xe3, 0x04, 0x08,
0x3e, 0x03, 0x57, 0x7a, 0xcb, 0x5f, 0xa9, 0x03,
0x33, 0x9f, 0xa4, 0xd7, 0x5a, 0x5a, 0xf4, 0x08,
0xe8, 0xdb, 0x06, 0x2e, 0xae, 0x5b, 0x9a, 0xb2,
0x07, 0x5c, 0xee, 0x00, 0x03, 0x4f, 0x86, 0x6c,
0xca, 0xae, 0x14, 0xa4, 0x25, 0xe4, 0xf3, 0xc0,
0xfb, 0x19, 0x7d, 0x53, 0x16, 0xe8, 0xbf, 0x05,
0x7a, 0xd6, 0xbc, 0xbe, 0x35, 0xcf, 0x71, 0xfa,
0x00, 0xa4, 0xa9, 0x57, 0xc9, 0x1b, 0xe0, 0xe0,
0x10, 0x18, 0x2d, 0x50, 0xf6, 0xba, 0xcf, 0xbb,
0xbb, 0xdb, 0xfb, 0xf6, 0x6f, 0x4d, 0xb3, 0x7f,
0x3f, 0x0a, 0x27, 0x72, 0x7d, 0x49, 0x29, 0x8b,
0xbb, 0x00, 0x00, 0x00, 0x09, 0x70, 0x48, 0x59,
0x73, 0x00, 0x00, 0x2e, 0x23, 0x00, 0x00, 0x2e,
0x23, 0x01, 0x78, 0xa5, 0x3f, 0x76, 0x00, 0x00,
0x00, 0x07, 0x74, 0x49, 0x4d, 0x45, 0x07, 0xe4,
0x06, 0x06, 0x0c, 0x23, 0x1d, 0x3f, 0x9f, 0xbb,
0x94, 0x00, 0x00, 0x00, 0x19, 0x74, 0x45, 0x58,
0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74,
0x00, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64,
0x20, 0x77, 0x69, 0x74, 0x68, 0x20, 0x47, 0x49,
0x4d, 0x50, 0x57, 0x81, 0x0e, 0x17, 0x00, 0x00,
0x00, 0x1e, 0x49, 0x44, 0x41, 0x54, 0x08, 0xd7,
0x65, 0xc9, 0xb1, 0x0d, 0x00, 0x00, 0x08, 0x03,
0x20, 0xea, 0xff, 0x3f, 0xd7, 0xd5, 0x44, 0x56,
0x52, 0x90, 0xc2, 0x38, 0xa2, 0xd0, 0xbc, 0x59,
0x8a, 0x9f, 0x04, 0x05, 0x6b, 0x38, 0x7b, 0xb2,
0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44,
0xae, 0x42, 0x60, 0x82,
];
testDecodeRect(decoder, 0, 0, 4, 4, data, display, 24);
let targetData = new Uint8Array([
0xff, 0x00, 0x00, 255, 0xff, 0x00, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,
0xff, 0x00, 0x00, 255, 0xff, 0x00, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,
0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0xff, 0x00, 0x00, 255, 0xff, 0x00, 0x00, 255,
0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0xff, 0x00, 0x00, 255, 0xff, 0x00, 0x00, 255
]);
// Firefox currently has some very odd rounding bug:
// https://bugzilla.mozilla.org/show_bug.cgi?id=1667747
function almost(a, b) {
let diff = Math.abs(a - b);
return diff < 30;
}
display.onflush = () => {
expect(display).to.have.displayed(targetData, almost);
done();
};
display.flush();
});
});
+27 -7
View File
@@ -2,6 +2,7 @@
const expect = chai.expect;
import * as Log from '../core/util/logging.js';
import { encodeUTF8, decodeUTF8 } from '../core/util/strings.js';
describe('Utils', function () {
"use strict";
@@ -21,45 +22,64 @@ describe('Utils', function () {
console.warn.restore();
console.error.restore();
console.info.restore();
Log.init_logging();
Log.initLogging();
});
it('should use noop for levels lower than the min level', function () {
Log.init_logging('warn');
Log.initLogging('warn');
Log.Debug('hi');
Log.Info('hello');
expect(console.log).to.not.have.been.called;
});
it('should use console.debug for Debug', function () {
Log.init_logging('debug');
Log.initLogging('debug');
Log.Debug('dbg');
expect(console.debug).to.have.been.calledWith('dbg');
});
it('should use console.info for Info', function () {
Log.init_logging('debug');
Log.initLogging('debug');
Log.Info('inf');
expect(console.info).to.have.been.calledWith('inf');
});
it('should use console.warn for Warn', function () {
Log.init_logging('warn');
Log.initLogging('warn');
Log.Warn('wrn');
expect(console.warn).to.have.been.called;
expect(console.warn).to.have.been.calledWith('wrn');
});
it('should use console.error for Error', function () {
Log.init_logging('error');
Log.initLogging('error');
Log.Error('err');
expect(console.error).to.have.been.called;
expect(console.error).to.have.been.calledWith('err');
});
});
describe('string functions', function () {
it('should decode UTF-8 to DOMString correctly', function () {
const utf8string = '\xd0\x9f';
const domstring = decodeUTF8(utf8string);
expect(domstring).to.equal("П");
});
it('should encode DOMString to UTF-8 correctly', function () {
const domstring = "åäöa";
const utf8string = encodeUTF8(domstring);
expect(utf8string).to.equal('\xc3\xa5\xc3\xa4\xc3\xb6\x61');
});
it('should allow Latin-1 strings if allowLatin1 is set when decoding', function () {
const latin1string = '\xe5\xe4\xf6';
expect(() => decodeUTF8(latin1string)).to.throw(Error);
expect(decodeUTF8(latin1string, true)).to.equal('åäö');
});
});
// TODO(directxman12): test the conf_default and conf_defaults methods
// TODO(directxman12): test decodeUTF8
// TODO(directxman12): test the event methods (addEvent, removeEvent, stopEvent)
// TODO(directxman12): figure out a good way to test getPosition and getEventPosition
// TODO(directxman12): figure out how to test the browser detection functions properly
+167 -56
View File
@@ -13,7 +13,7 @@ describe('Websock', function () {
beforeEach(function () {
sock = new Websock();
// skip init
sock._allocate_buffers();
sock._allocateBuffers();
sock._rQ.set(RQ_TEMPLATE);
sock._rQlen = RQ_TEMPLATE.length;
});
@@ -33,51 +33,51 @@ describe('Websock', function () {
describe('rQpeek8', function () {
it('should peek at the next byte without poping it off the queue', function () {
const bef_len = sock.rQlen;
const befLen = sock.rQlen;
const peek = sock.rQpeek8();
expect(sock.rQpeek8()).to.equal(peek);
expect(sock.rQlen).to.equal(bef_len);
expect(sock.rQlen).to.equal(befLen);
});
});
describe('rQshift8()', function () {
it('should pop a single byte from the receive queue', function () {
const peek = sock.rQpeek8();
const bef_len = sock.rQlen;
const befLen = sock.rQlen;
expect(sock.rQshift8()).to.equal(peek);
expect(sock.rQlen).to.equal(bef_len - 1);
expect(sock.rQlen).to.equal(befLen - 1);
});
});
describe('rQshift16()', function () {
it('should pop two bytes from the receive queue and return a single number', function () {
const bef_len = sock.rQlen;
const befLen = sock.rQlen;
const expected = (RQ_TEMPLATE[0] << 8) + RQ_TEMPLATE[1];
expect(sock.rQshift16()).to.equal(expected);
expect(sock.rQlen).to.equal(bef_len - 2);
expect(sock.rQlen).to.equal(befLen - 2);
});
});
describe('rQshift32()', function () {
it('should pop four bytes from the receive queue and return a single number', function () {
const bef_len = sock.rQlen;
const befLen = sock.rQlen;
const expected = (RQ_TEMPLATE[0] << 24) +
(RQ_TEMPLATE[1] << 16) +
(RQ_TEMPLATE[2] << 8) +
RQ_TEMPLATE[3];
expect(sock.rQshift32()).to.equal(expected);
expect(sock.rQlen).to.equal(bef_len - 4);
expect(sock.rQlen).to.equal(befLen - 4);
});
});
describe('rQshiftStr', function () {
it('should shift the given number of bytes off of the receive queue and return a string', function () {
const bef_len = sock.rQlen;
const bef_rQi = sock.rQi;
const befLen = sock.rQlen;
const befRQi = sock.rQi;
const shifted = sock.rQshiftStr(3);
expect(shifted).to.be.a('string');
expect(shifted).to.equal(String.fromCharCode.apply(null, Array.prototype.slice.call(new Uint8Array(RQ_TEMPLATE.buffer, bef_rQi, 3))));
expect(sock.rQlen).to.equal(bef_len - 3);
expect(shifted).to.equal(String.fromCharCode.apply(null, Array.prototype.slice.call(new Uint8Array(RQ_TEMPLATE.buffer, befRQi, 3))));
expect(sock.rQlen).to.equal(befLen - 3);
});
it('should shift the entire rest of the queue off if no length is given', function () {
@@ -112,12 +112,12 @@ describe('Websock', function () {
describe('rQshiftBytes', function () {
it('should shift the given number of bytes of the receive queue and return an array', function () {
const bef_len = sock.rQlen;
const bef_rQi = sock.rQi;
const befLen = sock.rQlen;
const befRQi = sock.rQi;
const shifted = sock.rQshiftBytes(3);
expect(shifted).to.be.an.instanceof(Uint8Array);
expect(shifted).to.array.equal(new Uint8Array(RQ_TEMPLATE.buffer, bef_rQi, 3));
expect(sock.rQlen).to.equal(bef_len - 3);
expect(shifted).to.array.equal(new Uint8Array(RQ_TEMPLATE.buffer, befRQi, 3));
expect(sock.rQlen).to.equal(befLen - 3);
});
it('should shift the entire rest of the queue off if no length is given', function () {
@@ -132,9 +132,9 @@ describe('Websock', function () {
});
it('should not modify the receive queue', function () {
const bef_len = sock.rQlen;
const befLen = sock.rQlen;
sock.rQslice(0, 2);
expect(sock.rQlen).to.equal(bef_len);
expect(sock.rQlen).to.equal(befLen);
});
it('should return an array containing the given slice of the receive queue', function () {
@@ -198,7 +198,7 @@ describe('Websock', function () {
sock._websocket.readyState = WebSocket.OPEN;
sock._sQ = new Uint8Array([1, 2, 3]);
sock._sQlen = 3;
const encoded = sock._encode_message();
const encoded = sock._encodeMessage();
sock.flush();
expect(sock._websocket.send).to.have.been.calledOnce;
@@ -232,35 +232,29 @@ describe('Websock', function () {
});
});
describe('send_string', function () {
describe('sendString', function () {
beforeEach(function () {
sock.send = sinon.spy();
});
it('should call send after converting the string to an array', function () {
sock.send_string("\x01\x02\x03");
sock.sendString("\x01\x02\x03");
expect(sock.send).to.have.been.calledWith([1, 2, 3]);
});
});
});
describe('lifecycle methods', function () {
let old_WS;
let oldWS;
before(function () {
old_WS = WebSocket;
oldWS = WebSocket;
});
let sock;
beforeEach(function () {
sock = new Websock();
// eslint-disable-next-line no-global-assign
WebSocket = sinon.spy();
WebSocket.OPEN = old_WS.OPEN;
WebSocket.CONNECTING = old_WS.CONNECTING;
WebSocket.CLOSING = old_WS.CLOSING;
WebSocket.CLOSED = old_WS.CLOSED;
WebSocket.prototype.binaryType = 'arraybuffer';
WebSocket = sinon.spy(FakeWebSocket);
});
describe('opening', function () {
@@ -276,9 +270,17 @@ describe('Websock', function () {
// it('should initialize the event handlers')?
});
describe('attaching', function () {
it('should attach to an existing websocket', function () {
let ws = new FakeWebSocket('ws://localhost:8675');
sock.attach(ws);
expect(WebSocket).to.not.have.been.called;
});
});
describe('closing', function () {
beforeEach(function () {
sock.open('ws://');
sock.open('ws://localhost');
sock._websocket.close = sinon.spy();
});
@@ -306,30 +308,30 @@ describe('Websock', function () {
expect(sock._websocket.close).not.to.have.been.called;
});
it('should reset onmessage to not call _recv_message', function () {
sinon.spy(sock, '_recv_message');
it('should reset onmessage to not call _recvMessage', function () {
sinon.spy(sock, '_recvMessage');
sock.close();
sock._websocket.onmessage(null);
try {
expect(sock._recv_message).not.to.have.been.called;
expect(sock._recvMessage).not.to.have.been.called;
} finally {
sock._recv_message.restore();
sock._recvMessage.restore();
}
});
});
describe('event handlers', function () {
beforeEach(function () {
sock._recv_message = sinon.spy();
sock._recvMessage = sinon.spy();
sock.on('open', sinon.spy());
sock.on('close', sinon.spy());
sock.on('error', sinon.spy());
sock.open('ws://');
sock.open('ws://localhost');
});
it('should call _recv_message on a message', function () {
it('should call _recvMessage on a message', function () {
sock._websocket.onmessage(null);
expect(sock._recv_message).to.have.been.calledOnce;
expect(sock._recvMessage).to.have.been.calledOnce;
});
it('should call the open event handler on opening', function () {
@@ -348,9 +350,96 @@ describe('Websock', function () {
});
});
describe('ready state', function () {
it('should be "unused" after construction', function () {
let sock = new Websock();
expect(sock.readyState).to.equal('unused');
});
it('should be "connecting" if WebSocket is connecting', function () {
let sock = new Websock();
let ws = new FakeWebSocket();
ws.readyState = WebSocket.CONNECTING;
sock.attach(ws);
expect(sock.readyState).to.equal('connecting');
});
it('should be "open" if WebSocket is open', function () {
let sock = new Websock();
let ws = new FakeWebSocket();
ws.readyState = WebSocket.OPEN;
sock.attach(ws);
expect(sock.readyState).to.equal('open');
});
it('should be "closing" if WebSocket is closing', function () {
let sock = new Websock();
let ws = new FakeWebSocket();
ws.readyState = WebSocket.CLOSING;
sock.attach(ws);
expect(sock.readyState).to.equal('closing');
});
it('should be "closed" if WebSocket is closed', function () {
let sock = new Websock();
let ws = new FakeWebSocket();
ws.readyState = WebSocket.CLOSED;
sock.attach(ws);
expect(sock.readyState).to.equal('closed');
});
it('should be "unknown" if WebSocket state is unknown', function () {
let sock = new Websock();
let ws = new FakeWebSocket();
ws.readyState = 666;
sock.attach(ws);
expect(sock.readyState).to.equal('unknown');
});
it('should be "connecting" if RTCDataChannel is connecting', function () {
let sock = new Websock();
let ws = new FakeWebSocket();
ws.readyState = 'connecting';
sock.attach(ws);
expect(sock.readyState).to.equal('connecting');
});
it('should be "open" if RTCDataChannel is open', function () {
let sock = new Websock();
let ws = new FakeWebSocket();
ws.readyState = 'open';
sock.attach(ws);
expect(sock.readyState).to.equal('open');
});
it('should be "closing" if RTCDataChannel is closing', function () {
let sock = new Websock();
let ws = new FakeWebSocket();
ws.readyState = 'closing';
sock.attach(ws);
expect(sock.readyState).to.equal('closing');
});
it('should be "closed" if RTCDataChannel is closed', function () {
let sock = new Websock();
let ws = new FakeWebSocket();
ws.readyState = 'closed';
sock.attach(ws);
expect(sock.readyState).to.equal('closed');
});
it('should be "unknown" if RTCDataChannel state is unknown', function () {
let sock = new Websock();
let ws = new FakeWebSocket();
ws.readyState = 'foobar';
sock.attach(ws);
expect(sock.readyState).to.equal('unknown');
});
});
after(function () {
// eslint-disable-next-line no-global-assign
WebSocket = old_WS;
WebSocket = oldWS;
});
});
@@ -358,13 +447,13 @@ describe('Websock', function () {
let sock;
beforeEach(function () {
sock = new Websock();
sock._allocate_buffers();
sock._allocateBuffers();
});
it('should support adding binary Uint8Array data to the receive queue', function () {
const msg = { data: new Uint8Array([1, 2, 3]) };
sock._mode = 'binary';
sock._recv_message(msg);
sock._recvMessage(msg);
expect(sock.rQshiftStr(3)).to.equal('\x01\x02\x03');
});
@@ -372,7 +461,7 @@ describe('Websock', function () {
sock._eventHandlers.message = sinon.spy();
const msg = { data: new Uint8Array([1, 2, 3]).buffer };
sock._mode = 'binary';
sock._recv_message(msg);
sock._recvMessage(msg);
expect(sock._eventHandlers.message).to.have.been.calledOnce;
});
@@ -380,37 +469,59 @@ describe('Websock', function () {
sock._eventHandlers.message = sinon.spy();
const msg = { data: new Uint8Array([]).buffer };
sock._mode = 'binary';
sock._recv_message(msg);
sock._recvMessage(msg);
expect(sock._eventHandlers.message).not.to.have.been.called;
});
it('should compact the receive queue', function () {
// NB(sross): while this is an internal implementation detail, it's important to
// test, otherwise the receive queue could become very large very quickly
it('should compact the receive queue when a message handler empties it', function () {
sock._eventHandlers.message = () => { sock.rQi = sock._rQlen; };
sock._rQ = new Uint8Array([0, 1, 2, 3, 4, 5, 0, 0, 0, 0]);
sock._rQlen = 6;
sock.rQi = 6;
sock._rQmax = 3;
const msg = { data: new Uint8Array([1, 2, 3]).buffer };
sock._mode = 'binary';
sock._recv_message(msg);
expect(sock._rQlen).to.equal(3);
sock._recvMessage(msg);
expect(sock._rQlen).to.equal(0);
expect(sock.rQi).to.equal(0);
});
it('should automatically resize the receive queue if the incoming message is too large', function () {
it('should compact the receive queue when we reach the end of the buffer', function () {
sock._rQ = new Uint8Array(20);
sock._rQbufferSize = 20;
sock._rQlen = 20;
sock.rQi = 10;
const msg = { data: new Uint8Array([1, 2]).buffer };
sock._mode = 'binary';
sock._recvMessage(msg);
expect(sock._rQlen).to.equal(12);
expect(sock.rQi).to.equal(0);
});
it('should automatically resize the receive queue if the incoming message is larger than the buffer', function () {
sock._rQ = new Uint8Array(20);
sock._rQlen = 0;
sock.rQi = 0;
sock._rQbufferSize = 20;
sock._rQmax = 2;
const msg = { data: new Uint8Array(30).buffer };
sock._mode = 'binary';
sock._recv_message(msg);
sock._recvMessage(msg);
expect(sock._rQlen).to.equal(30);
expect(sock.rQi).to.equal(0);
expect(sock._rQ.length).to.equal(240); // keep the invariant that rQbufferSize / 8 >= rQlen
});
it('should automatically resize the receive queue if the incoming message is larger than 1/8th of the buffer and we reach the end of the buffer', function () {
sock._rQ = new Uint8Array(20);
sock._rQlen = 16;
sock.rQi = 16;
sock._rQbufferSize = 20;
const msg = { data: new Uint8Array(6).buffer };
sock._mode = 'binary';
sock._recvMessage(msg);
expect(sock._rQlen).to.equal(6);
expect(sock.rQi).to.equal(0);
expect(sock._rQ.length).to.equal(48);
});
});
describe('Data encoding', function () {
@@ -428,13 +539,13 @@ describe('Websock', function () {
it('should only send the send queue up to the send queue length', function () {
sock._sQ = new Uint8Array([1, 2, 3, 4, 5]);
sock._sQlen = 3;
const res = sock._encode_message();
const res = sock._encodeMessage();
expect(res).to.array.equal(new Uint8Array([1, 2, 3]));
});
it('should properly pass the encoded data off to the actual WebSocket', function () {
sock.send([1, 2, 3]);
expect(sock._websocket._get_sent_data()).to.array.equal(new Uint8Array([1, 2, 3]));
expect(sock._websocket._getSentData()).to.array.equal(new Uint8Array([1, 2, 3]));
});
});
});
+45 -6
View File
@@ -7,6 +7,48 @@ import * as WebUtil from '../app/webutil.js';
describe('WebUtil', function () {
"use strict";
describe('config variables', function () {
it('should parse query string variables', function () {
// history.pushState() will not cause the browser to attempt loading
// the URL, this is exactly what we want here for the tests.
history.pushState({}, '', "test?myvar=myval");
expect(WebUtil.getConfigVar("myvar")).to.be.equal("myval");
});
it('should return default value when no query match', function () {
history.pushState({}, '', "test?myvar=myval");
expect(WebUtil.getConfigVar("other", "def")).to.be.equal("def");
});
it('should handle no query match and no default value', function () {
history.pushState({}, '', "test?myvar=myval");
expect(WebUtil.getConfigVar("other")).to.be.equal(null);
});
it('should parse fragment variables', function () {
history.pushState({}, '', "test#myvar=myval");
expect(WebUtil.getConfigVar("myvar")).to.be.equal("myval");
});
it('should return default value when no fragment match', function () {
history.pushState({}, '', "test#myvar=myval");
expect(WebUtil.getConfigVar("other", "def")).to.be.equal("def");
});
it('should handle no fragment match and no default value', function () {
history.pushState({}, '', "test#myvar=myval");
expect(WebUtil.getConfigVar("other")).to.be.equal(null);
});
it('should handle both query and fragment', function () {
history.pushState({}, '', "test?myquery=1#myhash=2");
expect(WebUtil.getConfigVar("myquery")).to.be.equal("1");
expect(WebUtil.getConfigVar("myhash")).to.be.equal("2");
});
it('should prioritize fragment if both provide same var', function () {
history.pushState({}, '', "test?myvar=1#myvar=2");
expect(WebUtil.getConfigVar("myvar")).to.be.equal("2");
});
});
describe('cookies', function () {
// TODO
});
describe('settings', function () {
describe('localStorage', function () {
@@ -22,11 +64,6 @@ describe('WebUtil', function () {
let origLocalStorage;
beforeEach(function () {
origLocalStorage = Object.getOwnPropertyDescriptor(window, "localStorage");
if (origLocalStorage === undefined) {
// Object.getOwnPropertyDescriptor() doesn't work
// properly in any version of IE
this.skip();
}
Object.defineProperty(window, "localStorage", {value: {}});
if (window.localStorage.setItem !== undefined) {
@@ -42,7 +79,9 @@ describe('WebUtil', function () {
return WebUtil.initSettings();
});
afterEach(function () {
Object.defineProperty(window, "localStorage", origLocalStorage);
if (origLocalStorage !== undefined) {
Object.defineProperty(window, "localStorage", origLocalStorage);
}
});
describe('writeSetting', function () {
-17
View File
@@ -2,21 +2,6 @@
<html lang="en">
<head>
<title>VNC Playback</title>
<!-- promise polyfills promises for IE11 -->
<script src="../vendor/promise.js"></script>
<!-- ES2015/ES6 modules polyfill -->
<script type="module">
window._noVNC_has_module_support = true;
</script>
<script>
window.addEventListener("load", function() {
if (window._noVNC_has_module_support) return;
var loader = document.createElement("script");
loader.src = "../vendor/browser-es-module-loader/dist/browser-es-module-loader.js";
document.head.appendChild(loader);
});
</script>
<!-- actual script modules -->
<script type="module" src="./playback-ui.js"></script>
</head>
<body>
@@ -37,7 +22,5 @@
<div id="VNC_screen">
<div id="VNC_status">Loading</div>
</div>
<script type="module" src="./playback-ui.js"></script>
</body>
</html>
+1 -1
View File
@@ -1,6 +1,6 @@
## WebSockets Proxy/Bridge
Websockify has been forked out into its own project. `launch.sh` wil
Websockify has been forked out into its own project. `novnc_proxy` will
automatically download it here if it is not already present and not
installed as system-wide.
+4 -4
View File
@@ -9,14 +9,14 @@
const fs = require('fs');
let show_help = process.argv.length === 2;
let showHelp = process.argv.length === 2;
let filename;
for (let i = 2; i < process.argv.length; ++i) {
switch (process.argv[i]) {
case "--help":
case "-h":
show_help = true;
showHelp = true;
break;
case "--file":
case "-f":
@@ -26,11 +26,11 @@ for (let i = 2; i < process.argv.length; ++i) {
}
if (!filename) {
show_help = true;
showHelp = true;
console.log("Error: No filename specified\n");
}
if (show_help) {
if (showHelp) {
console.log("Parses a *nix keysymdef.h to generate Unicode code point mappings");
console.log("Usage: node parse.js [options] filename:");
console.log(" -h [ --help ] Produce this help message");
-40
View File
@@ -1,40 +0,0 @@
#!/usr/bin/env python
#
# Convert image to Javascript compatible base64 Data URI
# Copyright (C) 2018 The noVNC Authors
# Licensed under MPL 2.0 (see docs/LICENSE.MPL-2.0)
#
import sys, base64
try:
from PIL import Image
except:
print "python PIL module required (python-imaging package)"
sys.exit(1)
if len(sys.argv) < 3:
print "Usage: %s IMAGE JS_VARIABLE" % sys.argv[0]
sys.exit(1)
fname = sys.argv[1]
var = sys.argv[2]
ext = fname.lower().split('.')[-1]
if ext == "png": mime = "image/png"
elif ext in ["jpg", "jpeg"]: mime = "image/jpeg"
elif ext == "gif": mime = "image/gif"
else:
print "Only PNG, JPEG and GIF images are supported"
sys.exit(1)
uri = "data:%s;base64," % mime
im = Image.open(fname)
w, h = im.size
raw = open(fname).read()
print '%s = {"width": %s, "height": %s, "data": "%s%s"};' % (
var, w, h, uri, base64.b64encode(raw))

Some files were not shown because too many files have changed in this diff Show More