mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-08-28 13:00:15 +03:00
1436 lines
44 KiB
JavaScript
1436 lines
44 KiB
JavaScript
/*
|
|
* Copyright (c) 2013 Antoine Martin <antoine@xpra.org>
|
|
* Copyright (c) 2014 Joshua Higgins <josh@kxes.net>
|
|
* Copyright (c) 2015 Spikes, Inc.
|
|
* Licensed under MPL 2.0
|
|
*
|
|
* xpra window
|
|
*
|
|
* Based on shape.js but no longer requires it
|
|
*
|
|
* requires:
|
|
* jQueryUI
|
|
*/
|
|
|
|
const TASKBAR_HEIGHT = 0;
|
|
|
|
function dummy() {
|
|
//this placeholder function does nothing
|
|
}
|
|
|
|
/**
|
|
* This is the class representing a window we draw on the canvas.
|
|
* It has a geometry, it may have borders and a top bar.
|
|
* The contents of the window is an image, which gets updated
|
|
* when we receive pixels from the server.
|
|
*/
|
|
class XpraWindow {
|
|
constructor(
|
|
client,
|
|
wid,
|
|
x,
|
|
y,
|
|
w,
|
|
h,
|
|
metadata,
|
|
override_redirect,
|
|
tray,
|
|
client_properties,
|
|
geometry_callback,
|
|
mouse_move_callback,
|
|
mouse_down_callback,
|
|
mouse_up_callback,
|
|
mouse_scroll_callback,
|
|
set_focus_callback,
|
|
window_closed_callback,
|
|
scale
|
|
) {
|
|
// use me in jquery callbacks as we lose 'this'
|
|
this.client = client;
|
|
|
|
//xpra specific attributes:
|
|
this.wid = wid;
|
|
//enclosing div in page DOM
|
|
this.div = document.getElementById(wid);
|
|
|
|
//these values represent the internal geometry
|
|
//i.e. geometry as windows appear to the compositor
|
|
this.x = x;
|
|
this.y = y;
|
|
this.w = w;
|
|
this.h = h;
|
|
// scaling for client display width override
|
|
this.scale = scale;
|
|
|
|
this.metadata = {};
|
|
this.override_redirect = override_redirect;
|
|
this.tray = tray;
|
|
this.has_alpha = false;
|
|
this.client_properties = client_properties;
|
|
|
|
this.set_focus_cb = set_focus_callback || dummy;
|
|
this.mouse_move_cb = mouse_move_callback || dummy;
|
|
this.mouse_down_cb = mouse_down_callback || dummy;
|
|
this.mouse_up_cb = mouse_up_callback || dummy;
|
|
this.mouse_scroll_cb = mouse_scroll_callback || dummy;
|
|
this.geometry_cb = geometry_callback || dummy;
|
|
this.window_closed_cb = window_closed_callback || dummy;
|
|
|
|
this.debug_categories = client.debug_categories;
|
|
|
|
this.canvas = null;
|
|
this.init_canvas();
|
|
|
|
//window attributes:
|
|
this.title = "";
|
|
this.windowtype = [];
|
|
this.fullscreen = false;
|
|
this.saved_geometry = null;
|
|
this.minimized = false;
|
|
this.maximized = false;
|
|
this.focused = false;
|
|
this.decorations = true; // whether the window should be decorated or not
|
|
this.decorated = true; // whether it actually is (fullscreen windows are not)
|
|
this.resizable = false;
|
|
this.stacking_layer = 0;
|
|
|
|
// Icon cache
|
|
this.icon = null;
|
|
|
|
// get offsets
|
|
this.leftoffset = 0;
|
|
this.rightoffset = 0;
|
|
this.topoffset = 0;
|
|
this.bottomoffset = 0;
|
|
|
|
// update metadata that is safe before window is drawn
|
|
this.update_metadata(metadata, true);
|
|
|
|
// create the decoration as part of the window, style is in CSS
|
|
jQuery(this.div).addClass("window");
|
|
for (const windowtype in this.windowtype) {
|
|
jQuery(this.div).addClass(`window-${windowtype}`);
|
|
}
|
|
|
|
const fullscreen = (metadata["fullscreen"]) ?? false;
|
|
|
|
if (this.client.server_is_desktop || this.client.server_is_shadow) {
|
|
jQuery(this.div).addClass("desktop");
|
|
this.resizable = false;
|
|
} else if (this.tray) {
|
|
jQuery(this.div).addClass("tray");
|
|
this.resizable = false;
|
|
} else if (this.override_redirect) {
|
|
jQuery(this.div).addClass("override-redirect");
|
|
this.resizable = false;
|
|
} else if (!fullscreen && ((this.windowtype.length === 0) || this.has_windowtype(["NORMAL", "DIALOG", "UTILITY"]))) {
|
|
this.resizable = true;
|
|
}
|
|
|
|
this.configure_border_class();
|
|
this.add_headerbar();
|
|
this.make_draggable()
|
|
this.update_offsets()
|
|
|
|
// stop propagation if we're over the window:
|
|
jQuery(this.div).mousedown((e) => e.stopPropagation());
|
|
//bug 2418: if we stop 'mouseup' propagation,
|
|
//jQuery can't ungrab the window with Firefox
|
|
// assign callback to focus window if header is clicked.
|
|
jQuery(this.d_header).click((e) => {
|
|
if (
|
|
!this.minimized &&
|
|
$(e.target).parents(".windowbuttons").length === 0
|
|
) {
|
|
this.focus();
|
|
}
|
|
});
|
|
|
|
// create the spinner overlay div
|
|
jQuery(this.div).prepend(
|
|
`<div id="spinner${wid}" class="spinneroverlay"><div class="spinnermiddle"><div class="spinner"></div></div></div>`
|
|
);
|
|
this.spinnerdiv = jQuery(`#spinner${wid}`);
|
|
|
|
this.cursor_data = null;
|
|
this.pointer_down = -1;
|
|
this.pointer_last_x = 0;
|
|
this.pointer_last_y = 0;
|
|
|
|
// adapt to screen size if needed (ie: shadow / desktop windows):
|
|
this.screen_resized();
|
|
// set the CSS geometry
|
|
this.updateCSSGeometry();
|
|
// now read all metadata
|
|
this.update_metadata(metadata);
|
|
}
|
|
|
|
log() {
|
|
if (this.client) this.client.log.apply(this.client, arguments);
|
|
}
|
|
warn() {
|
|
if (this.client) this.client.warn.apply(this.client, arguments);
|
|
}
|
|
error() {
|
|
if (this.client) this.client.error.apply(this.client, arguments);
|
|
}
|
|
exc() {
|
|
if (this.client) this.client.exc.apply(this.client, arguments);
|
|
}
|
|
debug() {
|
|
if (this.client) this.client.debug.apply(this.client, arguments);
|
|
}
|
|
|
|
configure_border_class() {
|
|
if (this.resizable || this.decorated) {
|
|
jQuery(this.div).addClass("border");
|
|
}
|
|
else {
|
|
jQuery(this.div).removeClass("border");
|
|
}
|
|
}
|
|
|
|
update_offsets() {
|
|
this.leftoffset = Number.parseInt(jQuery(this.div).css("border-left-width"), 10);
|
|
this.rightoffset = Number.parseInt(jQuery(this.div).css("border-right-width"), 10);
|
|
this.topoffset = Number.parseInt(jQuery(this.div).css("border-top-width"), 10)
|
|
this.bottomoffset = Number.parseInt(jQuery(this.div).css("border-bottom-width"), 10);
|
|
if (this.decorated) {
|
|
this.topoffset = this.topoffset + Number.parseInt(jQuery(this.d_header).css("height"), 10);
|
|
}
|
|
this.debug("geometry", "decorated=", this.decorated, "offsets=", [this.leftoffset, this.topoffset, this.rightoffset, this.bottomoffset]);
|
|
}
|
|
|
|
|
|
add_headerbar() {
|
|
const wid = this.wid;
|
|
// add a title bar to this window if we need to
|
|
// create header
|
|
let head =
|
|
`<div id="head${wid}" class="windowhead"> ` +
|
|
`<span class="windowicon"><img alt="window icon" class="windowicon" id="windowicon${wid}" /></span> ` +
|
|
`<span class="windowtitle" id="title${wid}">${this.title}</span> ` +
|
|
`<span class="windowbuttons"> `;
|
|
if (!jQuery(this.div).hasClass("modal")) {
|
|
//modal windows cannot be minimized (see #204)
|
|
head += `<span id="minimize${wid}"><img alt="minimize" src="icons/minimize.png" /></span>`;
|
|
}
|
|
head +=
|
|
`<span id="maximize${wid}"><img alt="maximize" src="icons/maximize.png" /></span> ` +
|
|
`<span id="close${wid}"><img alt="close" src="icons/close.png" /></span> ` +
|
|
`</span></div>`;
|
|
jQuery(this.div).prepend(head);
|
|
|
|
jQuery(`#head${wid}`).click(() => {
|
|
if (!this.minimized) {
|
|
this.focus();
|
|
}
|
|
});
|
|
this.d_header = `#head${wid}`;
|
|
this.d_closebtn = `#close${wid}`;
|
|
this.d_maximizebtn = `#maximize${wid}`;
|
|
this.d_minimizebtn = `#minimize${wid}`;
|
|
|
|
if (this.resizable) {
|
|
this.make_resizable();
|
|
jQuery(this.d_header).dblclick(() => this.toggle_maximized());
|
|
jQuery(this.d_closebtn).click(() => this.window_closed_cb(this));
|
|
jQuery(this.d_maximizebtn).click(() => this.toggle_maximized());
|
|
jQuery(this.d_minimizebtn).click(() => this.toggle_minimized());
|
|
} else {
|
|
jQuery(this.d_maximizebtn).hide();
|
|
jQuery(`#windowlistitemmax${wid}`).hide();
|
|
}
|
|
|
|
// we must set a sensible default early
|
|
// so geometry calculations have the correct offset:
|
|
if (!("decorations" in this.metadata)) {
|
|
let decorated = true;
|
|
if (this.override_redirect) {
|
|
decorated = false;
|
|
}
|
|
else {
|
|
decorated = !this.has_windowtype(["DROPDOWN", "TOOLTIP", "POPUP_MENU", "MENU", "COMBO"]);
|
|
}
|
|
this._set_decorated(decorated);
|
|
// console.log("decorated=", decorated, "for windowtype=", this.windowtype);
|
|
}
|
|
}
|
|
|
|
has_windowtype(windowtypes) {
|
|
const windowtypes_set = new Set(windowtypes);
|
|
return this.windowtype.some(element => windowtypes_set.has(element));
|
|
}
|
|
|
|
|
|
make_draggable() {
|
|
if (this.scale !== 1) {
|
|
jQuery(this.div).draggable({
|
|
transform: true
|
|
});
|
|
}
|
|
jQuery(this.div).draggable({
|
|
cancel: "canvas"
|
|
});
|
|
jQuery(this.div).on("dragstart", (event_) => {
|
|
this.client.release_buttons(event_, this);
|
|
this.focus();
|
|
this.client.mouse_grabbed = true;
|
|
});
|
|
jQuery(this.div).on("dragstop", (event_, ui) => {
|
|
this.client.mouse_grabbed = false;
|
|
this.handle_moved(ui);
|
|
});
|
|
}
|
|
|
|
make_resizable() {
|
|
// Use transform if scaled
|
|
// This disables helper highlight, so we
|
|
// move the resizable borders in transform plugin
|
|
if (this.scale !== 1) {
|
|
jQuery(this.div).resizable({
|
|
transform: true
|
|
});
|
|
}
|
|
// attach resize handles
|
|
jQuery(this.div).resizable({
|
|
containment: "parent",
|
|
helper: "ui-resizable-helper",
|
|
handles: "n, e, s, w, ne, se, sw, nw",
|
|
});
|
|
jQuery(this.div).on("resizestart", (evt) => {
|
|
this.client.do_window_mouse_click(evt, this, false);
|
|
this.client.mouse_grabbed = true;
|
|
});
|
|
jQuery(this.div).on("resizestop", (evt, ui) => {
|
|
this.handle_resized(ui);
|
|
this.focus();
|
|
this.client.mouse_grabbed = false;
|
|
//workaround for the window going blank,
|
|
//just force a refresh:
|
|
setTimeout(() => this.client.request_refresh(this.wid), 200);
|
|
setTimeout(() => this.client.request_refresh(this.wid), 500);
|
|
});
|
|
}
|
|
|
|
init_canvas() {
|
|
this.canvas = null;
|
|
jQuery(this.div).find("canvas").remove();
|
|
const canvas = document.createElement("canvas");
|
|
if (this.client.try_gpu) {
|
|
$(canvas).addClass("gpu-trigger");
|
|
}
|
|
// set initial sizes
|
|
canvas.width = this.w;
|
|
canvas.height = this.h;
|
|
this.canvas = canvas;
|
|
this.div.append(canvas);
|
|
if (this.client.offscreen_api && this.client.decode_worker) {
|
|
// Transfer canvas control.
|
|
this.transfer_canvas(canvas);
|
|
} else {
|
|
//we're going to paint from this class:
|
|
this.canvas_ctx = this.canvas.getContext("2d");
|
|
this.canvas_ctx.imageSmoothingEnabled = false;
|
|
|
|
this.init_offscreen_canvas();
|
|
|
|
this.draw_canvas = this.offscreen_canvas;
|
|
this.paint_queue = [];
|
|
this.paint_pending = 0;
|
|
}
|
|
this.register_canvas_mouse_events(this.canvas);
|
|
this.register_canvas_pointer_events(this.canvas);
|
|
}
|
|
|
|
transfer_canvas(canvas) {
|
|
const offscreen_handle = canvas.transferControlToOffscreen();
|
|
this.client.decode_worker.postMessage({
|
|
cmd: "canvas",
|
|
wid: this.wid,
|
|
canvas: offscreen_handle,
|
|
debug: this.debug_categories.includes("draw"),
|
|
},
|
|
[offscreen_handle]
|
|
);
|
|
}
|
|
|
|
init_offscreen_canvas() {
|
|
this.offscreen_canvas = document.createElement("canvas");
|
|
this.offscreen_canvas.width = this.w;
|
|
this.offscreen_canvas.height = this.h;
|
|
this.offscreen_canvas_ctx = this.offscreen_canvas.getContext("2d");
|
|
this.offscreen_canvas_ctx.imageSmoothingEnabled = false;
|
|
}
|
|
|
|
swap_buffers() {
|
|
//the up to date canvas is what we'll draw on screen:
|
|
this.debug("draw", "swap_buffers");
|
|
this.draw_canvas = this.offscreen_canvas;
|
|
this.init_offscreen_canvas();
|
|
this.offscreen_canvas_ctx.drawImage(this.draw_canvas, 0, 0);
|
|
}
|
|
|
|
register_canvas_mouse_events(canvas) {
|
|
// Hook up the events we want to receive:
|
|
jQuery(canvas).mousedown((e) => {
|
|
return this.mouse_down_cb(e, this);
|
|
});
|
|
jQuery(canvas).mouseup((e) => {
|
|
return this.mouse_up_cb(e, this);
|
|
});
|
|
jQuery(canvas).mousemove((e) => {
|
|
return this.mouse_move_cb(e, this);
|
|
});
|
|
}
|
|
|
|
register_canvas_pointer_events(canvas) {
|
|
if (!window.PointerEvent) {
|
|
return;
|
|
}
|
|
canvas.addEventListener("pointerdown", (event_) => {
|
|
this.debug("mouse", "pointerdown:", event_);
|
|
if (event_.pointerType === "touch") {
|
|
this.pointer_down = event_.pointerId;
|
|
this.pointer_last_x = event_.offsetX;
|
|
this.pointer_last_y = event_.offsetY;
|
|
}
|
|
});
|
|
|
|
canvas.addEventListener("mousemove", (event_) => {
|
|
this.debug("mouse", "mousemove:", event_);
|
|
if (this.pointer_down === event_.pointerId) {
|
|
const dx = event_.offsetX - this.pointer_last_x;
|
|
const dy = event_.offsetY - this.pointer_last_y;
|
|
this.pointer_last_x = event_.offsetX;
|
|
this.pointer_last_y = event_.offsetY;
|
|
const mult = 20 * (window.devicePixelRatio || 1);
|
|
event_.wheelDeltaX = Math.round(dx * mult);
|
|
event_.wheelDeltaY = Math.round(dy * mult);
|
|
return this.mouse_scroll_cb(event_, this);
|
|
}
|
|
});
|
|
canvas.addEventListener("pointerup", (event_) => {
|
|
this.debug("mouse", "pointerup:", event_);
|
|
this.pointer_down = -1;
|
|
});
|
|
canvas.addEventListener("pointercancel", (event_) => {
|
|
this.debug("mouse", "pointercancel:", event_);
|
|
this.pointer_down = -1;
|
|
});
|
|
canvas.addEventListener("pointerout", (event_) => {
|
|
this.debug("mouse", "pointerout:", event_);
|
|
});
|
|
//wheel events on a window:
|
|
const me = this;
|
|
|
|
function on_mousescroll(e) {
|
|
me.mouse_scroll_cb(e, me);
|
|
e.stopPropagation();
|
|
return e.preventDefault();
|
|
}
|
|
if (Utilities.isEventSupported("wheel")) {
|
|
canvas.addEventListener("wheel", on_mousescroll, false);
|
|
}
|
|
}
|
|
|
|
set_spinner(state) {
|
|
if (state) {
|
|
this.spinnerdiv.hide();
|
|
} else {
|
|
this.spinnerdiv.css("display", "table");
|
|
}
|
|
}
|
|
|
|
ensure_visible() {
|
|
if (this.client.server_is_desktop || this.client.server_is_shadow) {
|
|
//those windows should usually be centered on screen,
|
|
//moving them would mess that up
|
|
return true;
|
|
}
|
|
if (this.override_redirect) {
|
|
//OR windows cannot be moved server-side
|
|
return true;
|
|
}
|
|
const oldx = this.x;
|
|
const oldy = this.y;
|
|
// for now make sure we don't out of top left
|
|
// this will be much smarter!
|
|
const min_w_visible = Math.min(80, this.w);
|
|
const min_h_visible = Math.min(80, this.h);
|
|
const desktop_size = this.client._get_desktop_size();
|
|
const ww = desktop_size[0];
|
|
const wh = desktop_size[1];
|
|
this.debug("geometry", "ensure_visible() min_w_visible=", min_w_visible, "min_h_visible=", min_h_visible, "desktop_size=", desktop_size);
|
|
if (oldx < this.leftoffset && oldx + this.w <= min_w_visible) {
|
|
this.x = min_w_visible - this.w + this.leftoffset;
|
|
} else if (oldx >= ww - min_w_visible) {
|
|
this.x = Math.min(oldx, ww - min_w_visible);
|
|
}
|
|
if (oldy <= this.topoffset && oldy <= min_h_visible) {
|
|
this.y = this.topoffset;
|
|
} else if (oldy >= wh - min_h_visible) {
|
|
this.y = Math.min(oldy, wh - min_h_visible);
|
|
}
|
|
this.debug("geometry", "ensure_visible() oldx=", oldx, "oldy=", oldy, "x=", this.x, "y=", this.y);
|
|
if (oldx !== this.x || oldy !== this.y) {
|
|
this.updateCSSGeometry();
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
updateCanvasGeometry() {
|
|
if (this.client.offscreen_api && this.client.decode_worker) {
|
|
this.client.decode_worker.postMessage({
|
|
cmd: "canvas-geo",
|
|
wid: this.wid,
|
|
w: this.w,
|
|
h: this.h,
|
|
});
|
|
return;
|
|
}
|
|
// set size of both canvas if needed
|
|
if (this.canvas.width !== this.w) {
|
|
this.canvas.width = this.w;
|
|
}
|
|
if (this.canvas.height !== this.h) {
|
|
this.canvas.height = this.h;
|
|
}
|
|
if (this.offscreen_canvas.width !== this.w) {
|
|
this.offscreen_canvas.width = this.w;
|
|
}
|
|
if (this.offscreen_canvas.height !== this.h) {
|
|
this.offscreen_canvas.height = this.h;
|
|
}
|
|
}
|
|
|
|
updateCSSGeometry() {
|
|
// set size of canvas
|
|
this.updateCanvasGeometry();
|
|
if (this.client.server_is_desktop || this.client.server_is_shadow) {
|
|
if (this.client.server_resize_exact) {
|
|
jQuery(this.div).css("top", 0);
|
|
return;
|
|
}
|
|
}
|
|
// work out outer size
|
|
this.outerH = this.h + this.topoffset + this.bottomoffset;
|
|
this.outerW = this.w + this.leftoffset + this.rightoffset;
|
|
// set width and height
|
|
jQuery(this.div).css("width", this.outerW);
|
|
jQuery(this.div).css("height", this.outerH);
|
|
// set CSS attributes to outerX and outerY
|
|
this.outerX = this.x - this.leftoffset;
|
|
this.outerY = this.y - this.topoffset;
|
|
jQuery(this.div).css("left", this.outerX);
|
|
jQuery(this.div).css("top", this.outerY);
|
|
this.debug("geometry", "updateCSSGeometry() left=", this.outerX, ", top=", this.outerY, ", width=", this.outerW, ", height=", this.outerH);
|
|
}
|
|
|
|
focus() {
|
|
this.set_focus_cb(this);
|
|
}
|
|
|
|
updateFocus() {
|
|
if (this.focused) {
|
|
// set focused style to div
|
|
jQuery(this.div).addClass("windowinfocus");
|
|
|
|
// Update window title
|
|
if (this.client.session_name) {
|
|
jQuery("title").text(client.session_name);
|
|
}
|
|
else {
|
|
jQuery("title").text(
|
|
`${location.pathname.replaceAll("/", "")}: ${this.title}`
|
|
);
|
|
}
|
|
|
|
// Update the icon
|
|
if (this.icon !== null) {
|
|
const source = this.update_icon(this.icon.width, this.icon.height, this.icon.encoding, this.icon.img_data);
|
|
jQuery("#favicon").attr("href", source);
|
|
} else {
|
|
jQuery("#favicon").attr("href", "favicon.png");
|
|
}
|
|
} else {
|
|
// set not in focus style
|
|
jQuery(this.div).removeClass("windowinfocus");
|
|
}
|
|
}
|
|
|
|
suspend() {
|
|
//perhaps we should suspend updates?
|
|
}
|
|
|
|
resume() {
|
|
this.init_canvas();
|
|
}
|
|
|
|
/**
|
|
* toString allows us to identify windows by their unique window id.
|
|
*/
|
|
toString() {
|
|
return `Window(${this.wid})`;
|
|
}
|
|
|
|
update_zindex() {
|
|
let z = 5000 + this.stacking_layer;
|
|
if (this.tray) {
|
|
z = 0;
|
|
} else if (this.override_redirect || this.client.server_is_desktop || this.client.server_is_shadow) {
|
|
z = 30_000;
|
|
} else if (this.has_windowtype(["DROPDOWN", "TOOLTIP", "POPUP_MENU", "MENU", "COMBO"]))
|
|
{
|
|
z = 20_000;
|
|
} else if (this.has_windowtype(["UTILITY", "DIALOG"])) {
|
|
z = 15_000;
|
|
}
|
|
const above = this.metadata["above"];
|
|
if (above) {
|
|
z += 5000;
|
|
} else {
|
|
const below = this.metadata["below"];
|
|
if (below) {
|
|
z -= 5000;
|
|
}
|
|
}
|
|
if (this.focused) {
|
|
z += 2500;
|
|
}
|
|
jQuery(this.div).css("z-index", z);
|
|
}
|
|
|
|
/**
|
|
* Update our metadata cache with new key-values,
|
|
* then call set_metadata with these new key-values.
|
|
*/
|
|
update_metadata(metadata, safe) {
|
|
//update our metadata cache with new key-values:
|
|
this.debug("main", "update_metadata(", metadata, ")");
|
|
for (const attrname in metadata) {
|
|
this.metadata[attrname] = metadata[attrname];
|
|
}
|
|
if (safe) {
|
|
this.set_metadata_safe(metadata);
|
|
} else {
|
|
this.set_metadata(metadata);
|
|
}
|
|
this.update_zindex();
|
|
}
|
|
|
|
/**
|
|
* Apply only metadata settings that are safe before window is drawn
|
|
*/
|
|
set_metadata_safe(metadata) {
|
|
if ("title" in metadata) {
|
|
let title = Utilities.s(metadata["title"]);
|
|
if (this.title !== title) {
|
|
this.title = title;
|
|
this.log("title=", this.title);
|
|
jQuery(`#title${this.wid}`).html(this.title);
|
|
const trimmedTitle = Utilities.trimString(this.title, 30);
|
|
jQuery(`#windowlistitemtitle${this.wid}`).text(trimmedTitle);
|
|
}
|
|
}
|
|
if ("has-alpha" in metadata) {
|
|
this.has_alpha = Boolean(metadata["has-alpha"]);
|
|
}
|
|
if ("window-type" in metadata) {
|
|
this.windowtype = metadata["window-type"];
|
|
}
|
|
if ("opacity" in metadata) {
|
|
let opacity = metadata["opacity"];
|
|
opacity = opacity < 0 ? 1 : opacity / 0x1_00_00_00_00;
|
|
jQuery(this.div).css("opacity", `${opacity}`);
|
|
}
|
|
if ("iconic" in metadata) {
|
|
this.set_minimized(Boolean(metadata["iconic"]));
|
|
}
|
|
|
|
//if the attribute is set, add the corresponding css class:
|
|
const attributes = ["modal", "above", "below"];
|
|
for (const attribute of attributes) {
|
|
if (attribute in metadata) {
|
|
const value = metadata[attribute];
|
|
if (value) {
|
|
jQuery(this.div).addClass(attribute);
|
|
} else {
|
|
jQuery(this.div).removeClass(attribute);
|
|
}
|
|
}
|
|
}
|
|
if (this.resizable && "size-constraints" in metadata) {
|
|
this.apply_size_constraints();
|
|
}
|
|
if ("class-instance" in metadata) {
|
|
const wm_class = metadata["class-instance"];
|
|
const classes = jQuery(this.div).prop("classList");
|
|
if (classes) {
|
|
//remove any existing "wmclass-" classes not in the new wm_class list:
|
|
for (const class_ of classes) {
|
|
const tclass = `${class_}`;
|
|
if (tclass.indexOf("wmclass-") === 0 && wm_class && !wm_class.includes(tclass)) {
|
|
jQuery(this.div).removeClass(tclass);
|
|
}
|
|
}
|
|
}
|
|
if (wm_class) {
|
|
//add new wm-class:
|
|
for (const element of wm_class) {
|
|
const tclass = Utilities.s(element).replace(/[^\dA-Za-z]/g, "");
|
|
if (tclass && !jQuery(this.div).hasClass(tclass)) {
|
|
jQuery(this.div).addClass(`wmclass-${tclass}`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
apply_size_constraints() {
|
|
if (!this.resizable) {
|
|
return;
|
|
}
|
|
if (this.maximized) {
|
|
jQuery(this.div).draggable("disable");
|
|
} else {
|
|
jQuery(this.div).draggable("enable");
|
|
}
|
|
let hdec = 0;
|
|
const wdec = 0;
|
|
if (this.decorated) {
|
|
//adjust for header
|
|
hdec = jQuery(`#head${this.wid}`).outerHeight(true);
|
|
}
|
|
let min_size = null;
|
|
let max_size = null;
|
|
const size_constraints = this.metadata["size-constraints"];
|
|
if (size_constraints) {
|
|
min_size = size_constraints["minimum-size"];
|
|
max_size = size_constraints["maximum-size"];
|
|
}
|
|
let minw = 0;
|
|
let minh = 0;
|
|
if (min_size) {
|
|
minw = min_size[0] + wdec;
|
|
minh = min_size[1] + hdec;
|
|
}
|
|
let maxw = null;
|
|
let maxh = null;
|
|
if (max_size) {
|
|
maxw = max_size[0] + wdec;
|
|
maxh = max_size[1] + hdec;
|
|
}
|
|
if (minw > 0 && minw === maxw && minh > 0 && minh === maxh) {
|
|
jQuery(this.d_maximizebtn).hide();
|
|
jQuery(`#windowlistitemmax${this.wid}`).hide();
|
|
jQuery(this.div).resizable("disable");
|
|
} else {
|
|
jQuery(this.d_maximizebtn).show();
|
|
if (!this.maximized) {
|
|
jQuery(this.div).resizable("enable");
|
|
} else {
|
|
jQuery(this.div).resizable("disable");
|
|
}
|
|
}
|
|
if (!this.maximized) {
|
|
if (minw) {
|
|
jQuery(this.div).resizable("option", "minWidth", minw);
|
|
}
|
|
if (minh) {
|
|
jQuery(this.div).resizable("option", "minHeight", minh);
|
|
}
|
|
if (maxw) {
|
|
jQuery(this.div).resizable("option", "maxWidth", maxw);
|
|
}
|
|
if (maxh) {
|
|
jQuery(this.div).resizable("option", "maxHeight", maxh);
|
|
}
|
|
}
|
|
//TODO: aspectRatio, grid
|
|
}
|
|
|
|
/**
|
|
* Apply new metadata settings.
|
|
*/
|
|
set_metadata(metadata) {
|
|
this.set_metadata_safe(metadata);
|
|
if ("fullscreen" in metadata) {
|
|
this.set_fullscreen(Boolean(metadata["fullscreen"]));
|
|
}
|
|
if ("maximized" in metadata) {
|
|
this.set_maximized(Boolean(metadata["maximized"]));
|
|
}
|
|
if ("decorations" in metadata) {
|
|
this.decorations = Boolean(metadata["decorations"]);
|
|
this._set_decorated(this.decorations);
|
|
this.updateCSSGeometry();
|
|
this.handle_resized();
|
|
this.apply_size_constraints();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Save the window geometry so we can restore it later
|
|
* (ie: when un-maximizing or un-fullscreening)
|
|
*/
|
|
save_geometry() {
|
|
this.saved_geometry = {
|
|
x: this.x,
|
|
y: this.y,
|
|
w: this.w,
|
|
h: this.h,
|
|
};
|
|
this.debug("geometry", "save_geometry() saved-geometry=", this.saved_geometry);
|
|
}
|
|
/**
|
|
* Restores the saved geometry (if it exists).
|
|
*/
|
|
restore_geometry() {
|
|
if (!this.saved_geometry) {
|
|
return;
|
|
}
|
|
this.x = this.saved_geometry["x"];
|
|
this.y = this.saved_geometry["y"];
|
|
this.w = this.saved_geometry["w"];
|
|
this.h = this.saved_geometry["h"];
|
|
this.debug("geometry", "restore_geometry() saved-geometry=", this.saved_geometry);
|
|
// delete saved geometry
|
|
this.saved_geometry = null;
|
|
// then call local resized callback
|
|
this.handle_resized();
|
|
this.focus();
|
|
}
|
|
|
|
/**
|
|
* Maximize / unmaximizes the window.
|
|
*/
|
|
set_maximized(maximized) {
|
|
if (jQuery(this.div).is(":hidden")) {
|
|
jQuery(this.div).show();
|
|
}
|
|
|
|
if (this.maximized === maximized) {
|
|
return;
|
|
}
|
|
this.max_save_restore(maximized);
|
|
this.maximized = maximized;
|
|
this.handle_resized();
|
|
this.focus();
|
|
// this will take care of disabling the "draggable" code:
|
|
this.apply_size_constraints();
|
|
}
|
|
|
|
/**
|
|
* Toggle maximized state
|
|
*/
|
|
toggle_maximized() {
|
|
this.set_maximized(!this.maximized);
|
|
}
|
|
|
|
/**
|
|
* Minimizes / unminimizes the window.
|
|
*/
|
|
set_minimized(minimized) {
|
|
if (this.minimized === minimized) {
|
|
return;
|
|
}
|
|
this.minimized = minimized;
|
|
if (minimized) {
|
|
jQuery(this.div).hide(200);
|
|
} else {
|
|
jQuery(this.div).show(200);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Toggle minimized state
|
|
*/
|
|
toggle_minimized() {
|
|
//get the geometry before modifying the window:
|
|
const geom = this.get_internal_geometry();
|
|
this.set_minimized(!this.minimized);
|
|
if (this.minimized) {
|
|
this.client.send([PACKET_TYPES.unmap_window, this.wid, true]);
|
|
this.stacking_layer = 0;
|
|
if (this.client.focused_wid === this.wid) {
|
|
this.client.auto_focus();
|
|
}
|
|
} else {
|
|
this.client.send([PACKET_TYPES.map_window, this.wid, geom.x, geom.y, geom.w, geom.h, this.client_properties]);
|
|
//ugly force focus switch:
|
|
this.client.focused_wid = 0;
|
|
this.client.set_focus(this);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Fullscreen / unfullscreen the window.
|
|
*/
|
|
set_fullscreen(fullscreen) {
|
|
//the browser itself:
|
|
//we can't bring attention to the fullscreen widget,
|
|
//ie: $("#fullscreen").fadeIn(100).fadeOut(100).fadeIn(100).fadeOut(100).fadeIn(100);
|
|
//because the window is about to cover the top bar...
|
|
//so just fullscreen the window:
|
|
if (this.fullscreen === fullscreen) {
|
|
return;
|
|
}
|
|
if (this.resizable) {
|
|
if (fullscreen) {
|
|
this._set_decorated(false);
|
|
} else {
|
|
this._set_decorated(this.decorations);
|
|
}
|
|
}
|
|
this.max_save_restore(fullscreen);
|
|
this.fullscreen = fullscreen;
|
|
this.updateCSSGeometry();
|
|
this.handle_resized();
|
|
this.focus();
|
|
}
|
|
|
|
_set_decorated(decorated) {
|
|
this.decorated = decorated;
|
|
const head = document.getElementById("head"+this.wid);
|
|
if (decorated) {
|
|
head.style.display = 'block';
|
|
jQuery(this.div).removeClass("undecorated");
|
|
jQuery(this.div).addClass("window");
|
|
} else {
|
|
head.style.display = 'none';
|
|
jQuery(this.div).removeClass("window");
|
|
jQuery(this.div).addClass("undecorated");
|
|
}
|
|
this.update_offsets();
|
|
}
|
|
|
|
/**
|
|
* Either:
|
|
* - save the geometry and use all the space
|
|
* - or restore the geometry
|
|
*/
|
|
max_save_restore(use_all_space) {
|
|
if (use_all_space) {
|
|
this.save_geometry();
|
|
this.fill_screen();
|
|
} else {
|
|
this.restore_geometry();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Use up all the available screen space
|
|
*/
|
|
fill_screen() {
|
|
// should be as simple as this
|
|
// in future we may have a taskbar for minimized windows
|
|
// which should be subtracted from screen size
|
|
const screen_size = this.client._get_desktop_size();
|
|
this.x = this.leftoffset;
|
|
this.y = this.topoffset;
|
|
this.w = screen_size[0] - this.leftoffset - this.rightoffset;
|
|
this.h = screen_size[1] - this.topoffset - this.bottomoffset - TASKBAR_HEIGHT;
|
|
this.debug("geometry", "fill_screen() ", this.x, this.y, this.w, this.h);
|
|
}
|
|
|
|
/**
|
|
* We have resized the window, so we need to:
|
|
* - work out new position of internal canvas
|
|
* - update external CSS position
|
|
* - resize the backing image
|
|
* - fire the geometry_cb
|
|
*/
|
|
handle_resized(e) {
|
|
// this function is called on local resize only,
|
|
// remote resize will call this.resize()
|
|
// need to update the internal geometry
|
|
this.debug("geometry", "handle_resized(", e, ")");
|
|
if (e) {
|
|
this.x = this.x + Math.round(e.position.left - e.originalPosition.left);
|
|
this.y = this.y + Math.round(e.position.top - e.originalPosition.top);
|
|
this.w = Math.round(e.size.width) - this.leftoffset - this.rightoffset;
|
|
this.h = Math.round(e.size.height) - this.topoffset - this.bottomoffset;
|
|
}
|
|
// then update CSS and redraw backing
|
|
this.updateCSSGeometry();
|
|
// send geometry callback
|
|
this.geometry_cb(this);
|
|
}
|
|
|
|
/**
|
|
* Like handle_resized, except we should
|
|
* store internal geometry, external is always in CSS left and top
|
|
*/
|
|
handle_moved(e) {
|
|
const left = Math.round(e.position.left);
|
|
const top = Math.round(e.position.top);
|
|
this.debug("geometry", "handle_moved(", e, ") left=", left, ", top=", top);
|
|
// add on padding to the event position so that
|
|
// it reflects the internal geometry of the canvas
|
|
this.x = left + this.leftoffset;
|
|
this.y = top + this.topoffset;
|
|
// make sure we are visible after move
|
|
this.ensure_visible();
|
|
// tell remote we have moved window
|
|
this.geometry_cb(this);
|
|
}
|
|
|
|
/**
|
|
* The "screen" has been resized, we may need to resize our window to match
|
|
* if it is fullscreen or maximized.
|
|
*/
|
|
screen_resized() {
|
|
this.debug("geometry", "screen_resized() server_is_desktop=", this.client.server_is_desktop,
|
|
", server_is_shadow=", this.client.server_is_shadow);
|
|
if (this.client.server_is_desktop) {
|
|
this.match_screen_size();
|
|
}
|
|
if (this.client.server_is_shadow) {
|
|
//note: when this window is created,
|
|
// it may not have been added to the client's list yet
|
|
const ids = Object.keys(this.client.id_to_window);
|
|
if (ids.length === 0 || ids[0] === this.wid) {
|
|
//single window, recenter it:
|
|
this.recenter();
|
|
}
|
|
}
|
|
if (this.fullscreen || this.maximized) {
|
|
this.fill_screen();
|
|
this.handle_resized();
|
|
}
|
|
if (!this.ensure_visible()) {
|
|
this.geometry_cb(this);
|
|
}
|
|
}
|
|
|
|
recenter(force_update_geometry) {
|
|
let x = this.x;
|
|
let y = this.y;
|
|
this.debug("geometry", "recenter() x=", x, ", y=", y, ", desktop size: ", this.client.desktop_width, this.client.desktop_height);
|
|
x = Math.round((this.client.desktop_width - this.w) / 2);
|
|
y = Math.round((this.client.desktop_height - this.h) / 2);
|
|
if (this.x !== x || this.y !== y || force_update_geometry) {
|
|
this.debug("geometry", "window re-centered to:", x, y);
|
|
this.x = x;
|
|
this.y = y;
|
|
this.updateCSSGeometry();
|
|
this.geometry_cb(this);
|
|
} else {
|
|
this.debug("geometry", "recenter() unchanged at ", x, y);
|
|
}
|
|
if (this.x < 0 || this.y < 0) {
|
|
this.warn("window does not fit in canvas, offsets: ", x, y);
|
|
}
|
|
}
|
|
|
|
match_screen_size() {
|
|
const maxw = this.client.desktop_width;
|
|
const maxh = this.client.desktop_height;
|
|
let neww = 0;
|
|
let newh = 0;
|
|
if (this.client.server_resize_exact) {
|
|
neww = maxw;
|
|
newh = maxh;
|
|
this.log("resizing to exact size:", neww, newh);
|
|
} else {
|
|
if (this.client.server_screen_sizes.length === 0) {
|
|
this.recenter();
|
|
return;
|
|
}
|
|
//try to find the best screen size to use,
|
|
//cannot be larger than the browser area
|
|
let best = 0;
|
|
let w = 0;
|
|
let h = 0;
|
|
const screen_sizes = this.client.server_screen_sizes;
|
|
for (const screen_size of screen_sizes) {
|
|
w = screen_size[0];
|
|
h = screen_size[1];
|
|
if (w <= maxw && h <= maxh && w * h > best) {
|
|
best = w * h;
|
|
neww = w;
|
|
newh = h;
|
|
}
|
|
}
|
|
if (neww === 0 && newh === 0) {
|
|
//not found, try to find the smallest one:
|
|
best = 0;
|
|
for (const screen_size of screen_sizes) {
|
|
w = screen_size[0];
|
|
h = screen_size[1];
|
|
if (best === 0 || w * h < best) {
|
|
best = w * h;
|
|
neww = w;
|
|
newh = h;
|
|
}
|
|
}
|
|
}
|
|
this.log("best screen size:", neww, newh);
|
|
}
|
|
this.w = neww;
|
|
this.h = newh;
|
|
this.recenter(true);
|
|
}
|
|
|
|
/**
|
|
* Things ported from original shape
|
|
*/
|
|
|
|
move_resize(x, y, w, h) {
|
|
this.debug("geometry", "move_resize(", x, y, w, h, ")");
|
|
// only do it if actually changed!
|
|
if (this.w !== w || this.h !== h || this.x !== x || this.y !== y) {
|
|
this.w = w;
|
|
this.h = h;
|
|
this.x = x;
|
|
this.y = y;
|
|
if (!this.ensure_visible()) {
|
|
// we had to move the window so that it was visible
|
|
// is this the right thing to do?
|
|
this.geometry_cb(this);
|
|
} else {
|
|
this.updateCSSGeometry();
|
|
}
|
|
}
|
|
}
|
|
|
|
move(x, y) {
|
|
this.debug("geometry", "move(", x, y, ")");
|
|
this.move_resize(x, y, this.w, this.h);
|
|
}
|
|
|
|
resize(w, h) {
|
|
this.debug("geometry", "resize(", w, h, ")");
|
|
this.move_resize(this.x, this.y, w, h);
|
|
}
|
|
|
|
initiate_moveresize(mousedown_event, x_root, y_root, direction, button, source_indication) {
|
|
const dir_str = MOVERESIZE_DIRECTION_STRING[direction];
|
|
this.log("initiate_moveresize", dir_str, [x_root, y_root, direction, button, source_indication]);
|
|
if (direction === MOVERESIZE_MOVE && mousedown_event) {
|
|
const e = mousedown_event;
|
|
e.type = "mousedown.draggable";
|
|
e.target = this.div[0];
|
|
jQuery(this.div).trigger(e);
|
|
} else if (direction === MOVERESIZE_CANCEL) {
|
|
jQuery(this.div).draggable("disable");
|
|
jQuery(this.div).draggable("enable");
|
|
} else if (direction in MOVERESIZE_DIRECTION_JS_NAME) {
|
|
const js_dir = MOVERESIZE_DIRECTION_JS_NAME[direction];
|
|
const resize_widget = jQuery(this.div)
|
|
.find(`.ui-resizable-handle.ui-resizable-${js_dir}`)
|
|
.first();
|
|
if (resize_widget) {
|
|
const pageX = resize_widget.offset().left;
|
|
const pageY = resize_widget.offset().top;
|
|
resize_widget.trigger("mouseover");
|
|
resize_widget.trigger({
|
|
type: "mousedown",
|
|
which: 1,
|
|
pageX,
|
|
pageY,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the geometry of the window backing image,
|
|
* the inner window geometry (without any borders or top bar).
|
|
*/
|
|
get_internal_geometry() {
|
|
/* we store the internal geometry only
|
|
* and work out external geometry on the fly whilst
|
|
* updating CSS
|
|
*/
|
|
return {
|
|
x: this.x,
|
|
y: this.y,
|
|
w: this.w,
|
|
h: this.h,
|
|
};
|
|
}
|
|
|
|
update_icon(width, height, encoding, img_data) {
|
|
// Cache the icon.
|
|
this.icon = {
|
|
width,
|
|
height,
|
|
encoding,
|
|
img_data,
|
|
};
|
|
|
|
let source = "favicon.png";
|
|
if (encoding === "png") {
|
|
//move title to the right:
|
|
$(`#title${this.wid}`).css("left", 32);
|
|
if (typeof img_data === "string") {
|
|
const uint = new Uint8Array(img_data.length);
|
|
for (let index = 0; index < img_data.length; ++index) {
|
|
uint[index] = img_data.charCodeAt(index);
|
|
}
|
|
img_data = uint;
|
|
}
|
|
source = this.construct_base64_image_url(encoding, img_data);
|
|
}
|
|
jQuery(`#windowicon${this.wid}`).attr("src", source);
|
|
jQuery(`#windowlistitemicon${this.wid}`).attr("src", source);
|
|
return source;
|
|
}
|
|
|
|
reset_cursor() {
|
|
jQuery(`#${this.wid}`).css("cursor", "default");
|
|
this.cursor_data = null;
|
|
}
|
|
|
|
set_cursor(encoding, w, h, xhot, yhot, img_data) {
|
|
if (encoding !== "png") {
|
|
this.warn("received an invalid cursor encoding:", encoding);
|
|
return;
|
|
}
|
|
const window_element = jQuery(`#${this.wid}`);
|
|
const cursor_url = this.construct_base64_image_url(encoding, img_data);
|
|
const me = this;
|
|
|
|
function set_cursor_url(url, x, y, w, h) {
|
|
const url_string = `url('${url}')`;
|
|
window_element.css("cursor", `${url_string}, default`);
|
|
//CSS3 with hotspot:
|
|
window_element.css("cursor", `${url_string} ${x} ${y}, auto`);
|
|
me.cursor_data = [url, x, y, w, h];
|
|
}
|
|
let zoom = detectZoom.zoom();
|
|
//prefer fractional zoom values if possible:
|
|
if (Math.round(zoom * 4) === 2 * Math.round(zoom * 2)) {
|
|
zoom = Math.round(zoom * 2) / 2;
|
|
}
|
|
if (zoom !== 1 && !Utilities.isMacOS()) {
|
|
//scale it:
|
|
this.debug("geometry", "scaling cursor by zoom factor:", zoom);
|
|
const temporary_img = new Image();
|
|
temporary_img.addEventListener("load", () => {
|
|
const canvas = document.createElement("canvas");
|
|
const context = canvas.getContext("2d");
|
|
context.imageSmoothingEnabled = false;
|
|
canvas.width = Math.round(w * window.devicePixelRatio);
|
|
canvas.height = Math.round(h * window.devicePixelRatio);
|
|
context.drawImage(temporary_img, 0, 0, canvas.width, canvas.height);
|
|
const scaled_cursor_url = canvas.toDataURL();
|
|
set_cursor_url(
|
|
scaled_cursor_url,
|
|
Math.round(xhot * window.devicePixelRatio),
|
|
Math.round(yhot * window.devicePixelRatio),
|
|
Math.round(canvas.width),
|
|
Math.round(canvas.height),
|
|
);
|
|
});
|
|
temporary_img.src = cursor_url;
|
|
} else {
|
|
set_cursor_url(cursor_url, xhot, yhot, w, h);
|
|
}
|
|
}
|
|
|
|
eos() {
|
|
//we don't handle video streams in this class,
|
|
//so this should never be called
|
|
}
|
|
|
|
/**
|
|
* This function draws the contents of the off-screen canvas to the visible
|
|
* canvas. However the drawing is requested by requestAnimationFrame which allows
|
|
* the browser to group screen redraws together, and automatically adjusts the
|
|
* framerate e.g if the browser window/tab is not visible.
|
|
*/
|
|
draw() {
|
|
//pass the 'buffer' canvas directly to visible canvas context
|
|
if (this.has_alpha || this.tray) {
|
|
this.canvas_ctx.clearRect(0, 0, this.draw_canvas.width, this.draw_canvas.height);
|
|
}
|
|
this.canvas_ctx.drawImage(this.draw_canvas, 0, 0);
|
|
}
|
|
|
|
/**
|
|
* Updates the window image with new pixel data
|
|
* we have received from the server.
|
|
* The image is painted into off-screen canvas.
|
|
*/
|
|
paint() {
|
|
if (this.client.decode_worker) {
|
|
//no need to synchronize paint packets here
|
|
//the decode worker ensures that we get the packets
|
|
//in the correct order, ready to update the canvas
|
|
Reflect.apply(this.do_paint, this, arguments);
|
|
return;
|
|
}
|
|
//process all paint request in order using the paint_queue:
|
|
const item = Array.prototype.slice.call(arguments);
|
|
this.paint_queue.push(item);
|
|
this.may_paint_now();
|
|
}
|
|
|
|
/**
|
|
* Pick items from the paint_queue
|
|
* if we're not already in the process of painting something.
|
|
*/
|
|
may_paint_now() {
|
|
this.debug("draw", "may_paint_now() paint pending=", this.paint_pending,
|
|
", paint queue length=", this.paint_queue.length);
|
|
let now = performance.now();
|
|
while (
|
|
(this.paint_pending === 0 || now - this.paint_pending >= 2000) &&
|
|
this.paint_queue.length > 0
|
|
) {
|
|
this.paint_pending = now;
|
|
const item = this.paint_queue.shift();
|
|
this.do_paint.apply(this, item);
|
|
now = performance.now();
|
|
}
|
|
}
|
|
|
|
paint_box(color, px, py, pw, ph) {
|
|
this.offscreen_canvas_ctx.strokeStyle = color;
|
|
this.offscreen_canvas_ctx.lineWidth = 2;
|
|
this.offscreen_canvas_ctx.strokeRect(px, py, pw, ph);
|
|
}
|
|
|
|
do_paint(packet, decode_callback) {
|
|
const me = this;
|
|
|
|
const x = packet[2];
|
|
const y = packet[3];
|
|
const width = packet[4];
|
|
const height = packet[5];
|
|
const img_data = packet[7];
|
|
const options = packet[10] || {};
|
|
let coding = Utilities.s(packet[6]);
|
|
let enc_width = width;
|
|
let enc_height = height;
|
|
const scaled_size = options["scaled_size"];
|
|
if (scaled_size) {
|
|
enc_width = scaled_size[0];
|
|
enc_height = scaled_size[1];
|
|
}
|
|
const bitmap = coding.startsWith("bitmap:");
|
|
if (bitmap) {
|
|
coding = coding.split(":")[1];
|
|
this.debug("draw", coding, img_data, " at ", `${x},${y}`, ") focused=", this.focused);
|
|
} else {
|
|
this.debug("draw", "do_paint(", img_data.length, " bytes of", coding, " data ", width, "x", height,
|
|
" at ", x, ",", y, ") focused=", this.focused);
|
|
}
|
|
|
|
function painted(skip_box) {
|
|
me.paint_pending = 0;
|
|
if (!skip_box && me.debug_categories.includes("draw")) {
|
|
const color = DEFAULT_BOX_COLORS[coding] || "white";
|
|
me.paint_box(color, x, y, width, height);
|
|
}
|
|
decode_callback();
|
|
}
|
|
|
|
function paint_error(e) {
|
|
me.error("error painting", coding, e);
|
|
me.paint_pending = 0;
|
|
decode_callback(`${e}`);
|
|
}
|
|
|
|
function paint_bitmap() {
|
|
//the decode worker is giving us a Bitmap object ready to use:
|
|
me.offscreen_canvas_ctx.clearRect(x, y, img_data.width, img_data.height);
|
|
me.offscreen_canvas_ctx.drawImage(img_data, x, y);
|
|
painted();
|
|
//this isn't really needed since we don't use the paint_queue at all
|
|
//when decoding in the worker (bitmaps can only come from the decode worker)
|
|
me.may_paint_now();
|
|
}
|
|
|
|
try {
|
|
if (coding === "void") {
|
|
painted(true);
|
|
this.may_paint_now();
|
|
} else if (coding === "rgb32" || coding === "rgb24") {
|
|
if (bitmap) {
|
|
paint_bitmap();
|
|
return;
|
|
}
|
|
const rgb_data = decode_rgb(packet);
|
|
const img = this.offscreen_canvas_ctx.createImageData(enc_width, enc_height);
|
|
img.data.set(rgb_data);
|
|
this.offscreen_canvas_ctx.putImageData(img, x, y, 0, 0, width, height);
|
|
painted();
|
|
this.may_paint_now();
|
|
} else if (coding === "jpeg" || coding.startsWith("png") || coding === "webp") {
|
|
if (bitmap) {
|
|
paint_bitmap();
|
|
return;
|
|
}
|
|
const image = new Image();
|
|
image.addEventListener("load", () => {
|
|
if (image.width === 0 || image.height === 0) {
|
|
paint_error(`invalid image size: ${image.width}x${image.height}`);
|
|
} else {
|
|
this.offscreen_canvas_ctx.clearRect(x, y, width, height);
|
|
this.offscreen_canvas_ctx.drawImage(image, x, y, width, height);
|
|
painted();
|
|
}
|
|
this.may_paint_now();
|
|
});
|
|
image.onerror = () => {
|
|
paint_error(`failed to load ${coding} into image tag`);
|
|
this.may_paint_now();
|
|
};
|
|
const paint_coding = coding.split("/")[0]; //ie: "png/P" -> "png"
|
|
image.src = this.construct_base64_image_url(paint_coding, img_data);
|
|
} else if (coding === "h264") {
|
|
paint_error("h264 decoding is only supported via the decode workers");
|
|
this.may_paint_now();
|
|
} else if (coding === "scroll") {
|
|
// newer servers use options,
|
|
// older ones overload the image data:
|
|
const scrolls = options["scroll"] || img_data;
|
|
for (let index = 0, stop = scrolls.length; index < stop; ++index) {
|
|
const scroll_data = scrolls[index];
|
|
this.debug("draw", "scroll", index, ":", scroll_data);
|
|
const sx = scroll_data[0];
|
|
const sy = scroll_data[1];
|
|
const sw = scroll_data[2];
|
|
const sh = scroll_data[3];
|
|
const xdelta = scroll_data[4];
|
|
const ydelta = scroll_data[5];
|
|
this.offscreen_canvas_ctx.drawImage(
|
|
this.draw_canvas,
|
|
sx, sy, sw, sh,
|
|
sx + xdelta, sy + ydelta, sw, sh
|
|
);
|
|
if (this.debug_categories.includes("draw")) {
|
|
this.paint_box("brown", sx + xdelta, sy + ydelta, sw, sh);
|
|
}
|
|
}
|
|
painted(true);
|
|
this.may_paint_now();
|
|
} else {
|
|
paint_error("unsupported encoding");
|
|
}
|
|
} catch (error) {
|
|
const packet_sequence = packet[8];
|
|
this.exc(error, "error painting", coding, "sequence no", packet_sequence);
|
|
paint_error(error);
|
|
}
|
|
}
|
|
|
|
construct_base64_image_url(encoding, imageDataArrayBuffer) {
|
|
const imageDataBase64 = Utilities.ArrayBufferToBase64(imageDataArrayBuffer);
|
|
return `data:image/${encoding};base64,${imageDataBase64}`;
|
|
}
|
|
|
|
/**
|
|
* Close the window and free all resources
|
|
*/
|
|
destroy() {
|
|
// remove div
|
|
this.div.remove();
|
|
}
|
|
}
|