prac2025vscode/media/main.js
2025-12-22 14:39:20 +03:00

378 lines
16 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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);