From d8b0c738c9f664a2adc451513db96831fa9d923d Mon Sep 17 00:00:00 2001 From: IDK Date: Fri, 26 Dec 2025 19:28:22 +0300 Subject: [PATCH] refactor --- .dockerignore | 12 + .gitignore | 1 - Dockerfile | 25 ++ README.md | 83 ++++ package-lock.json | 11 + package.json | 1 + src/constants.ts | 50 +++ src/extension.ts | 104 +---- src/generator/CodeGenerator.ts | 84 ++++- src/generator/eventHelpers.ts | 43 ++- src/generator/types.ts | 21 +- src/generator/utils.ts | 3 +- src/parser/CodeParser.ts | 146 ++----- src/parser/astConverter.ts | 180 ++++++--- src/parser/astTypes.ts | 60 +++ src/parser/pythonRunner.ts | 233 ++++++++---- .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 390 bytes .../__pycache__/grid_layout.cpython-314.pyc | Bin 0 -> 3395 bytes .../tk_ast/__pycache__/parser.cpython-314.pyc | Bin 0 -> 2845 bytes .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 266 bytes .../analyzer/__pycache__/base.cpython-314.pyc | Bin 0 -> 14789 bytes .../__pycache__/calls.cpython-314.pyc | Bin 0 -> 2445 bytes .../__pycache__/connections.cpython-314.pyc | Bin 0 -> 4139 bytes .../__pycache__/context.cpython-314.pyc | Bin 0 -> 1235 bytes .../__pycache__/events.cpython-314.pyc | Bin 0 -> 4816 bytes .../__pycache__/extractors.cpython-314.pyc | Bin 0 -> 2016 bytes .../__pycache__/imports.cpython-314.pyc | Bin 0 -> 1638 bytes .../__pycache__/placements.cpython-314.pyc | Bin 0 -> 2006 bytes .../__pycache__/values.cpython-314.pyc | Bin 0 -> 11515 bytes .../widget_creation.cpython-314.pyc | Bin 0 -> 6181 bytes src/parser/tk_ast/analyzer/base.py | 127 ++++++- src/parser/tk_ast/analyzer/calls.py | 4 - src/parser/tk_ast/analyzer/values.py | 14 +- src/parser/tk_ast/analyzer/widget_creation.py | 17 +- src/parser/tk_ast/parser.py | 3 +- src/parser/tkinter_ast_parser.py | 12 +- src/parser/utils.ts | 20 +- src/webview/TkinterDesignerProvider.ts | 343 +++++++++++++---- src/webview/projectIO.ts | 95 +++++ src/webview/react/App.tsx | 27 +- src/webview/react/components/Canvas.tsx | 326 ++++++++++++++-- .../react/components/ErrorBoundary.tsx | 45 +++ src/webview/react/components/EventsPanel.tsx | 112 ++++-- src/webview/react/components/Palette.tsx | 2 +- .../react/components/PropertiesPanel.tsx | 77 +++- src/webview/react/components/Toolbar.tsx | 105 +++--- src/webview/react/index.tsx | 2 +- src/webview/react/state.tsx | 356 ++++++++++-------- src/webview/react/types.ts | 11 + src/webview/react/useMessaging.ts | 13 +- src/webview/style.css | 339 +++++++++++++++-- 51 files changed, 2270 insertions(+), 837 deletions(-) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 src/constants.ts create mode 100644 src/parser/astTypes.ts create mode 100644 src/parser/tk_ast/__pycache__/__init__.cpython-314.pyc create mode 100644 src/parser/tk_ast/__pycache__/grid_layout.cpython-314.pyc create mode 100644 src/parser/tk_ast/__pycache__/parser.cpython-314.pyc create mode 100644 src/parser/tk_ast/analyzer/__pycache__/__init__.cpython-314.pyc create mode 100644 src/parser/tk_ast/analyzer/__pycache__/base.cpython-314.pyc create mode 100644 src/parser/tk_ast/analyzer/__pycache__/calls.cpython-314.pyc create mode 100644 src/parser/tk_ast/analyzer/__pycache__/connections.cpython-314.pyc create mode 100644 src/parser/tk_ast/analyzer/__pycache__/context.cpython-314.pyc create mode 100644 src/parser/tk_ast/analyzer/__pycache__/events.cpython-314.pyc create mode 100644 src/parser/tk_ast/analyzer/__pycache__/extractors.cpython-314.pyc create mode 100644 src/parser/tk_ast/analyzer/__pycache__/imports.cpython-314.pyc create mode 100644 src/parser/tk_ast/analyzer/__pycache__/placements.cpython-314.pyc create mode 100644 src/parser/tk_ast/analyzer/__pycache__/values.cpython-314.pyc create mode 100644 src/parser/tk_ast/analyzer/__pycache__/widget_creation.cpython-314.pyc create mode 100644 src/webview/projectIO.ts create mode 100644 src/webview/react/components/ErrorBoundary.tsx diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..0f90a4a --- /dev/null +++ b/.dockerignore @@ -0,0 +1,12 @@ +node_modules +out +.vscode-test +.git +.gitignore +.dockerignore +Dockerfile +.gitea +README.md +examples/ +.vscode/ +docs/ diff --git a/.gitignore b/.gitignore index 5a2ea14..3f7a7a1 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,4 @@ out/ docs/ node_modules/ -README.md examples/ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..81bf7d6 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,25 @@ +# Use Node.js LTS +FROM node:18-slim + +# Install Python 3 and git +RUN apt-get update && apt-get install -y python3 python3-pip git && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +# Copy package files +COPY package.json package-lock.json ./ + +# Install dependencies +RUN npm ci + +# Copy source code +COPY . . + +# Build the extension +RUN npm run compile + +# Install vsce to package the extension +RUN npm install -g @vscode/vsce + +# Default command to package the extension +CMD ["vsce", "package", "--out", "extension.vsix"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..567c173 --- /dev/null +++ b/README.md @@ -0,0 +1,83 @@ +## Tkinter Visual Designer (VS Code Extension) + +Tkinter Visual Designer — это расширение для Visual Studio Code, которое превращает +разработку Tkinter‑интерфейсов в наглядный процесс: вместо ручного написания +координат и вызовов `.place()` / `.grid()` вы работаете с визуальным конструктором. + +Основная идея — **разводить логику и разметку**: + +- разметка окна (виджеты, их положение и свойства) проектируется в визуальном редакторе; +- логика приложения (обработчики событий, бизнес‑код) остаётся в Python‑файле. + +### Что умеет приложение + +- Визуальное редактирование формы: + - перетаскивание виджетов (Label, Button, Entry, Text, Checkbutton, Radiobutton) на холст; + - изменение размера и положения виджетов мышью; + - настройка свойств (текст, размеры, позиция) через панель свойств. +- Работа с событиями: + - привязка обработчиков к виджетам (например, `command` у кнопки); + - просмотр уже существующих привязок. +- Генерация кода: + - создание Python‑кода по текущему дизайну; + - экспорт/импорт проекта в JSON. +- Разбор существующего кода: + - парсер анализирует ваш Tkinter‑код и восстанавливает дизайн формы; + - поддерживаются классические приложения с классом‑обёрткой и методами `__init__`, `create_widgets` и т.п. + +### Типичный сценарий работы + +1. Открыть или создать Python‑файл с Tkinter‑приложением. +2. Запустить визуальный дизайнер (см. раздел «Запуск приложения» ниже). +3. В дизайнере: + - перетаскивать виджеты из палитры на холст; + - настраивать свойства и события в боковой панели; + - при необходимости импортировать существующий дизайн из кода. +4. Сгенерировать обновлённый Python‑код и сохранить его. +5. Запустить приложение Python уже с новым интерфейсом. + +### Требования + +- Visual Studio Code ≥ 1.74 +- Node.js и npm +- Python 3 (доступный в системе как `python`, `python3`, `py` или указанный в настройках VS Code) + +### Установка зависимостей + +```bash +npm install +``` + +### Сборка расширения и веб‑интерфейса + +```bash +npm run compile +``` + +### Запуск приложения (расширения) + +1. Откройте папку проекта в VS Code. +2. Выполните `npm install` и `npm run compile`, если ещё не делали это. +3. Нажмите `F5` или выберите в меню: + - Run → Start Debugging (Запуск → Начать отладку). +4. Откроется новое окно VS Code с установленным в нём расширением. + +В этом окне вы можете: + +- Открыть любой `.py` файл с Tkinter‑кодом. +- Открыть визуальный дизайнер: + - через командную палитру (`Ctrl+Shift+P` → `Tkinter: Open Tkinter Designer`), или + - через контекстное меню в проводнике: правый клик по `.py` → `Open Tkinter Designer`. + +### Основные команды + +Расширение регистрирует следующие команды: + +- `Tkinter: Open Tkinter Designer` — открыть визуальный дизайнер для текущего Python‑файла. +- `Tkinter: Generate Python Code` — сгенерировать Python‑код из текущего дизайна. +- `Tkinter: Parse Tkinter Code` — разобрать существующий Tkinter‑код и загрузить его в дизайнер. + +### Примечания + +- Для корректной работы парсера обязательно наличие установленного Python 3. +- Генерация и разбор кода основаны на стандартной структуре Tkinter‑приложений (классы, методы `__init__`, `create_widgets`, и т.п.). diff --git a/package-lock.json b/package-lock.json index ec9970e..17cfecc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "tkinter-designer", "version": "0.1.0", "dependencies": { + "immer": "^11.0.1", "react": "^18.3.1", "react-dom": "^18.3.1" }, @@ -1243,6 +1244,16 @@ "node": ">= 4" } }, + "node_modules/immer": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.0.1.tgz", + "integrity": "sha512-naDCyggtcBWANtIrjQEajhhBEuL9b0Zg4zmlWK2CzS6xCWSE39/vvf4LqnMjUAWHBhot4m9MHCM/Z+mfWhUkiA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", diff --git a/package.json b/package.json index 881b592..e8e56b2 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "format": "prettier --write \"**/*.{ts,tsx,js,jsx,json,css,md}\"" }, "dependencies": { + "immer": "^11.0.1", "react": "^18.3.1", "react-dom": "^18.3.1" }, diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 0000000..3b7ddae --- /dev/null +++ b/src/constants.ts @@ -0,0 +1,50 @@ +export enum WidgetType { + Label = 'Label', + Button = 'Button', + Entry = 'Entry', + Text = 'Text', + Checkbutton = 'Checkbutton', + Radiobutton = 'Radiobutton', + Frame = 'Frame', + Widget = 'Widget', +} + +export const DEFAULT_FORM_CONFIG = { + TITLE: 'Parsed App', + WIDTH: 800, + HEIGHT: 600, + CLASS_NAME: 'Application', + FORM_NAME: 'Form', +}; + +export const WIDGET_DIMENSIONS = { + [WidgetType.Label]: { width: 100, height: 25 }, + [WidgetType.Button]: { width: 80, height: 30 }, + [WidgetType.Entry]: { width: 150, height: 25 }, + [WidgetType.Text]: { width: 200, height: 100 }, + [WidgetType.Checkbutton]: { width: 100, height: 25 }, + [WidgetType.Radiobutton]: { width: 100, height: 25 }, + [WidgetType.Frame]: { width: 200, height: 200 }, + [WidgetType.Widget]: { width: 100, height: 25 }, + DEFAULT: { width: 100, height: 25 }, +}; + +export const EVENT_TYPES = { + COMMAND: 'command', +}; + +export const PYTHON_CODE = { + IMPORTS: { + TKINTER: 'import tkinter as tk', + TTK: 'from tkinter import ttk', + }, + METHODS: { + INIT: '__init__', + CREATE_WIDGETS: 'create_widgets', + RUN: 'run', + }, + VARIABLES: { + ROOT: 'self.root', + APP: 'app', + } +}; diff --git a/src/extension.ts b/src/extension.ts index 0519eeb..0e4bf79 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,130 +1,59 @@ import * as vscode from 'vscode'; import * as path from 'path'; -import { CodeGenerator } from './generator/CodeGenerator'; -import { CodeParser } from './parser/CodeParser'; -import { TkinterDesignerProvider } from './webview/TkinterDesignerProvider'; - -export function activate(context: vscode.ExtensionContext) { - const provider = new TkinterDesignerProvider(context.extensionUri); - - TkinterDesignerProvider._instance = provider; +import { CodeParser } from './parser/codeParser'; +import { TkinterDesignerProvider } from './webview/tkinterDesignerProvider'; +export function activate(context: vscode.ExtensionContext) {3 const openDesignerCommand = vscode.commands.registerCommand( 'tkinter-designer.openDesigner', () => { - TkinterDesignerProvider.createOrShow(context.extensionUri); - } - ); - - const generateCodeCommand = vscode.commands.registerCommand( - 'tkinter-designer.generateCode', - async () => { - console.log('[GenerateCode] Command invoked'); - const generator = new CodeGenerator(); - const designData = await provider.getDesignData(); - if ( - !designData || - !designData.widgets || - designData.widgets.length === 0 - ) { - console.log('[GenerateCode] No design data'); - vscode.window.showWarningMessage( - 'No design data found. Please open the designer and create some widgets first.' - ); - return; - } - - const pythonCode = generator.generateTkinterCode(designData); - const activeEditor = vscode.window.activeTextEditor; - - if (activeEditor && activeEditor.document.languageId === 'python') { - console.log('[GenerateCode] Writing into active Python file'); - const doc = activeEditor.document; - const start = new vscode.Position(0, 0); - const end = doc.lineCount - ? doc.lineAt(doc.lineCount - 1).range.end - : start; - const fullRange = new vscode.Range(start, end); - await activeEditor.edit((editBuilder) => { - editBuilder.replace(fullRange, pythonCode); - }); - await doc.save(); - vscode.window.showInformationMessage( - 'Python code generated into the active file' - ); - } else { - console.log('[GenerateCode] Creating new Python file'); - const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; - if (!workspaceFolder) { - vscode.window.showErrorMessage( - 'No workspace folder is open. Please open a folder first.' - ); - return; - } - const fileName = `app_${Date.now()}.py`; - const filePath = path.join( - workspaceFolder.uri.fsPath, - fileName - ); - const fileUri = vscode.Uri.file(filePath); - const encoder = new TextEncoder(); - const fileBytes = encoder.encode(pythonCode); - await vscode.workspace.fs.writeFile(fileUri, fileBytes); - const doc = await vscode.workspace.openTextDocument(fileUri); - await vscode.window.showTextDocument(doc, { preview: false }); - vscode.window.showInformationMessage( - `Python file created: ${fileName}` - ); - } - console.log('[GenerateCode] Done'); + TkinterDesignerProvider.createNew(context.extensionUri); } ); const parseCodeCommand = vscode.commands.registerCommand( 'tkinter-designer.parseCode', async () => { - console.log('[ParseCode] Command invoked'); const activeEditor = vscode.window.activeTextEditor; if (activeEditor && activeEditor.document.languageId === 'python') { const parser = new CodeParser(); const code = activeEditor.document.getText(); - console.log('[ParseCode] Code length:', code.length); + const fileName = path.basename( + activeEditor.document.fileName, + '.py' + ); try { - const designData = await parser.parseCodeToDesign(code); + const designData = await parser.parseCodeToDesign( + code, + fileName + ); if ( designData && designData.widgets && designData.widgets.length > 0 ) { - console.log('[ParseCode] Widgets found:', designData.widgets.length); - const designerInstance = - TkinterDesignerProvider.createOrShow( + const designerInstance = TkinterDesignerProvider.createNew( context.extensionUri - ); - if (designerInstance) { - designerInstance.loadDesignData(designData); - } else { - } + ); + + designerInstance.loadDesignData(designData); vscode.window.showInformationMessage( `Code parsed successfully! Found ${designData.widgets.length} widgets.` ); } else { - console.log('[ParseCode] No widgets found'); vscode.window.showWarningMessage( 'No tkinter widgets found in the code. Make sure your code contains tkinter widget creation statements like tk.Label(), tk.Button(), etc.' ); } } catch (error) { - console.error('[ParseCode] Error:', error); vscode.window.showErrorMessage( `Error parsing code: ${error}` ); } } else { - console.log('[ParseCode] No active Python editor'); vscode.window.showErrorMessage( 'Please open a Python file with tkinter code' ); @@ -134,7 +63,6 @@ export function activate(context: vscode.ExtensionContext) { context.subscriptions.push( openDesignerCommand, - generateCodeCommand, parseCodeCommand ); } diff --git a/src/generator/CodeGenerator.ts b/src/generator/CodeGenerator.ts index 014cfbc..9a51dfc 100644 --- a/src/generator/CodeGenerator.ts +++ b/src/generator/CodeGenerator.ts @@ -1,9 +1,10 @@ -import { DesignData, WidgetData } from './types'; +import { DesignData, WidgetData, DesignEvent } from './types'; import { getVariableName, generateVariableNames, getWidgetTypeForGeneration, indentText, + escapeString, } from './utils'; import { getWidgetParameters, @@ -11,6 +12,7 @@ import { generateWidgetContent, } from './widgetHelpers'; import { generateEventHandlers, getWidgetEventBindings } from './eventHelpers'; +import { DEFAULT_FORM_CONFIG, PYTHON_CODE } from '../constants'; export class CodeGenerator { private indentLevel = 0; @@ -18,48 +20,46 @@ export class CodeGenerator { public generateTkinterCode(designData: DesignData): string { this.designData = designData; - console.log('[Generator] Start, widgets:', designData.widgets.length, 'events:', designData.events?.length || 0); + const className = designData.form.className || DEFAULT_FORM_CONFIG.CLASS_NAME; const lines: string[] = []; const nameMap = generateVariableNames(designData.widgets); - lines.push('import tkinter as tk'); + lines.push(PYTHON_CODE.IMPORTS.TKINTER); + lines.push(PYTHON_CODE.IMPORTS.TTK); lines.push(''); - lines.push('class Application:'); + lines.push(`class ${className}:`); this.indentLevel = 1; - lines.push(this.indent('def __init__(self):')); + lines.push(this.indent(`def ${PYTHON_CODE.METHODS.INIT}(self):`)); this.indentLevel = 2; - lines.push(this.indent('self.root = tk.Tk()')); - lines.push(this.indent(`self.root.title("${designData.form.title}")`)); + lines.push(this.indent(`${PYTHON_CODE.VARIABLES.ROOT} = tk.Tk()`)); + lines.push(this.indent(`${PYTHON_CODE.VARIABLES.ROOT}.title("${escapeString(designData.form.title)}")`)); lines.push( this.indent( - `self.root.geometry("${designData.form.size.width}x${designData.form.size.height}")` + `${PYTHON_CODE.VARIABLES.ROOT}.geometry("${designData.form.size.width}x${designData.form.size.height}")` ) ); - lines.push(this.indent('self.create_widgets()')); + lines.push(this.indent(`self.${PYTHON_CODE.METHODS.CREATE_WIDGETS}()`)); lines.push(''); this.indentLevel = 1; - lines.push(this.indent('def create_widgets(self):')); + lines.push(this.indent(`def ${PYTHON_CODE.METHODS.CREATE_WIDGETS}(self):`)); this.indentLevel = 2; designData.widgets.forEach((widget) => { - console.log('[Generator] Widget:', widget.id, widget.type); lines.push(...this.generateWidgetCode(widget, nameMap)); - lines.push(''); }); this.indentLevel = 1; - lines.push(this.indent('def run(self):')); + lines.push(this.indent(`def ${PYTHON_CODE.METHODS.RUN}(self):`)); this.indentLevel = 2; - lines.push(this.indent('self.root.mainloop()')); + lines.push(this.indent(`${PYTHON_CODE.VARIABLES.ROOT}.mainloop()`)); lines.push(''); const hasEvents = designData.events && designData.events.length > 0; if (hasEvents) { - console.log('[Generator] Generating event handlers'); lines.push( ...generateEventHandlers( designData, @@ -72,12 +72,58 @@ export class CodeGenerator { this.indentLevel = 0; lines.push('if __name__ == "__main__":'); this.indentLevel = 1; - lines.push(this.indent('app = Application()')); - lines.push(this.indent('app.run()')); + lines.push(this.indent('try:')); + this.indentLevel = 2; + lines.push(this.indent(`${PYTHON_CODE.VARIABLES.APP} = ${className}()`)); + lines.push(this.indent(`${PYTHON_CODE.VARIABLES.APP}.${PYTHON_CODE.METHODS.RUN}()`)); + this.indentLevel = 1; + lines.push(this.indent('except Exception as e:')); + this.indentLevel = 2; + lines.push(this.indent('import traceback')); + lines.push(this.indent('traceback.print_exc()')); + lines.push(this.indent('input("Press Enter to exit...")')); return lines.join('\n'); } + public generateCreateWidgetsBody(designData: DesignData): string { + this.designData = designData; + const lines: string[] = []; + const nameMap = generateVariableNames(designData.widgets); + this.indentLevel = 2; + + designData.widgets.forEach((widget) => { + lines.push(...this.generateWidgetCode(widget, nameMap)); + }); + + return lines.join('\n'); + } + + public generateEventHandler(event: DesignEvent): string { + const lines: string[] = []; + + if (event.signature) { + lines.push(` ${event.signature}:`); + } else { + lines.push(` def ${event.name}(self, event=None):`); + } + + if (event.code) { + const codeContent = + typeof event.code === 'string' + ? event.code + : String(event.code || ''); + const body = codeContent + .split('\n') + .map((l: string) => indentText(2, l)) + .join('\n'); + lines.push(body); + } else { + lines.push(' pass'); + } + return lines.join('\n'); + } + private generateWidgetCode( widget: WidgetData, nameMap: Map @@ -96,7 +142,9 @@ export class CodeGenerator { lines.push(this.indent(`self.${varName}.place(${placeParams})`)); const contentLines = generateWidgetContent(widget, varName); - contentLines.forEach((line) => lines.push(this.indent(line))); + if (contentLines.length > 0) { + contentLines.forEach((l) => lines.push(this.indent(l))); + } lines.push( ...getWidgetEventBindings( diff --git a/src/generator/eventHelpers.ts b/src/generator/eventHelpers.ts index fc10496..be2aa11 100644 --- a/src/generator/eventHelpers.ts +++ b/src/generator/eventHelpers.ts @@ -1,4 +1,4 @@ -import { DesignData, Event, WidgetData } from './types'; +import { DesignData, DesignEvent, WidgetData } from './types'; export function generateEventHandlers( designData: DesignData, @@ -8,22 +8,39 @@ export function generateEventHandlers( const lines: string[] = []; if (designData.events && designData.events.length > 0) { - designData.events.forEach((event: Event) => { - const handlerName = event.name; - const isBindEvent = - event.type.startsWith('<') && event.type.endsWith('>'); + const handledNames = new Set(); - let hasCode = false; - const widget = (designData.widgets || []).find( - (w) => w.id === event.widget - ); - if (isBindEvent) { - lines.push(indentDef(`def ${handlerName}(self, event):`)); + designData.events.forEach((event: DesignEvent) => { + const handlerName = event.name; + + if (handledNames.has(handlerName)) { + return; + } + handledNames.add(handlerName); + + const signature = event.signature; + + if (signature) { + lines.push(indentDef(`${signature}:`)); } else { - lines.push(indentDef(`def ${handlerName}(self):`)); + const isBindEvent = + event.type.startsWith('<') && event.type.endsWith('>'); + + if (isBindEvent) { + lines.push( + indentDef(`def ${handlerName}(self, event=None):`) + ); + } else { + lines.push(indentDef(`def ${handlerName}(self):`)); + } } - const codeLines = (event.code || '').split('\n'); + let hasCode = false; + const codeContent = + typeof event.code === 'string' + ? event.code + : String(event.code || ''); + const codeLines = codeContent.split('\n'); for (const line of codeLines) { if (line.trim()) { lines.push(indentBody(line)); diff --git a/src/generator/types.ts b/src/generator/types.ts index 3f67540..e1b4815 100644 --- a/src/generator/types.ts +++ b/src/generator/types.ts @@ -1,8 +1,21 @@ -export interface Event { +export interface DesignEvent { widget: string; type: string; name: string; code: string; + signature?: string; +} + +export interface WidgetProperties { + text?: string; + command?: string | { name?: string; lambda_body?: string }; + variable?: string; + orient?: string; + from_?: number; + to?: number; + width?: number; + height?: number; + [key: string]: any; } export interface WidgetData { @@ -12,17 +25,19 @@ export interface WidgetData { y: number; width: number; height: number; - properties: { [key: string]: any }; + properties: WidgetProperties; } export interface DesignData { form: { + name: string; title: string; size: { width: number; height: number; }; + className?: string; }; widgets: WidgetData[]; - events?: Event[]; + events?: DesignEvent[]; } diff --git a/src/generator/utils.ts b/src/generator/utils.ts index 7d8796e..01b222a 100644 --- a/src/generator/utils.ts +++ b/src/generator/utils.ts @@ -25,12 +25,11 @@ export function generateVariableNames( widgets.forEach((widget) => { let baseName = widget.type.toLowerCase(); - // Handle special cases or short forms if desired, e.g. 'button' -> 'btn' if (baseName === 'button') baseName = 'btn'; + if (baseName === 'entry') baseName = 'entry'; if (baseName === 'checkbutton') baseName = 'chk'; if (baseName === 'radiobutton') baseName = 'radio'; if (baseName === 'label') baseName = 'lbl'; - const count = (counts.get(baseName) || 0) + 1; counts.set(baseName, count); diff --git a/src/parser/CodeParser.ts b/src/parser/CodeParser.ts index 8f2ac7f..c7dd98f 100644 --- a/src/parser/CodeParser.ts +++ b/src/parser/CodeParser.ts @@ -1,137 +1,33 @@ -import { DesignData, WidgetData, Event } from '../generator/types'; +import * as vscode from 'vscode'; +import { DesignData, WidgetData, DesignEvent } from '../generator/types'; import { runPythonAst } from './pythonRunner'; import { convertASTResultToDesignData } from './astConverter'; +import { ASTResult } from './astTypes'; export class CodeParser { public async parseCodeToDesign( - pythonCode: string + pythonCode: string, + filename?: string ): Promise { - console.log( - '[Parser] parseCodeToDesign start, code length:', - pythonCode.length - ); const astRaw = await runPythonAst(pythonCode); - const astDesign = convertASTResultToDesignData(astRaw); - if (astDesign && astDesign.widgets && astDesign.widgets.length > 0) { - console.log( - '[Parser] AST parsed widgets:', - astDesign.widgets.length - ); - return astDesign; - } - console.log('[Parser] AST returned no widgets, using regex fallback'); - const regexDesign = this.parseWithRegexInline(pythonCode); - console.log( - '[Parser] Regex parsed widgets:', - regexDesign?.widgets?.length || 0 - ); - return regexDesign; - } - private parseWithRegexInline(code: string): DesignData | null { - const widgetRegex = - /(self\.)?(\w+)\s*=\s*tk\.(Label|Button|Text|Checkbutton|Radiobutton)\s*\(([^)]*)\)/g; - const placeRegex = /(self\.)?(\w+)\.place\s*\(([^)]*)\)/g; - const titleRegex = /\.title\s*\(\s*(["'])(.*?)\1\s*\)/; - const geometryRegex = /\.geometry\s*\(\s*(["'])(\d+)x(\d+)\1\s*\)/; - - const widgets: WidgetData[] = []; - const widgetMap = new Map(); - - let formTitle = 'App'; - let formWidth = 800; - let formHeight = 600; - - const tMatch = code.match(titleRegex); - if (tMatch) formTitle = tMatch[2]; - const gMatch = code.match(geometryRegex); - if (gMatch) { - formWidth = parseInt(gMatch[2], 10) || 800; - formHeight = parseInt(gMatch[3], 10) || 600; - } - - let m: RegExpExecArray | null; - while ((m = widgetRegex.exec(code)) !== null) { - const varName = m[2]; - const type = m[3]; - const paramStr = m[4] || ''; - const id = varName; - const w: WidgetData = { - id, - type, - x: 0, - y: 0, - width: 100, - height: 25, - properties: {}, - }; - const textMatch = paramStr.match(/text\s*=\s*(["'])(.*?)\1/); - if (textMatch) { - w.properties.text = textMatch[2]; - } - const widthMatch = paramStr.match(/width\s*=\s*(\d+)/); - if (widthMatch) { - const wv = parseInt(widthMatch[1], 10); - if (!isNaN(wv)) w.width = wv; - } - const heightMatch = paramStr.match(/height\s*=\s*(\d+)/); - if (heightMatch) { - const hv = parseInt(heightMatch[1], 10); - if (!isNaN(hv)) w.height = hv; - } - widgets.push(w); - widgetMap.set(varName, w); - } - - let p: RegExpExecArray | null; - while ((p = placeRegex.exec(code)) !== null) { - const varName = p[2]; - const params = p[3]; - const w = widgetMap.get(varName); - if (!w) continue; - const getNum = (key: string) => { - const r = new RegExp(key + '\\s*[:=]\\s*(\d+)'); - const mm = params.match(r); - return mm ? parseInt(mm[1], 10) : undefined; - }; - const x = getNum('x'); - const y = getNum('y'); - const width = getNum('width'); - const height = getNum('height'); - if (typeof x === 'number') w.x = x; - if (typeof y === 'number') w.y = y; - if (typeof width === 'number') w.width = width; - if (typeof height === 'number') w.height = height; - } - - const events: Event[] = []; - const configRegex = - /(self\.)?(\w+)\.config\s*\(\s*command\s*=\s*(self\.)?(\w+)\s*\)/g; - let c: RegExpExecArray | null; - while ((c = configRegex.exec(code)) !== null) { - const varName = c[2]; - const handler = c[4]; - if (widgetMap.has(varName)) { - events.push({ - widget: varName, - type: 'command', - name: handler, - code: '', - }); - } - } - - if (widgets.length === 0) { - console.log('[Parser] Regex found 0 widgets'); + if (astRaw && astRaw.error) { + vscode.window.showErrorMessage(`Parser Error: ${astRaw.error}`); return null; } - return { - form: { - title: formTitle, - size: { width: formWidth, height: formHeight }, - }, - widgets, - events, - }; + + const astDesign = convertASTResultToDesignData(astRaw as ASTResult); + + if (astDesign) { + if (filename) { + astDesign.form.name = filename; + } + return astDesign; + } + + vscode.window.showErrorMessage( + 'Could not parse Python code safely. Please ensure your code has no syntax errors and follows the standard structure (class-based Tkinter app).' + ); + return null; } } diff --git a/src/parser/astConverter.ts b/src/parser/astConverter.ts index 5eb1da9..4420212 100644 --- a/src/parser/astConverter.ts +++ b/src/parser/astConverter.ts @@ -1,10 +1,15 @@ -import { DesignData, WidgetData, Event } from '../generator/types'; +import { DesignData, WidgetData, DesignEvent, WidgetProperties } from '../generator/types'; +import { ASTResult, ASTWidget, ASTMethodData } from './astTypes'; import { getDefaultWidth, getDefaultHeight } from './utils'; +import { WidgetType, DEFAULT_FORM_CONFIG, EVENT_TYPES } from '../constants'; -export function convertASTResultToDesignData( - astResult: any -): DesignData | null { - if (!astResult || !astResult.widgets || astResult.widgets.length === 0) { +export function convertASTResultToDesignData(astResult: ASTResult): DesignData | null { + if (!astResult) { + return null; + } + + + if (!astResult.window && !astResult.widgets) { return null; } @@ -12,48 +17,44 @@ export function convertASTResultToDesignData( let formTitle = (astResult.window && astResult.window.title) || astResult.title || - 'Parsed App'; + DEFAULT_FORM_CONFIG.TITLE; let formWidth = - (astResult.window && astResult.window.width) || astResult.width || 800; + (astResult.window && astResult.window.width) || astResult.width || DEFAULT_FORM_CONFIG.WIDTH; let formHeight = (astResult.window && astResult.window.height) || astResult.height || - 600; + DEFAULT_FORM_CONFIG.HEIGHT; + let className = + (astResult.window && astResult.window.className) || DEFAULT_FORM_CONFIG.CLASS_NAME; let counter = 0; const allowedTypes = new Set([ - 'Label', - 'Button', - 'Text', - 'Checkbutton', - 'Radiobutton', + WidgetType.Label, + WidgetType.Button, + WidgetType.Entry, + WidgetType.Text, + WidgetType.Checkbutton, + WidgetType.Radiobutton, ]); - for (const w of astResult.widgets) { + + + const inputWidgets = astResult.widgets || []; + + for (const w of inputWidgets) { counter++; - const type = w.type || 'Widget'; - if (!allowedTypes.has(type)) { + const type = w.type || WidgetType.Widget; + if (!allowedTypes.has(type as WidgetType)) { continue; } const place = w.placement || {}; const x = place.x !== undefined ? place.x : w.x !== undefined ? w.x : 0; const y = place.y !== undefined ? place.y : w.y !== undefined ? w.y : 0; - const p = (w.properties || w.params || {}) as any; - const width = - place.width !== undefined - ? place.width - : w.width !== undefined - ? w.width - : p.width !== undefined - ? p.width - : getDefaultWidth(type); - const height = - place.height !== undefined - ? place.height - : w.height !== undefined - ? w.height - : p.height !== undefined - ? p.height - : getDefaultHeight(type); + + + const p: Record = w.properties || w.params || {}; + + const width = [place.width, w.width, p.width].find(v => v !== undefined) ?? getDefaultWidth(type); + const height = [place.height, w.height, p.height].find(v => v !== undefined) ?? getDefaultHeight(type); const id = w.variable_name || `ast_widget_${counter}`; const widget: WidgetData = { id, @@ -67,33 +68,58 @@ export function convertASTResultToDesignData( widgets.push(widget); } - const events: Event[] = []; + const events: DesignEvent[] = []; + const methods = astResult.methods || {}; + if (astResult.command_callbacks) { for (const callback of astResult.command_callbacks) { - const rawName = callback.command?.name; - const cleanName = rawName - ? String(rawName).replace(/^self\./, '') - : `on_${callback.widget}_command`; - events.push({ - widget: callback.widget, - type: 'command', - name: cleanName, - code: callback.command?.lambda_body || '', - }); + addDesignEvent( + events, + methods, + callback.widget, + EVENT_TYPES.COMMAND, + callback.command?.name, + () => `on_${callback.widget}_command`, + callback.command?.lambda_body + ); } } + if (astResult.bind_events) { for (const bindEvent of astResult.bind_events) { - const rawName = bindEvent.callback?.name; - const cleanName = rawName - ? String(rawName).replace(/^self\./, '') - : `on_${bindEvent.widget}_${String(bindEvent.event).replace(/[<>]/g, '').replace(/-/g, '_')}`; - events.push({ - widget: bindEvent.widget, - type: bindEvent.event, - name: cleanName, - code: bindEvent.callback?.lambda_body || '', - }); + addDesignEvent( + events, + methods, + bindEvent.widget, + bindEvent.event, + bindEvent.callback?.name, + () => `on_${bindEvent.widget}_${String(bindEvent.event).replace(/[<>]/g, '').replace(/-/g, '_')}`, + bindEvent.callback?.lambda_body + ); + } + } + + if (astResult.widgets) { + for (const w of astResult.widgets) { + const p: Record = w.properties || w.params || {}; + if (p.command) { + const widgetId = w.variable_name; + if (!widgetId) continue; + + const cmd = p.command; + const rawName = cmd.name || cmd; + + addDesignEvent( + events, + methods, + widgetId, + EVENT_TYPES.COMMAND, + rawName, + () => `on_${widgetId}_command`, + undefined, + true + ); + } } } @@ -102,8 +128,10 @@ export function convertASTResultToDesignData( const result: DesignData = { form: { + name: DEFAULT_FORM_CONFIG.FORM_NAME, title: formTitle, size: { width: formWidth, height: formHeight }, + className: className, }, widgets, events: filteredEvents.length ? filteredEvents : [], @@ -111,8 +139,8 @@ export function convertASTResultToDesignData( return result; } -function extractWidgetPropertiesFromAST(w: any): any { - const props: any = {}; +function extractWidgetPropertiesFromAST(w: ASTWidget): WidgetProperties { + const props: WidgetProperties = {}; if (!w) return props; const p = w.properties || w.params || {}; if (p.text) props.text = p.text; @@ -125,3 +153,43 @@ function extractWidgetPropertiesFromAST(w: any): any { if (p.height !== undefined) props.height = p.height; return props; } + +function addDesignEvent( + events: DesignEvent[], + methods: Record, + widgetId: string, + eventType: string, + rawName: string | undefined | null, + defaultNameGen: () => string, + lambdaBody: string | undefined, + checkExists: boolean = false +) { + const cleanName = rawName + ? String(rawName).replace(/^self\./, '') + : defaultNameGen(); + + if (checkExists) { + const exists = events.find( + (e) => e.widget === widgetId && e.type === eventType + ); + if (exists) return; + } + + const methodData = methods[cleanName]; + const codeBody = + typeof methodData === 'object' && methodData !== null + ? (methodData as ASTMethodData).body + : (methodData as string); + const signature = + typeof methodData === 'object' && methodData !== null + ? (methodData as ASTMethodData).signature + : undefined; + + events.push({ + widget: widgetId, + type: eventType, + name: cleanName, + code: codeBody || lambdaBody || '', + signature: signature, + }); +} diff --git a/src/parser/astTypes.ts b/src/parser/astTypes.ts new file mode 100644 index 0000000..bcc99db --- /dev/null +++ b/src/parser/astTypes.ts @@ -0,0 +1,60 @@ +import { WidgetProperties } from '../generator/types'; + +export interface ASTPlacement { + x?: number; + y?: number; + width?: number; + height?: number; +} + +export interface ASTWidget { + type?: string; + variable_name?: string; + x?: number; + y?: number; + width?: number; + height?: number; + placement?: ASTPlacement; + properties?: Record; + params?: Record; +} + +export interface ASTCommandCallback { + widget: string; + command?: { + name?: string; + lambda_body?: string; + }; +} + +export interface ASTBindEvent { + widget: string; + event: string; + callback?: { + name?: string; + lambda_body?: string; + }; +} + +export interface ASTMethodData { + body: string; + signature?: string; +} + +export interface ASTResult { + window?: { + title?: string; + width?: number; + height?: number; + className?: string; + }; + title?: string; + width?: number; + height?: number; + widgets: ASTWidget[]; + command_callbacks?: ASTCommandCallback[]; + bind_events?: ASTBindEvent[]; + methods?: { [key: string]: string | ASTMethodData }; + success?: boolean; + error?: string; +} diff --git a/src/parser/pythonRunner.ts b/src/parser/pythonRunner.ts index c9160a3..2df7d4c 100644 --- a/src/parser/pythonRunner.ts +++ b/src/parser/pythonRunner.ts @@ -2,79 +2,178 @@ import * as path from 'path'; import * as fs from 'fs'; import * as os from 'os'; import { spawn } from 'child_process'; +import * as vscode from 'vscode'; +import { ASTResult } from './astTypes'; -async function executePythonScript( - pythonScriptPath: string, - pythonFilePath: string -): Promise { - return await new Promise((resolve, reject) => { - const pythonCommand = getPythonCommand(); - const start = Date.now(); - console.log('[PythonRunner] Spawning:', pythonCommand, pythonScriptPath, pythonFilePath); - const process = spawn(pythonCommand, [ - pythonScriptPath, - pythonFilePath, - ]); - let result = ''; - let errorOutput = ''; +class PythonExecutor { + private static instance: PythonExecutor; + private cachedPythonPath: string | null = null; - process.stdout.on('data', (data) => { - result += data.toString(); + private constructor() {} + + public static getInstance(): PythonExecutor { + if (!PythonExecutor.instance) { + PythonExecutor.instance = new PythonExecutor(); + } + return PythonExecutor.instance; + } + + public async getPythonPath(): Promise { + if (this.cachedPythonPath) { + return this.cachedPythonPath; + } + + const commands = await this.findPythonCommands(); + + for (const cmd of commands) { + try { + await this.verifyPython(cmd); + this.cachedPythonPath = cmd; + return cmd; + } catch (e) { + + } + } + + throw new Error('No suitable Python interpreter found. Please install Python or configure "python.defaultInterpreterPath" in VS Code.'); + } + + private async verifyPython(cmd: string): Promise { + return new Promise((resolve, reject) => { + + const process = spawn(cmd, ['--version'], { shell: false }); + + process.on('close', (code) => { + if (code === 0) { + resolve(); + } else { + reject(new Error(`Python check failed with code ${code}`)); + } + }); + + process.on('error', (err) => { + reject(err); + }); }); + } - process.stderr.on('data', (data) => { - errorOutput += data.toString(); - }); + private async findPythonCommands(): Promise { + const commands: string[] = []; - process.on('close', (code) => { - const ms = Date.now() - start; - console.log('[PythonRunner] Exit code:', code, 'time(ms):', ms); - if (code === 0) { - resolve(result); - } else { - reject( - new Error( - `Python script failed with code ${code}: ${errorOutput}` - ) - ); + + const extension = vscode.extensions.getExtension('ms-python.python'); + if (extension) { + if (!extension.isActive) { + await extension.activate(); + } + } + + const config = vscode.workspace.getConfiguration('python'); + let pythonPath = + config.get('defaultInterpreterPath') || + config.get('pythonPath'); + + if (pythonPath && pythonPath !== 'python') { + if (pythonPath.includes('${workspaceFolder}')) { + const workspaceFolders = vscode.workspace.workspaceFolders; + if (workspaceFolders && workspaceFolders.length > 0) { + pythonPath = pythonPath.replace( + '${workspaceFolder}', + workspaceFolders[0].uri.fsPath + ); + } + } + commands.push(pythonPath); + } + + + if (process.platform === 'win32') { + commands.push('py'); + commands.push('python'); + } else { + commands.push('python3'); + commands.push('python'); + } + + return Array.from(new Set(commands)); + } + + public async executeScript(scriptPath: string, args: string[] = [], stdinInput?: string): Promise { + const pythonPath = await this.getPythonPath(); + const allArgs = [scriptPath, ...args]; + + return new Promise((resolve, reject) => { + const process = spawn(pythonPath, allArgs, { + stdio: ['pipe', 'pipe', 'pipe'], + }); + + let stdout = ''; + let stderr = ''; + + process.stdout.on('data', (data) => { + stdout += data.toString(); + }); + + process.stderr.on('data', (data) => { + stderr += data.toString(); + }); + + process.on('close', (code) => { + if (code !== 0) { + reject(new Error(stderr || `Process exited with code ${code}`)); + } else { + resolve(stdout); + } + }); + + process.on('error', (err) => { + reject(new Error(`Failed to spawn python process: ${err.message}`)); + }); + + if (stdinInput) { + try { + process.stdin.write(stdinInput); + process.stdin.end(); + } catch (e: any) { + reject(new Error(`Failed to write to stdin: ${e.message}`)); + } } }); - }); -} - -function getPythonCommand(): string { - return process.platform === 'win32' ? 'python' : 'python3'; -} - -function createTempPythonFile(pythonCode: string): string { - const tempDir = os.tmpdir(); - const tempFilePath = path.join(tempDir, `tk_ast_${Date.now()}.py`); - fs.writeFileSync(tempFilePath, pythonCode, 'utf8'); - return tempFilePath; -} - -function cleanupTempFile(tempFile: string): void { - try { - fs.unlinkSync(tempFile); - } catch {} -} - -export async function runPythonAst(pythonCode: string): Promise { - const tempFilePath = createTempPythonFile(pythonCode); - try { - const pythonScriptPath = path.join(__dirname, 'tkinter_ast_parser.py'); - const output = await executePythonScript( - pythonScriptPath, - tempFilePath - ); - console.log('[PythonRunner] Received AST JSON length:', output.length); - const parsed = JSON.parse(output); - return parsed; - } catch (err) { - console.error('[PythonRunner] Error running Python AST:', err); - return null; - } finally { - cleanupTempFile(tempFilePath); - console.log('[PythonRunner] Temp file cleaned:', tempFilePath); + } +} + + +export async function executePythonScript( + pythonScriptPath: string, + pythonCode: string +): Promise { + try { + const executor = PythonExecutor.getInstance(); + return await executor.executeScript(pythonScriptPath, [], pythonCode); + } catch (error: any) { + throw error; + } +} + +export async function runPythonAst(code: string): Promise { + try { + const executor = PythonExecutor.getInstance(); + const scriptPath = path.join( + __dirname, + '..', + '..', + 'src', + 'parser', + 'tkinter_ast_parser.py' + ); + + const output = await executor.executeScript(scriptPath, [], code); + try { + return JSON.parse(output) as ASTResult; + } catch (e) { + return { error: 'Failed to parse JSON output: ' + e }; + } + } catch (error: any) { + return { error: error.message }; } } diff --git a/src/parser/tk_ast/__pycache__/__init__.cpython-314.pyc b/src/parser/tk_ast/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..729f25fdb91705169dea804b4e70401c9d2f22a8 GIT binary patch literal 390 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 R{_u+me}x{N0T|&feg(3sUD^Nu literal 0 HcmV?d00001 diff --git a/src/parser/tk_ast/analyzer/__pycache__/__init__.cpython-314.pyc b/src/parser/tk_ast/analyzer/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..22fcf4eff64a793da7df7c2cf0c30a4918ba5a05 GIT binary patch literal 266 zcmdPq6=g_ literal 0 HcmV?d00001 diff --git a/src/parser/tk_ast/analyzer/__pycache__/base.cpython-314.pyc b/src/parser/tk_ast/analyzer/__pycache__/base.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5f275dbe30ded2fec6e8581487d6e5bf8ebce10c GIT binary patch literal 14789 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 J6U@^Q{|^qoS;GJT literal 0 HcmV?d00001 diff --git a/src/parser/tk_ast/analyzer/__pycache__/calls.cpython-314.pyc b/src/parser/tk_ast/analyzer/__pycache__/calls.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7755d4bcbfa3bb1434d27078aca448e06f8afa0b GIT binary patch literal 2445 zcmb^yTWAzl^v=vaW*>>0O=74fX5H23_(;^OnrNxmv=FUL*8$g3hAiXGWHWYO+?i~$ zp&v`z52chQ)RG8=Zm6*ORY6)xY5Q03GiyRNy-mT=FaIJ6mVWiz*`4fa6YNJ1%)MvM z` zF6k=PrK=OVk(b^{t8ZW}r%H=A7Jpv+q5RR}kBi?f{#5?3{D~C3^qv%!vSG=1UrQS* zKCGy^mPiAV)8pXma4szAI4)&n3>HBu2CgMJEhqES@E(T{^xkZqcw;d+oz57tp~hm^ z13}o}(L3NMpl4_gbLufYZt10|g((w#0xT47Oc=gPwN&Rpw}2$8geb~vRv3wTFd^cw zu=*XYS>9J8CCq}0D71yO2w&H=uq-RlOib83hKSeV*Y9vG?kpGOBHQY83lkYj!ir6S z-t0tCPGk@5u%r+IeOBZK-i2m~n|vnXt8*=E!^Xv}Wk%6+c5MxnwKY`HZ`rKhFIG|M zG`-k1%R7o9ZFNlx6FK@<(Yl+pXGNQt(RW)%yOGhYt!4hljGDL3C~JS4H6yo6w2@7S z+O4lQ2=7XSYVSmw@guL)R2nXELwgeX6DOpd7BlLw>~2v<2d!g+yz?dU&KK&=n{{tD zvqh0^%WwU@d{pGabW{Pr#G`)-E4u!Jmg`?yZc_`o0lFGRc2{jSo|lhWoweb~^PEno zQM!u1s6!(RAPG9M3J#>e1nGp>yUge{beiiz8Y6f|rtP|#9P2&xh>n<%5n?wqBdHQT zp=MI5f%Al!BKAp5F~*5wT-6fe2C-+8a$F_01lAN{yP~BP;)rL`V_HJ+6St;oY2A?1 zRAOu*9mh^EOk8IT18Y|%44}$@Vp>gN&&kONmAFA38~UUMPHZwx=!752m_`iJGzs=I zld-A;!s!Mkc0HTa48p-!ChjOT@g~L@OgbjA3Z1eLF5Nt4m?;&6{I#hZqrt}19Hniy zXjRMLAeEBSiog@zn&FrZPvU_(jAs;ce-SrCTH9)7s7EjzJebZ0+zN+IXJt=MzywSe zEAfwmS=fIyGjuQDL%^UjT3>*PSU_u@_8DbPo7HZ$mbwd|{N;*s?*r%F)%NZp`^D%r_d}C7~#+ZfpH4e?33n^63ZHY^Jv_hrYUS^FqZVJn#ssy~n<}dgto3;fgm{ z_6Cb5m%IlbbI5yO-GSWg^X_F=uxOO>rF=2}5^{;xGZ-i#XKR>+e z>0W2;P3>!eP;vO{rb?i{9O%D0v=kVaZJM#K9Xozky?0=SulRb(zMkS_+1Iz$f8uWL z-suh6)ROPWV;AxU)_pV{=d!DNmG@Qn{bhdt?H%8rT;YdSU2XG@W!J7o`iT|(_2=lJ zG9N0QU*V5aD!=Rsn9}>o{Jz`n6@Cy>cJKb{X)9{m^%OBp?Znr%&cXe+PQUCg9cR76 zt>}KMXE?~+-_5`&*s9I#Fxyx+d)oZ8cMzuAuloJ@S5p~fBB{QH`+!C_piX-ROf$@H P$o)Io`^*OW-_-aA^2_8g literal 0 HcmV?d00001 diff --git a/src/parser/tk_ast/analyzer/__pycache__/connections.cpython-314.pyc b/src/parser/tk_ast/analyzer/__pycache__/connections.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0f43f82031aa8a32900e146e5ad65467a18863aa GIT binary patch literal 4139 zcmbVPOKcm*8J^wcW651TEK(#T@u7$Hpk#}&q*^kq#8qo3Qsv0;P4+P=^4*@FQoI`=2aEcy!DbN69r4<)@iGiYrpcg6*66Dr@AEcI& zT&EdeW@rDM`Ty^K&HOvk+~h>idgF`fzc(ZFFZHF?aAWdv3?|#?E@H5GG>3`U94_K> zgh+(YU5vr!Ept}U8bS==MU2IZGOePGvG$>`?F*Gbm{2U&(@9YwX0ls~Z z7UJow$nlA!nBL^#kJ71ooS4L5gWA<*gHAwy`8yc4Q4HP1r+$M_3TS?G3%ido7z5ab9JDr$U+ApO5i-vi{Qub*!9gwJ3ssh4*C7jvjpxJ-J{z|1R-p4LWQ7n! zo>zf@sW52e49{#^cb>3Qi- zrBBPhD*v91-CkuU*!%=5KGe*0F~te#ds&XZxG5xaDemIt1S{}KHlN^uBdm%i1d&Z- z6Pc}F!h158&2nJjTviy(Zz*&j>4+m=&TVC_9EVio{b3}+g=zogs0IqnGy$!- zfz|?)Rx>4nM=TRYbcfIa1h1+W=v0r`V<@0g5_Gc1?B78r%q-=5>U0hyoip=coIq)7 zpr~5fPibjCrKQ!RC8ul2LM-TQ!g%J%S9&vQc(c*QE;E)vgNxzYF7Pe6Y)$0<)W za53SWevLO&OsY2;Ez-d4J7fBvT+YBmLNvfxns>w;CuB5%-i{^8q_8n%+%-P69s0Ss zFn|!_ia8hu)55fmq6m?9zS0kGzTOWp$J9za!{CgKIhl5|pI#A-Co&b#P!YU_7;@v< zYrHZ0dT)GBWGC_ska?I>xe*~uSmz9^IMZ;;oTYipY1;Q6ecxS_r(Wp+$R6!nYs>_% z*}^sdnlgd9i(P=os5Rh^GlY51S=iv*JLwhQsqFEYxiB54zIU3xH(2SOt}WBW){(`q znl%_?qMkUT>ua!vH4SH8eT~0iO@lw`YlxpxOu)3xxPwO3J17M9CfJsmpdtwn^=+SlD!1ML^h}gjPR!BOyd-^~^2zs7Kn`Ysl(r%OzPz8ziCd>hmFEoo>}MP{?E=lUYk7A1W4Iw2oh~YoOPlPE^%Z98e`1Uz{K~pWz;-MfGDY zwWUyo&e}_p2vtqSC?f@>S%0mY!*#mW6vCu(&d?@ag9FuEBA@3#G-b9oA#hN?CQ>QI zDToPP6jVnkM25@qYK$|p21&|Po5nrBK+lIPTF&yZH*W`f865Y}mB;D=BR zZKD@tsMscxy~Q8NWJh7lJopQ1)h2tNZF}LU4UI?l+oB&2?%mprORoOnd;fF|eLT3m zP-=_5aQiFn(F6DB4k>$rg~yU-Xor$1&mq+>)83LVBGGSD=*coYDYyCxBXVn?@V*q9 z{KO`OuF0((g{k7a)HMn-+1*uGDdwcWW!dd7+%FAIO2MhBmGHM!9pshfuMyeQa%4yC zk!mOMhAN)n1JCe|Q}%QheG<&fGRGu(yh2CIbo4Bl-a=>bZK>meDm_|!Aaz|bq_6I^ zNP#I;`h!yViWH3M(kaI@NOx$`y(3@wk=y&h54LZ>y1L?&Yj<9vf`!crHE>7`$h5CQ z`^&T+6iQ2hH=cQb?;(BV$g0A(nc-9IyALF)r#Mid!iQA&c<1V#Qwoefo0S4r59w*x z=^c4VqK^I@`g6+r+vy@*>7FciPkusHx@Qi$XI`{=3bYIZsj4+rwOjnnJ2$FM6zH$` zFCX|X?~<~st1vIQ28x@KYxLQLL)TlfySvySxkoDQv9fzi_H`GB_NJuX3E3AcIzage z&)0}(_k!@iWR*st!AfxKAUL+`lwE_g19d%PK3I>z;bCd`suY~w?|f6u zn`xO0JQ1Eg+V8vb^wD0=LG)&6=~jtZk(Pc|iLU-}ra1HTQMvEVeu#y%3PW0zTF;j) z{na)EX=3qw`{mDZGfP{Bg4JLUk94{TOz1`ZdC%qu+LF%8TI}f Ng}9#*}U4-WH-!v39YA~ z-n@G0sT2w&KfsHqzahPdtVd71scrG<%-d{Riv$D*c6N4V-pu#ro4uMF8ek{|XU<>X zjD4ezM2xPncLR+UTVfV}z!bBLJhM`HRvlK{GV@FsRv&uO+LN;S(PLVCoRu=WRAA1- zx{MwYs+q!PZyHsLmD!Rp{hF}~`bOC!zQQeTV?_pXEqg%aT6NfaoivXm8*o~z#zh)tIIs97W>6Gzw(3BR%$!DnbqbVhM0H!+J^%x z>QiMtPbnv)%z|XV%wjkoK8Is51GgrkA+G@-0_Au#pSBTY5~z^IGLjlhOaQGxG>CZu z;!HS)9*sALMvHy9czI)SeX(`_vv~os==wX%vpV_laDeYv2oO+>5{F@YwGE)8$gG2C z{?UFwtomJ?Kts~_GHB@ThkgJ1K7olnN(S^3tigMYz*Bx0=v9JK=!^GAk4Uqql@1(H zStS07TQ|37HfOZiV@Cf%Wjr|pP7}6V!evdUm9PS52;6Z3b1{L--7CN-CTO5t4#x@` z{<=>bj`YCSo5K;T3Fl9C9@5+$HXg$`Es5;XH&#emmiqLZ^?GTRHAtWXZKgrRyO_CM o3oDJPxC56kM4U^y&G!`N{3{zcF!9g%ndZE3fU3BUYFwZG4KuImrvLx| literal 0 HcmV?d00001 diff --git a/src/parser/tk_ast/analyzer/__pycache__/events.cpython-314.pyc b/src/parser/tk_ast/analyzer/__pycache__/events.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d3cce1aecec62fb5ba7b22363b9361d6a00f7f02 GIT binary patch literal 4816 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 BC-ndT literal 0 HcmV?d00001 diff --git a/src/parser/tk_ast/analyzer/__pycache__/placements.cpython-314.pyc b/src/parser/tk_ast/analyzer/__pycache__/placements.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..adaa8e86988ce4ca4b0b8eb02afb9cd9a6deb737 GIT binary patch literal 2006 zcmZ{l&u`;I6vxN0<0P(=Ce070f!&st{z!ka5J6cXI4r8D5T&(tvuI`2GKn`0O_Qwc z{s?fWfI|?M9`+CDG2D6p384iE35grB@u-Jbs?}%*@yC zd7fuOK@Ne9$*aX9FG7Elhpu@#&iN!b2WSI{)Cw}t1|@n{sP{aEX9S6~fJ8<>rLaMZ z)Hq6epOQ(dX$lMKzM&~OL(VCsl3Z0ZWm`2=P0vvPkc$`#7Y*0>8Z-wei#9w;SDP^Tp`xKt5B_*eBt7Su9sMatq%SyRiF%(0UWi13sAWcYbr{n^SQQR46z8gO! zw_nG9oBn0maZhgFbw`@9J6f_s53BC!JWuK{)3{<>Qhnm!affuXCO>KSU5y`?_M!)Y+`m( zEfzN6%xh|C-)U>u4}NRJgE}xNpf$3HVdOp(fAs^{sYtsMh=!+!FV%x?Id`uPPGR3y8Oq$ zwWHLZ1GBAoqQ3l?kDU2X==ueR_@T%C&_BXLW8W5TnM~}7FL)@N_>xcf*kPf?51Nse znvn@BGI5k^+^{3_S9V3s_(U_Fw&Ll=lpTMy#mCIVWHT{iC1x5+b|TZ_hs@Z^&DgXR zn{FiR*eh3bB+TS=GdX7^=Nk9yCC(cMGn)>uK%s=N`9xLzO6051X9NIFf^w$MNxkt?q9Tc!N3o|nejiQOtz~4 literal 0 HcmV?d00001 diff --git a/src/parser/tk_ast/analyzer/__pycache__/values.cpython-314.pyc b/src/parser/tk_ast/analyzer/__pycache__/values.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8c00abfff928600816de88642c528b4f27c9eaaa GIT binary patch literal 11515 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 BCc6Lt literal 0 HcmV?d00001 diff --git a/src/parser/tk_ast/analyzer/base.py b/src/parser/tk_ast/analyzer/base.py index ec39738..16a3544 100644 --- a/src/parser/tk_ast/analyzer/base.py +++ b/src/parser/tk_ast/analyzer/base.py @@ -1,4 +1,7 @@ import ast +import textwrap +import tokenize +import io from typing import Dict, List, Any, Optional from .imports import handle_import, handle_import_from @@ -14,7 +17,15 @@ from .connections import create_widget_handler_connections class TkinterAnalyzer(ast.NodeVisitor): - def __init__(self): + def __init__(self, source_code: str = ""): + self.source_code = source_code + self.line_offsets = [0] + current = 0 + if source_code: + for line in source_code.splitlines(keepends=True): + current += len(line) + self.line_offsets.append(current) + self.widgets: List[Dict[str, Any]] = [] self.window_config = {'title': 'App', 'width': 800, 'height': 600} self.imports: Dict[str, str] = {} @@ -24,6 +35,7 @@ class TkinterAnalyzer(ast.NodeVisitor): self.event_handlers: List[Dict[str, Any]] = [] self.command_callbacks: List[Dict[str, Any]] = [] self.bind_events: List[Dict[str, Any]] = [] + self.methods: Dict[str, str] = {} def visit_Import(self, node: ast.Import): handle_import(self, node) @@ -36,12 +48,119 @@ class TkinterAnalyzer(ast.NodeVisitor): def visit_ClassDef(self, node: ast.ClassDef): prev = self.current_class enter_class(self, node) + + is_tk_class = False + for base in node.bases: + if isinstance(base, ast.Attribute) and base.attr == 'Tk': + is_tk_class = True + elif isinstance(base, ast.Name) and base.id == 'Tk': + is_tk_class = True + + if node.name == 'Application': + self.window_config['className'] = node.name + elif is_tk_class and self.window_config.get('className') != 'Application': + self.window_config['className'] = node.name + elif not self.window_config.get('className'): + self.window_config['className'] = node.name + self.generic_visit(node) exit_class(self, prev) def visit_FunctionDef(self, node: ast.FunctionDef): prev = self.current_method enter_function(self, node) + + if self.current_class and node.name not in ['__init__', 'create_widgets', 'run'] and self.source_code and node.body: + try: + + + + + start_line = node.lineno + end_line = getattr(node, 'end_lineno', None) + + if end_line is None: + + end_line = node.body[-1].lineno + + lines = self.source_code.splitlines(keepends=True) + + func_lines = lines[start_line-1 : end_line] + func_text = "".join(func_lines) + + + + + + body_start_idx = -1 + + try: + tokens = list(tokenize.tokenize(io.BytesIO(func_text.encode('utf-8')).readline)) + + nesting = 0 + colon_token = None + + for tok in tokens: + if tok.type == tokenize.OP: + if tok.string in '([{': + nesting += 1 + elif tok.string in ')]}': + nesting -= 1 + elif tok.string == ':' and nesting == 0: + colon_token = tok + + break + + if colon_token: + + + + + colon_line_idx = colon_token.end[0] - 1 + + + + + + + + + + body_lines = func_lines[colon_line_idx + 1:] + + + colon_line = func_lines[colon_line_idx] + after_colon = colon_line[colon_token.end[1]:] + + if after_colon.strip(): + body_lines.insert(0, after_colon) + + body_text = "".join(body_lines) + + + dedented_body = textwrap.dedent(body_text) + + + + + sig_lines = func_lines[:colon_line_idx] + sig_lines.append(colon_line[:colon_token.end[1]]) + + sig_raw = "".join(sig_lines).strip() + if sig_raw.endswith(':'): + sig_raw = sig_raw[:-1].strip() + + self.methods[node.name] = { + 'body': dedented_body.strip(), + 'signature': sig_raw + } + + except tokenize.TokenError: + pass + + except Exception as e: + pass + self.generic_visit(node) exit_function(self, prev) @@ -101,9 +220,3 @@ class TkinterAnalyzer(ast.NodeVisitor): def analyze_widget_creation_commands(self, node: ast.Assign): return analyze_widget_creation_commands(self, node) - - def create_widget_handler_connections(self, widgets: List[Dict[str, Any]]) -> Dict[str, Any]: - return create_widget_handler_connections(self, widgets) - - def is_interactive_widget(self, widget_type: str) -> bool: - return is_interactive_widget(widget_type) \ No newline at end of file diff --git a/src/parser/tk_ast/analyzer/calls.py b/src/parser/tk_ast/analyzer/calls.py index d305cd5..4108f76 100644 --- a/src/parser/tk_ast/analyzer/calls.py +++ b/src/parser/tk_ast/analyzer/calls.py @@ -15,15 +15,11 @@ def handle_method_call(analyzer, node: ast.Call): arg0 = node.args[0] if isinstance(arg0, ast.Constant): analyzer.window_config['title'] = arg0.value - elif isinstance(arg0, ast.Str): - analyzer.window_config['title'] = arg0.s elif method_name == 'geometry' and node.args: arg0 = node.args[0] if isinstance(arg0, ast.Constant): geometry = arg0.value - elif isinstance(arg0, ast.Str): - geometry = arg0.s else: return if 'x' in str(geometry): diff --git a/src/parser/tk_ast/analyzer/values.py b/src/parser/tk_ast/analyzer/values.py index e16491b..9bf17d0 100644 --- a/src/parser/tk_ast/analyzer/values.py +++ b/src/parser/tk_ast/analyzer/values.py @@ -15,10 +15,6 @@ def get_variable_name(node: ast.AST) -> str: def extract_value(node: ast.AST) -> Any: if isinstance(node, ast.Constant): return node.value - elif isinstance(node, ast.Str): - return node.s - elif isinstance(node, ast.Num): - return node.n elif isinstance(node, ast.Name): return f"${node.id}" elif isinstance(node, ast.Attribute): @@ -65,7 +61,7 @@ def get_operator_symbol(op_node: ast.AST) -> str: def analyze_lambda_complexity(lambda_node: ast.Lambda) -> str: body = lambda_node.body - if isinstance(body, (ast.Constant, ast.Str, ast.Num)): + if isinstance(body, ast.Constant): return 'simple' elif isinstance(body, (ast.Name, ast.Attribute)): return 'simple' @@ -76,11 +72,9 @@ def analyze_lambda_complexity(lambda_node: ast.Lambda) -> str: def extract_lambda_body(body_node: ast.AST) -> str: if isinstance(body_node, ast.Constant): + if isinstance(body_node.value, str): + return f'"{body_node.value}"' return str(body_node.value) - elif isinstance(body_node, ast.Str): - return f'"{body_node.s}"' - elif isinstance(body_node, ast.Num): - return str(body_node.n) elif isinstance(body_node, ast.Name): return body_node.id elif isinstance(body_node, ast.Attribute): @@ -138,8 +132,6 @@ def extract_lambda_body(body_node: ast.AST) -> str: for value in body_node.values: if isinstance(value, ast.Constant): parts.append(str(value.value)) - elif isinstance(value, ast.Str): - parts.append(value.s) else: parts.append(f"{{{extract_lambda_body(value)}}}") return f"f\"{''.join(parts)}\"" diff --git a/src/parser/tk_ast/analyzer/widget_creation.py b/src/parser/tk_ast/analyzer/widget_creation.py index 9b52f08..c323ace 100644 --- a/src/parser/tk_ast/analyzer/widget_creation.py +++ b/src/parser/tk_ast/analyzer/widget_creation.py @@ -19,10 +19,13 @@ def is_tkinter_widget_call(analyzer, call_node: ast.Call) -> bool: widget_type = call_node.func.attr if module_name in ['tk', 'tkinter'] or module_name in analyzer.imports.values(): - return widget_type in ['Label', 'Button', 'Text', 'Checkbutton', 'Radiobutton'] - if module_name in ['ttk'] or 'ttk' in analyzer.imports.values(): - return False - return widget_type in ['Label', 'Button', 'Text', 'Checkbutton', 'Radiobutton'] + return widget_type in ['Label', 'Button', 'Entry', 'Text', 'Frame', 'Checkbutton', 'Radiobutton'] + + resolved = analyzer.imports.get(module_name, module_name) + if resolved in ['ttk', 'tkinter.ttk']: + return widget_type in ['Label', 'Button', 'Entry', 'Frame', 'Checkbutton', 'Radiobutton'] + + return widget_type in ['Label', 'Button', 'Entry', 'Text', 'Frame', 'Checkbutton', 'Radiobutton'] def is_widget_creation(analyzer, node: ast.Assign) -> bool: if not isinstance(node.value, ast.Call): @@ -31,7 +34,7 @@ def is_widget_creation(analyzer, node: ast.Assign) -> bool: if isinstance(node.value.func, ast.Attribute): return is_tkinter_widget_call(analyzer, node.value) elif isinstance(node.value.func, ast.Name): - return node.value.func.id in ['Label', 'Button', 'Text', 'Checkbutton'] + return node.value.func.id in ['Label', 'Button', 'Entry', 'Text', 'Frame', 'Checkbutton', 'Radiobutton'] return False def extract_widget_info(analyzer, node: ast.Assign) -> Optional[Dict[str, Any]]: @@ -49,10 +52,6 @@ def extract_widget_info(analyzer, node: ast.Assign) -> Optional[Dict[str, Any]]: if isinstance(call_node.func, ast.Attribute): widget_type = call_node.func.attr - if isinstance(call_node.func.value, ast.Name): - module_name = call_node.func.value.id - if module_name in ['ttk'] or 'ttk' in analyzer.imports.values(): - return None elif isinstance(call_node.func, ast.Name): widget_type = call_node.func.id else: diff --git a/src/parser/tk_ast/parser.py b/src/parser/tk_ast/parser.py index 3e7dc3b..6f9e729 100644 --- a/src/parser/tk_ast/parser.py +++ b/src/parser/tk_ast/parser.py @@ -9,7 +9,7 @@ from tk_ast.grid_layout import GridLayoutAnalyzer def parse_tkinter_code(code: str) -> Dict[str, Any]: try: tree = ast.parse(code) - analyzer = TkinterAnalyzer() + analyzer = TkinterAnalyzer(code) analyzer.visit(tree) grid_analyzer = GridLayoutAnalyzer() widgets = grid_analyzer.analyze_grid_layout(analyzer.widgets) @@ -18,6 +18,7 @@ def parse_tkinter_code(code: str) -> Dict[str, Any]: 'widgets': widgets, 'command_callbacks': analyzer.command_callbacks, 'bind_events': analyzer.bind_events, + 'methods': analyzer.methods, 'success': True } diff --git a/src/parser/tkinter_ast_parser.py b/src/parser/tkinter_ast_parser.py index e14db7f..4acdf10 100644 --- a/src/parser/tkinter_ast_parser.py +++ b/src/parser/tkinter_ast_parser.py @@ -1,12 +1,16 @@ -#!/usr/bin/env python3 + import sys import json +import os + +sys.path.append(os.path.dirname(os.path.abspath(__file__))) try: from tk_ast.parser import parse_tkinter_code, parse_file -except Exception as e: +except ImportError as e: + raise RuntimeError( f"Failed to import tk_ast package: {e}. Ensure 'tk_ast' exists next to this script." ) @@ -14,8 +18,8 @@ except Exception as e: if __name__ == '__main__': if len(sys.argv) > 1: - print(parse_file(sys.argv[1])) + sys.stdout.write(str(parse_file(sys.argv[1])) + '\n') else: code = sys.stdin.read() result = parse_tkinter_code(code) - print(json.dumps(result, indent=2, ensure_ascii=False)) \ No newline at end of file + sys.stdout.write(json.dumps(result, indent=2, ensure_ascii=False) + '\n') \ No newline at end of file diff --git a/src/parser/utils.ts b/src/parser/utils.ts index 912f731..fea8c6e 100644 --- a/src/parser/utils.ts +++ b/src/parser/utils.ts @@ -1,21 +1,9 @@ +import { WidgetType, WIDGET_DIMENSIONS } from '../constants'; + export function getDefaultWidth(type: string): number { - const defaults: { [key: string]: number } = { - Label: 100, - Button: 80, - Text: 200, - Checkbutton: 100, - Radiobutton: 100, - }; - return defaults[type] || 100; + return WIDGET_DIMENSIONS[type as WidgetType]?.width || WIDGET_DIMENSIONS.DEFAULT.width; } export function getDefaultHeight(type: string): number { - const defaults: { [key: string]: number } = { - Label: 25, - Button: 30, - Text: 100, - Checkbutton: 25, - Radiobutton: 25, - }; - return defaults[type] || 25; + return WIDGET_DIMENSIONS[type as WidgetType]?.height || WIDGET_DIMENSIONS.DEFAULT.height; } diff --git a/src/webview/TkinterDesignerProvider.ts b/src/webview/TkinterDesignerProvider.ts index 1118f89..46500c4 100644 --- a/src/webview/TkinterDesignerProvider.ts +++ b/src/webview/TkinterDesignerProvider.ts @@ -1,34 +1,26 @@ import * as vscode from 'vscode'; import * as path from 'path'; -import { Uri } from 'vscode'; +import { runPythonAst } from '../parser/pythonRunner'; +import { DesignData } from '../generator/types'; +import { WebviewMessage } from './react/types'; +import { ProjectIO } from './projectIO'; export class TkinterDesignerProvider implements vscode.WebviewViewProvider { public static readonly viewType = 'tkinter-designer'; - public static _instance: TkinterDesignerProvider | undefined; - private _view?: vscode.WebviewPanel; - private _designData: any = { + private _view?: vscode.WebviewPanel | vscode.WebviewView; + private _designData: DesignData = { widgets: [], events: [], - form: { title: 'My App', size: { width: 800, height: 600 } }, + form: { name: 'App', title: 'My App', size: { width: 800, height: 600 } }, }; constructor(private readonly _extensionUri: vscode.Uri) {} - public static createOrShow(extensionUri: vscode.Uri) { - const column = vscode.window.activeTextEditor - ? vscode.window.activeTextEditor.viewColumn - : undefined; - if (TkinterDesignerProvider._instance?._view) { - console.log('[Webview] Revealing existing panel'); - TkinterDesignerProvider._instance._view.reveal(column); - return TkinterDesignerProvider._instance; - } - - console.log('[Webview] Creating new panel'); + public static createNew(extensionUri: vscode.Uri): TkinterDesignerProvider { const panel = vscode.window.createWebviewPanel( TkinterDesignerProvider.viewType, 'Tkinter Designer', - column || vscode.ViewColumn.One, + vscode.ViewColumn.Beside, { enableScripts: true, retainContextWhenHidden: true, @@ -40,24 +32,17 @@ export class TkinterDesignerProvider implements vscode.WebviewViewProvider { ], } ); - if (!TkinterDesignerProvider._instance) { - TkinterDesignerProvider._instance = new TkinterDesignerProvider( - extensionUri - ); - } - TkinterDesignerProvider._instance._view = panel; - TkinterDesignerProvider._instance._setWebviewContent(panel.webview); - TkinterDesignerProvider._instance._setupMessageHandling(panel.webview); + const provider = new TkinterDesignerProvider(extensionUri); + provider._view = panel; + provider._setWebviewContent(panel.webview); + provider._setupMessageHandling(panel.webview); panel.onDidDispose(() => { - console.log('[Webview] Panel disposed'); - if (TkinterDesignerProvider._instance) { - TkinterDesignerProvider._instance._view = undefined; - } + provider._view = undefined; }); - return TkinterDesignerProvider._instance; + return provider; } public resolveWebviewView( @@ -65,7 +50,7 @@ export class TkinterDesignerProvider implements vscode.WebviewViewProvider { context: vscode.WebviewViewResolveContext, _token: vscode.CancellationToken ) { - this._view = webviewView as any; + this._view = webviewView; webviewView.webview.options = { enableScripts: true, localResourceRoots: [ @@ -85,8 +70,7 @@ export class TkinterDesignerProvider implements vscode.WebviewViewProvider { } private _setupMessageHandling(webview: vscode.Webview) { - webview.onDidReceiveMessage((message) => { - console.log('[Webview] Message:', message.type); + webview.onDidReceiveMessage(async (message) => { switch (message.type) { case 'designUpdated': this._designData = message.data; @@ -107,12 +91,13 @@ export class TkinterDesignerProvider implements vscode.WebviewViewProvider { type: 'loadDesign', data: this._designData, }); - console.log('[Webview] Sent loadDesign'); } break; case 'generateCode': - console.log('[Webview] Generating code from webview'); - this.handleGenerateCode(message.data); + await this.handleGenerateCode(message.data); + break; + case 'applyChanges': + await this.handleApplyChanges(message.data); break; case 'showInfo': vscode.window.showInformationMessage(message.text); @@ -120,6 +105,15 @@ export class TkinterDesignerProvider implements vscode.WebviewViewProvider { case 'showError': vscode.window.showErrorMessage(message.text); break; + case 'exportProject': + await ProjectIO.exportProject(message.data); + break; + case 'importProject': + const data = await ProjectIO.importProject(); + if (data) { + this.loadDesignData(data); + } + break; } }, undefined); } @@ -136,67 +130,260 @@ export class TkinterDesignerProvider implements vscode.WebviewViewProvider { type: 'loadDesign', data: this._designData, }); - console.log('[Webview] loadDesign posted'); - } else { } } private async handleGenerateCode(designData: any): Promise { try { - console.log('[GenerateCode] Start'); const { CodeGenerator } = await import( - '../generator/CodeGenerator' + '../generator/codeGenerator' ); const generator = new CodeGenerator(); const pythonCode = generator.generateTkinterCode(designData); - const activeEditor = vscode.window.activeTextEditor; - if (activeEditor && activeEditor.document.languageId === 'python') { - console.log('[GenerateCode] Writing into active editor'); - const doc = activeEditor.document; - const start = new vscode.Position(0, 0); - const end = doc.lineCount - ? doc.lineAt(doc.lineCount - 1).range.end - : start; - const fullRange = new vscode.Range(start, end); - await activeEditor.edit((editBuilder) => { - editBuilder.replace(fullRange, pythonCode); - }); - await doc.save(); - vscode.window.showInformationMessage( - 'Python code generated into the active file' + const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; + if (!workspaceFolder) { + vscode.window.showErrorMessage( + 'No workspace folder is open. Please open a folder first.' ); - } else { - console.log('[GenerateCode] Creating new file'); - const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; - if (!workspaceFolder) { - vscode.window.showErrorMessage( - 'No workspace folder is open. Please open a folder first.' - ); - return; + return; + } + + const formName = designData.form.name || 'Form1'; + let fileName = `${formName}.py`; + let fileUri = vscode.Uri.joinPath(workspaceFolder.uri, fileName); + let counter = 1; + + const exists = async (u: vscode.Uri) => { + try { + await vscode.workspace.fs.stat(u); + return true; + } catch { + return false; } - const fileName = `app_${Date.now()}.py`; - const filePath = path.join( - workspaceFolder.uri.fsPath, - fileName + }; + + while (await exists(fileUri)) { + fileName = `${formName}_${counter}.py`; + fileUri = vscode.Uri.joinPath(workspaceFolder.uri, fileName); + counter++; + } + + const encoder = new TextEncoder(); + const fileBytes = encoder.encode(pythonCode); + await vscode.workspace.fs.writeFile(fileUri, fileBytes); + + const doc = await vscode.workspace.openTextDocument(fileUri); + await vscode.window.showTextDocument(doc, { + preview: false, + }); + vscode.window.showInformationMessage( + `Python file created: ${fileName}` + ); + + const newFormName = path.basename(fileName, '.py'); + if (newFormName !== formName) { + if (this._view) { + const webview = (this._view as any).webview || this._view; + webview.postMessage({ + type: 'updateFormName', + name: newFormName, + }); + } + } + } catch (error) { + vscode.window.showErrorMessage(`Error generating code: ${error}`); + } + } + + private async handleApplyChanges(designData: DesignData): Promise { + try { + const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; + if (!workspaceFolder) { + vscode.window.showErrorMessage( + 'No workspace folder is open. Please open a folder first.' ); - const fileUri = Uri.file(filePath); - const encoder = new TextEncoder(); - const fileBytes = encoder.encode(pythonCode); - await vscode.workspace.fs.writeFile(fileUri, fileBytes); + return; + } + + const formName = designData.form.name || 'Form1'; + const fileName = `${formName}.py`; + const fileUri = vscode.Uri.joinPath(workspaceFolder.uri, fileName); + + let existingMethods: any = {}; + let oldClassName = ''; + let fileContent = ''; + let fileExists = false; + + try { + await vscode.workspace.fs.stat(fileUri); + fileExists = true; const doc = await vscode.workspace.openTextDocument(fileUri); - await vscode.window.showTextDocument(doc, { - preview: false, - }); + fileContent = doc.getText(); + const astResult = await runPythonAst(fileContent); + + if (astResult && !('error' in astResult)) { + if (astResult.methods) { + existingMethods = astResult.methods as Record; + if (designData.events) { + for (const event of designData.events) { + if (existingMethods[event.name]) { + const methodData = existingMethods[event.name]; + if (typeof methodData === 'object' && methodData !== null) { + event.code = methodData.body; + event.signature = methodData.signature; + } else if (typeof methodData === 'string') { + event.code = methodData; + } + } + } + } + } + if (astResult.window && astResult.window.className) { + oldClassName = astResult.window.className; + } + } + } catch (e) { + } + + const { CodeGenerator } = await import( + '../generator/codeGenerator' + ); + const generator = new CodeGenerator(); + + if (!fileExists) { + const pythonCode = generator.generateTkinterCode(designData); + const encoder = new TextEncoder(); + await vscode.workspace.fs.writeFile( + fileUri, + encoder.encode(pythonCode) + ); vscode.window.showInformationMessage( - `Python file created: ${fileName}` + `Created new file: ${fileName}` + ); + } else { + const newCreateWidgets = + generator.generateCreateWidgetsBody(designData); + + const createWidgetsRegex = + /(def\s+create_widgets\s*\(\s*self[^)]*\)\s*(?:->\s*[^:]+)?\s*:)([\s\S]*?)(?=\n\s*def\s|\n\s*if\s+__name__|\Z)/; + + let newFileContent = fileContent; + + if (createWidgetsRegex.test(fileContent)) { + newFileContent = fileContent.replace( + createWidgetsRegex, + (match, defLine) => { + return `${defLine}\n${newCreateWidgets}\n`; + } + ); + } else { + vscode.window.showWarningMessage( + 'Could not find create_widgets method to update. Regenerating full file might be needed if structure is broken.' + ); + } + + if (designData.events) { + const methodsToInject: string[] = []; + for (const event of designData.events) { + if (!existingMethods[event.name]) { + methodsToInject.push( + generator.generateEventHandler(event) + ); + } + } + + if (methodsToInject.length > 0) { + const runRegex = /\s*def\s+run\s*\(/; + if (runRegex.test(newFileContent)) { + newFileContent = newFileContent.replace( + runRegex, + (match) => { + return `\n${methodsToInject.join('\n\n')}\n\n${match}`; + } + ); + } else { + const mainRegex = /if\s+__name__\s*==/; + if (mainRegex.test(newFileContent)) { + newFileContent = newFileContent.replace( + mainRegex, + (match) => { + return `${methodsToInject.join('\n\n')}\n\n${match}`; + } + ); + } else { + newFileContent += `\n\n${methodsToInject.join('\n\n')}`; + } + } + } + } + + const titleRegex = /self\.root\.title\s*\(\s*["'].*?["']\s*\)/; + newFileContent = newFileContent.replace( + titleRegex, + `self.root.title("${designData.form.title}")` + ); + + const geoRegex = /self\.root\.geometry\s*\(\s*["'].*?["']\s*\)/; + newFileContent = newFileContent.replace( + geoRegex, + `self.root.geometry("${designData.form.size.width}x${designData.form.size.height}")` + ); + + if ( + oldClassName && + designData.form.className && + oldClassName !== designData.form.className + ) { + const classDefRegex = new RegExp( + `class\\s+${oldClassName}\\s*:` + ); + newFileContent = newFileContent.replace( + classDefRegex, + `class ${designData.form.className}:` + ); + + const instanceRegex = new RegExp( + `=\\s*${oldClassName}\\s*\\(` + ); + newFileContent = newFileContent.replace( + instanceRegex, + `= ${designData.form.className}(` + ); + } + + const doc = await vscode.workspace.openTextDocument(fileUri); + const editor = vscode.window.visibleTextEditors.find( + (e) => e.document.uri.toString() === fileUri.toString() + ); + + if (editor) { + const fullRange = doc.validateRange( + new vscode.Range( + 0, + 0, + Number.MAX_VALUE, + Number.MAX_VALUE + ) + ); + await editor.edit((editBuilder) => { + editBuilder.replace(fullRange, newFileContent); + }); + await doc.save(); + } else { + const encoder = new TextEncoder(); + await vscode.workspace.fs.writeFile( + fileUri, + encoder.encode(newFileContent) + ); + } + vscode.window.showInformationMessage( + `Smart updated ${fileName}` ); } - console.log('[GenerateCode] Done'); } catch (error) { - console.error('[GenerateCode] Error:', error); - vscode.window.showErrorMessage(`Error generating code: ${error}`); + vscode.window.showErrorMessage(`Error applying changes: ${error}`); } } diff --git a/src/webview/projectIO.ts b/src/webview/projectIO.ts new file mode 100644 index 0000000..999ffb0 --- /dev/null +++ b/src/webview/projectIO.ts @@ -0,0 +1,95 @@ +import * as vscode from 'vscode'; +import { DesignData } from '../generator/types'; + +export class ProjectIO { + public static async exportProject(data: DesignData): Promise { + try { + const options: vscode.SaveDialogOptions = { + defaultUri: vscode.Uri.file(`tkinter-project-${new Date().toISOString().split('T')[0]}.json`), + filters: { + 'JSON': ['json'] + } + }; + const fileUri = await vscode.window.showSaveDialog(options); + if (fileUri) { + const jsonString = JSON.stringify(data, null, 2); + const encoder = new TextEncoder(); + await vscode.workspace.fs.writeFile(fileUri, encoder.encode(jsonString)); + vscode.window.showInformationMessage('Project exported successfully!'); + } + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : String(error); + vscode.window.showErrorMessage(`Export failed: ${errorMessage}`); + } + } + + public static async importProject(): Promise { + try { + const options: vscode.OpenDialogOptions = { + canSelectMany: false, + openLabel: 'Import', + filters: { + 'JSON': ['json'] + } + }; + const fileUris = await vscode.window.showOpenDialog(options); + if (fileUris && fileUris[0]) { + const fileUri = fileUris[0]; + const fileData = await vscode.workspace.fs.readFile(fileUri); + const decoder = new TextDecoder(); + const jsonString = decoder.decode(fileData); + const data = JSON.parse(jsonString); + + + const designData = data.data || data; + + if (ProjectIO.validateDesignData(designData)) { + vscode.window.showInformationMessage('Project imported successfully!'); + return designData; + } else { + vscode.window.showErrorMessage('Invalid project file format.'); + return null; + } + } + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : String(error); + vscode.window.showErrorMessage(`Import failed: ${errorMessage}`); + } + return null; + } + + private static validateDesignData(data: any): data is DesignData { + if (!data || typeof data !== 'object') { + return false; + } + + + if (!data.form || typeof data.form !== 'object') { + return false; + } + if (typeof data.form.name !== 'string' || typeof data.form.title !== 'string') { + return false; + } + if (!data.form.size || typeof data.form.size.width !== 'number' || typeof data.form.size.height !== 'number') { + return false; + } + + + if (!Array.isArray(data.widgets)) { + return false; + } + for (const widget of data.widgets) { + if (!widget.id || typeof widget.id !== 'string') { + return false; + } + if (!widget.type || typeof widget.type !== 'string') { + return false; + } + if (typeof widget.x !== 'number' || typeof widget.y !== 'number') { + return false; + } + } + + return true; + } +} diff --git a/src/webview/react/App.tsx b/src/webview/react/App.tsx index d205fc2..17865d1 100644 --- a/src/webview/react/App.tsx +++ b/src/webview/react/App.tsx @@ -5,23 +5,26 @@ import { PropertiesPanel } from './components/PropertiesPanel'; import { EventsPanel } from './components/EventsPanel'; import { Canvas } from './components/Canvas'; import { useMessaging } from './useMessaging'; +import { ErrorBoundary } from './components/ErrorBoundary'; export function App() { useMessaging(); return ( -
- -
-
-

