KASM-5078 Multi monitor display ui

This commit is contained in:
Chris Hunt 2023-09-29 13:09:52 +00:00
parent aef462ce62
commit 1873ec0c91
4 changed files with 439 additions and 10 deletions

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="1em" viewBox="0 0 576 512"><path fill="#fff" d="M512 48H64c-8.8 0-16 7.2-16 16V256H528V64c0-8.8-7.2-16-16-16zm64 208v48 48c0 35.3-28.7 64-64 64H364.3l8 48H424c13.3 0 24 10.7 24 24s-10.7 24-24 24H352 224 152c-13.3 0-24-10.7-24-24s10.7-24 24-24h51.7l8-48H64c-35.3 0-64-28.7-64-64V304 256 64C0 28.7 28.7 0 64 0H512c35.3 0 64 28.7 64 64V256zM48 304v48c0 8.8 7.2 16 16 16H239.5c.3 0 .6 0 .8 0h95.2c.3 0 .6 0 .8 0H512c8.8 0 16-7.2 16-16V304H48zM252.3 464h71.3l-8-48H260.3l-8 48z"/></svg>

After

Width:  |  Height:  |  Size: 529 B

View File

@ -1191,3 +1191,122 @@ a:link {
a:visited {
color: white;
}
/* ----------------------------------------
* Multi Display Arrangement
* ----------------------------------------
*/
.flex {
display: flex;
}
.flex-col {
flex-direction: column;
}
.canvas {
background: rgb(225,225,226);
background: linear-gradient(0deg, rgba(225,225,226,1) 0%, rgba(237,237,238,1) 100%);
}
.row {
background: #e9e9ea;
border-radius: 8px;
border: 1px solid #dcdcdd;
padding: 6px 15px;
display: flex;
justify-content: space-between;
align-items: center;
}
.row input {
border: 1px solid #dcdcdd;
padding: 8px 2px;
text-align: right;
background-color: #ededee;
}
#noVNC_displays {
position: fixed;
display: none;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 100;
background: rgba(0,0,0,0.7);
cursor: pointer;
transition: 0.5s ease-in-out;
padding: 5px;
flex-direction: row;
justify-content: center;
align-items: center;
line-height: 25px;
word-wrap: break-word;
}
#noVNC_displays.noVNC_open {
display: flex;
transform: translateY(0);
visibility: visible;
opacity: 1;
}
#noVNC_displays .canvas {
display: flex;
flex-direction: column;
border-radius: 15px;
background: #fdfdfd;
padding: 15px 20px;
box-shadow: 0 0 15px rgba(0,0,0,0.4);
position: relative;
}
#noVNC_displays .canvas-title {
font-size: 14px;
font-weight: bold;
padding: 0 10px;
line-height: 1;
line-height: 1.4;
}
#noVNC_displays .canvas-text {
font-size: 12px;
padding: 0 10px;
opacity: 0.6;
line-height: 1.4;
margin-bottom: 15px;
}
#noVNC_displays .arrange-buttons {
margin-top: 15px;
display: flex;
justify-content: space-between;
}
#noVNC_displays .arrange-buttons button {
border: none;
display: flex;
align-items: center;
padding: 4px 7px;
border-radius: 5px;
cursor: pointer;
}
#noVNC_refreshMonitors {
position: absolute;
top: 20px;
right: 25px;
}
#noVNC_refreshMonitors_icon {
transition: all 0.3s;
transform-origin: center;
}
#noVNC_addMonitor {
background-color: #2196F3;
color: white;
}
#noVNC_addMonitor svg {
margin-right: 5px;
}
#noVNC_displays .canvas canvas {
background: #f7f7f7;
border: 1px solid #ececec;
border-radius: 8px;
}

303
app/ui.js
View File

