You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1833 lines
72 KiB
1833 lines
72 KiB
(function (global) {
|
|
'use strict';
|
|
|
|
var STORAGE_KEY = 'preview-workbench-layout-v7';
|
|
var PANEL_MEM_KEY = 'preview-workbench-panel-mem-v1';
|
|
var COMPACT_W = 960;
|
|
var COMPACT_SHORT = 720;
|
|
var PANEL_TITLES = {
|
|
hierarchy: '层级',
|
|
inspector: '属性',
|
|
assets: '资源',
|
|
perf: '性能',
|
|
console: '控制台',
|
|
};
|
|
var SPLIT_SIZE = 4;
|
|
|
|
/**
|
|
* Creator 风格递归停靠布局:
|
|
* - 拖到标题栏/中心区 = 合并为同一区域页签
|
|
* - 拖到区域边沿 = 在该方向新建布局区域
|
|
* @constructor
|
|
*/
|
|
function WorkbenchLayout(root) {
|
|
this.root = root;
|
|
this.workspace = document.getElementById('pw-workspace');
|
|
this.gameHost = document.getElementById('pw-game-host');
|
|
this.dropOverlay = document.getElementById('pw-drop-overlay');
|
|
this.mode = 'desktop';
|
|
this.preset = 'analyze';
|
|
this.activeDrawer = '';
|
|
this._panelBodies = {};
|
|
this._panelMeta = {};
|
|
this._tree = null;
|
|
this._leafEls = {};
|
|
this._nodeEls = {};
|
|
this._dragPanel = null;
|
|
this._hoverTargetId = null;
|
|
this._hoverEdge = null;
|
|
this._hoverGroup = null;
|
|
this._hoverGroupCols = null;
|
|
this._splitDrag = null;
|
|
this._leafSeq = 1;
|
|
/** 用户是否已改过布局(拖拽/开关面板);未改动时引擎就绪可按横竖屏重套默认 */
|
|
this._dirty = false;
|
|
/** @type {Object.<string, object>} 面板开关记忆 */
|
|
this._panelMemory = {};
|
|
/** @type {Object.<string, {x:number,y:number,w:number,h:number,el?:HTMLElement}>} */
|
|
this._floats = {};
|
|
this._floatLayer = null;
|
|
this._floatDrag = null;
|
|
/** @type {function(Object.<string,boolean>):void|null} */
|
|
this.onVisibilityChange = null;
|
|
/** @type {HTMLElement|null} 游戏占位格(iframe 永不插入此处) */
|
|
this._gamePlaceholder = null;
|
|
/** 关闭工作台时隐藏调试面板,仍保留设备预览选项(默认开,由 Host 按偏好关闭) */
|
|
this._workbenchOff = false;
|
|
this._treeBeforeOff = null;
|
|
this._onResize = this._onWindowResize.bind(this);
|
|
this._onDragOver = this._onDocDragOver.bind(this);
|
|
this._onDrop = this._onDocDrop.bind(this);
|
|
this._onDragEnd = this._onDocDragEnd.bind(this);
|
|
this._onFloatMove = this._onFloatMove.bind(this);
|
|
this._onFloatUp = this._onFloatUp.bind(this);
|
|
}
|
|
|
|
WorkbenchLayout.prototype.init = function () {
|
|
this._load();
|
|
this._ensureFloatLayer();
|
|
if (!this._tree) this._tree = this._presetTree(this.preset);
|
|
this._bindToolbar();
|
|
window.addEventListener('resize', this._onResize);
|
|
document.addEventListener('dragover', this._onDragOver);
|
|
document.addEventListener('drop', this._onDrop);
|
|
document.addEventListener('dragend', this._onDragEnd);
|
|
this.refreshMode();
|
|
this.rebuild();
|
|
this._restoreFloats();
|
|
};
|
|
|
|
WorkbenchLayout.prototype._notifyViewResize = function () {
|
|
if (global.__PreviewWorkbench && global.__PreviewWorkbench.device) {
|
|
global.__PreviewWorkbench.device.apply(false);
|
|
} else if (global.__PW_Bridge) {
|
|
global.__PW_Bridge.call('device.apply', { force: true }).catch(function () { /* */ });
|
|
}
|
|
};
|
|
|
|
/** @returns {Object.<string,boolean>} */
|
|
WorkbenchLayout.prototype.getVisiblePanels = function () {
|
|
var map = {};
|
|
var ids = ['hierarchy', 'inspector', 'assets', 'perf', 'console'];
|
|
for (var i = 0; i < ids.length; i++) {
|
|
map[ids[i]] = this.isPanelVisible(ids[i]);
|
|
}
|
|
return map;
|
|
};
|
|
|
|
/** @param {string} panelId */
|
|
WorkbenchLayout.prototype.isPanelVisible = function (panelId) {
|
|
if (this._workbenchOff) return false;
|
|
if (this.mode === 'compact') return this.activeDrawer === panelId;
|
|
if (this._floats[panelId]) {
|
|
var f = this._floats[panelId];
|
|
if (f && f.el && f.el.hidden) return false;
|
|
return true;
|
|
}
|
|
return !!this._findTabsWith(this._tree, panelId);
|
|
};
|
|
|
|
WorkbenchLayout.prototype._emitVisibility = function () {
|
|
if (typeof this.onVisibilityChange === 'function') {
|
|
this.onVisibilityChange(this.getVisiblePanels());
|
|
}
|
|
};
|
|
|
|
WorkbenchLayout.prototype.registerPanel = function (id, el, title) {
|
|
this._panelBodies[id] = el;
|
|
this._panelMeta[id] = { title: title || PANEL_TITLES[id] || id };
|
|
if (!el.classList.contains('pw-panel-root')) {
|
|
el.classList.add('pw-panel-root');
|
|
}
|
|
// 去掉面板自带标题(区域页签负责标题)
|
|
var head = null;
|
|
for (var i = 0; i < el.children.length; i++) {
|
|
if (el.children[i].classList.contains('pw-panel-head')) {
|
|
head = el.children[i];
|
|
break;
|
|
}
|
|
}
|
|
if (head) {
|
|
// 保留工具条到面板顶部工具行
|
|
var tools = head.querySelector('.pw-panel-tools');
|
|
if (tools && tools.childNodes.length) {
|
|
var bar = document.createElement('div');
|
|
bar.className = 'pw-panel-toolbar';
|
|
while (tools.firstChild) bar.appendChild(tools.firstChild);
|
|
head.parentNode.insertBefore(bar, head);
|
|
}
|
|
head.parentNode.removeChild(head);
|
|
}
|
|
};
|
|
|
|
WorkbenchLayout.prototype.refreshMode = function () {
|
|
var w = window.innerWidth;
|
|
var h = window.innerHeight;
|
|
var short = Math.min(w, h);
|
|
var compact = w < COMPACT_W || short < COMPACT_SHORT;
|
|
this.mode = compact ? 'compact' : 'desktop';
|
|
this.root.classList.toggle('pw-mode-compact', compact);
|
|
this.root.classList.toggle('pw-mode-desktop', !compact);
|
|
this.root.classList.toggle('pw-compact-portrait', compact && h >= w);
|
|
this.root.classList.toggle('pw-compact-landscape', compact && h < w);
|
|
if (!compact) this.closeDrawer();
|
|
};
|
|
|
|
WorkbenchLayout.prototype.applyPreset = function (name, persist) {
|
|
this.preset = name || 'analyze';
|
|
this._dirty = false;
|
|
this._tree = this._presetTree(this.preset);
|
|
var sel = document.getElementById('pw-layout-preset');
|
|
if (sel) sel.value = this.preset;
|
|
this.rebuild();
|
|
if (persist !== false) this._save();
|
|
};
|
|
|
|
WorkbenchLayout.prototype.resetLayout = function () {
|
|
try { localStorage.removeItem(STORAGE_KEY); } catch (e) { /* */ }
|
|
this._dirty = false;
|
|
this.applyPreset(this.preset || 'analyze', true);
|
|
};
|
|
|
|
/**
|
|
* 引擎就绪后:未手动改布局时,按设计分辨率横竖屏重套「分析」默认。
|
|
*/
|
|
WorkbenchLayout.prototype.onEngineReady = function () {
|
|
if (this._dirty) return;
|
|
if (this.preset !== 'analyze' && this.preset !== 'portrait' && this.preset !== 'landscape') return;
|
|
if (this.preset === 'analyze') {
|
|
var next = this._presetTree('analyze');
|
|
// 工作台关闭时只更新备份,避免与「仅游戏」树冲突导致布局抖动
|
|
if (this._workbenchOff) {
|
|
this._treeBeforeOff = next;
|
|
return;
|
|
}
|
|
this._tree = next;
|
|
this.rebuild();
|
|
this._save();
|
|
}
|
|
};
|
|
|
|
/** 设计分辨率是否竖屏(高 ≥ 宽) */
|
|
WorkbenchLayout.prototype._isPortraitGame = function () {
|
|
var cc = global.cc;
|
|
try {
|
|
if (cc && cc.view && typeof cc.view.getDesignResolutionSize === 'function') {
|
|
var dr = cc.view.getDesignResolutionSize();
|
|
if (dr && dr.width > 0 && dr.height > 0) return dr.height >= dr.width;
|
|
}
|
|
} catch (e) { /* */ }
|
|
// 本项目默认 750×1600
|
|
return true;
|
|
};
|
|
|
|
WorkbenchLayout.prototype._presetTree = function (name) {
|
|
if (name === 'scene') {
|
|
return {
|
|
type: 'split', dir: 'h', sizes: [0.22, 0.56, 0.22],
|
|
children: [
|
|
this._tabsNode(['hierarchy']),
|
|
this._gameNode(),
|
|
this._tabsNode(['inspector']),
|
|
],
|
|
};
|
|
}
|
|
if (name === 'minimal') {
|
|
return {
|
|
type: 'split', dir: 'v', sizes: [0.78, 0.22],
|
|
children: [
|
|
this._gameNode(),
|
|
this._tabsNode(['perf']),
|
|
],
|
|
};
|
|
}
|
|
if (name === 'portrait') return this._portraitAnalyzeTree();
|
|
if (name === 'landscape') return this._landscapeAnalyzeTree();
|
|
// analyze:按游戏设计分辨率自动选竖屏/横屏默认
|
|
return this._isPortraitGame() ? this._portraitAnalyzeTree() : this._landscapeAnalyzeTree();
|
|
};
|
|
|
|
/**
|
|
* 竖屏默认(图1):
|
|
* 左上 = 层级 | 属性
|
|
* 左下 = 控制台(横跨层级+属性两列下方)
|
|
* 中 = 游戏,右 = 性能
|
|
*/
|
|
WorkbenchLayout.prototype._portraitAnalyzeTree = function () {
|
|
return {
|
|
type: 'split', dir: 'h', sizes: [0.34, 0.46, 0.20],
|
|
children: [
|
|
{
|
|
type: 'split', dir: 'v', sizes: [0.58, 0.42],
|
|
children: [
|
|
{
|
|
type: 'split', dir: 'h', sizes: [0.5, 0.5],
|
|
children: [
|
|
this._tabsNode(['hierarchy']),
|
|
this._tabsNode(['inspector']),
|
|
],
|
|
},
|
|
this._tabsNode(['console']),
|
|
],
|
|
},
|
|
this._gameNode(),
|
|
this._tabsNode(['perf']),
|
|
],
|
|
};
|
|
};
|
|
|
|
/**
|
|
* 横屏默认(图2):上=层级|属性|游戏|性能,下=控制台通栏
|
|
*/
|
|
WorkbenchLayout.prototype._landscapeAnalyzeTree = function () {
|
|
return {
|
|
type: 'split', dir: 'v', sizes: [0.72, 0.28],
|
|
children: [
|
|
{
|
|
type: 'split', dir: 'h', sizes: [0.14, 0.16, 0.50, 0.20],
|
|
children: [
|
|
this._tabsNode(['hierarchy']),
|
|
this._tabsNode(['inspector']),
|
|
this._gameNode(),
|
|
this._tabsNode(['perf']),
|
|
],
|
|
},
|
|
this._tabsNode(['console']),
|
|
],
|
|
};
|
|
};
|
|
|
|
WorkbenchLayout.prototype._tabsNode = function (panels) {
|
|
return {
|
|
type: 'tabs',
|
|
id: 'leaf_' + (this._leafSeq++),
|
|
panels: panels.slice(),
|
|
active: panels[0] || '',
|
|
};
|
|
};
|
|
|
|
WorkbenchLayout.prototype._gameNode = function () {
|
|
return { type: 'game', id: 'leaf_game' };
|
|
};
|
|
|
|
WorkbenchLayout.prototype._ensureId = function (node) {
|
|
if (!node) return;
|
|
if (node.type === 'game') {
|
|
node.id = 'leaf_game';
|
|
return;
|
|
}
|
|
if (!node.id) node.id = (node.type === 'split' ? 'split_' : 'leaf_') + (this._leafSeq++);
|
|
};
|
|
|
|
WorkbenchLayout.prototype.rebuild = function () {
|
|
if (!this.workspace) return;
|
|
// 只卸面板 DOM;游戏 iframe 宿主永不搬移(搬移会导致 iframe 整页重载)
|
|
for (var id in this._panelBodies) {
|
|
if (!Object.prototype.hasOwnProperty.call(this._panelBodies, id)) continue;
|
|
if (this._floats[id]) continue;
|
|
var p = this._panelBodies[id];
|
|
if (p && p.parentNode) p.parentNode.removeChild(p);
|
|
}
|
|
this._ensureGameHostParked();
|
|
this._gamePlaceholder = null;
|
|
this._leafEls = {};
|
|
this._nodeEls = {};
|
|
while (this.workspace.firstChild) this.workspace.removeChild(this.workspace.firstChild);
|
|
if (this.mode === 'compact') {
|
|
if (this.gameHost) this.gameHost.classList.add('pw-game-compact');
|
|
this._syncGameHostGeom();
|
|
this._notifyViewResize();
|
|
this._emitVisibility();
|
|
return;
|
|
}
|
|
if (this.gameHost) this.gameHost.classList.remove('pw-game-compact');
|
|
var el = this._renderNode(this._tree);
|
|
if (el) this.workspace.appendChild(el);
|
|
var floatIds = [];
|
|
for (var fid in this._floats) {
|
|
if (Object.prototype.hasOwnProperty.call(this._floats, fid)) floatIds.push(fid);
|
|
}
|
|
for (var fi = 0; fi < floatIds.length; fi++) {
|
|
var pid = floatIds[fi];
|
|
if (this._findTabsWith(this._tree, pid)) {
|
|
this._closeFloat(pid, false);
|
|
continue;
|
|
}
|
|
var f = this._floats[pid];
|
|
var body = this._panelBodies[pid];
|
|
var fb = f && f.el ? f.el.querySelector('.pw-float-body') : null;
|
|
if (fb && body && body.parentNode !== fb) fb.appendChild(body);
|
|
}
|
|
var self = this;
|
|
this._syncGameHostGeom();
|
|
if (typeof requestAnimationFrame === 'function') {
|
|
requestAnimationFrame(function () { self._syncGameHostGeom(); });
|
|
}
|
|
this._notifyViewResize();
|
|
this._emitVisibility();
|
|
};
|
|
|
|
/** 游戏宿主固定挂在 root 下,禁止插入停靠树(避免 iframe reload) */
|
|
WorkbenchLayout.prototype._ensureGameHostParked = function () {
|
|
if (!this.gameHost || !this.root) return;
|
|
if (this.gameHost.parentNode !== this.root) {
|
|
this.root.appendChild(this.gameHost);
|
|
}
|
|
this.gameHost.classList.add('pw-game-overlay');
|
|
};
|
|
|
|
/** 将 overlay 对齐到占位格(或紧凑/关闭模式下的工作区) */
|
|
WorkbenchLayout.prototype._syncGameHostGeom = function () {
|
|
if (!this.gameHost || !this.root) return;
|
|
this._ensureGameHostParked();
|
|
var rootRect = this.root.getBoundingClientRect();
|
|
var target = null;
|
|
if (this._workbenchOff) {
|
|
target = this.workspace || this.root;
|
|
} else if (this.mode === 'compact') {
|
|
target = this.workspace;
|
|
} else {
|
|
target = this._gamePlaceholder;
|
|
}
|
|
if (!target) {
|
|
this.gameHost.hidden = true;
|
|
return;
|
|
}
|
|
var r = target.getBoundingClientRect();
|
|
if (r.width < 2 || r.height < 2) {
|
|
this.gameHost.hidden = true;
|
|
return;
|
|
}
|
|
this.gameHost.hidden = false;
|
|
this.gameHost.style.pointerEvents = 'auto';
|
|
this.gameHost.style.left = Math.max(0, r.left - rootRect.left) + 'px';
|
|
this.gameHost.style.top = Math.max(0, r.top - rootRect.top) + 'px';
|
|
this.gameHost.style.width = Math.max(1, r.width) + 'px';
|
|
this.gameHost.style.height = Math.max(1, r.height) + 'px';
|
|
var iframe = document.getElementById('pw-game-iframe');
|
|
if (iframe) iframe.style.pointerEvents = 'auto';
|
|
};
|
|
|
|
WorkbenchLayout.prototype._renderNode = function (node) {
|
|
if (!node) return null;
|
|
if (node.type === 'split') return this._renderSplit(node);
|
|
if (node.type === 'game') return this._renderGame(node);
|
|
return this._renderTabs(node);
|
|
};
|
|
|
|
WorkbenchLayout.prototype._renderSplit = function (node) {
|
|
var self = this;
|
|
this._ensureId(node);
|
|
var wrap = document.createElement('div');
|
|
wrap.className = 'pw-split-node pw-split-dir-' + (node.dir === 'v' ? 'v' : 'h');
|
|
wrap.setAttribute('data-node-id', node.id);
|
|
this._nodeEls[node.id] = { el: wrap, node: node };
|
|
var kids = node.children || [];
|
|
var sizes = node.sizes || [];
|
|
this._normalizeSizes(sizes, kids.length);
|
|
|
|
for (var i = 0; i < kids.length; i++) {
|
|
if (i > 0) {
|
|
(function (index) {
|
|
var splitter = document.createElement('div');
|
|
splitter.className = 'pw-splitter ' + (node.dir === 'v' ? 'pw-splitter-v' : 'pw-splitter-h');
|
|
splitter.addEventListener('pointerdown', function (ev) {
|
|
self._beginSplitDrag(ev, node, wrap, index - 1);
|
|
});
|
|
wrap.appendChild(splitter);
|
|
})(i);
|
|
}
|
|
var childWrap = document.createElement('div');
|
|
childWrap.className = 'pw-split-child';
|
|
childWrap.style.flex = String(sizes[i]) + ' 1 0';
|
|
childWrap.style.minWidth = '0';
|
|
childWrap.style.minHeight = '0';
|
|
var childEl = this._renderNode(kids[i]);
|
|
if (childEl) childWrap.appendChild(childEl);
|
|
wrap.appendChild(childWrap);
|
|
}
|
|
return wrap;
|
|
};
|
|
|
|
WorkbenchLayout.prototype._normalizeSizes = function (sizes, n) {
|
|
while (sizes.length < n) sizes.push(1 / n);
|
|
sizes.length = n;
|
|
var sum = 0;
|
|
for (var i = 0; i < n; i++) sum += sizes[i] || 0;
|
|
if (sum <= 0) {
|
|
for (var j = 0; j < n; j++) sizes[j] = 1 / n;
|
|
return;
|
|
}
|
|
for (var k = 0; k < n; k++) sizes[k] = sizes[k] / sum;
|
|
};
|
|
|
|
WorkbenchLayout.prototype._beginSplitDrag = function (ev, node, wrap, leftIndex) {
|
|
ev.preventDefault();
|
|
ev.stopPropagation();
|
|
var children = [];
|
|
for (var i = 0; i < wrap.children.length; i++) {
|
|
if (wrap.children[i].classList.contains('pw-split-child')) children.push(wrap.children[i]);
|
|
}
|
|
if (leftIndex < 0 || leftIndex >= children.length - 1) return;
|
|
var a = children[leftIndex];
|
|
var b = children[leftIndex + 1];
|
|
this._splitDrag = {
|
|
node: node,
|
|
dir: node.dir,
|
|
leftIndex: leftIndex,
|
|
startPos: node.dir === 'h' ? ev.clientX : ev.clientY,
|
|
a0: node.dir === 'h' ? a.getBoundingClientRect().width : a.getBoundingClientRect().height,
|
|
b0: node.dir === 'h' ? b.getBoundingClientRect().width : b.getBoundingClientRect().height,
|
|
aEl: a,
|
|
bEl: b,
|
|
};
|
|
var prevCursor = document.body.style.cursor;
|
|
document.body.style.cursor = node.dir === 'h' ? 'col-resize' : 'row-resize';
|
|
document.body.classList.add('pw-split-dragging');
|
|
// iframe 会吞掉 pointermove;拖动期间禁用命中
|
|
this._setGameIframePassThrough(true);
|
|
var target = ev.currentTarget || ev.target;
|
|
if (target && target.setPointerCapture && ev.pointerId != null) {
|
|
try { target.setPointerCapture(ev.pointerId); } catch (e) { /* */ }
|
|
}
|
|
var self = this;
|
|
var move = function (e) { self._onSplitMove(e); };
|
|
var up = function (e) {
|
|
window.removeEventListener('pointermove', move);
|
|
window.removeEventListener('pointerup', up);
|
|
window.removeEventListener('pointercancel', up);
|
|
if (target && target.releasePointerCapture && e && e.pointerId != null) {
|
|
try { target.releasePointerCapture(e.pointerId); } catch (err) { /* */ }
|
|
}
|
|
document.body.style.cursor = prevCursor || '';
|
|
document.body.classList.remove('pw-split-dragging');
|
|
self._setGameIframePassThrough(false);
|
|
self._splitDrag = null;
|
|
self._dirty = true;
|
|
self._save();
|
|
self._syncGameHostGeom();
|
|
self._notifyViewResize();
|
|
};
|
|
window.addEventListener('pointermove', move);
|
|
window.addEventListener('pointerup', up);
|
|
window.addEventListener('pointercancel', up);
|
|
};
|
|
|
|
/** @param {boolean} on */
|
|
WorkbenchLayout.prototype._setGameIframePassThrough = function (on) {
|
|
var iframe = document.getElementById('pw-game-iframe');
|
|
if (iframe) iframe.style.pointerEvents = on ? 'none' : '';
|
|
var host = this.gameHost;
|
|
if (host) host.classList.toggle('pw-iframe-passthrough', !!on);
|
|
};
|
|
|
|
WorkbenchLayout.prototype._onSplitMove = function (ev) {
|
|
var d = this._splitDrag;
|
|
if (!d) return;
|
|
var delta = (d.dir === 'h' ? ev.clientX : ev.clientY) - d.startPos;
|
|
var total = d.a0 + d.b0;
|
|
if (total <= 1) return;
|
|
var a1 = Math.max(80, Math.min(total - 80, d.a0 + delta));
|
|
var b1 = total - a1;
|
|
var ra = a1 / total;
|
|
var rb = b1 / total;
|
|
var baseA = d.node.sizes[d.leftIndex];
|
|
var baseB = d.node.sizes[d.leftIndex + 1];
|
|
var pair = baseA + baseB;
|
|
d.node.sizes[d.leftIndex] = pair * ra;
|
|
d.node.sizes[d.leftIndex + 1] = pair * rb;
|
|
d.aEl.style.flex = d.node.sizes[d.leftIndex] + ' 1 0';
|
|
d.bEl.style.flex = d.node.sizes[d.leftIndex + 1] + ' 1 0';
|
|
this._syncGameHostGeom();
|
|
};
|
|
|
|
WorkbenchLayout.prototype._renderGame = function (node) {
|
|
this._ensureId(node);
|
|
var leaf = document.createElement('div');
|
|
leaf.className = 'pw-leaf pw-leaf-game';
|
|
leaf.setAttribute('data-leaf-id', node.id);
|
|
leaf.setAttribute('data-node-id', node.id);
|
|
this._leafEls[node.id] = { el: leaf, node: node };
|
|
this._nodeEls[node.id] = { el: leaf, node: node };
|
|
var ph = document.createElement('div');
|
|
ph.className = 'pw-game-placeholder';
|
|
ph.setAttribute('data-game-placeholder', '1');
|
|
leaf.appendChild(ph);
|
|
this._gamePlaceholder = ph;
|
|
return leaf;
|
|
};
|
|
|
|
WorkbenchLayout.prototype._renderTabs = function (node) {
|
|
var self = this;
|
|
this._ensureId(node);
|
|
var leaf = document.createElement('div');
|
|
leaf.className = 'pw-leaf pw-leaf-tabs';
|
|
leaf.setAttribute('data-leaf-id', node.id);
|
|
leaf.setAttribute('data-node-id', node.id);
|
|
this._leafEls[node.id] = { el: leaf, node: node };
|
|
this._nodeEls[node.id] = { el: leaf, node: node };
|
|
|
|
var tabs = document.createElement('div');
|
|
tabs.className = 'pw-leaf-tabbar';
|
|
tabs.setAttribute('data-leaf-id', node.id);
|
|
|
|
var body = document.createElement('div');
|
|
body.className = 'pw-leaf-body';
|
|
body.setAttribute('data-leaf-id', node.id);
|
|
|
|
var panels = node.panels || [];
|
|
if (!panels.length) {
|
|
leaf.appendChild(tabs);
|
|
leaf.appendChild(body);
|
|
return leaf;
|
|
}
|
|
if (panels.indexOf(node.active) < 0) node.active = panels[0];
|
|
|
|
for (var i = 0; i < panels.length; i++) {
|
|
(function (pid) {
|
|
var tab = document.createElement('button');
|
|
tab.type = 'button';
|
|
tab.className = 'pw-leaf-tab' + (pid === node.active ? ' active' : '');
|
|
tab.textContent = PANEL_TITLES[pid] || pid;
|
|
tab.setAttribute('data-panel', pid);
|
|
tab.draggable = true;
|
|
tab.addEventListener('click', function (ev) {
|
|
ev.preventDefault();
|
|
if (node.active === pid) return;
|
|
node.active = pid;
|
|
// 仅切换页签内容,禁止 rebuild(会搬移/重排导致 iframe 重载)
|
|
self._activateTabLeaf(leaf, tabs, body, node);
|
|
self._save();
|
|
self._emitVisibility();
|
|
});
|
|
tab.addEventListener('dragstart', function (ev) {
|
|
self._beginDrag(pid, ev);
|
|
});
|
|
tabs.appendChild(tab);
|
|
})(panels[i]);
|
|
}
|
|
|
|
var activeEl = this._panelBodies[node.active];
|
|
if (activeEl) body.appendChild(activeEl);
|
|
|
|
leaf.appendChild(tabs);
|
|
leaf.appendChild(body);
|
|
return leaf;
|
|
};
|
|
|
|
/**
|
|
* 就地切换页签,不重建停靠树。
|
|
* @param {HTMLElement} leaf
|
|
* @param {HTMLElement} tabs
|
|
* @param {HTMLElement} body
|
|
* @param {object} node
|
|
*/
|
|
WorkbenchLayout.prototype._activateTabLeaf = function (leaf, tabs, body, node) {
|
|
var buttons = tabs.querySelectorAll('.pw-leaf-tab');
|
|
for (var i = 0; i < buttons.length; i++) {
|
|
var b = buttons[i];
|
|
b.classList.toggle('active', b.getAttribute('data-panel') === node.active);
|
|
}
|
|
while (body.firstChild) body.removeChild(body.firstChild);
|
|
var el = this._panelBodies[node.active];
|
|
if (el) body.appendChild(el);
|
|
};
|
|
|
|
WorkbenchLayout.prototype._beginDrag = function (panelId, ev) {
|
|
if (this.mode === 'compact') {
|
|
ev.preventDefault();
|
|
return;
|
|
}
|
|
this._dragPanel = panelId;
|
|
this._hoverGroupCols = null;
|
|
this._setGameIframePassThrough(true);
|
|
ev.dataTransfer.setData('text/pw-panel', panelId);
|
|
ev.dataTransfer.effectAllowed = 'move';
|
|
if (this.dropOverlay) this.dropOverlay.hidden = true;
|
|
};
|
|
|
|
WorkbenchLayout.prototype._onDocDragEnd = function () {
|
|
var self = this;
|
|
this._setGameIframePassThrough(false);
|
|
// drop 常在 dragend 之后触发,延迟清理避免丢掉落点信息
|
|
setTimeout(function () {
|
|
self._dragPanel = null;
|
|
self._hoverTargetId = null;
|
|
self._hoverEdge = null;
|
|
self._hoverGroup = null;
|
|
self._hoverGroupCols = null;
|
|
if (self.dropOverlay) {
|
|
self.dropOverlay.hidden = true;
|
|
self.dropOverlay.className = 'pw-drop-overlay';
|
|
}
|
|
}, 0);
|
|
};
|
|
|
|
WorkbenchLayout.prototype._clearDragVisual = function () {
|
|
if (this.dropOverlay) {
|
|
this.dropOverlay.hidden = true;
|
|
this.dropOverlay.className = 'pw-drop-overlay';
|
|
}
|
|
};
|
|
|
|
WorkbenchLayout.prototype._onDocDragOver = function (ev) {
|
|
if (!this._dragPanel) return;
|
|
var leafEl = ev.target.closest && ev.target.closest('.pw-leaf');
|
|
if (!leafEl) {
|
|
if (this.dropOverlay) this.dropOverlay.hidden = true;
|
|
return;
|
|
}
|
|
ev.preventDefault();
|
|
var leafId = leafEl.getAttribute('data-leaf-id');
|
|
var info = this._leafEls[leafId];
|
|
if (!info) return;
|
|
|
|
var rect = leafEl.getBoundingClientRect();
|
|
var x = (ev.clientX - rect.left) / Math.max(1, rect.width);
|
|
var y = (ev.clientY - rect.top) / Math.max(1, rect.height);
|
|
var edge = this._hitEdge(info.node, x, y, ev.target);
|
|
var resolved = this._resolveDropTarget(info.node, edge);
|
|
|
|
this._hoverTargetId = resolved.node ? resolved.node.id : null;
|
|
this._hoverEdge = resolved.edge;
|
|
this._hoverGroup = resolved.group || null;
|
|
this._hoverGroupCols = (resolved.group && resolved.group.length)
|
|
? this._snapshotGroupColumns(resolved.group, this._dragPanel)
|
|
: null;
|
|
|
|
if (resolved.group && resolved.group.length) {
|
|
this._showOverlayRect(this._edgeStripRect(this._unionNodeRects(resolved.group), resolved.edge), 'region', false);
|
|
} else if (resolved.node && resolved.node.type === 'split' && resolved.edge !== 'center') {
|
|
var splitInfo = this._nodeEls[resolved.node.id];
|
|
if (splitInfo && splitInfo.el) {
|
|
var sr = splitInfo.el.getBoundingClientRect();
|
|
var rootRect = this.root.getBoundingClientRect();
|
|
this._showOverlayRect(this._edgeStripRect({
|
|
left: sr.left - rootRect.left,
|
|
top: sr.top - rootRect.top,
|
|
width: sr.width,
|
|
height: sr.height,
|
|
}, resolved.edge), 'region', false);
|
|
} else {
|
|
this._showOverlay(leafEl, resolved.edge, false);
|
|
}
|
|
} else {
|
|
var targetInfo = resolved.node ? this._nodeEls[resolved.node.id] : null;
|
|
var targetEl = targetInfo ? targetInfo.el : leafEl;
|
|
this._showOverlay(targetEl, resolved.edge, resolved.node && resolved.node.type === 'game');
|
|
}
|
|
};
|
|
|
|
/** 取目标矩形某一侧的新建区预览条带 */
|
|
WorkbenchLayout.prototype._edgeStripRect = function (rect, edge) {
|
|
var r = { left: rect.left, top: rect.top, width: rect.width, height: rect.height };
|
|
var ratio = 0.36;
|
|
if (edge === 'bottom') {
|
|
r.top = rect.top + rect.height * (1 - ratio);
|
|
r.height = rect.height * ratio;
|
|
} else if (edge === 'top') {
|
|
r.height = rect.height * ratio;
|
|
} else if (edge === 'left') {
|
|
r.width = rect.width * ratio;
|
|
} else if (edge === 'right') {
|
|
r.left = rect.left + rect.width * (1 - ratio);
|
|
r.width = rect.width * ratio;
|
|
}
|
|
return r;
|
|
};
|
|
|
|
WorkbenchLayout.prototype._isGameNode = function (node) {
|
|
return !!(node && node.type === 'game');
|
|
};
|
|
|
|
WorkbenchLayout.prototype._splitHasGame = function (split) {
|
|
if (!split || split.type !== 'split') return false;
|
|
for (var i = 0; i < split.children.length; i++) {
|
|
if (this._isGameNode(split.children[i])) return true;
|
|
if (split.children[i].type === 'split' && this._splitHasGame(split.children[i])) return true;
|
|
}
|
|
return false;
|
|
};
|
|
|
|
/**
|
|
* 取横/竖排中与 leaf 相邻的连续「非游戏」兄弟,用于跨多列/多行建区且不吞掉游戏区。
|
|
*/
|
|
WorkbenchLayout.prototype._contiguousNonGameSiblings = function (parent, leafIndex) {
|
|
var kids = parent.children || [];
|
|
var start = leafIndex;
|
|
var end = leafIndex;
|
|
while (start > 0 && !this._isGameNode(kids[start - 1])) start--;
|
|
while (end < kids.length - 1 && !this._isGameNode(kids[end + 1])) end++;
|
|
var group = [];
|
|
for (var i = start; i <= end; i++) group.push(kids[i]);
|
|
return { start: start, end: end, nodes: group };
|
|
};
|
|
|
|
WorkbenchLayout.prototype._unionNodeRects = function (nodes) {
|
|
var rootRect = this.root.getBoundingClientRect();
|
|
var left = Infinity;
|
|
var top = Infinity;
|
|
var right = -Infinity;
|
|
var bottom = -Infinity;
|
|
for (var i = 0; i < nodes.length; i++) {
|
|
this._ensureId(nodes[i]);
|
|
var info = this._nodeEls[nodes[i].id];
|
|
if (!info || !info.el) continue;
|
|
var r = info.el.getBoundingClientRect();
|
|
left = Math.min(left, r.left);
|
|
top = Math.min(top, r.top);
|
|
right = Math.max(right, r.right);
|
|
bottom = Math.max(bottom, r.bottom);
|
|
}
|
|
if (!isFinite(left)) {
|
|
return { left: 0, top: 0, width: 0, height: 0 };
|
|
}
|
|
return {
|
|
left: left - rootRect.left,
|
|
top: top - rootRect.top,
|
|
width: right - left,
|
|
height: bottom - top,
|
|
};
|
|
};
|
|
|
|
/**
|
|
* 边沿停靠解析:
|
|
* - 标题中心 → 合并页签
|
|
* - 横排含游戏区时的上/下 → 只包住连续非游戏兄弟(红框范围),不横跨游戏
|
|
* - 竖排含游戏区时的左/右 → 同上
|
|
* - 不含游戏的横排上/下 → 提到该横排父级整行
|
|
*/
|
|
WorkbenchLayout.prototype._resolveDropTarget = function (leafNode, edge) {
|
|
if (edge === 'center') return { node: leafNode, edge: edge, group: null };
|
|
|
|
var parentInfo = this._findParent(this._tree, leafNode);
|
|
if (!parentInfo || !parentInfo.parent || parentInfo.parent.type !== 'split') {
|
|
this._ensureId(leafNode);
|
|
return { node: leafNode, edge: edge, group: null };
|
|
}
|
|
var parent = parentInfo.parent;
|
|
var index = parentInfo.index;
|
|
|
|
// 横排 + 上/下:若同排有游戏,只圈选连续非游戏列
|
|
if ((edge === 'top' || edge === 'bottom') && parent.dir === 'h') {
|
|
if (this._splitHasGame(parent) || this._parentRowHasGameSibling(parent)) {
|
|
var g = this._contiguousNonGameSiblings(parent, index);
|
|
if (g.nodes.length >= 1) {
|
|
return { node: null, edge: edge, group: g.nodes, groupParent: parent, groupStart: g.start, groupEnd: g.end };
|
|
}
|
|
}
|
|
this._ensureId(parent);
|
|
return { node: parent, edge: edge, group: null };
|
|
}
|
|
|
|
// 竖排 + 左/右:若同列有游戏,只圈选连续非游戏行
|
|
if ((edge === 'left' || edge === 'right') && parent.dir === 'v') {
|
|
if (this._splitHasGame(parent) || this._parentRowHasGameSibling(parent)) {
|
|
var g2 = this._contiguousNonGameSiblings(parent, index);
|
|
if (g2.nodes.length >= 1) {
|
|
return { node: null, edge: edge, group: g2.nodes, groupParent: parent, groupStart: g2.start, groupEnd: g2.end };
|
|
}
|
|
}
|
|
this._ensureId(parent);
|
|
return { node: parent, edge: edge, group: null };
|
|
}
|
|
|
|
// 外侧边:提升到父级(仍避开把游戏一并包进「整行上下」)
|
|
if (parent.dir === 'h' && edge === 'left' && index === 0) {
|
|
this._ensureId(parent);
|
|
return { node: parent, edge: edge, group: null };
|
|
}
|
|
if (parent.dir === 'h' && edge === 'right' && index === parent.children.length - 1) {
|
|
this._ensureId(parent);
|
|
return { node: parent, edge: edge, group: null };
|
|
}
|
|
if (parent.dir === 'v' && edge === 'top' && index === 0) {
|
|
this._ensureId(parent);
|
|
return { node: parent, edge: edge, group: null };
|
|
}
|
|
if (parent.dir === 'v' && edge === 'bottom' && index === parent.children.length - 1) {
|
|
this._ensureId(parent);
|
|
return { node: parent, edge: edge, group: null };
|
|
}
|
|
|
|
this._ensureId(leafNode);
|
|
return { node: leafNode, edge: edge, group: null };
|
|
};
|
|
|
|
WorkbenchLayout.prototype._parentRowHasGameSibling = function (split) {
|
|
// 父级自身子节点含 game
|
|
for (var i = 0; i < split.children.length; i++) {
|
|
if (this._isGameNode(split.children[i])) return true;
|
|
}
|
|
return false;
|
|
};
|
|
|
|
WorkbenchLayout.prototype._hitEdge = function (node, x, y, target) {
|
|
// 标题栏优先:合并
|
|
if (target && target.closest && target.closest('.pw-leaf-tabbar, .pw-leaf-tab')) {
|
|
return 'center';
|
|
}
|
|
var m = 0.22;
|
|
var onLeft = x < m;
|
|
var onRight = x > 1 - m;
|
|
var onTop = y < m;
|
|
var onBottom = y > 1 - m;
|
|
// 角落按更近边
|
|
if (onLeft && onTop) return x < y ? 'left' : 'top';
|
|
if (onRight && onTop) return (1 - x) < y ? 'right' : 'top';
|
|
if (onLeft && onBottom) return x < (1 - y) ? 'left' : 'bottom';
|
|
if (onRight && onBottom) return (1 - x) < (1 - y) ? 'right' : 'bottom';
|
|
if (onLeft) return 'left';
|
|
if (onRight) return 'right';
|
|
if (onTop) return 'top';
|
|
if (onBottom) return 'bottom';
|
|
// 游戏区域中心不允许合并页签,默认下方提示
|
|
if (node.type === 'game') return 'bottom';
|
|
return 'center';
|
|
};
|
|
|
|
WorkbenchLayout.prototype._showOverlay = function (leafEl, edge, isGame) {
|
|
var rect = leafEl.getBoundingClientRect();
|
|
var rootRect = this.root.getBoundingClientRect();
|
|
this._showOverlayRect({
|
|
left: rect.left - rootRect.left,
|
|
top: rect.top - rootRect.top,
|
|
width: rect.width,
|
|
height: rect.height,
|
|
}, edge, isGame);
|
|
};
|
|
|
|
WorkbenchLayout.prototype._showOverlayRect = function (rect, edge, isGame) {
|
|
var overlay = this.dropOverlay;
|
|
if (!overlay) return;
|
|
overlay.hidden = false;
|
|
overlay.style.left = rect.left + 'px';
|
|
overlay.style.top = rect.top + 'px';
|
|
overlay.style.width = Math.max(0, rect.width) + 'px';
|
|
overlay.style.height = Math.max(0, rect.height) + 'px';
|
|
var isRegion = edge === 'region';
|
|
overlay.className = 'pw-drop-overlay edge-' + (isRegion ? 'center' : edge) +
|
|
(isGame ? ' is-game' : '') + (isRegion ? ' is-region' : '');
|
|
var center = overlay.querySelector('.pw-drop-e-center');
|
|
if (center) {
|
|
center.hidden = !!isGame;
|
|
center.textContent = isRegion ? '在此新建区域' : '合并到此区域';
|
|
}
|
|
};
|
|
|
|
WorkbenchLayout.prototype._onDocDrop = function (ev) {
|
|
var panelId = ev.dataTransfer.getData('text/pw-panel') || this._dragPanel;
|
|
var targetId = this._hoverTargetId;
|
|
var edge = this._hoverEdge;
|
|
var group = this._hoverGroup;
|
|
var groupCols = this._hoverGroupCols;
|
|
this._clearDragVisual();
|
|
this._dragPanel = null;
|
|
this._hoverTargetId = null;
|
|
this._hoverEdge = null;
|
|
this._hoverGroup = null;
|
|
this._hoverGroupCols = null;
|
|
if (!panelId || !edge) return;
|
|
if (!targetId && !(groupCols && groupCols.length) && !(group && group.length)) return;
|
|
ev.preventDefault();
|
|
this._applyDrop(panelId, targetId, edge, group, groupCols);
|
|
};
|
|
|
|
WorkbenchLayout.prototype._applyDrop = function (panelId, targetId, edge, group, groupCols) {
|
|
this._closeFloat(panelId, false);
|
|
|
|
// 组停靠:用「列内容快照」而非节点引用,避免 remove/cleanup 后 id 失效导致只包一列
|
|
if (edge !== 'center' && ((groupCols && groupCols.length) || (group && group.length))) {
|
|
var cols = (groupCols && groupCols.length)
|
|
? groupCols
|
|
: this._snapshotGroupColumns(group, panelId);
|
|
this._removePanel(panelId);
|
|
var nodes = this._resolveColumnsFromSnapshot(cols);
|
|
if (nodes.length >= 1) {
|
|
this._splitBesideGroup(nodes, edge, panelId);
|
|
} else {
|
|
this._openFloat(panelId, null);
|
|
}
|
|
this._dirty = true;
|
|
this._tree = this._cleanup(this._tree) || this._presetTree('minimal');
|
|
this.rebuild();
|
|
this._rememberPanelFromTree(panelId);
|
|
this._save();
|
|
return;
|
|
}
|
|
|
|
var target = this._findNodeById(this._tree, targetId);
|
|
if (!target) return;
|
|
if (target.type === 'tabs' && (target.panels || []).indexOf(panelId) >= 0 && edge === 'center') {
|
|
target.active = panelId;
|
|
this.rebuild();
|
|
this._save();
|
|
return;
|
|
}
|
|
this._removePanel(panelId);
|
|
target = this._findNodeById(this._tree, targetId);
|
|
if (!target) {
|
|
this._openFloat(panelId, null);
|
|
this._dirty = true;
|
|
this._save();
|
|
return;
|
|
}
|
|
|
|
if (edge === 'center') {
|
|
if (target.type === 'game' || target.type === 'split') {
|
|
this._splitBeside(target, 'bottom', panelId);
|
|
} else if (target.type === 'tabs') {
|
|
if (target.panels.indexOf(panelId) < 0) target.panels.push(panelId);
|
|
target.active = panelId;
|
|
}
|
|
} else {
|
|
this._splitBeside(target, edge, panelId);
|
|
}
|
|
this._dirty = true;
|
|
this._tree = this._cleanup(this._tree) || this._presetTree('minimal');
|
|
this.rebuild();
|
|
this._rememberPanelFromTree(panelId);
|
|
this._save();
|
|
};
|
|
|
|
/** 组停靠前快照:记录各列在排除拖拽面板后仍保留的 panelId */
|
|
WorkbenchLayout.prototype._snapshotGroupColumns = function (group, excludePanelId) {
|
|
var cols = [];
|
|
for (var i = 0; i < group.length; i++) {
|
|
var node = group[i];
|
|
if (this._isGameNode(node)) {
|
|
cols.push({ kind: 'game' });
|
|
continue;
|
|
}
|
|
var panels = this._collectPanelIds(node);
|
|
var kept = [];
|
|
for (var j = 0; j < panels.length; j++) {
|
|
if (panels[j] !== excludePanelId) kept.push(panels[j]);
|
|
}
|
|
if (!kept.length) continue;
|
|
cols.push({ kind: 'panels', panels: kept });
|
|
}
|
|
return cols;
|
|
};
|
|
|
|
WorkbenchLayout.prototype._collectPanelIds = function (node) {
|
|
var out = [];
|
|
this._walkPanelIds(node, out);
|
|
return out;
|
|
};
|
|
|
|
WorkbenchLayout.prototype._walkPanelIds = function (node, out) {
|
|
if (!node) return;
|
|
if (node.type === 'tabs') {
|
|
var panels = node.panels || [];
|
|
for (var i = 0; i < panels.length; i++) out.push(panels[i]);
|
|
return;
|
|
}
|
|
if (node.type === 'split') {
|
|
for (var j = 0; j < node.children.length; j++) this._walkPanelIds(node.children[j], out);
|
|
}
|
|
};
|
|
|
|
/** 按快照在 cleanup 后的树中找回同级列节点 */
|
|
WorkbenchLayout.prototype._resolveColumnsFromSnapshot = function (cols) {
|
|
var leaves = [];
|
|
for (var i = 0; i < cols.length; i++) {
|
|
if (cols[i].kind === 'game') {
|
|
var g = this._findNodeById(this._tree, 'leaf_game');
|
|
if (g) leaves.push(g);
|
|
continue;
|
|
}
|
|
var panels = cols[i].panels || [];
|
|
var found = null;
|
|
for (var p = 0; p < panels.length; p++) {
|
|
found = this._findTabsWith(this._tree, panels[p]);
|
|
if (found) break;
|
|
}
|
|
if (found && leaves.indexOf(found) < 0) leaves.push(found);
|
|
}
|
|
return this._liftToSiblingBundle(leaves);
|
|
};
|
|
|
|
WorkbenchLayout.prototype._pathNodes = function (target) {
|
|
var path = [];
|
|
var walk = function (node, acc) {
|
|
if (node === target) {
|
|
path = acc.concat([node]);
|
|
return true;
|
|
}
|
|
if (node && node.type === 'split') {
|
|
for (var i = 0; i < node.children.length; i++) {
|
|
if (walk(node.children[i], acc.concat([node]))) return true;
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
walk(this._tree, []);
|
|
return path;
|
|
};
|
|
|
|
WorkbenchLayout.prototype._containsNode = function (root, target) {
|
|
if (root === target) return true;
|
|
if (!root || root.type !== 'split') return false;
|
|
for (var i = 0; i < root.children.length; i++) {
|
|
if (this._containsNode(root.children[i], target)) return true;
|
|
}
|
|
return false;
|
|
};
|
|
|
|
WorkbenchLayout.prototype._directChildContaining = function (parent, target) {
|
|
if (!parent || parent.type !== 'split') return null;
|
|
for (var i = 0; i < parent.children.length; i++) {
|
|
if (this._containsNode(parent.children[i], target)) return parent.children[i];
|
|
}
|
|
return null;
|
|
};
|
|
|
|
/** 将多个叶子提升为同一父 split 下的直接子节点(保持顺序) */
|
|
WorkbenchLayout.prototype._liftToSiblingBundle = function (leaves) {
|
|
if (!leaves || !leaves.length) return [];
|
|
if (leaves.length === 1) return leaves;
|
|
var paths = [];
|
|
for (var i = 0; i < leaves.length; i++) paths.push(this._pathNodes(leaves[i]));
|
|
var parent = null;
|
|
var depth = 0;
|
|
while (true) {
|
|
var ref = paths[0][depth];
|
|
if (!ref) break;
|
|
var same = true;
|
|
for (var a = 1; a < paths.length; a++) {
|
|
if (paths[a][depth] !== ref) { same = false; break; }
|
|
}
|
|
if (!same) break;
|
|
parent = ref;
|
|
depth++;
|
|
}
|
|
if (!parent || parent.type !== 'split') return leaves;
|
|
var bundle = [];
|
|
var seen = {};
|
|
for (var L = 0; L < leaves.length; L++) {
|
|
var child = this._directChildContaining(parent, leaves[L]);
|
|
if (child && child.id && !seen[child.id]) {
|
|
seen[child.id] = true;
|
|
bundle.push(child);
|
|
}
|
|
}
|
|
bundle.sort(function (a, b) {
|
|
return parent.children.indexOf(a) - parent.children.indexOf(b);
|
|
});
|
|
return bundle.length ? bundle : leaves;
|
|
};
|
|
|
|
/**
|
|
* 将一组兄弟节点包成整体,再在指定边沿外侧新建区域。
|
|
* 例:h[层级,属性,游戏] 底边 → h[ v[h[层级,属性], 控制台], 游戏]
|
|
*/
|
|
WorkbenchLayout.prototype._splitBesideGroup = function (nodes, edge, panelId) {
|
|
if (!nodes || !nodes.length) return;
|
|
var first = nodes[0];
|
|
var parentInfo = this._findParent(this._tree, first);
|
|
if (!parentInfo || !parentInfo.parent) {
|
|
this._splitBeside(first, edge, panelId);
|
|
return;
|
|
}
|
|
var parent = parentInfo.parent;
|
|
var start = parentInfo.index;
|
|
// 校验 nodes 仍是 parent 中连续片段(允许 id 匹配)
|
|
for (var i = 0; i < nodes.length; i++) {
|
|
var child = parent.children[start + i];
|
|
if (!child || (child !== nodes[i] && child.id !== nodes[i].id)) {
|
|
this._splitBeside(first, edge, panelId);
|
|
return;
|
|
}
|
|
nodes[i] = child;
|
|
}
|
|
var end = start + nodes.length - 1;
|
|
var newLeaf = this._tabsNode([panelId]);
|
|
var dir = (edge === 'left' || edge === 'right') ? 'h' : 'v';
|
|
var innerDir = parent.dir;
|
|
var groupNode;
|
|
if (nodes.length === 1) {
|
|
groupNode = nodes[0];
|
|
} else {
|
|
var groupSizes = [];
|
|
for (var s = start; s <= end; s++) {
|
|
groupSizes.push(parent.sizes[s] != null ? parent.sizes[s] : 1);
|
|
}
|
|
this._normalizeSizes(groupSizes, groupSizes.length);
|
|
groupNode = {
|
|
type: 'split',
|
|
id: 'split_' + (this._leafSeq++),
|
|
dir: innerDir,
|
|
sizes: groupSizes,
|
|
children: nodes.slice(),
|
|
};
|
|
}
|
|
|
|
var wrapped = {
|
|
type: 'split',
|
|
id: 'split_' + (this._leafSeq++),
|
|
dir: dir,
|
|
sizes: [0.64, 0.36],
|
|
children: (edge === 'left' || edge === 'top') ? [newLeaf, groupNode] : [groupNode, newLeaf],
|
|
};
|
|
|
|
var newChildren = [];
|
|
var newSizes = [];
|
|
for (var c = 0; c < parent.children.length; c++) {
|
|
if (c === start) {
|
|
newChildren.push(wrapped);
|
|
var sum = 0;
|
|
for (var k = start; k <= end; k++) sum += parent.sizes[k] != null ? parent.sizes[k] : 0;
|
|
newSizes.push(sum > 0 ? sum : 1);
|
|
c = end;
|
|
continue;
|
|
}
|
|
if (c < start || c > end) {
|
|
newChildren.push(parent.children[c]);
|
|
newSizes.push(parent.sizes[c] != null ? parent.sizes[c] : 1);
|
|
}
|
|
}
|
|
parent.children = newChildren;
|
|
parent.sizes = newSizes;
|
|
this._normalizeSizes(parent.sizes, parent.children.length);
|
|
};
|
|
|
|
WorkbenchLayout.prototype._splitBeside = function (targetNode, edge, panelId) {
|
|
this._ensureId(targetNode);
|
|
var parentInfo = this._findParent(this._tree, targetNode);
|
|
var newLeaf = this._tabsNode([panelId]);
|
|
var dir = (edge === 'left' || edge === 'right') ? 'h' : 'v';
|
|
var first = (edge === 'left' || edge === 'top') ? newLeaf : targetNode;
|
|
var second = (edge === 'left' || edge === 'top') ? targetNode : newLeaf;
|
|
|
|
if (!parentInfo) {
|
|
this._tree = {
|
|
type: 'split',
|
|
id: 'split_' + (this._leafSeq++),
|
|
dir: dir,
|
|
sizes: [0.5, 0.5],
|
|
children: [first, second],
|
|
};
|
|
return;
|
|
}
|
|
|
|
var parent = parentInfo.parent;
|
|
var index = parentInfo.index;
|
|
this._ensureId(parent);
|
|
|
|
// 同方向则插入为兄弟;否则把整个目标(可为 split 整行/整列)包进新 split
|
|
if (parent.type === 'split' && parent.dir === dir) {
|
|
var insertAt = (edge === 'left' || edge === 'top') ? index : index + 1;
|
|
parent.children.splice(insertAt, 0, newLeaf);
|
|
var n = parent.children.length;
|
|
var sizes = [];
|
|
for (var i = 0; i < n; i++) sizes.push(1 / n);
|
|
parent.sizes = sizes;
|
|
} else {
|
|
var split = {
|
|
type: 'split',
|
|
id: 'split_' + (this._leafSeq++),
|
|
dir: dir,
|
|
sizes: [0.5, 0.5],
|
|
children: [first, second],
|
|
};
|
|
parent.children[index] = split;
|
|
}
|
|
};
|
|
|
|
WorkbenchLayout.prototype._removePanel = function (panelId) {
|
|
this._removePanelFrom(this._tree, panelId);
|
|
this._tree = this._cleanup(this._tree);
|
|
};
|
|
|
|
WorkbenchLayout.prototype._removePanelFrom = function (node, panelId) {
|
|
if (!node) return false;
|
|
if (node.type === 'tabs') {
|
|
var idx = (node.panels || []).indexOf(panelId);
|
|
if (idx >= 0) {
|
|
node.panels.splice(idx, 1);
|
|
if (node.active === panelId) node.active = node.panels[0] || '';
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
if (node.type === 'split') {
|
|
for (var i = 0; i < node.children.length; i++) {
|
|
if (this._removePanelFrom(node.children[i], panelId)) return true;
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
|
|
WorkbenchLayout.prototype._cleanup = function (node) {
|
|
if (!node) return null;
|
|
if (node.type === 'game') return node;
|
|
if (node.type === 'tabs') {
|
|
if (!node.panels || !node.panels.length) return null;
|
|
return node;
|
|
}
|
|
if (node.type === 'split') {
|
|
var kids = [];
|
|
var sizes = [];
|
|
for (var i = 0; i < node.children.length; i++) {
|
|
var c = this._cleanup(node.children[i]);
|
|
if (c) {
|
|
kids.push(c);
|
|
sizes.push(node.sizes && node.sizes[i] != null ? node.sizes[i] : 1);
|
|
}
|
|
}
|
|
if (!kids.length) return null;
|
|
if (kids.length === 1) return kids[0];
|
|
// 合并同方向嵌套
|
|
var flatKids = [];
|
|
var flatSizes = [];
|
|
for (var j = 0; j < kids.length; j++) {
|
|
if (kids[j].type === 'split' && kids[j].dir === node.dir) {
|
|
var sub = kids[j];
|
|
var part = sizes[j];
|
|
var subSum = 0;
|
|
for (var s = 0; s < sub.sizes.length; s++) subSum += sub.sizes[s];
|
|
for (var k = 0; k < sub.children.length; k++) {
|
|
flatKids.push(sub.children[k]);
|
|
flatSizes.push(part * (sub.sizes[k] / (subSum || 1)));
|
|
}
|
|
} else {
|
|
flatKids.push(kids[j]);
|
|
flatSizes.push(sizes[j]);
|
|
}
|
|
}
|
|
this._normalizeSizes(flatSizes, flatKids.length);
|
|
return { type: 'split', dir: node.dir, sizes: flatSizes, children: flatKids };
|
|
}
|
|
return node;
|
|
};
|
|
|
|
WorkbenchLayout.prototype._findNodeById = function (node, id) {
|
|
if (!node) return null;
|
|
if (node.id === id) return node;
|
|
if (node.type === 'split') {
|
|
for (var i = 0; i < node.children.length; i++) {
|
|
var f = this._findNodeById(node.children[i], id);
|
|
if (f) return f;
|
|
}
|
|
}
|
|
return null;
|
|
};
|
|
|
|
WorkbenchLayout.prototype._findParent = function (node, child, parent, index) {
|
|
if (!node) return null;
|
|
if (node === child) return parent ? { parent: parent, index: index } : null;
|
|
if (node.type === 'split') {
|
|
for (var i = 0; i < node.children.length; i++) {
|
|
var f = this._findParent(node.children[i], child, node, i);
|
|
if (f) return f;
|
|
}
|
|
}
|
|
return null;
|
|
};
|
|
|
|
WorkbenchLayout.prototype._findTabsWith = function (node, panelId) {
|
|
if (!node) return null;
|
|
if (node.type === 'tabs' && (node.panels || []).indexOf(panelId) >= 0) return node;
|
|
if (node.type === 'split') {
|
|
for (var i = 0; i < node.children.length; i++) {
|
|
var f = this._findTabsWith(node.children[i], panelId);
|
|
if (f) return f;
|
|
}
|
|
}
|
|
return null;
|
|
};
|
|
|
|
WorkbenchLayout.prototype.togglePanel = function (panelId) {
|
|
if (this.mode === 'compact') {
|
|
if (this.activeDrawer === panelId) this.closeDrawer();
|
|
else this.openDrawer(panelId);
|
|
return;
|
|
}
|
|
if (this._floats[panelId]) {
|
|
this._rememberFloat(panelId);
|
|
this._closeFloat(panelId, true);
|
|
this._savePanelMemory();
|
|
this._emitVisibility();
|
|
return;
|
|
}
|
|
var existing = this._findTabsWith(this._tree, panelId);
|
|
if (existing) {
|
|
if (existing.panels.length === 1 && existing.active === panelId) {
|
|
this._rememberPanelFromTree(panelId);
|
|
this._removePanel(panelId);
|
|
this._tree = this._cleanup(this._tree) || this._presetTree('minimal');
|
|
this._dirty = true;
|
|
this.rebuild();
|
|
this._save();
|
|
return;
|
|
}
|
|
existing.active = panelId;
|
|
this.rebuild();
|
|
this._save();
|
|
return;
|
|
}
|
|
// 打开:优先记忆位置;无记忆则悬浮,禁止强行改当前停靠树
|
|
this._openPanelByMemory(panelId);
|
|
};
|
|
|
|
WorkbenchLayout.prototype._openPanelByMemory = function (panelId) {
|
|
var mem = this._panelMemory[panelId];
|
|
if (mem && mem.mode === 'tabs' && mem.mates && mem.mates.length) {
|
|
for (var i = 0; i < mem.mates.length; i++) {
|
|
var host = this._findTabsWith(this._tree, mem.mates[i]);
|
|
if (host) {
|
|
if (host.panels.indexOf(panelId) < 0) host.panels.push(panelId);
|
|
host.active = panelId;
|
|
this._dirty = true;
|
|
this.rebuild();
|
|
this._save();
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
if (mem && mem.mode === 'dock' && mem.anchors && mem.anchors.length) {
|
|
var leaves = [];
|
|
for (var a = 0; a < mem.anchors.length; a++) {
|
|
var leaf = this._findTabsWith(this._tree, mem.anchors[a]);
|
|
if (leaf && leaves.indexOf(leaf) < 0) leaves.push(leaf);
|
|
}
|
|
var bundle = this._liftToSiblingBundle(leaves);
|
|
if (bundle.length) {
|
|
if (bundle.length === 1) this._splitBeside(bundle[0], mem.edge || 'bottom', panelId);
|
|
else this._splitBesideGroup(bundle, mem.edge || 'bottom', panelId);
|
|
this._dirty = true;
|
|
this._tree = this._cleanup(this._tree) || this._tree;
|
|
this.rebuild();
|
|
this._rememberPanelFromTree(panelId);
|
|
this._save();
|
|
return;
|
|
}
|
|
}
|
|
this._openFloat(panelId, mem && mem.float ? mem.float : null);
|
|
this._savePanelMemory();
|
|
};
|
|
|
|
WorkbenchLayout.prototype._rememberPanelFromTree = function (panelId) {
|
|
var leaf = this._findTabsWith(this._tree, panelId);
|
|
if (!leaf) return;
|
|
if ((leaf.panels || []).length > 1) {
|
|
var mates = [];
|
|
for (var i = 0; i < leaf.panels.length; i++) {
|
|
if (leaf.panels[i] !== panelId) mates.push(leaf.panels[i]);
|
|
}
|
|
this._panelMemory[panelId] = { mode: 'tabs', mates: mates };
|
|
this._savePanelMemory();
|
|
return;
|
|
}
|
|
var parentInfo = this._findParent(this._tree, leaf);
|
|
var floatRect = null;
|
|
var info = this._leafEls[leaf.id];
|
|
if (info && info.el) {
|
|
var r = info.el.getBoundingClientRect();
|
|
var rootRect = this.root.getBoundingClientRect();
|
|
floatRect = {
|
|
x: Math.max(8, r.left - rootRect.left),
|
|
y: Math.max(40, r.top - rootRect.top),
|
|
w: Math.max(240, r.width),
|
|
h: Math.max(160, r.height),
|
|
};
|
|
}
|
|
if (!parentInfo || !parentInfo.parent) {
|
|
this._panelMemory[panelId] = { mode: 'float', float: floatRect };
|
|
this._savePanelMemory();
|
|
return;
|
|
}
|
|
var parent = parentInfo.parent;
|
|
var idx = parentInfo.index;
|
|
var neighbor = idx > 0 ? parent.children[idx - 1] : parent.children[idx + 1];
|
|
var edge = 'bottom';
|
|
if (parent.dir === 'h') edge = idx > 0 ? 'right' : 'left';
|
|
else edge = idx > 0 ? 'bottom' : 'top';
|
|
var anchors = neighbor ? this._collectPanelIds(neighbor) : [];
|
|
this._panelMemory[panelId] = {
|
|
mode: anchors.length ? 'dock' : 'float',
|
|
anchors: anchors,
|
|
edge: edge,
|
|
float: floatRect,
|
|
};
|
|
this._savePanelMemory();
|
|
};
|
|
|
|
WorkbenchLayout.prototype._ensureFloatLayer = function () {
|
|
if (this._floatLayer) return;
|
|
var layer = document.createElement('div');
|
|
layer.className = 'pw-float-layer';
|
|
layer.id = 'pw-float-layer';
|
|
this.root.appendChild(layer);
|
|
this._floatLayer = layer;
|
|
};
|
|
|
|
WorkbenchLayout.prototype._openFloat = function (panelId, rect) {
|
|
this._ensureFloatLayer();
|
|
if (this._floats[panelId]) return;
|
|
var body = this._panelBodies[panelId];
|
|
if (!body) return;
|
|
var win = document.createElement('div');
|
|
win.className = 'pw-float-win';
|
|
win.setAttribute('data-float-panel', panelId);
|
|
var x = (rect && rect.x != null) ? rect.x : 72 + Object.keys(this._floats).length * 24;
|
|
var y = (rect && rect.y != null) ? rect.y : 72 + Object.keys(this._floats).length * 24;
|
|
var w = (rect && rect.w != null) ? rect.w : 360;
|
|
var h = (rect && rect.h != null) ? rect.h : 320;
|
|
win.style.left = x + 'px';
|
|
win.style.top = y + 'px';
|
|
win.style.width = w + 'px';
|
|
win.style.height = h + 'px';
|
|
|
|
var head = document.createElement('div');
|
|
head.className = 'pw-float-head';
|
|
head.innerHTML =
|
|
'<span class="pw-float-title" draggable="true"></span>' +
|
|
'<span class="pw-float-hint">拖标题可停靠</span>' +
|
|
'<button type="button" class="pw-btn pw-float-close">关闭</button>';
|
|
head.querySelector('.pw-float-title').textContent = PANEL_TITLES[panelId] || panelId;
|
|
var content = document.createElement('div');
|
|
content.className = 'pw-float-body';
|
|
content.appendChild(body);
|
|
win.appendChild(head);
|
|
win.appendChild(content);
|
|
this._floatLayer.appendChild(win);
|
|
|
|
var self = this;
|
|
head.querySelector('.pw-float-close').addEventListener('click', function () {
|
|
self._rememberFloat(panelId);
|
|
self._closeFloat(panelId, true);
|
|
self._savePanelMemory();
|
|
});
|
|
// 标题文字:HTML5 拖放到布局区停靠
|
|
var title = head.querySelector('.pw-float-title');
|
|
title.addEventListener('dragstart', function (ev) {
|
|
self._floatDrag = null;
|
|
win.classList.add('pw-float-dock-dragging');
|
|
self._beginDrag(panelId, ev);
|
|
});
|
|
title.addEventListener('dragend', function () {
|
|
win.classList.remove('pw-float-dock-dragging');
|
|
self._clearDragVisual();
|
|
self._dragPanel = null;
|
|
self._hoverTargetId = null;
|
|
self._hoverEdge = null;
|
|
self._hoverGroup = null;
|
|
self._hoverGroupCols = null;
|
|
});
|
|
// 标题栏空白处:移动悬浮窗位置(用当前坐标,禁止用创建时闭包坐标)
|
|
head.addEventListener('pointerdown', function (ev) {
|
|
if (ev.target && ev.target.closest && ev.target.closest('.pw-float-close')) return;
|
|
if (ev.target && ev.target.closest && ev.target.closest('.pw-float-title')) return;
|
|
var f = self._floats[panelId];
|
|
if (!f) return;
|
|
ev.preventDefault();
|
|
head.setPointerCapture && head.setPointerCapture(ev.pointerId);
|
|
self._setGameIframePassThrough(true);
|
|
self._floatDrag = {
|
|
id: panelId,
|
|
ox: ev.clientX,
|
|
oy: ev.clientY,
|
|
x: f.x,
|
|
y: f.y,
|
|
moved: false,
|
|
};
|
|
window.addEventListener('pointermove', self._onFloatMove);
|
|
window.addEventListener('pointerup', self._onFloatUp);
|
|
});
|
|
|
|
this._floats[panelId] = { x: x, y: y, w: w, h: h, el: win };
|
|
this._panelMemory[panelId] = { mode: 'float', float: { x: x, y: y, w: w, h: h } };
|
|
};
|
|
|
|
WorkbenchLayout.prototype._onFloatMove = function (ev) {
|
|
var d = this._floatDrag;
|
|
if (!d || !this._floats[d.id]) return;
|
|
var nx = d.x + (ev.clientX - d.ox);
|
|
var ny = d.y + (ev.clientY - d.oy);
|
|
var f = this._floats[d.id];
|
|
var rootRect = this.root.getBoundingClientRect();
|
|
f.x = Math.max(0, Math.min(rootRect.width - 80, nx));
|
|
f.y = Math.max(28, Math.min(rootRect.height - 40, ny));
|
|
f.el.style.left = f.x + 'px';
|
|
f.el.style.top = f.y + 'px';
|
|
d.moved = true;
|
|
// 移动时探测下方停靠区(标题拖放之外的快捷停靠)
|
|
this._probeFloatDockHover(ev.clientX, ev.clientY, d.id);
|
|
};
|
|
|
|
WorkbenchLayout.prototype._onFloatUp = function (ev) {
|
|
var d = this._floatDrag;
|
|
this._floatDrag = null;
|
|
window.removeEventListener('pointermove', this._onFloatMove);
|
|
window.removeEventListener('pointerup', this._onFloatUp);
|
|
this._setGameIframePassThrough(false);
|
|
if (!d) return;
|
|
var docked = false;
|
|
if (d.moved && this._hoverEdge && (this._hoverTargetId || (this._hoverGroupCols && this._hoverGroupCols.length))) {
|
|
docked = this._dockFloatingPanel(d.id, ev.clientX, ev.clientY);
|
|
}
|
|
this._clearDragVisual();
|
|
this._dragPanel = null;
|
|
this._hoverTargetId = null;
|
|
this._hoverEdge = null;
|
|
this._hoverGroup = null;
|
|
this._hoverGroupCols = null;
|
|
if (!docked) {
|
|
this._rememberFloat(d.id);
|
|
this._savePanelMemory();
|
|
}
|
|
};
|
|
|
|
/** 悬浮窗移动时:临时穿透命中下方 leaf,刷新停靠指示 */
|
|
WorkbenchLayout.prototype._probeFloatDockHover = function (cx, cy, panelId) {
|
|
var floats = this._floats;
|
|
var ids = [];
|
|
for (var id in floats) {
|
|
if (Object.prototype.hasOwnProperty.call(floats, id) && floats[id].el) {
|
|
ids.push(id);
|
|
floats[id].el.style.pointerEvents = 'none';
|
|
}
|
|
}
|
|
var el = document.elementFromPoint(cx, cy);
|
|
for (var i = 0; i < ids.length; i++) {
|
|
if (floats[ids[i]] && floats[ids[i]].el) floats[ids[i]].el.style.pointerEvents = '';
|
|
}
|
|
var leafEl = el && el.closest && el.closest('.pw-leaf');
|
|
if (!leafEl) {
|
|
this._clearDragVisual();
|
|
this._hoverTargetId = null;
|
|
this._hoverEdge = null;
|
|
this._hoverGroup = null;
|
|
this._hoverGroupCols = null;
|
|
return;
|
|
}
|
|
this._dragPanel = panelId;
|
|
var leafId = leafEl.getAttribute('data-leaf-id');
|
|
var info = this._leafEls[leafId];
|
|
if (!info) return;
|
|
var rect = leafEl.getBoundingClientRect();
|
|
var x = (cx - rect.left) / Math.max(1, rect.width);
|
|
var y = (cy - rect.top) / Math.max(1, rect.height);
|
|
var edge = this._hitEdge(info.node, x, y, el);
|
|
var resolved = this._resolveDropTarget(info.node, edge);
|
|
this._hoverTargetId = resolved.node ? resolved.node.id : null;
|
|
this._hoverEdge = resolved.edge;
|
|
this._hoverGroup = resolved.group || null;
|
|
this._hoverGroupCols = (resolved.group && resolved.group.length)
|
|
? this._snapshotGroupColumns(resolved.group, panelId)
|
|
: null;
|
|
if (resolved.group && resolved.group.length) {
|
|
this._showOverlayRect(this._edgeStripRect(this._unionNodeRects(resolved.group), resolved.edge), 'region', false);
|
|
} else if (resolved.node && resolved.node.type === 'split' && resolved.edge !== 'center') {
|
|
var splitInfo = this._nodeEls[resolved.node.id];
|
|
if (splitInfo && splitInfo.el) {
|
|
var sr = splitInfo.el.getBoundingClientRect();
|
|
var rootRect = this.root.getBoundingClientRect();
|
|
this._showOverlayRect(this._edgeStripRect({
|
|
left: sr.left - rootRect.left,
|
|
top: sr.top - rootRect.top,
|
|
width: sr.width,
|
|
height: sr.height,
|
|
}, resolved.edge), 'region', false);
|
|
}
|
|
} else {
|
|
var targetInfo = resolved.node ? this._nodeEls[resolved.node.id] : null;
|
|
var targetEl = targetInfo ? targetInfo.el : leafEl;
|
|
this._showOverlay(targetEl, resolved.edge, resolved.node && resolved.node.type === 'game');
|
|
}
|
|
};
|
|
|
|
WorkbenchLayout.prototype._dockFloatingPanel = function (panelId, cx, cy) {
|
|
this._probeFloatDockHover(cx, cy, panelId);
|
|
var targetId = this._hoverTargetId;
|
|
var edge = this._hoverEdge;
|
|
var group = this._hoverGroup;
|
|
var groupCols = this._hoverGroupCols;
|
|
if (!edge) return false;
|
|
if (!targetId && !(groupCols && groupCols.length) && !(group && group.length)) return false;
|
|
this._applyDrop(panelId, targetId, edge, group, groupCols);
|
|
return !this._floats[panelId];
|
|
};
|
|
|
|
WorkbenchLayout.prototype._rememberFloat = function (panelId) {
|
|
var f = this._floats[panelId];
|
|
if (!f) return;
|
|
this._panelMemory[panelId] = {
|
|
mode: 'float',
|
|
float: { x: f.x, y: f.y, w: f.w, h: f.h },
|
|
};
|
|
};
|
|
|
|
WorkbenchLayout.prototype._closeFloat = function (panelId, keepMemory) {
|
|
var f = this._floats[panelId];
|
|
if (!f) return;
|
|
if (f.el && f.el.parentNode) f.el.parentNode.removeChild(f.el);
|
|
delete this._floats[panelId];
|
|
if (!keepMemory) return;
|
|
};
|
|
|
|
WorkbenchLayout.prototype._restoreFloats = function () {
|
|
if (this._workbenchOff) return;
|
|
for (var id in this._panelMemory) {
|
|
if (!Object.prototype.hasOwnProperty.call(this._panelMemory, id)) continue;
|
|
var mem = this._panelMemory[id];
|
|
if (mem && mem.mode === 'float' && !this._findTabsWith(this._tree, id)) {
|
|
this._openFloat(id, mem.float || null);
|
|
}
|
|
}
|
|
};
|
|
|
|
WorkbenchLayout.prototype._savePanelMemory = function () {
|
|
try {
|
|
localStorage.setItem(PANEL_MEM_KEY, JSON.stringify(this._panelMemory));
|
|
} catch (e) { /* */ }
|
|
};
|
|
|
|
WorkbenchLayout.prototype._loadPanelMemory = function () {
|
|
try {
|
|
var raw = localStorage.getItem(PANEL_MEM_KEY);
|
|
if (!raw) return;
|
|
var data = JSON.parse(raw);
|
|
if (data && typeof data === 'object') this._panelMemory = data;
|
|
} catch (e) {
|
|
this._panelMemory = {};
|
|
}
|
|
};
|
|
|
|
WorkbenchLayout.prototype._findFirstTabs = function (node) {
|
|
if (!node) return null;
|
|
if (node.type === 'tabs') return node;
|
|
if (node.type === 'split') {
|
|
for (var i = 0; i < node.children.length; i++) {
|
|
var f = this._findFirstTabs(node.children[i]);
|
|
if (f) return f;
|
|
}
|
|
}
|
|
return null;
|
|
};
|
|
|
|
WorkbenchLayout.prototype.openDrawer = function (panelId) {
|
|
var drawer = document.getElementById('pw-drawer');
|
|
var body = document.getElementById('pw-drawer-body');
|
|
var title = document.getElementById('pw-drawer-title');
|
|
var el = this._panelBodies[panelId];
|
|
if (!drawer || !body || !el) return;
|
|
body.innerHTML = '';
|
|
body.appendChild(el);
|
|
if (title) title.textContent = PANEL_TITLES[panelId] || panelId;
|
|
drawer.hidden = false;
|
|
this.activeDrawer = panelId;
|
|
this._emitVisibility();
|
|
};
|
|
|
|
WorkbenchLayout.prototype.closeDrawer = function () {
|
|
var drawer = document.getElementById('pw-drawer');
|
|
if (drawer) drawer.hidden = true;
|
|
var id = this.activeDrawer;
|
|
this.activeDrawer = '';
|
|
if (id) this.rebuild();
|
|
else this._emitVisibility();
|
|
};
|
|
|
|
WorkbenchLayout.prototype._bindToolbar = function () {
|
|
var self = this;
|
|
var preset = document.getElementById('pw-layout-preset');
|
|
if (preset) {
|
|
preset.value = this.preset;
|
|
preset.addEventListener('change', function () {
|
|
self.applyPreset(preset.value, true);
|
|
});
|
|
}
|
|
var reset = document.getElementById('pw-layout-reset');
|
|
if (reset) reset.addEventListener('click', function () { self.resetLayout(); });
|
|
var buttons = this.root.querySelectorAll('.pw-panel-btn');
|
|
for (var i = 0; i < buttons.length; i++) {
|
|
buttons[i].addEventListener('click', function (ev) {
|
|
self.togglePanel(ev.currentTarget.getAttribute('data-panel'));
|
|
});
|
|
}
|
|
var close = document.getElementById('pw-drawer-close');
|
|
if (close) close.addEventListener('click', function () { self.closeDrawer(); });
|
|
};
|
|
|
|
WorkbenchLayout.prototype._onWindowResize = function () {
|
|
this.refreshMode();
|
|
if (this.mode === 'desktop' && !this.workspace.firstChild) this.rebuild();
|
|
this._syncGameHostGeom();
|
|
this._notifyViewResize();
|
|
};
|
|
|
|
/**
|
|
* 一键开关工作台:关闭时备份布局并只留游戏与设备选项,开启时还原。
|
|
* @param {boolean} on
|
|
*/
|
|
WorkbenchLayout.prototype.setWorkbenchEnabled = function (on) {
|
|
on = !!on;
|
|
if (on === !this._workbenchOff) return;
|
|
this._workbenchOff = !on;
|
|
this.root.classList.toggle('pw-workbench-off', this._workbenchOff);
|
|
if (this._workbenchOff) {
|
|
try {
|
|
this._treeBeforeOff = JSON.parse(JSON.stringify(this._tree));
|
|
} catch (e) {
|
|
this._treeBeforeOff = this._tree;
|
|
}
|
|
// 隐藏悬浮窗但不销毁,便于恢复
|
|
for (var fid in this._floats) {
|
|
if (!Object.prototype.hasOwnProperty.call(this._floats, fid)) continue;
|
|
var f = this._floats[fid];
|
|
if (f && f.el) f.el.hidden = true;
|
|
}
|
|
this.closeDrawer();
|
|
this._tree = this._gameNode();
|
|
this.rebuild();
|
|
} else {
|
|
for (var id in this._floats) {
|
|
if (!Object.prototype.hasOwnProperty.call(this._floats, id)) continue;
|
|
var fl = this._floats[id];
|
|
if (fl && fl.el) fl.el.hidden = false;
|
|
}
|
|
if (this._treeBeforeOff) {
|
|
this._tree = this._treeBeforeOff;
|
|
this._treeBeforeOff = null;
|
|
this._reseedLeafIds(this._tree);
|
|
} else {
|
|
this._tree = this._presetTree(this.preset || 'analyze');
|
|
}
|
|
this.rebuild();
|
|
}
|
|
};
|
|
|
|
/** @returns {boolean} */
|
|
WorkbenchLayout.prototype.isWorkbenchEnabled = function () {
|
|
return !this._workbenchOff;
|
|
};
|
|
|
|
WorkbenchLayout.prototype._save = function () {
|
|
try {
|
|
localStorage.setItem(STORAGE_KEY, JSON.stringify({
|
|
preset: this.preset,
|
|
tree: this._tree,
|
|
dirty: !!this._dirty,
|
|
}));
|
|
this._savePanelMemory();
|
|
} catch (e) {
|
|
console.warn('[调试工作台] 布局保存失败', e);
|
|
}
|
|
};
|
|
|
|
WorkbenchLayout.prototype._load = function () {
|
|
this._loadPanelMemory();
|
|
try {
|
|
var raw = localStorage.getItem(STORAGE_KEY);
|
|
if (!raw) return;
|
|
var data = JSON.parse(raw);
|
|
if (data.preset) this.preset = data.preset;
|
|
if (data.tree) {
|
|
this._tree = data.tree;
|
|
this._reseedLeafIds(this._tree);
|
|
this._dirty = !!data.dirty;
|
|
}
|
|
} catch (e) {
|
|
console.warn('[调试工作台] 布局读取失败', e);
|
|
this._tree = null;
|
|
}
|
|
};
|
|
|
|
WorkbenchLayout.prototype._reseedLeafIds = function (node) {
|
|
if (!node) return;
|
|
if (node.type === 'tabs' || node.type === 'game') {
|
|
if (!node.id) node.id = node.type === 'game' ? 'leaf_game' : ('leaf_' + (this._leafSeq++));
|
|
if (node.type === 'game') node.id = 'leaf_game';
|
|
var n = parseInt(String(node.id).replace(/\D/g, ''), 10);
|
|
if (!isNaN(n) && n >= this._leafSeq) this._leafSeq = n + 1;
|
|
}
|
|
if (node.type === 'split') {
|
|
for (var i = 0; i < node.children.length; i++) this._reseedLeafIds(node.children[i]);
|
|
}
|
|
};
|
|
|
|
global.PWLayout = WorkbenchLayout;
|
|
})(window);
|
|
|