378 lines
16 KiB
JavaScript
378 lines
16 KiB
JavaScript
const vscode = acquireVsCodeApi();
|
||
|
||
// ============================================
|
||
// 1. Инициализация
|
||
// ============================================
|
||
const editor = grapesjs.init({
|
||
container: '#editor',
|
||
height: '100%',
|
||
storageManager: false,
|
||
dragMode: 'absolute',
|
||
selectorManager: { componentFirst: true },
|
||
undoManager: { trackSelection: false },
|
||
styleManager: { sectors: [{ name: 'Geometry', buildProps: ['left', 'top', 'width', 'height'] }] },
|
||
|
||
canvas: {
|
||
styles: [
|
||
'body { background-color: #444444; margin: 0; padding: 0; display: flex; justify-content: center; align-items: center; min-height: 100vh; }',
|
||
'::-webkit-scrollbar { width: 10px; height: 10px; }',
|
||
'::-webkit-scrollbar-track { background: #333; }',
|
||
'::-webkit-scrollbar-thumb { background: #666; border-radius: 5px; }'
|
||
]
|
||
}
|
||
});
|
||
|
||
// ============================================
|
||
// 2. Панель и Кнопки
|
||
// ============================================
|
||
editor.Panels.getButton('views', 'open-blocks').set('active', true);
|
||
|
||
|
||
editor.Panels.addButton('options', {
|
||
id: 'import-python-btn', className: 'fa fa-upload', command: 'import-python-cmd',
|
||
attributes: { title: 'Import from Python File' }
|
||
});
|
||
editor.Commands.add('import-python-cmd', { run: () => { vscode.postMessage({ type: 'request-import' }); } });
|
||
|
||
|
||
editor.Panels.addButton('options', {
|
||
id: 'window-settings-btn', className: 'fa fa-cog', command: 'window-settings-cmd',
|
||
attributes: { title: 'Window Settings' }
|
||
});
|
||
|
||
editor.Commands.add('window-settings-cmd', {
|
||
run: (editor, sender) => {
|
||
sender.set('active', 0);
|
||
const wrapper = editor.getWrapper();
|
||
const attrs = wrapper.getAttributes();
|
||
|
||
const currW = attrs['data-width'] || 500;
|
||
const currH = attrs['data-height'] || 400;
|
||
const currTitle = attrs['data-title'] || 'Form';
|
||
|
||
const content = `
|
||
<div style="padding: 10px; font-family: sans-serif; color: #333;">
|
||
<div style="margin-bottom: 10px;">
|
||
<label>Window Title:</label>
|
||
<input type="text" id="win-title" value="${currTitle}" style="width: 100%; padding: 5px;">
|
||
</div>
|
||
<div style="display: flex; gap: 10px; margin-bottom: 20px;">
|
||
<div style="flex: 1;"><label>Width:</label><input type="number" id="win-width" value="${currW}" style="width: 100%; padding: 5px;"></div>
|
||
<div style="flex: 1;"><label>Height:</label><input type="number" id="win-height" value="${currH}" style="width: 100%; padding: 5px;"></div>
|
||
</div>
|
||
<button id="win-save" style="background: #444; color: #fff; border: none; padding: 8px 15px; cursor: pointer;">Apply</button>
|
||
</div>
|
||
`;
|
||
|
||
const modal = editor.Modal;
|
||
modal.setTitle('Window Settings');
|
||
modal.setContent(content);
|
||
modal.open();
|
||
|
||
document.getElementById('win-save').onclick = () => {
|
||
const newTitle = document.getElementById('win-title').value;
|
||
const newW = parseInt(document.getElementById('win-width').value) || 500;
|
||
const newH = parseInt(document.getElementById('win-height').value) || 400;
|
||
|
||
// Применяем размеры
|
||
wrapper.addStyle({ width: newW + 'px', height: newH + 'px' });
|
||
|
||
const newAttrs = { ...wrapper.getAttributes() };
|
||
newAttrs['data-title'] = newTitle;
|
||
newAttrs['data-width'] = newW;
|
||
newAttrs['data-height'] = newH;
|
||
wrapper.setAttributes(newAttrs);
|
||
|
||
modal.close();
|
||
sendDataToExtension();
|
||
};
|
||
}
|
||
});
|
||
|
||
// ============================================
|
||
// 3. Загрузка (Настройка Wrapper)
|
||
// ============================================
|
||
editor.on('load', () => {
|
||
editor.Keymaps.add('redo-custom', 'ctrl+y', 'core:redo');
|
||
editor.Keymaps.add('undo-custom', 'ctrl+z', 'core:undo');
|
||
|
||
const wrapper = editor.getWrapper();
|
||
|
||
|
||
wrapper.addStyle({
|
||
'position': 'relative',
|
||
'background-color': '#ffffff',
|
||
'overflow': 'hidden',
|
||
'border': '2px solid #333',
|
||
'box-shadow': '0 0 30px rgba(0,0,0,0.5)',
|
||
'width': '500px',
|
||
'height': '400px',
|
||
'margin': '0'
|
||
});
|
||
|
||
const attrs = wrapper.getAttributes();
|
||
if (!attrs['data-width']) {
|
||
wrapper.setAttributes({ 'data-width': 500, 'data-height': 400, 'data-title': 'My App' });
|
||
} else {
|
||
|
||
wrapper.addStyle({
|
||
width: attrs['data-width'] + 'px',
|
||
height: attrs['data-height'] + 'px'
|
||
});
|
||
}
|
||
});
|
||
|
||
const geoTraits = [
|
||
{ type: 'number', name: 'x', label: 'X' }, { type: 'number', name: 'y', label: 'Y' },
|
||
{ type: 'number', name: 'width', label: 'W' }, { type: 'number', name: 'height', label: 'H' },
|
||
{ type: 'color', name: 'bg', label: 'Bg' }, { type: 'color', name: 'fg', label: 'Fg' }
|
||
];
|
||
|
||
// ============================================
|
||
// 4. Компоненты
|
||
// ============================================
|
||
|
||
function addTkComponent(type, tkType, label, defaultContent = '', defW = 80, defH = 30) {
|
||
editor.DomComponents.addType(type, {
|
||
model: {
|
||
defaults: {
|
||
tagName: 'div', draggable: true, droppable: false,
|
||
attributes: { 'data-tk-type': tkType },
|
||
traits: [ { type: 'text', name: 'text', label: 'Text', default: defaultContent }, ...geoTraits ],
|
||
style: {
|
||
position: 'absolute', left: '0px', top: '0px', width: defW + 'px', height: defH + 'px',
|
||
padding: '5px', border: '1px solid #999', background: '#f0f0f0',
|
||
display: 'flex', 'align-items': 'center', 'justify-content': 'center', 'overflow': 'hidden',
|
||
...(tkType === 'Entry' || tkType === 'Text' ? { background: '#ffffff', border: '2px inset #ccc', 'align-items': 'start', 'justify-content': 'start', 'white-space': 'pre-wrap' } : {})
|
||
},
|
||
content: defaultContent || label
|
||
},
|
||
updated(p) { if(p==='traits') { const text = this.getTrait('text')?.getValue(); if(text!==undefined && tkType!=='Entry') this.components(text); updateStylesAndSync(this); sendDataToExtension(); } }
|
||
}
|
||
});
|
||
editor.BlockManager.add(type, { label: label, category: 'Tkinter', content: { type: type } });
|
||
}
|
||
|
||
addTkComponent('tk-label', 'Label', 'Label', 'Label', 100, 30);
|
||
addTkComponent('tk-button', 'Button', 'Button', 'Button', 100, 40);
|
||
addTkComponent('tk-entry', 'Entry', 'Entry', 'Input here', 120, 30);
|
||
addTkComponent('tk-check', 'Checkbutton', 'Checkbox', 'Check', 100, 30);
|
||
addTkComponent('tk-radio', 'Radiobutton', 'Radio', 'Radio', 100, 30);
|
||
addTkComponent('tk-text', 'Text', 'Text', 'Multiline\nText', 150, 100);
|
||
|
||
editor.DomComponents.addType('tk-listbox', {
|
||
model: {
|
||
defaults: {
|
||
tagName: 'div', draggable: true, droppable: false,
|
||
attributes: { 'data-tk-type': 'Listbox', 'line_count': 3 },
|
||
style: { position: 'absolute', left: '0', top: '0', width: '120px', height: '100px', background: '#fff', border: '2px inset #ccc', overflow: 'hidden', padding: '0' },
|
||
traits: [{ type: 'number', name: 'line_count', label: 'Rows Count', default: 3, changeProp: 1 }, ...geoTraits]
|
||
},
|
||
init() { this.on('change:line_count', this.updateTraits); this.updateTraits(); this.renderList(); },
|
||
updateTraits() {
|
||
const count = parseInt(this.get('line_count') || 0);
|
||
const newTraits = [{ type: 'number', name: 'line_count', label: 'Rows Count', changeProp: 1 }];
|
||
for (let i = 0; i < count; i++) newTraits.push({ type: 'text', name: `line_${i}`, label: `Line ${i + 1}`, changeProp: 1 });
|
||
newTraits.push(...geoTraits);
|
||
this.set('traits', newTraits);
|
||
},
|
||
renderList() {
|
||
const count = parseInt(this.get('line_count') || 0);
|
||
let htmlItems = '', dataItems = [];
|
||
for (let i = 0; i < count; i++) {
|
||
let val = this.get(`line_${i}`);
|
||
if (val === undefined) { val = `Item ${i + 1}`; this.set(`line_${i}`, val); }
|
||
htmlItems += `<div style="border-bottom:1px solid #eee; padding:2px;">${val}</div>`;
|
||
dataItems.push(val);
|
||
}
|
||
this.components(htmlItems);
|
||
const attrs = { ...this.getAttributes() };
|
||
attrs['data-items'] = dataItems.join('\n');
|
||
this.setAttributes(attrs);
|
||
},
|
||
updated(p) {
|
||
if (p==='traits' || p.startsWith('line_') || p==='line_count') {
|
||
this.renderList();
|
||
updateStylesAndSync(this);
|
||
sendDataToExtension();
|
||
}
|
||
}
|
||
}
|
||
});
|
||
editor.BlockManager.add('tk-listbox', { label: 'Listbox', category: 'Tkinter', content: { type: 'tk-listbox' } });
|
||
|
||
editor.DomComponents.addType('tk-canvas', { extend: 'tk-label', model: { defaults: { attributes: { 'data-tk-type': 'Canvas' }, traits: geoTraits, style: { position: 'absolute', left:'0', top:'0', border: '1px solid black', width: '150px', height: '150px', background: '#fff' } }, updated(p){ if(p==='traits') { updateStylesAndSync(this); sendDataToExtension(); } } } });
|
||
editor.BlockManager.add('tk-canvas', { label: 'Canvas', category: 'Tkinter', content: { type: 'tk-canvas' } });
|
||
|
||
editor.DomComponents.addType('tk-frame', {
|
||
model: {
|
||
defaults: {
|
||
tagName: 'div', draggable: true, droppable: true,
|
||
attributes: { 'data-tk-type': 'Frame' },
|
||
traits: geoTraits,
|
||
style: { position: 'absolute', left:'0', top:'0', width: '200px', height: '200px', border: '2px dashed #666', background: 'rgba(0,0,0,0.05)' }
|
||
},
|
||
updated(p) { if(p==='traits') { updateStylesAndSync(this); sendDataToExtension(); } }
|
||
}
|
||
});
|
||
editor.BlockManager.add('tk-frame', { label: 'Frame', category: 'Tkinter', content: { type: 'tk-frame' } });
|
||
|
||
// ============================================
|
||
// 5. Вложенность и Синхронизация
|
||
// ============================================
|
||
editor.on('component:drag:end', (model) => {
|
||
const target = model.target;
|
||
if (!target) return;
|
||
if (target.getAttributes()['data-tk-type'] === 'Frame') { syncCoordinates(target); sendDataToExtension(); return; }
|
||
|
||
const targetEl = target.getEl();
|
||
if (!targetEl) return;
|
||
|
||
const currentStyle = target.getStyle();
|
||
let safeWidth = currentStyle.width;
|
||
let safeHeight = currentStyle.height;
|
||
if (!safeWidth || safeWidth === 'auto') safeWidth = targetEl.offsetWidth + 'px';
|
||
if (!safeHeight || safeHeight === 'auto') safeHeight = targetEl.offsetHeight + 'px';
|
||
|
||
const targetRect = targetEl.getBoundingClientRect();
|
||
const zoom = editor.Canvas.getZoom() / 100 * 100;
|
||
|
||
const wrapper = editor.getWrapper();
|
||
const frames = wrapper.find('[data-tk-type="Frame"]');
|
||
|
||
// Центр
|
||
const tCx = targetRect.left + targetRect.width / 2;
|
||
const tCy = targetRect.top + targetRect.height / 2;
|
||
|
||
let bestParent = wrapper;
|
||
let parentRect = wrapper.getEl().getBoundingClientRect();
|
||
|
||
// Если кинули в фрейм
|
||
for (let frame of frames) {
|
||
if (frame === target) continue;
|
||
const fRect = frame.getEl().getBoundingClientRect();
|
||
if (tCx > fRect.left && tCx < fRect.right && tCy > fRect.top && tCy < fRect.bottom) {
|
||
bestParent = frame;
|
||
parentRect = fRect;
|
||
break;
|
||
}
|
||
}
|
||
|
||
const currentParent = target.parent();
|
||
if (bestParent === currentParent) {
|
||
syncCoordinates(target);
|
||
sendDataToExtension();
|
||
return;
|
||
}
|
||
|
||
// Расчет новых координат
|
||
let borderOffset = 0;
|
||
let newX = (targetRect.left - parentRect.left - borderOffset) / zoom;
|
||
let newY = (targetRect.top - parentRect.top - borderOffset) / zoom;
|
||
newX = Math.round(newX); newY = Math.round(newY);
|
||
|
||
bestParent.append(target);
|
||
target.addStyle({ position: 'absolute', left: newX + 'px', top: newY + 'px', width: safeWidth, height: safeHeight, margin: '0' });
|
||
|
||
syncCoordinates(target);
|
||
sendDataToExtension();
|
||
});
|
||
|
||
function updateStylesAndSync(component) {
|
||
if (!component.getTrait) return;
|
||
const x = component.getTrait('x')?.getValue();
|
||
const y = component.getTrait('y')?.getValue();
|
||
const w = component.getTrait('width')?.getValue();
|
||
const h = component.getTrait('height')?.getValue();
|
||
const bg = component.getTrait('bg')?.getValue();
|
||
const fg = component.getTrait('fg')?.getValue();
|
||
|
||
if (x !== undefined && x !== "") component.addStyle({ left: x + 'px' });
|
||
if (y !== undefined && y !== "") component.addStyle({ top: y + 'px' });
|
||
if (w !== undefined && w !== "") component.addStyle({ width: w + 'px' });
|
||
if (h !== undefined && h !== "") component.addStyle({ height: h + 'px' });
|
||
if (bg) component.addStyle({ background: bg });
|
||
if (fg) component.addStyle({ color: fg });
|
||
|
||
syncCoordinates(component);
|
||
}
|
||
|
||
function syncCoordinates(component) {
|
||
if (!component || !component.getStyle) return;
|
||
const style = component.getStyle();
|
||
const attrs = { ...component.getAttributes() };
|
||
attrs['data-x'] = parseInt(style.left || 0);
|
||
attrs['data-y'] = parseInt(style.top || 0);
|
||
attrs['data-width'] = parseInt(style.width);
|
||
attrs['data-height'] = parseInt(style.height);
|
||
const bg = component.getTrait('bg')?.getValue();
|
||
const fg = component.getTrait('fg')?.getValue();
|
||
if(bg) attrs['data-bg'] = bg;
|
||
if(fg) attrs['data-fg'] = fg;
|
||
if (attrs['data-tk-type'] !== 'Listbox') {
|
||
const text = component.getTrait('text')?.getValue();
|
||
if (text) attrs['data-text'] = text;
|
||
}
|
||
component.setAttributes(attrs);
|
||
}
|
||
|
||
function syncAllComponents() {
|
||
const wrapper = editor.getWrapper();
|
||
const syncRecursive = (comp) => {
|
||
if(comp.getAttributes()['data-tk-type']) syncCoordinates(comp);
|
||
comp.components().each(child => syncRecursive(child));
|
||
}
|
||
syncRecursive(wrapper);
|
||
}
|
||
|
||
let isReadyToSave = false;
|
||
function sendDataToExtension() {
|
||
if (!isReadyToSave) return;
|
||
vscode.postMessage({ type: 'update-code', payload: editor.getProjectData() });
|
||
}
|
||
|
||
editor.on('component:add', (c) => { setTimeout(() => { syncCoordinates(c); sendDataToExtension(); }, 50); });
|
||
editor.on('component:remove', () => { sendDataToExtension(); });
|
||
|
||
function handleUndoRedo() {
|
||
editor.select(null);
|
||
setTimeout(() => {
|
||
const currentData = editor.getProjectData();
|
||
editor.loadProjectData(currentData);
|
||
syncAllComponents();
|
||
sendDataToExtension();
|
||
}, 150);
|
||
}
|
||
|
||
editor.on('run:undo', handleUndoRedo);
|
||
editor.on('run:redo', handleUndoRedo);
|
||
|
||
window.addEventListener('message', e => {
|
||
const d = e.data;
|
||
if (d.type === 'load-data' || d.type === 'import-data') {
|
||
try {
|
||
if(d.payload && Object.keys(d.payload).length > 0) {
|
||
if (d.type === 'import-data') editor.DomComponents.clear();
|
||
editor.loadProjectData(d.payload);
|
||
editor.UndoManager.clear();
|
||
|
||
// ВОССТАНОВЛЕНИЕ РАЗМЕРОВ ОКНА
|
||
const wrapper = editor.getWrapper();
|
||
const wAttrs = wrapper.getAttributes();
|
||
if (wAttrs['data-width']) {
|
||
wrapper.addStyle({
|
||
width: wAttrs['data-width'] + 'px',
|
||
height: wAttrs['data-height'] + 'px'
|
||
});
|
||
}
|
||
|
||
if (d.type === 'import-data') {
|
||
setTimeout(() => { syncAllComponents(); sendDataToExtension(); }, 200);
|
||
}
|
||
}
|
||
} catch(err){ console.error(err); }
|
||
setTimeout(() => isReadyToSave = true, 500);
|
||
}
|
||
});
|
||
|
||
setTimeout(() => { vscode.postMessage({ type: 'request-load' }); }, 100); |