Widgets

- - - -
-
- + +
+ +
+
+

Widgets

+ + + +
+
+ +
-
+ ); } diff --git a/src/webview/react/components/Canvas.tsx b/src/webview/react/components/Canvas.tsx index c4e0baf..d1577e8 100644 --- a/src/webview/react/components/Canvas.tsx +++ b/src/webview/react/components/Canvas.tsx @@ -2,6 +2,69 @@ import React, { useRef } from 'react'; import { useAppDispatch, useAppState } from '../state'; import type { WidgetType } from '../types'; +import { Widget } from '../types'; + +function renderWidgetContent(w: Widget) { + switch (w.type) { + case 'Label': + return ( +
+ {w.properties?.text || 'Label'} +
+ ); + case 'Button': + return ( +
+ {w.properties?.text || 'Button'} +
+ ); + case 'Entry': + return ( + + ); + case 'Text': + return ( + + ); + case 'Checkbutton': + return ( +
+ + {w.properties?.text || 'Check'} +
+ ); + case 'Radiobutton': + return ( +
+ + {w.properties?.text || 'Radio'} +
+ ); + default: + return w.properties?.text || w.type; + } +} + export function Canvas() { const dispatch = useAppDispatch(); const { design, selectedWidgetId } = useAppState(); @@ -17,12 +80,10 @@ export function Canvas() { const rect = containerRef.current?.getBoundingClientRect(); const x = e.clientX - (rect?.left || 0); const y = e.clientY - (rect?.top || 0); - console.log('[Canvas] Drop widget', type, 'at', x, y); if (type) dispatch({ type: 'addWidget', payload: { type, x, y } }); }; const onSelect = (id: string | null) => { - console.log('[Canvas] Select widget', id); dispatch({ type: 'selectWidget', payload: { id } }); }; @@ -34,19 +95,126 @@ export function Canvas() { if (!w) return; const initX = w.x; const initY = w.y; - console.log('[Canvas] Drag start', id, 'at', initX, initY); const onMove = (ev: MouseEvent) => { const dx = ev.clientX - startX; const dy = ev.clientY - startY; + const newX = initX + dx; + const newY = initY + dy; dispatch({ type: 'updateWidget', - payload: { id, patch: { x: initX + dx, y: initY + dy } }, + payload: { id, patch: { x: newX, y: newY } }, }); }; const onUp = () => { window.removeEventListener('mousemove', onMove); window.removeEventListener('mouseup', onUp); - console.log('[Canvas] Drag end', id); + dispatch({ type: 'pushHistory' }); + }; + window.addEventListener('mousemove', onMove); + window.addEventListener('mouseup', onUp); + }; + + const onResizeStart = ( + e: React.MouseEvent, + id: string, + direction: string + ) => { + e.stopPropagation(); + const startX = e.clientX; + const startY = e.clientY; + const w = design.widgets.find((w) => w.id === id); + if (!w) return; + const initX = w.x; + const initY = w.y; + const initWidth = w.width; + const initHeight = w.height; + + const onMove = (ev: MouseEvent) => { + const dx = ev.clientX - startX; + const dy = ev.clientY - startY; + const patch: Partial = {}; + + if (direction.includes('e')) { + patch.width = Math.max(10, initWidth + dx); + } + if (direction.includes('w')) { + const newWidth = Math.max(10, initWidth - dx); + patch.width = newWidth; + patch.x = initX + (initWidth - newWidth); + } + if (direction.includes('s')) { + patch.height = Math.max(10, initHeight + dy); + } + if (direction.includes('n')) { + const newHeight = Math.max(10, initHeight - dy); + patch.height = newHeight; + patch.y = initY + (initHeight - newHeight); + } + + dispatch({ + type: 'updateWidget', + payload: { id, patch }, + }); + }; + + const onUp = () => { + window.removeEventListener('mousemove', onMove); + window.removeEventListener('mouseup', onUp); + dispatch({ type: 'pushHistory' }); + }; + window.addEventListener('mousemove', onMove); + window.addEventListener('mouseup', onUp); + }; + + const onWindowResizeStart = ( + e: React.MouseEvent, + direction: string + ) => { + e.stopPropagation(); + const startX = e.clientX; + const startY = e.clientY; + const initWidth = design.form.size.width; + const initHeight = design.form.size.height; + + let animationFrameId: number | null = null; + let currentEv: MouseEvent | null = null; + + const onMove = (ev: MouseEvent) => { + ev.preventDefault(); + currentEv = ev; + + if (animationFrameId) return; + + animationFrameId = requestAnimationFrame(() => { + if (!currentEv) return; + + const dx = currentEv.clientX - startX; + const dy = currentEv.clientY - startY; + + let newWidth = initWidth; + let newHeight = initHeight; + + if (direction.includes('e')) { + newWidth = Math.max(100, initWidth + dx); + } + if (direction.includes('s')) { + newHeight = Math.max(100, initHeight + dy); + } + + dispatch({ + type: 'setForm', + payload: { size: { width: newWidth, height: newHeight } }, + }); + + animationFrameId = null; + }); + }; + + const onUp = () => { + if (animationFrameId) cancelAnimationFrame(animationFrameId); + window.removeEventListener('mousemove', onMove); + window.removeEventListener('mouseup', onUp); + dispatch({ type: 'pushHistory' }); }; window.addEventListener('mousemove', onMove); window.addEventListener('mouseup', onUp); @@ -54,41 +222,123 @@ export function Canvas() { return (
-
onSelect(null)} - > - {design.widgets.length === 0 && ( -
-

Drag widgets here to start designing

+
+
+
+ {design.form?.title || 'Tkinter App'}
- )} - {design.widgets.map((w) => ( -
{ - e.stopPropagation(); - onSelect(w.id); - }} - onMouseDown={(e) => onMouseDown(e, w.id)} - > -
- {w.properties?.text || w.type} +
+
+
+
+
+
+
onSelect(null)} + > + {design.widgets.length === 0 && ( +
+

Drag widgets here to start designing

-
- ))} + )} + {design.widgets.map((w) => ( +
{ + e.stopPropagation(); + onSelect(w.id); + }} + onMouseDown={(e) => onMouseDown(e, w.id)} + > +
+ {renderWidgetContent(w)} +
+ {selectedWidgetId === w.id && ( + <> +
+ onResizeStart(e, w.id, 'n') + } + /> +
+ onResizeStart(e, w.id, 's') + } + /> +
+ onResizeStart(e, w.id, 'e') + } + /> +
+ onResizeStart(e, w.id, 'w') + } + /> +
+ onResizeStart(e, w.id, 'ne') + } + /> +
+ onResizeStart(e, w.id, 'nw') + } + /> +
+ onResizeStart(e, w.id, 'se') + } + /> +
+ onResizeStart(e, w.id, 'sw') + } + /> + + )} +
+ ))} +
+
onWindowResizeStart(e, 'e')} + onDragStart={(e) => e.preventDefault()} + /> +
onWindowResizeStart(e, 's')} + onDragStart={(e) => e.preventDefault()} + /> +
onWindowResizeStart(e, 'se')} + onDragStart={(e) => e.preventDefault()} + />
); diff --git a/src/webview/react/components/ErrorBoundary.tsx b/src/webview/react/components/ErrorBoundary.tsx new file mode 100644 index 0000000..51aec3b --- /dev/null +++ b/src/webview/react/components/ErrorBoundary.tsx @@ -0,0 +1,45 @@ +import React, { Component, ErrorInfo, ReactNode } from 'react'; + +interface Props { + children: ReactNode; +} + +interface State { + hasError: boolean; + error: Error | null; +} + +export class ErrorBoundary extends Component { + public state: State = { + hasError: false, + error: null, + }; + + public static getDerivedStateFromError(error: Error): State { + return { hasError: true, error }; + } + + public componentDidCatch(error: Error, errorInfo: ErrorInfo) { + } + + public render() { + if (this.state.hasError) { + return ( +
+

Something went wrong.

+
+ {this.state.error && this.state.error.toString()} +
+ +
+ ); + } + + return this.props.children; + } +} diff --git a/src/webview/react/components/EventsPanel.tsx b/src/webview/react/components/EventsPanel.tsx index 0701ce2..d2b7d0c 100644 --- a/src/webview/react/components/EventsPanel.tsx +++ b/src/webview/react/components/EventsPanel.tsx @@ -5,8 +5,7 @@ export function EventsPanel() { const dispatch = useAppDispatch(); const { design, selectedWidgetId, vscode } = useAppState(); const [eventType, setEventType] = useState('command'); - const [eventName, setEventName] = useState('onClick'); - const [eventCode, setEventCode] = useState('print("clicked")'); + const [eventName, setEventName] = useState('on_click'); const w = design.widgets.find((x) => x.id === selectedWidgetId); const widgetEvents = (id: string | undefined) => @@ -20,10 +19,17 @@ export function EventsPanel() { }); return; } - if (!eventType || !eventName || !eventCode) { + if (!eventName) { vscode.postMessage({ type: 'showError', - text: 'Please fill in all fields: event type, name, and code', + text: 'Please fill in handler name', + }); + return; + } + if (!eventType) { + vscode.postMessage({ + type: 'showError', + text: 'Please fill in event type', }); return; } @@ -33,21 +39,39 @@ export function EventsPanel() { widget: w.id, type: eventType, name: eventName, - code: eventCode, + code: 'pass', }, }); vscode.postMessage({ type: 'showInfo', - text: `Event added: ${eventType} -> ${eventName}`, + text: `Event added: ${eventName}`, }); }; - const remove = (type: string) => { + const remove = (type: string, name: string) => { if (!w) return; - dispatch({ type: 'removeEvent', payload: { widget: w.id, type } }); + dispatch({ + type: 'removeEvent', + payload: { widget: w.id, type, name }, + }); vscode.postMessage({ type: 'showInfo', text: 'Event removed' }); }; + const commonEvents = [ + 'command', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + ]; + return (

Events & Commands

@@ -60,12 +84,41 @@ export function EventsPanel() { - setEventType(e.target.value)} - /> +
+ + {!commonEvents.includes(eventType) && ( + + setEventType(e.target.value) + } + /> + )} +
@@ -75,14 +128,6 @@ export function EventsPanel() { value={eventName} onChange={(e) => setEventName(e.target.value)} /> - -