@ -68,6 +68,9 @@ const UI = {
inhibitReconnect: true,
reconnectCallback: null,
reconnectPassword: null,
monitors: [],
selectedMonitor: null,
refreshRotation: 0,
supportsBroadcastChannel: (typeof BroadcastChannel !== "undefined"),
@ -132,7 +135,8 @@ const UI = {
UI.addMachineHandlers();
UI.addClipboardHandlers();
UI.addSettingsHandlers();
UI.addMultiMonitorAddHandler();
UI.addDisplaysHandler();
// UI.addMultiMonitorAddHandler();
document.getElementById("noVNC_status")
.addEventListener('click', UI.hideStatus);
UI.openControlbar();
@ -574,13 +578,23 @@ const UI = {
window.addEventListener('msfullscreenchange', UI.updateFullscreenButton);
},
addMultiMonitorAddHandler() {
addDisplaysHandler() {
if (UI.supportsBroadcastChannel) {
UI.showControlInput("noVNC_addmonitor_button");
UI.addClickHandle('noVNC_addmonitor_button', UI.addSecondaryMonitor);
UI.showControlInput("noVNC_displays_button");
UI.addClickHandle('noVNC_displays_button', UI.openDisplays);
UI.addClickHandle('noVNC_close_displays', UI.closeDisplays);
UI.addClickHandle('noVNC_addMonitor', UI.addSecondaryMonitor);
UI.addClickHandle('noVNC_refreshMonitors', UI.displaysRefresh);
}
},
/*addMultiMonitorAddHandler() {
if (UI.supportsBroadcastChannel) {
UI.addClickHandle('noVNC_addmonitor_button', UI.addSecondaryMonitor);
}
},*/
/* ------^-------
* /EVENT HANDLERS
* ==============
@ -1815,6 +1829,27 @@ const UI = {
* /MULTI-MONITOR SUPPORT
* ==============*/
openDisplays() {
document.getElementById('noVNC_displays').classList.add("noVNC_open");
let screenPlan = UI.rfb.getScreenPlan();
UI.initMonitors(screenPlan)
UI.displayMonitors()
},
closeDisplays() {
document.getElementById('noVNC_displays').classList.remove("noVNC_open");
},
displaysRefresh() {
const rotation = UI.refreshRotation + 180;
let screenPlan = UI.rfb.getScreenPlan();
document.getElementById('noVNC_refreshMonitors_icon').style.transform = "rotate(" + rotation + "deg)"
UI.refreshRotation = rotation
UI.updateMonitors(screenPlan)
UI.recenter()
UI.draw()
},
addSecondaryMonitor() {
let new_display_path = window.location.pathname.replace(/[^/]*$/, '')
let new_display_url = `${window.location.protocol}//${window.location.host}${new_display_path}screen.html`;
@ -1823,6 +1858,261 @@ const UI = {
window.open(new_display_url);
},
initMonitors(screenPlan) {
const { scale } = UI.multiMonitorSettings()
let monitors = []
screenPlan.screens.forEach(screen => {
monitors.push({
id: screen.screenID,
x: screen.x / scale,
y: screen.y / scale,
w: screen.serverWidth / scale,
h: screen.serverHeight / scale,
scale: 1,
fill: '#eeeeeecc',
isDragging: false
})
})
UI.monitors = monitors
},
updateMonitors(screenPlan) {
UI.initMonitors(screenPlan)
UI.recenter()
UI.draw()
},
multiMonitorSettings() {
const canvas = document.getElementById("noVNC_multiMonitorWidget")
return {
canvas,
ctx: canvas.getContext("2d"),
bb: canvas.getBoundingClientRect(),
scale: 12,
canvasWidth: 560,
canvasHeight: 230,
}
},
recenter() {
const monitors = UI.monitors
UI.removeSpaces()
const { startLeft, startTop } = UI.getSizes(monitors)
for (var i = 0; i < monitors.length; i++) {
var m = monitors[i];
m.x += startLeft
m.y += startTop
}
},
removeSpaces() {
const monitors = UI.monitors
let prev = monitors[0]
if (monitors.length > 1) {
for (var i = 1; i < monitors.length; i++) {
var a = monitors[i];
let prevStart = prev.x + prev.w
let prevStartTop = prev.y + prev.h
if (a.x > prevStart) {
a.x = prevStart
}
if (a.x < prevStart) {
if (a.y <= prevStartTop) {
a.x = prevStart
}
}
if (a.y > prevStartTop) {
if (a.x <= prevStart) {
a.y = prevStartTop
}
}
prev = monitors[i]
}
}
},
rect(ctx, x, y, w, h) {
ctx.beginPath();
ctx.roundRect(x, y, w, h, 5);
ctx.stroke();
ctx.closePath();
ctx.fill();
},
draw() {
const { ctx, canvasWidth, canvasHeight, scale } = UI.multiMonitorSettings()
const monitors = UI.monitors
ctx.clearRect(0, 0, canvasWidth, canvasHeight);
ctx.rect(0, 0, canvasWidth, canvasHeight);
for (var i = 0; i < monitors.length; i++) {
var m = monitors[i];
ctx.fillStyle = m.fill;
ctx.lineWidth = 1;
ctx.lineJoin = "round";
ctx.strokeStyle = m === UI.selectedMonitor ? "#2196F3" : "#aaa";
UI.rect(ctx, m.x, m.y, (m.w / m.scale), (m.h / m.scale));
ctx.font = "13px sans-serif";
ctx.textAlign = "right";
ctx.textBaseline = "top";
ctx.fillStyle = "#000";
ctx.fillText((i + 1), (m.x + m.w) - 4, m.y + 4);
ctx.font = "200 11px sans-serif";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText(m.w * scale + ' x ' + m.h * scale, m.x + (m.w / 2), m.y + (m.h / 2));
}
},
getSizes(monitors) {
const { canvasWidth, canvasHeight } = UI.multiMonitorSettings()
let top = monitors[0].y
let left = monitors[0].x
let width = monitors[0].w
let height = monitors[0].h
for (var i = 0; i < monitors.length; i++) {
var m = monitors[i];
if (m.x < left) {
left = m.x
}
if (m.y < top) {
top = m.y
}
if(m.x + m.w > width) {
width = m.x + m.w
}
if(m.y + m.h > height) {
height = m.y + m.h
}
}
const startLeft = ((canvasWidth - width - left) / 2);
const startTop = ((canvasHeight - height - top) / 2);
return { top, left, width, height, startLeft, startTop }
},
displayMonitors() {
const { canvas, ctx, bb, canvasWidth, canvasHeight, scale } = UI.multiMonitorSettings()
let offsetX
let offsetY
let dragok = false
let startX;
let startY;
offsetX = bb.left
offsetY = bb.top
canvas.addEventListener("mousedown", myDown, false);
canvas.addEventListener("mouseup", myUp, false);
canvas.addEventListener("mousemove", myMove, false);
UI.recenter()
UI.draw()
function myDown(e) {
let monitors = UI.monitors
e.preventDefault();
e.stopPropagation();
let mx = parseInt(e.clientX - offsetX);
let my = parseInt(e.clientY - offsetY);
for (var i = 0; i < monitors.length; i++) {
var mon = monitors[i];
var monw = mon.w / mon.scale
var monh = mon.h / mon.scale
// Find the closest rect to drag
if (mx > mon.x && mx < mon.x + monw && my > mon.y && my < mon.y + monh) {
dragok = true;
mon.isDragging = true;
UI.selectedMonitor = mon
break // get out of the loop rather than dragging multiple
}
}
startX = mx;
startY = my;
UI.draw()
}
function myUp(e) {
let monitors = UI.monitors
e.preventDefault();
e.stopPropagation();
// clear all the dragging flags
dragok = false;
for (var i = 0; i < monitors.length; i++) {
monitors[i].isDragging = false;
}
const screenplan = setScreenPlan()
UI.recenter()
UI.draw()
}
function myMove(e) {
if (dragok) {
let monitors = UI.monitors
e.preventDefault();
e.stopPropagation();
// get the current mouse position
var mx = parseInt(e.clientX - offsetX);
var my = parseInt(e.clientY - offsetY);
// calculate the distance the mouse has moved
// since the last mousemove
var dx = mx - startX;
var dy = my - startY;
// move each rect that isDragging
// by the distance the mouse has moved
// since the last mousemove
for (var i = 0; i < monitors.length; i++) {
var m = monitors[i];
if (m.isDragging) {
m.x += dx;
m.y += dy;
if (m.x) { // don't move into another monitor
// if (m.y )
}
}
}
// redraw the scene with the new rect positions
UI.draw();
// reset the starting mouse position for the next mousemove
startX = mx;
startY = my;
}
}
function setScreenPlan() {
let monitors = UI.monitors
const { top, left, width, height } = UI.getSizes(monitors)
const screens = []
for (var i = 0; i < monitors.length; i++) {
var a = monitors[i];
screens.push({
screenID: a.id,
serverHeight: a.h * scale,
serverWidth: a.w * scale,
x: (a.x - left) * scale,
y: (a.y - top) * scale
})
}
const screenPlan = {
serverHeight: height * scale,
serverWidth: width * scale,
screens
}
UI.rfb.applyScreenPlan(screenPlan);
}
},
/* ------^-------
@ -2478,11 +2768,12 @@ const UI = {
let screenPlan = UI.rfb.getScreenPlan();
// Now make adjustments to the screen plan, this is just an example
screenPlan.screens[1].y = 100;
// screenPlan.screens[1].y = 0;
// Finally apply the screen plan
UI.rfb.applyScreenPlan(screenPlan);
console.log(screenPlan);
UI.updateMonitors(screenPlan)
},
//Helper to add options to dropdown.

View File

@ -187,10 +187,11 @@
<!-- Add Second Screen -->
<div class="noVNC_button_div noVNC_hidden" >
<input type="image" alt="Fullscreen" src="app/images/fullscreen.svg"
id="noVNC_addmonitor_button" class="noVNC_button"
title="Add Monitor">
Add Monitor
<input type="image" alt="Fullscreen" src="app/images/desktop-regular.svg"
id="noVNC_displays_button" class="noVNC_button"
style="margin: 10px 3px;"
title="Show Displays">
Displays
</div>
<!-- Toggle game mode -->
@ -545,6 +546,23 @@
<!-- Status Dialog -->
<div id="noVNC_status"></div>
<!-- Status Dialog -->
<div id="noVNC_displays">
<div class="canvas">
<div class="canvas-title">Arrange Displays</div>
<div class="canvas-text">Drag and drop to arrange displays</div>
<div id="noVNC_refreshMonitors"><svg id="noVNC_refreshMonitors_icon" xmlns="http://www.w3.org/2000/svg" height="1em" viewBox="0 0 512 512"><path class="fa-primary" d="M105.1 202.6c7.7-21.8 20.2-42.3 37.8-59.8c62.5-62.5 163.8-62.5 226.3 0L386.3 160H336c-17.7 0-32 14.3-32 32s14.3 32 32 32H463.5c0 0 0 0 0 0h.4c17.7 0 32-14.3 32-32V64c0-17.7-14.3-32-32-32s-32 14.3-32 32v51.2L414.4 97.6c-87.5-87.5-229.3-87.5-316.8 0C73.2 122 55.6 150.7 44.8 181.4c-5.9 16.7 2.9 34.9 19.5 40.8s34.9-2.9 40.8-19.5z"/><path class="fa-secondary" d="M80 396.9V448c0 17.7-14.3 32-32 32s-32-14.3-32-32V320c0-17.7 14.3-32 32-32H176c17.7 0 32 14.3 32 32s-14.3 32-32 32H125.6l17.2 17.1c62.5 62.5 163.8 62.5 226.3 0c17.5-17.5 30.1-38 37.8-59.8c5.9-16.7 24.2-25.4 40.8-19.5s25.4 24.2 19.5 40.8c-10.8 30.6-28.4 59.3-52.9 83.8c-87.5 87.5-229.3 87.5-316.7 0L80 396.9z"/></svg></div>
<canvas id="noVNC_multiMonitorWidget" class="" width="560" height="230"></canvas>
<div class="arrange-buttons">
<button id="noVNC_addMonitor">
<svg xmlns="http://www.w3.org/2000/svg" height="1em" viewBox="0 0 448 512"><path fill="currentColor" d="M240 64c0-8.8-7.2-16-16-16s-16 7.2-16 16V240H32c-8.8 0-16 7.2-16 16s7.2 16 16 16H208V448c0 8.8 7.2 16 16 16s16-7.2 16-16V272H416c8.8 0 16-7.2 16-16s-7.2-16-16-16H240V64z"/></svg>
Add Monitor
</button>
<button id="noVNC_close_displays">Done</button>
</div>
</div>
</div>
<!-- Connect button -->
<div class="noVNC_center">
<div id="noVNC_connect_dlg">