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 = `
`; 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 += `
${val}
`; 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);