2020-11-06 17:59:01 +01:00
|
|
|
<template>
|
2022-07-28 12:20:42 +02:00
|
|
|
<div class="neko-overlay-wrap">
|
2022-04-28 22:48:04 +02:00
|
|
|
<canvas ref="overlay" class="neko-overlay" tabindex="0" />
|
|
|
|
<textarea
|
|
|
|
ref="textarea"
|
|
|
|
class="neko-overlay"
|
|
|
|
:style="{ cursor }"
|
2022-09-27 21:38:43 +02:00
|
|
|
v-model="textInput"
|
2022-07-23 14:06:47 +02:00
|
|
|
@click.stop.prevent="wsControl.emit('overlay.click', $event)"
|
|
|
|
@contextmenu.stop.prevent="wsControl.emit('overlay.contextmenu', $event)"
|
2022-04-28 22:48:04 +02:00
|
|
|
@wheel.stop.prevent="onWheel"
|
|
|
|
@mousemove.stop.prevent="onMouseMove"
|
|
|
|
@mousedown.stop.prevent="onMouseDown"
|
|
|
|
@mouseenter.stop.prevent="onMouseEnter"
|
|
|
|
@mouseleave.stop.prevent="onMouseLeave"
|
|
|
|
@dragenter.stop.prevent="onDragEnter"
|
|
|
|
@dragleave.stop.prevent="onDragLeave"
|
|
|
|
@dragover.stop.prevent="onDragOver"
|
|
|
|
@drop.stop.prevent="onDrop"
|
|
|
|
/>
|
|
|
|
</div>
|
2020-11-06 17:59:01 +01:00
|
|
|
</template>
|
|
|
|
|
2022-07-28 12:20:42 +02:00
|
|
|
<style lang="scss">
|
|
|
|
/* hide elements around textarea if added by browsers extensions */
|
|
|
|
.neko-overlay-wrap *:not(.neko-overlay) {
|
|
|
|
display: none;
|
|
|
|
}
|
|
|
|
|
2021-02-10 20:57:13 +01:00
|
|
|
.neko-overlay {
|
2020-11-06 17:59:01 +01:00
|
|
|
position: absolute;
|
|
|
|
top: 0;
|
|
|
|
bottom: 0;
|
|
|
|
width: 100%;
|
|
|
|
height: 100%;
|
2022-04-29 00:00:41 +02:00
|
|
|
font-size: 1px; /* chrome would not paste text if 0px */
|
2022-07-18 22:59:21 +02:00
|
|
|
resize: none; /* hide textarea resize corner */
|
2020-11-06 20:48:19 +01:00
|
|
|
outline: 0;
|
2022-04-28 22:48:04 +02:00
|
|
|
border: 0;
|
|
|
|
color: transparent;
|
|
|
|
background: transparent;
|
2020-11-06 17:59:01 +01:00
|
|
|
}
|
|
|
|
</style>
|
|
|
|
|
|
|
|
<script lang="ts">
|
2020-12-02 10:45:23 +01:00
|
|
|
import { Vue, Component, Ref, Prop, Watch } from 'vue-property-decorator'
|
2020-11-06 20:48:19 +01:00
|
|
|
|
2022-10-04 20:28:07 +02:00
|
|
|
import { KeyboardInterface, NewKeyboard } from './utils/keyboard'
|
2022-04-28 22:48:04 +02:00
|
|
|
import { KeyTable, keySymsRemap } from './utils/keyboard-remapping'
|
2021-01-09 15:31:01 +01:00
|
|
|
import { getFilesFromDataTansfer } from './utils/file-upload'
|
2022-07-21 19:37:34 +02:00
|
|
|
import { NekoControl } from './internal/control'
|
2020-11-28 21:47:16 +01:00
|
|
|
import { NekoWebRTC } from './internal/webrtc'
|
2021-11-15 19:10:12 +01:00
|
|
|
import { Session, Scroll } from './types/state'
|
2021-10-18 11:14:05 +00:00
|
|
|
import { CursorPosition, CursorImage } from './types/webrtc'
|
2021-11-01 21:15:57 +01:00
|
|
|
import { CursorDrawFunction, Dimension, KeyboardModifiers } from './types/cursors'
|
2021-02-10 20:49:02 +01:00
|
|
|
|
2021-05-04 22:33:35 +00:00
|
|
|
const WHEEL_STEP = 53 // Delta threshold for a mouse wheel step
|
2021-05-04 21:48:57 +00:00
|
|
|
const WHEEL_LINE_HEIGHT = 19
|
2021-10-26 20:21:28 +02:00
|
|
|
|
2021-10-18 11:14:05 +00:00
|
|
|
const CANVAS_SCALE = 2
|
2021-05-04 21:48:57 +00:00
|
|
|
|
2021-10-26 20:21:28 +02:00
|
|
|
const INACTIVE_CURSOR_INTERVAL = 250 // ms
|
|
|
|
|
2020-11-06 17:59:01 +01:00
|
|
|
@Component({
|
|
|
|
name: 'neko-overlay',
|
|
|
|
})
|
|
|
|
export default class extends Vue {
|
2021-02-11 21:17:47 +01:00
|
|
|
@Ref('overlay') readonly _overlay!: HTMLCanvasElement
|
2022-04-28 22:48:04 +02:00
|
|
|
@Ref('textarea') readonly _textarea!: HTMLTextAreaElement
|
2021-10-18 11:14:05 +00:00
|
|
|
private _ctx!: CanvasRenderingContext2D
|
2020-11-06 17:59:01 +01:00
|
|
|
|
2022-10-04 20:28:07 +02:00
|
|
|
private keyboard!: KeyboardInterface
|
2022-09-27 21:38:43 +02:00
|
|
|
private textInput = ''
|
2022-10-04 20:28:07 +02:00
|
|
|
|
2020-11-06 17:59:01 +01:00
|
|
|
private focused = false
|
|
|
|
|
2022-07-21 19:37:34 +02:00
|
|
|
@Prop()
|
2022-07-23 14:06:47 +02:00
|
|
|
private readonly wsControl!: NekoControl
|
2022-07-21 19:37:34 +02:00
|
|
|
|
2021-11-15 19:10:12 +01:00
|
|
|
@Prop()
|
|
|
|
private readonly sessions!: Record<string, Session>
|
|
|
|
|
|
|
|
@Prop()
|
|
|
|
private readonly hostId!: string
|
|
|
|
|
2020-11-06 17:59:01 +01:00
|
|
|
@Prop()
|
|
|
|
private readonly webrtc!: NekoWebRTC
|
|
|
|
|
|
|
|
@Prop()
|
2021-02-13 17:29:35 +01:00
|
|
|
private readonly scroll!: Scroll
|
2020-11-06 17:59:01 +01:00
|
|
|
|
|
|
|
@Prop()
|
2021-10-18 11:14:05 +00:00
|
|
|
private readonly screenSize!: Dimension
|
2020-11-06 17:59:01 +01:00
|
|
|
|
|
|
|
@Prop()
|
2021-10-18 11:14:05 +00:00
|
|
|
private readonly canvasSize!: Dimension
|
2020-11-06 17:59:01 +01:00
|
|
|
|
2021-10-18 11:14:05 +00:00
|
|
|
@Prop()
|
|
|
|
private readonly cursorDraw!: CursorDrawFunction | null
|
|
|
|
|
2020-11-06 17:59:01 +01:00
|
|
|
@Prop()
|
|
|
|
private readonly isControling!: boolean
|
|
|
|
|
2020-12-02 10:45:23 +01:00
|
|
|
@Prop()
|
|
|
|
private readonly implicitControl!: boolean
|
|
|
|
|
2021-10-26 20:21:28 +02:00
|
|
|
@Prop()
|
|
|
|
private readonly inactiveCursors!: boolean
|
|
|
|
|
2021-01-09 23:28:56 +01:00
|
|
|
get cursor(): string {
|
2021-10-18 11:14:05 +00:00
|
|
|
if (!this.isControling || !this.cursorImage) {
|
2022-05-29 20:22:28 +02:00
|
|
|
return 'default'
|
2021-02-10 20:49:02 +01:00
|
|
|
}
|
2021-01-09 23:28:56 +01:00
|
|
|
|
2021-02-13 17:29:35 +01:00
|
|
|
const { uri, x, y } = this.cursorImage
|
2022-05-29 20:22:28 +02:00
|
|
|
return 'url(' + uri + ') ' + x + ' ' + y + ', default'
|
2021-01-09 23:28:56 +01:00
|
|
|
}
|
|
|
|
|
2020-11-06 17:59:01 +01:00
|
|
|
mounted() {
|
2022-09-15 21:58:51 +02:00
|
|
|
// register mouseup globally as user can release mouse button outside of overlay
|
|
|
|
window.addEventListener('mouseup', this.onMouseUp, true)
|
|
|
|
|
2021-10-18 11:14:05 +00:00
|
|
|
// get canvas overlay context
|
|
|
|
const ctx = this._overlay.getContext('2d')
|
|
|
|
if (ctx != null) {
|
|
|
|
this._ctx = ctx
|
|
|
|
}
|
2021-02-11 21:17:47 +01:00
|
|
|
|
|
|
|
// synchronize intrinsic with extrinsic dimensions
|
|
|
|
const { width, height } = this._overlay.getBoundingClientRect()
|
2022-08-25 00:14:47 +02:00
|
|
|
this.canvasResize({ width, height })
|
2021-02-11 21:17:47 +01:00
|
|
|
|
2022-05-01 21:51:10 +02:00
|
|
|
let ctrlKey = 0
|
|
|
|
let noKeyUp = {} as Record<number, boolean>
|
|
|
|
|
2022-10-04 20:28:07 +02:00
|
|
|
// Initialize Keyboard
|
|
|
|
this.keyboard = NewKeyboard()
|
2020-11-06 17:59:01 +01:00
|
|
|
this.keyboard.onkeydown = (key: number) => {
|
2022-05-03 21:56:37 +02:00
|
|
|
key = keySymsRemap(key)
|
|
|
|
|
2022-05-03 21:46:16 +02:00
|
|
|
if (!this.isControling) {
|
2022-05-01 21:51:10 +02:00
|
|
|
noKeyUp[key] = true
|
2020-11-06 17:59:01 +01:00
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
2022-04-28 22:48:04 +02:00
|
|
|
// ctrl+v is aborted
|
2022-05-01 21:51:10 +02:00
|
|
|
if (ctrlKey != 0 && key == KeyTable.XK_v) {
|
|
|
|
this.keyboard.release(ctrlKey)
|
|
|
|
noKeyUp[key] = true
|
2022-04-28 22:48:04 +02:00
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
// save information if it is ctrl key event
|
|
|
|
const isCtrlKey = key == KeyTable.XK_Control_L || key == KeyTable.XK_Control_R
|
2022-05-01 21:51:10 +02:00
|
|
|
if (isCtrlKey) ctrlKey = key
|
2022-04-28 22:48:04 +02:00
|
|
|
|
2022-07-23 14:05:29 +02:00
|
|
|
if (this.webrtc.connected) {
|
|
|
|
this.webrtc.send('keydown', { key })
|
|
|
|
} else {
|
2022-07-23 14:06:47 +02:00
|
|
|
this.wsControl.keyDown(key)
|
2022-07-23 14:05:29 +02:00
|
|
|
}
|
|
|
|
|
2022-04-28 22:48:04 +02:00
|
|
|
return isCtrlKey
|
2020-11-06 17:59:01 +01:00
|
|
|
}
|
|
|
|
this.keyboard.onkeyup = (key: number) => {
|
2022-05-03 21:56:37 +02:00
|
|
|
key = keySymsRemap(key)
|
|
|
|
|
2022-05-01 21:51:10 +02:00
|
|
|
if (key in noKeyUp) {
|
|
|
|
delete noKeyUp[key]
|
2020-11-06 17:59:01 +01:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-04-28 22:48:04 +02:00
|
|
|
const isCtrlKey = key == KeyTable.XK_Control_L || key == KeyTable.XK_Control_R
|
2022-05-01 21:51:10 +02:00
|
|
|
if (isCtrlKey) ctrlKey = 0
|
2022-04-28 22:48:04 +02:00
|
|
|
|
2022-07-23 14:05:29 +02:00
|
|
|
if (this.webrtc.connected) {
|
|
|
|
this.webrtc.send('keyup', { key })
|
|
|
|
} else {
|
2022-07-23 14:06:47 +02:00
|
|
|
this.wsControl.keyUp(key)
|
2022-07-23 14:05:29 +02:00
|
|
|
}
|
2020-11-06 17:59:01 +01:00
|
|
|
}
|
2022-04-28 22:48:04 +02:00
|
|
|
this.keyboard.listenTo(this._textarea)
|
2021-02-13 17:29:35 +01:00
|
|
|
|
|
|
|
this.webrtc.addListener('cursor-position', this.onCursorPosition)
|
|
|
|
this.webrtc.addListener('cursor-image', this.onCursorImage)
|
2021-02-18 20:53:08 +01:00
|
|
|
this.webrtc.addListener('disconnected', this.canvasClear)
|
2021-10-18 11:14:05 +00:00
|
|
|
this.cursorElement.onload = this.canvasRequestRedraw
|
2020-11-06 17:59:01 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
beforeDestroy() {
|
2022-09-15 21:58:51 +02:00
|
|
|
window.removeEventListener('mouseup', this.onMouseUp, true)
|
|
|
|
|
2022-10-04 20:28:07 +02:00
|
|
|
if (this.keyboard) {
|
|
|
|
this.keyboard.removeListener()
|
|
|
|
}
|
2021-02-13 17:29:35 +01:00
|
|
|
|
|
|
|
this.webrtc.removeListener('cursor-position', this.onCursorPosition)
|
|
|
|
this.webrtc.removeListener('cursor-image', this.onCursorImage)
|
2021-02-18 20:53:08 +01:00
|
|
|
this.webrtc.removeListener('disconnected', this.canvasClear)
|
2021-02-17 21:54:15 +01:00
|
|
|
this.cursorElement.onload = null
|
2021-10-26 20:21:28 +02:00
|
|
|
|
|
|
|
// stop inactive cursor interval if exists
|
|
|
|
if (this.inactiveCursorInterval !== null) {
|
|
|
|
window.clearInterval(this.inactiveCursorInterval)
|
|
|
|
this.inactiveCursorInterval = null
|
|
|
|
}
|
2020-11-06 17:59:01 +01:00
|
|
|
}
|
|
|
|
|
2021-01-07 20:21:40 +01:00
|
|
|
getMousePos(clientX: number, clientY: number) {
|
2020-11-06 17:59:01 +01:00
|
|
|
const rect = this._overlay.getBoundingClientRect()
|
|
|
|
|
2021-01-07 20:21:40 +01:00
|
|
|
return {
|
2021-02-12 17:11:28 +01:00
|
|
|
x: Math.round((this.screenSize.width / rect.width) * (clientX - rect.left)),
|
|
|
|
y: Math.round((this.screenSize.height / rect.height) * (clientY - rect.top)),
|
2021-01-07 20:21:40 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-10-26 20:21:28 +02:00
|
|
|
sendMousePos(e: MouseEvent) {
|
2021-02-11 19:03:31 +01:00
|
|
|
const pos = this.getMousePos(e.clientX, e.clientY)
|
|
|
|
this.webrtc.send('mousemove', pos)
|
2021-02-13 17:29:35 +01:00
|
|
|
Vue.set(this, 'cursorPosition', pos)
|
2020-11-06 17:59:01 +01:00
|
|
|
}
|
|
|
|
|
2021-05-04 21:48:57 +00:00
|
|
|
private wheelX = 0
|
|
|
|
private wheelY = 0
|
2021-05-04 22:33:35 +00:00
|
|
|
private wheelDate = Date.now()
|
2021-05-04 21:48:57 +00:00
|
|
|
|
|
|
|
// negative sensitivity can be acheived using increased step value
|
|
|
|
get wheelStep() {
|
2021-05-04 22:33:35 +00:00
|
|
|
let x = WHEEL_STEP
|
2021-05-04 21:48:57 +00:00
|
|
|
|
|
|
|
if (this.scroll.sensitivity < 0) {
|
|
|
|
x *= Math.abs(this.scroll.sensitivity) + 1
|
2020-11-06 17:59:01 +01:00
|
|
|
}
|
|
|
|
|
2021-05-04 21:48:57 +00:00
|
|
|
return x
|
|
|
|
}
|
|
|
|
|
|
|
|
// sensitivity can only be positive
|
|
|
|
get wheelSensitivity() {
|
|
|
|
let x = 1
|
|
|
|
|
|
|
|
if (this.scroll.sensitivity > 0) {
|
|
|
|
x = Math.abs(this.scroll.sensitivity) + 1
|
|
|
|
}
|
2020-11-06 17:59:01 +01:00
|
|
|
|
2021-02-13 17:29:35 +01:00
|
|
|
if (this.scroll.inverse) {
|
2020-11-06 17:59:01 +01:00
|
|
|
x *= -1
|
|
|
|
}
|
|
|
|
|
2021-05-04 21:48:57 +00:00
|
|
|
return x
|
|
|
|
}
|
|
|
|
|
2022-09-27 21:38:43 +02:00
|
|
|
// use v-model instead of @input because v-model
|
|
|
|
// doesn't get updated during IME composition
|
|
|
|
@Watch('textInput')
|
|
|
|
onTextInputChange() {
|
|
|
|
if (this.textInput == '') return
|
|
|
|
this.wsControl.paste(this.textInput)
|
|
|
|
this.textInput = ''
|
2022-04-29 00:00:41 +02:00
|
|
|
}
|
2022-04-28 22:48:04 +02:00
|
|
|
|
2021-05-04 21:48:57 +00:00
|
|
|
onWheel(e: WheelEvent) {
|
|
|
|
if (!this.isControling) {
|
|
|
|
return
|
|
|
|
}
|
2020-11-06 17:59:01 +01:00
|
|
|
|
2021-05-04 22:33:35 +00:00
|
|
|
const now = Date.now()
|
|
|
|
const firstScroll = now - this.wheelDate > 250
|
|
|
|
|
|
|
|
if (firstScroll) {
|
|
|
|
this.wheelX = 0
|
|
|
|
this.wheelY = 0
|
|
|
|
this.wheelDate = now
|
|
|
|
}
|
|
|
|
|
2021-05-04 21:48:57 +00:00
|
|
|
let dx = e.deltaX
|
|
|
|
let dy = e.deltaY
|
|
|
|
|
|
|
|
if (e.deltaMode !== 0) {
|
|
|
|
dx *= WHEEL_LINE_HEIGHT
|
|
|
|
dy *= WHEEL_LINE_HEIGHT
|
|
|
|
}
|
|
|
|
|
|
|
|
this.wheelX += dx
|
|
|
|
this.wheelY += dy
|
|
|
|
|
|
|
|
let x = 0
|
2021-05-04 22:33:35 +00:00
|
|
|
if (Math.abs(this.wheelX) >= this.wheelStep || firstScroll) {
|
2021-05-04 21:48:57 +00:00
|
|
|
if (this.wheelX < 0) {
|
|
|
|
x = this.wheelSensitivity * -1
|
|
|
|
} else if (this.wheelX > 0) {
|
|
|
|
x = this.wheelSensitivity
|
|
|
|
}
|
|
|
|
|
2021-05-04 22:33:35 +00:00
|
|
|
if (!firstScroll) {
|
|
|
|
this.wheelX = 0
|
|
|
|
}
|
2021-05-04 21:48:57 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
let y = 0
|
2021-05-04 22:33:35 +00:00
|
|
|
if (Math.abs(this.wheelY) >= this.wheelStep || firstScroll) {
|
2021-05-04 21:48:57 +00:00
|
|
|
if (this.wheelY < 0) {
|
|
|
|
y = this.wheelSensitivity * -1
|
|
|
|
} else if (this.wheelY > 0) {
|
|
|
|
y = this.wheelSensitivity
|
|
|
|
}
|
|
|
|
|
2021-05-04 22:33:35 +00:00
|
|
|
if (!firstScroll) {
|
|
|
|
this.wheelY = 0
|
|
|
|
}
|
2021-05-04 21:48:57 +00:00
|
|
|
}
|
|
|
|
|
2022-07-23 14:05:29 +02:00
|
|
|
// skip if not scrolled
|
|
|
|
if (x == 0 && y == 0) return
|
|
|
|
|
|
|
|
if (this.webrtc.connected) {
|
|
|
|
this.sendMousePos(e)
|
2021-05-04 21:48:57 +00:00
|
|
|
this.webrtc.send('wheel', { x, y })
|
2022-07-23 14:05:29 +02:00
|
|
|
} else {
|
2022-07-23 14:17:03 +02:00
|
|
|
this.wsControl.scroll({ x, y })
|
2021-05-04 21:48:57 +00:00
|
|
|
}
|
2020-11-06 17:59:01 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
onMouseMove(e: MouseEvent) {
|
2021-10-26 20:21:28 +02:00
|
|
|
if (this.isControling) {
|
|
|
|
this.sendMousePos(e)
|
2021-10-26 22:55:38 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if (this.inactiveCursors) {
|
2021-10-26 20:21:28 +02:00
|
|
|
this.saveInactiveMousePos(e)
|
|
|
|
}
|
2020-11-06 17:59:01 +01:00
|
|
|
}
|
|
|
|
|
2022-09-15 21:58:51 +02:00
|
|
|
isMouseDown = false
|
|
|
|
|
2020-11-06 17:59:01 +01:00
|
|
|
onMouseDown(e: MouseEvent) {
|
2022-09-15 21:58:51 +02:00
|
|
|
this.isMouseDown = true
|
|
|
|
|
2020-11-06 17:59:01 +01:00
|
|
|
if (!this.isControling) {
|
2021-02-10 20:49:02 +01:00
|
|
|
this.implicitControlRequest(e)
|
2020-11-06 17:59:01 +01:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-07-23 14:05:29 +02:00
|
|
|
const key = e.button + 1
|
|
|
|
if (this.webrtc.connected) {
|
|
|
|
this.sendMousePos(e)
|
|
|
|
this.webrtc.send('mousedown', { key })
|
|
|
|
} else {
|
2022-07-23 14:17:03 +02:00
|
|
|
const pos = this.getMousePos(e.clientX, e.clientY)
|
|
|
|
this.wsControl.buttonDown(key, pos)
|
2022-07-23 14:05:29 +02:00
|
|
|
}
|
2020-11-06 17:59:01 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
onMouseUp(e: MouseEvent) {
|
2022-09-15 21:58:51 +02:00
|
|
|
// only if we are the one who started the mouse down
|
|
|
|
if (!this.isMouseDown) return
|
|
|
|
this.isMouseDown = false
|
|
|
|
|
2020-11-06 17:59:01 +01:00
|
|
|
if (!this.isControling) {
|
2021-02-10 20:49:02 +01:00
|
|
|
this.implicitControlRequest(e)
|
2020-11-06 17:59:01 +01:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-07-23 14:05:29 +02:00
|
|
|
const key = e.button + 1
|
|
|
|
if (this.webrtc.connected) {
|
|
|
|
this.sendMousePos(e)
|
|
|
|
this.webrtc.send('mouseup', { key })
|
|
|
|
} else {
|
2022-07-23 14:17:03 +02:00
|
|
|
const pos = this.getMousePos(e.clientX, e.clientY)
|
|
|
|
this.wsControl.buttonUp(key, pos)
|
2022-07-23 14:05:29 +02:00
|
|
|
}
|
2020-11-06 17:59:01 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
onMouseEnter(e: MouseEvent) {
|
2022-04-28 22:48:04 +02:00
|
|
|
this._textarea.focus()
|
2020-11-06 17:59:01 +01:00
|
|
|
this.focused = true
|
|
|
|
|
2021-02-18 16:53:19 +01:00
|
|
|
if (this.isControling) {
|
2021-06-18 21:57:46 +02:00
|
|
|
this.updateKeyboardModifiers(e)
|
2020-11-06 17:59:01 +01:00
|
|
|
}
|
2020-12-02 10:45:23 +01:00
|
|
|
}
|
2020-11-06 17:59:01 +01:00
|
|
|
|
2020-12-02 10:45:23 +01:00
|
|
|
onMouseLeave(e: MouseEvent) {
|
|
|
|
if (this.isControling) {
|
2021-06-18 21:57:46 +02:00
|
|
|
// save current keyboard modifiers state
|
|
|
|
Vue.set(this, 'keyboardModifiers', {
|
2021-02-18 16:53:19 +01:00
|
|
|
capslock: e.getModifierState('CapsLock'),
|
|
|
|
numlock: e.getModifierState('NumLock'),
|
|
|
|
})
|
2020-12-02 10:45:23 +01:00
|
|
|
}
|
2021-04-12 13:32:52 +00:00
|
|
|
|
|
|
|
this.focused = false
|
2020-12-02 10:45:23 +01:00
|
|
|
}
|
|
|
|
|
2021-01-08 22:15:49 +01:00
|
|
|
onDragEnter(e: DragEvent) {
|
|
|
|
this.onMouseEnter(e as MouseEvent)
|
2021-01-07 20:21:40 +01:00
|
|
|
}
|
|
|
|
|
2021-01-08 22:15:49 +01:00
|
|
|
onDragLeave(e: DragEvent) {
|
|
|
|
this.onMouseLeave(e as MouseEvent)
|
|
|
|
}
|
|
|
|
|
|
|
|
onDragOver(e: DragEvent) {
|
|
|
|
this.onMouseMove(e as MouseEvent)
|
|
|
|
}
|
2021-01-07 20:21:40 +01:00
|
|
|
|
2021-01-09 15:31:01 +01:00
|
|
|
async onDrop(e: DragEvent) {
|
2021-01-08 22:15:49 +01:00
|
|
|
if (this.isControling || this.implicitControl) {
|
|
|
|
let dt = e.dataTransfer
|
|
|
|
if (!dt) return
|
2021-01-07 20:21:40 +01:00
|
|
|
|
2021-01-09 15:31:01 +01:00
|
|
|
const files = await getFilesFromDataTansfer(dt)
|
|
|
|
if (files.length === 0) return
|
2021-01-07 20:21:40 +01:00
|
|
|
|
2021-06-18 21:57:46 +02:00
|
|
|
this.$emit('uploadDrop', { ...this.getMousePos(e.clientX, e.clientY), files })
|
2021-01-08 22:15:49 +01:00
|
|
|
}
|
2021-01-07 20:21:40 +01:00
|
|
|
}
|
|
|
|
|
2021-10-26 20:21:28 +02:00
|
|
|
//
|
|
|
|
// inactive cursor position
|
|
|
|
//
|
|
|
|
|
|
|
|
private inactiveCursorInterval: number | null = null
|
|
|
|
private inactiveCursorPosition: CursorPosition | null = null
|
|
|
|
|
|
|
|
@Watch('focused')
|
|
|
|
@Watch('isControling')
|
|
|
|
@Watch('inactiveCursors')
|
|
|
|
restartInactiveCursorInterval() {
|
|
|
|
// clear interval if exists
|
|
|
|
if (this.inactiveCursorInterval !== null) {
|
|
|
|
window.clearInterval(this.inactiveCursorInterval)
|
|
|
|
this.inactiveCursorInterval = null
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this.inactiveCursors && this.focused && !this.isControling) {
|
|
|
|
this.inactiveCursorInterval = window.setInterval(this.sendInactiveMousePos.bind(this), INACTIVE_CURSOR_INTERVAL)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
saveInactiveMousePos(e: MouseEvent) {
|
|
|
|
const pos = this.getMousePos(e.clientX, e.clientY)
|
|
|
|
Vue.set(this, 'inactiveCursorPosition', pos)
|
|
|
|
}
|
|
|
|
|
|
|
|
sendInactiveMousePos() {
|
|
|
|
if (this.inactiveCursorPosition != null) {
|
|
|
|
this.webrtc.send('mousemove', this.inactiveCursorPosition)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-02-18 16:53:19 +01:00
|
|
|
//
|
|
|
|
// keyboard modifiers
|
|
|
|
//
|
|
|
|
|
2021-10-18 11:14:05 +00:00
|
|
|
private keyboardModifiers: KeyboardModifiers | null = null
|
2021-02-18 16:53:19 +01:00
|
|
|
|
2021-06-18 21:57:46 +02:00
|
|
|
updateKeyboardModifiers(e: MouseEvent) {
|
2021-02-18 16:53:19 +01:00
|
|
|
const capslock = e.getModifierState('CapsLock')
|
|
|
|
const numlock = e.getModifierState('NumLock')
|
|
|
|
|
|
|
|
if (
|
2021-06-18 21:57:46 +02:00
|
|
|
this.keyboardModifiers === null ||
|
|
|
|
this.keyboardModifiers.capslock !== capslock ||
|
|
|
|
this.keyboardModifiers.numlock !== numlock
|
2021-02-18 16:53:19 +01:00
|
|
|
) {
|
2021-06-18 21:57:46 +02:00
|
|
|
this.$emit('updateKeyboardModifiers', { capslock, numlock })
|
2021-02-18 16:53:19 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-02-13 17:29:35 +01:00
|
|
|
//
|
|
|
|
// canvas
|
|
|
|
//
|
|
|
|
|
2021-10-18 11:14:05 +00:00
|
|
|
private cursorImage: CursorImage | null = null
|
2021-02-13 17:29:35 +01:00
|
|
|
private cursorElement: HTMLImageElement = new Image()
|
2021-10-18 11:14:05 +00:00
|
|
|
private cursorPosition: CursorPosition | null = null
|
|
|
|
private canvasRequestedFrame = false
|
2021-02-18 21:09:08 +01:00
|
|
|
|
2021-02-12 17:11:28 +01:00
|
|
|
@Watch('canvasSize')
|
2021-10-18 11:14:05 +00:00
|
|
|
onCanvasSizeChange({ width, height }: Dimension) {
|
2022-08-25 00:14:47 +02:00
|
|
|
this.canvasResize({ width, height })
|
2021-10-18 11:14:05 +00:00
|
|
|
this.canvasRequestRedraw()
|
2021-02-12 17:11:28 +01:00
|
|
|
}
|
|
|
|
|
2021-10-18 11:14:05 +00:00
|
|
|
onCursorPosition(data: CursorPosition) {
|
2021-02-13 17:29:35 +01:00
|
|
|
if (!this.isControling) {
|
|
|
|
Vue.set(this, 'cursorPosition', data)
|
2021-10-18 11:14:05 +00:00
|
|
|
this.canvasRequestRedraw()
|
2021-02-13 17:29:35 +01:00
|
|
|
}
|
2021-02-11 21:17:47 +01:00
|
|
|
}
|
|
|
|
|
2021-10-18 11:14:05 +00:00
|
|
|
onCursorImage(data: CursorImage) {
|
2021-02-13 17:29:35 +01:00
|
|
|
Vue.set(this, 'cursorImage', data)
|
|
|
|
|
|
|
|
if (!this.isControling) {
|
|
|
|
this.cursorElement.src = data.uri
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-08-25 00:14:47 +02:00
|
|
|
canvasResize({ width, height }: Dimension) {
|
|
|
|
this._overlay.width = width * CANVAS_SCALE
|
|
|
|
this._overlay.height = height * CANVAS_SCALE
|
|
|
|
this._ctx.setTransform(CANVAS_SCALE, 0, 0, CANVAS_SCALE, 0, 0)
|
|
|
|
}
|
|
|
|
|
2021-11-15 19:10:12 +01:00
|
|
|
@Watch('hostId')
|
2021-10-18 11:14:05 +00:00
|
|
|
@Watch('cursorDraw')
|
|
|
|
canvasRequestRedraw() {
|
|
|
|
// skip rendering if there is already in progress
|
|
|
|
if (this.canvasRequestedFrame) return
|
|
|
|
|
|
|
|
// request animation frame from a browser
|
|
|
|
this.canvasRequestedFrame = true
|
|
|
|
window.requestAnimationFrame(() => {
|
|
|
|
if (this.isControling) {
|
|
|
|
this.canvasClear()
|
|
|
|
} else {
|
|
|
|
this.canvasRedraw()
|
|
|
|
}
|
|
|
|
|
|
|
|
this.canvasRequestedFrame = false
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2021-02-13 17:29:35 +01:00
|
|
|
canvasRedraw() {
|
2021-10-26 20:12:55 +02:00
|
|
|
if (!this.cursorPosition || !this.screenSize || !this.cursorImage) return
|
2021-02-11 21:17:47 +01:00
|
|
|
|
2021-02-17 20:39:12 +01:00
|
|
|
// clear drawings
|
2021-10-18 11:14:05 +00:00
|
|
|
this.canvasClear()
|
2021-02-17 20:39:12 +01:00
|
|
|
|
|
|
|
// ignore hidden cursor
|
|
|
|
if (this.cursorImage.width <= 1 && this.cursorImage.height <= 1) return
|
|
|
|
|
2021-10-18 11:14:05 +00:00
|
|
|
// get intrinsic dimensions
|
|
|
|
let { width, height } = this.canvasSize
|
2022-08-25 00:14:47 +02:00
|
|
|
|
|
|
|
// reset transformation, X and Y will be 0 again
|
2021-10-18 11:14:05 +00:00
|
|
|
this._ctx.setTransform(CANVAS_SCALE, 0, 0, CANVAS_SCALE, 0, 0)
|
|
|
|
|
|
|
|
// get cursor position
|
|
|
|
let x = Math.round((this.cursorPosition.x / this.screenSize.width) * width)
|
|
|
|
let y = Math.round((this.cursorPosition.y / this.screenSize.height) * height)
|
2021-02-17 20:39:12 +01:00
|
|
|
|
2021-10-18 11:14:05 +00:00
|
|
|
// use custom draw function, if available
|
|
|
|
if (this.cursorDraw) {
|
2021-11-15 19:10:12 +01:00
|
|
|
this.cursorDraw(this._ctx, x, y, this.cursorElement, this.cursorImage, this.hostId)
|
2021-10-18 11:14:05 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// draw cursor image
|
|
|
|
this._ctx.drawImage(
|
|
|
|
this.cursorElement,
|
|
|
|
x - this.cursorImage.x,
|
|
|
|
y - this.cursorImage.y,
|
|
|
|
this.cursorImage.width,
|
|
|
|
this.cursorImage.height,
|
|
|
|
)
|
|
|
|
|
|
|
|
// draw cursor tag
|
2021-11-15 19:10:12 +01:00
|
|
|
const cursorTag = this.sessions[this.hostId]?.profile.name || ''
|
|
|
|
if (cursorTag) {
|
2022-08-25 00:14:47 +02:00
|
|
|
const x = this.cursorImage.width
|
|
|
|
const y = this.cursorImage.height
|
2021-02-17 20:39:12 +01:00
|
|
|
|
|
|
|
this._ctx.font = '14px Arial, sans-serif'
|
|
|
|
this._ctx.textBaseline = 'top'
|
|
|
|
this._ctx.shadowColor = 'black'
|
|
|
|
this._ctx.shadowBlur = 2
|
|
|
|
this._ctx.lineWidth = 2
|
|
|
|
this._ctx.fillStyle = 'black'
|
2021-11-15 19:10:12 +01:00
|
|
|
this._ctx.strokeText(cursorTag, x, y)
|
2021-02-17 20:39:12 +01:00
|
|
|
this._ctx.shadowBlur = 0
|
|
|
|
this._ctx.fillStyle = 'white'
|
2021-11-15 19:10:12 +01:00
|
|
|
this._ctx.fillText(cursorTag, x, y)
|
2021-02-17 20:39:12 +01:00
|
|
|
}
|
2021-02-11 21:17:47 +01:00
|
|
|
}
|
|
|
|
|
2021-02-13 17:29:35 +01:00
|
|
|
canvasClear() {
|
2022-08-30 23:02:44 +02:00
|
|
|
// reset transformation, X and Y will be 0 again
|
|
|
|
this._ctx.setTransform(CANVAS_SCALE, 0, 0, CANVAS_SCALE, 0, 0)
|
|
|
|
|
2021-02-13 17:29:35 +01:00
|
|
|
const { width, height } = this._overlay
|
|
|
|
this._ctx.clearRect(0, 0, width, height)
|
|
|
|
}
|
|
|
|
|
|
|
|
//
|
|
|
|
// implicit hosting
|
|
|
|
//
|
|
|
|
|
2021-10-26 19:32:00 +02:00
|
|
|
private reqMouseDown: MouseEvent | null = null
|
|
|
|
private reqMouseUp: MouseEvent | null = null
|
2021-02-13 17:29:35 +01:00
|
|
|
|
2020-12-02 10:45:23 +01:00
|
|
|
@Watch('isControling')
|
|
|
|
onControlChange(isControling: boolean) {
|
2021-06-18 21:57:46 +02:00
|
|
|
Vue.set(this, 'keyboardModifiers', null)
|
2021-02-18 16:53:19 +01:00
|
|
|
|
2021-02-10 20:49:02 +01:00
|
|
|
if (isControling && this.reqMouseDown) {
|
2021-06-18 21:57:46 +02:00
|
|
|
this.updateKeyboardModifiers(this.reqMouseDown)
|
2022-07-23 01:43:03 +02:00
|
|
|
this.onMouseDown(this.reqMouseDown)
|
2021-02-10 20:49:02 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
if (isControling && this.reqMouseUp) {
|
2022-07-23 01:43:03 +02:00
|
|
|
this.onMouseUp(this.reqMouseUp)
|
2021-02-10 20:49:02 +01:00
|
|
|
}
|
|
|
|
|
2021-10-18 11:14:05 +00:00
|
|
|
this.canvasRequestRedraw()
|
2021-02-11 21:17:47 +01:00
|
|
|
|
2021-02-10 20:49:02 +01:00
|
|
|
this.reqMouseDown = null
|
|
|
|
this.reqMouseUp = null
|
2020-12-02 10:45:23 +01:00
|
|
|
}
|
|
|
|
|
2021-02-10 20:49:02 +01:00
|
|
|
implicitControlRequest(e: MouseEvent) {
|
2022-09-15 21:11:42 +02:00
|
|
|
if (this.implicitControl && e.type === 'mousedown') {
|
2021-02-10 20:49:02 +01:00
|
|
|
this.reqMouseDown = e
|
2022-09-15 21:11:42 +02:00
|
|
|
this.reqMouseUp = null
|
2022-07-23 14:06:47 +02:00
|
|
|
this.wsControl.request()
|
2020-12-02 10:45:23 +01:00
|
|
|
}
|
2021-02-10 20:49:02 +01:00
|
|
|
|
2022-09-15 21:11:42 +02:00
|
|
|
if (this.implicitControl && e.type === 'mouseup') {
|
2021-02-10 20:49:02 +01:00
|
|
|
this.reqMouseUp = e
|
|
|
|
}
|
2020-12-02 10:45:23 +01:00
|
|
|
}
|
|
|
|
|
2021-02-10 20:49:02 +01:00
|
|
|
// unused
|
2020-12-02 10:45:23 +01:00
|
|
|
implicitControlRelease() {
|
|
|
|
if (this.implicitControl) {
|
2022-07-23 14:06:47 +02:00
|
|
|
this.wsControl.release()
|
2020-12-02 10:45:23 +01:00
|
|
|
}
|
2020-11-06 17:59:01 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
</script>
|