From 9754f36c37b52b70cc25f61ae2f5c2237850afd8 Mon Sep 17 00:00:00 2001 From: IDK Date: Tue, 23 Dec 2025 05:21:30 +0300 Subject: [PATCH] init commit --- .dockerignore | 12 + Dockerfile | 25 ++ package-lock.json | 11 + package.json | 1 + src/extension.ts | 117 ++--- src/generator/CodeGenerator.ts | 71 ++- src/generator/eventHelpers.ts | 45 +- src/generator/types.ts | 2 + src/generator/utils.ts | 2 +- src/parser/CodeParser.ts | 140 ++---- src/parser/astConverter.ts | 70 ++- src/parser/pythonRunner.ts | 172 +++++--- .../__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 -> 3217 bytes .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 266 bytes .../analyzer/__pycache__/base.cpython-314.pyc | Bin 0 -> 16113 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 -> 7137 bytes src/parser/tk_ast/analyzer/base.py | 141 +++++- src/parser/tk_ast/analyzer/calls.py | 4 - src/parser/tk_ast/analyzer/values.py | 14 +- src/parser/tk_ast/analyzer/widget_creation.py | 24 +- src/parser/tk_ast/parser.py | 3 +- src/webview/TkinterDesignerProvider.ts | 414 +++++++++++++++--- src/webview/react/App.tsx | 27 +- src/webview/react/components/Canvas.tsx | 342 +++++++++++++-- .../react/components/ErrorBoundary.tsx | 46 ++ src/webview/react/components/EventsPanel.tsx | 115 +++-- src/webview/react/components/Palette.tsx | 1 + .../react/components/PropertiesPanel.tsx | 69 ++- src/webview/react/components/Toolbar.tsx | 113 ++--- src/webview/react/state.tsx | 364 ++++++++------- src/webview/react/types.ts | 3 + src/webview/react/useMessaging.ts | 13 +- src/webview/style.css | 232 +++++++++- 43 files changed, 1927 insertions(+), 666 deletions(-) create mode 100644 .dockerignore create mode 100644 Dockerfile 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/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/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/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/extension.ts b/src/extension.ts index 0519eeb..87b5702 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,84 +1,32 @@ import * as vscode from 'vscode'; import * as path from 'path'; -import { CodeGenerator } from './generator/CodeGenerator'; -import { CodeParser } from './parser/CodeParser'; +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; + // We don't register a provider here anymore for the global viewType + // because we create panels on demand. + + // Store active panels if needed, but for now we just create them. const openDesignerCommand = vscode.commands.registerCommand( 'tkinter-designer.openDesigner', () => { - TkinterDesignerProvider.createOrShow(context.extensionUri); + TkinterDesignerProvider.createNew(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'); - } - ); + // Removed global generateCodeCommand because generate is now handled inside the Webview Provider instance + // or we need a way to target the specific panel. + // The previous generateCodeCommand was flawed because it relied on a singleton provider. + // Now, generation is triggered FROM the UI (webview) via messages, which is handled in TkinterDesignerProvider.handleGenerateCode. + // If we want a command palette action, it needs to know WHICH designer to use. + // For simplicity, we remove the global command or make it check for active webview (hard with multiple). + // Let's keep it but warn it only works if we tracked instances. + // Actually, best practice: actions are context-based. The webview has a "Generate" button. + + // However, parseCodeCommand is useful. It should open a NEW designer with the parsed code. const parseCodeCommand = vscode.commands.registerCommand( 'tkinter-designer.parseCode', @@ -89,24 +37,36 @@ export function activate(context: vscode.ExtensionContext) { if (activeEditor && activeEditor.document.languageId === 'python') { const parser = new CodeParser(); const code = activeEditor.document.getText(); + const fileName = path.basename( + activeEditor.document.fileName, + '.py' + ); console.log('[ParseCode] Code length:', code.length); 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( + console.log( + '[ParseCode] Widgets found:', + designData.widgets.length + ); + // Create a NEW designer instance for this file + const designerInstance = TkinterDesignerProvider.createNew( context.extensionUri - ); - if (designerInstance) { - designerInstance.loadDesignData(designData); - } else { - } + ); + + // We need to wait for webview to be ready? + // The createNew sets up message handling. + // We can send data immediately? No, webview needs to load React. + // We can set initial data in the provider. + designerInstance.loadDesignData(designData); vscode.window.showInformationMessage( `Code parsed successfully! Found ${designData.widgets.length} widgets.` @@ -134,7 +94,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..fd60b20 100644 --- a/src/generator/CodeGenerator.ts +++ b/src/generator/CodeGenerator.ts @@ -18,14 +18,21 @@ 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 || 'Application'; + console.log( + '[Generator] Start, widgets:', + designData.widgets.length, + 'events:', + designData.events?.length || 0 + ); const lines: string[] = []; const nameMap = generateVariableNames(designData.widgets); lines.push('import tkinter as tk'); + lines.push('from tkinter import ttk'); lines.push(''); - lines.push('class Application:'); + lines.push(`class ${className}:`); this.indentLevel = 1; lines.push(this.indent('def __init__(self):')); @@ -47,7 +54,6 @@ export class CodeGenerator { designData.widgets.forEach((widget) => { console.log('[Generator] Widget:', widget.id, widget.type); lines.push(...this.generateWidgetCode(widget, nameMap)); - lines.push(''); }); this.indentLevel = 1; @@ -72,12 +78,65 @@ export class CodeGenerator { this.indentLevel = 0; lines.push('if __name__ == "__main__":'); this.indentLevel = 1; - lines.push(this.indent('app = Application()')); + lines.push(this.indent('try:')); + this.indentLevel = 2; + lines.push(this.indent(`app = ${className}()`)); lines.push(this.indent('app.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; // Inside class -> inside create_widgets + + designData.widgets.forEach((widget) => { + lines.push(...this.generateWidgetCode(widget, nameMap)); + }); + + return lines.join('\n'); + } + + public generateEventHandler(event: any): string { + // Simple helper to generate a single event handler method + // Used when injecting new methods + const lines: string[] = []; + // Use existing signature if available (to preserve user arguments) + // Note: signature does not include 'def ' prefix in our current extraction logic? + // Let's check base.py: sig_raw = self.source_code[def_start_idx : start_idx].strip() + // It includes 'def name(...)'. + // So we just use it directly. + + 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) => ' ' + l) + .join('\n'); + lines.push(body); + } else { + lines.push(' pass'); + } + return lines.join('\n'); + } + private generateWidgetCode( widget: WidgetData, nameMap: Map @@ -96,7 +155,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..0293d76 100644 --- a/src/generator/eventHelpers.ts +++ b/src/generator/eventHelpers.ts @@ -8,22 +8,47 @@ export function generateEventHandlers( const lines: string[] = []; if (designData.events && designData.events.length > 0) { + // Use a Set to avoid duplicates if multiple widgets use the same handler + const handledNames = new Set(); + designData.events.forEach((event: Event) => { const handlerName = event.name; - const isBindEvent = - event.type.startsWith('<') && event.type.endsWith('>'); - let hasCode = false; - const widget = (designData.widgets || []).find( - (w) => w.id === event.widget - ); - if (isBindEvent) { - lines.push(indentDef(`def ${handlerName}(self, event):`)); + if (handledNames.has(handlerName)) { + return; + } + handledNames.add(handlerName); + + // Use preserved signature if available + // Need to cast to any because Event interface might not have signature yet in types.ts (but we added it in runtime) + const signature = (event as any).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'); + // If we have parsed code, use it. Otherwise, use 'pass'. + // The parser should populate event.code if the method exists. + // If the user just added the event in the UI, event.code might be 'pass' or empty. + + let hasCode = false; + // Ensure event.code is a string before split + 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..f49ae44 100644 --- a/src/generator/types.ts +++ b/src/generator/types.ts @@ -17,11 +17,13 @@ export interface WidgetData { export interface DesignData { form: { + name: string; title: string; size: { width: number; height: number; }; + className?: string; }; widgets: WidgetData[]; events?: Event[]; diff --git a/src/generator/utils.ts b/src/generator/utils.ts index 7d8796e..50e6553 100644 --- a/src/generator/utils.ts +++ b/src/generator/utils.ts @@ -27,10 +27,10 @@ export function generateVariableNames( 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..2d5d304 100644 --- a/src/parser/CodeParser.ts +++ b/src/parser/CodeParser.ts @@ -1,137 +1,51 @@ +import * as vscode from 'vscode'; import { DesignData, WidgetData, Event } from '../generator/types'; import { runPythonAst } from './pythonRunner'; import { convertASTResultToDesignData } from './astConverter'; 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); + + if (astRaw && astRaw.error) { + console.error('[Parser] Python AST error:', astRaw.error); + vscode.window.showErrorMessage(`Parser Error: ${astRaw.error}`); + return null; + } + const astDesign = convertASTResultToDesignData(astRaw); - if (astDesign && astDesign.widgets && astDesign.widgets.length > 0) { + + if (astDesign) { + // AST parsing was successful (even if 0 widgets, it's a valid result) console.log( '[Parser] AST parsed widgets:', astDesign.widgets.length ); + if (filename) { + astDesign.form.name = filename; + } return astDesign; } - console.log('[Parser] AST returned no widgets, using regex fallback'); - const regexDesign = this.parseWithRegexInline(pythonCode); + + // If AST failed completely (e.g. syntax error or script failure), we can't trust regex to be safe for editing. + // It's better to fail and let user know the code is too complex or broken. + console.log( - '[Parser] Regex parsed widgets:', - regexDesign?.widgets?.length || 0 + '[Parser] AST failed. Regex fallback is disabled for safety.' ); - return regexDesign; + 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; } - 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'); - return null; - } - return { - form: { - title: formTitle, - size: { width: formWidth, height: formHeight }, - }, - widgets, - events, - }; - } + // Regex parser removed to prevent "Zombie Code" issues where regex sees widgets + // but AST doesn't, leading to partial updates that break the file. } diff --git a/src/parser/astConverter.ts b/src/parser/astConverter.ts index 5eb1da9..9ac5255 100644 --- a/src/parser/astConverter.ts +++ b/src/parser/astConverter.ts @@ -19,11 +19,14 @@ export function convertASTResultToDesignData( (astResult.window && astResult.window.height) || astResult.height || 600; + let className = + (astResult.window && astResult.window.className) || 'Application'; let counter = 0; const allowedTypes = new Set([ 'Label', 'Button', + 'Entry', 'Text', 'Checkbutton', 'Radiobutton', @@ -68,17 +71,30 @@ export function convertASTResultToDesignData( } const events: Event[] = []; + 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`; + + const methodData = methods[cleanName]; + // methodData is { body: string, signature: string } or string (legacy/lambda) + const codeBody = + typeof methodData === 'object' ? methodData.body : methodData; + const signature = + typeof methodData === 'object' + ? methodData.signature + : undefined; + events.push({ widget: callback.widget, type: 'command', name: cleanName, - code: callback.command?.lambda_body || '', + code: codeBody || callback.command?.lambda_body || '', + // @ts-ignore - attaching signature for generator + signature: signature, }); } } @@ -88,22 +104,72 @@ export function convertASTResultToDesignData( const cleanName = rawName ? String(rawName).replace(/^self\./, '') : `on_${bindEvent.widget}_${String(bindEvent.event).replace(/[<>]/g, '').replace(/-/g, '_')}`; + + const methodData = methods[cleanName]; + const codeBody = + typeof methodData === 'object' ? methodData.body : methodData; + const signature = + typeof methodData === 'object' + ? methodData.signature + : undefined; + events.push({ widget: bindEvent.widget, type: bindEvent.event, name: cleanName, - code: bindEvent.callback?.lambda_body || '', + code: codeBody || bindEvent.callback?.lambda_body || '', + // @ts-ignore + signature: signature, }); } } + if (astResult.widgets) { + for (const w of astResult.widgets) { + const p = w.properties || w.params || {}; + if (p.command) { + const widgetId = w.variable_name; + const cmd = p.command; + const rawName = cmd.name || cmd; + const cleanName = String(rawName).replace(/^self\./, ''); + + // Avoid duplicates if already added by command_callbacks + const exists = events.find( + (e) => e.widget === widgetId && e.type === 'command' + ); + if (!exists) { + const methodData = methods[cleanName]; + const codeBody = + typeof methodData === 'object' + ? methodData.body + : methodData; + const signature = + typeof methodData === 'object' + ? methodData.signature + : undefined; + + events.push({ + widget: widgetId, + type: 'command', + name: cleanName, + code: codeBody || '', + // @ts-ignore + signature: signature, + }); + } + } + } + } + const keptIds = new Set(widgets.map((w) => w.id)); const filteredEvents = events.filter((e) => keptIds.has(e.widget)); const result: DesignData = { form: { + name: 'Form', title: formTitle, size: { width: formWidth, height: formHeight }, + className: className, }, widgets, events: filteredEvents.length ? filteredEvents : [], diff --git a/src/parser/pythonRunner.ts b/src/parser/pythonRunner.ts index c9160a3..42fe0af 100644 --- a/src/parser/pythonRunner.ts +++ b/src/parser/pythonRunner.ts @@ -2,79 +2,143 @@ 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'; async function executePythonScript( pythonScriptPath: string, - pythonFilePath: string + pythonCode: 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 = ''; + const commandsToTry = await getPythonCommands(); + let lastError: Error | null = null; - process.stdout.on('data', (data) => { - result += data.toString(); - }); + for (const cmd of commandsToTry) { + try { + return await new Promise((resolve, reject) => { + const start = Date.now(); + console.log('[PythonRunner] Spawning:', cmd, pythonScriptPath); - process.stderr.on('data', (data) => { - errorOutput += data.toString(); - }); + const process = spawn(cmd, [pythonScriptPath]); - 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}` - ) + let result = ''; + let errorOutput = ''; + + // Write code to stdin + try { + process.stdin.write(pythonCode); + process.stdin.end(); + } catch (e) { + console.error('[PythonRunner] Error writing to stdin:', e); + } + + process.stdout.on('data', (data) => { + result += data.toString(); + }); + + process.stderr.on('data', (data) => { + errorOutput += data.toString(); + }); + + 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}` + ) + ); + } + }); + + process.on('error', (err) => { + reject( + new Error( + `Failed to spawn python process: ${err.message}` + ) + ); + }); + }); + } catch (err: any) { + console.warn(`[PythonRunner] Command ${cmd} failed:`, err.message); + lastError = err; + // Continue to next command + } + } + + throw lastError || new Error('No suitable Python interpreter found'); +} + +async function getPythonCommands(): Promise { + const commands: string[] = []; + + // 1. Try VS Code Python extension API + const extension = vscode.extensions.getExtension('ms-python.python'); + if (extension) { + if (!extension.isActive) { + await extension.activate(); + } + // The API might be complex, but let's check settings first which is easier + } + + // 2. Settings + const config = vscode.workspace.getConfiguration('python'); + let pythonPath = + config.get('defaultInterpreterPath') || + config.get('pythonPath'); + + if (pythonPath && pythonPath !== 'python') { + // Handle ${workspaceFolder} variable + 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); + } -function getPythonCommand(): string { - return process.platform === 'win32' ? 'python' : 'python3'; -} + // 3. Common commands + if (process.platform === 'win32') { + commands.push('py'); + commands.push('python'); + } else { + commands.push('python3'); + commands.push('python'); + } -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 {} + // Unique + return Array.from(new Set(commands)); } 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 - ); + const output = await executePythonScript(pythonScriptPath, pythonCode); console.log('[PythonRunner] Received AST JSON length:', output.length); - const parsed = JSON.parse(output); - return parsed; + try { + const parsed = JSON.parse(output); + return parsed; + } catch (parseError) { + console.error( + '[PythonRunner] JSON parse error:', + parseError, + 'Output:', + output + ); + return null; + } } catch (err) { console.error('[PythonRunner] Error running Python AST:', err); return null; - } finally { - cleanupTempFile(tempFilePath); - console.log('[PythonRunner] Temp file cleaned:', tempFilePath); } } 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..59fcf57a591bdb3e5aa78af6b97779dfac630eb8 GIT binary patch literal 390 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 Lunm7n93}q+7L3g| literal 0 HcmV?d00001 diff --git a/src/parser/tk_ast/__pycache__/parser.cpython-314.pyc b/src/parser/tk_ast/__pycache__/parser.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b920b08547f9b0482be69c58e3443073b59608d1 GIT binary patch literal 3217 zcmbVOUrbxq89(>0FaCqU#{4M|JFpZSpfq3wlw@TTCm|AJ^oF2mx{7P;3%Lngd*@z5 zY)vc5_CVW*Fs;&f=mQU(M5R7N+A2j=H?2(`_GpVtMyE`xwCUIbOHLchRv+;kuH*|k22tQLHqtZ6d?sf*(i8_C z=$|&&4-(-2SOv0y7SMHm{Fex&LDq#{<8E*&r-7#0R93epLEJ@#Er$#Xpqpl-|}BuKh%dY1x=&SNA`Z)-7Y%)bY@oMHe1g zJEK`RrDc;CJc3msY1y^WOW8c}CK5^0%-Bg=Pb9D(7N-|(^#IvGPf$1K{)&=!uU2UH z-uwr>A6UEnzrD_+kHHGDGo=h|Uih;!eI53`fjWx8JPze?-k6i&}JIdOku`fR$U1;&3=sTo6?WbnTAhs8?l>AA7%! zMsO~Qxop3ZVO1IGL$;zr*^~}p&G{S?ecbL!d-AMIieS8Y0JPGweClV&=RO0KLdCR4X8;#mZngubSmwnbd4x_u*)2FA*z zQo3c$6G_K7gA0vU^QN7=9buz$kxR1${~V;8VJ1&-ar+;OL8Ch19$Up<+oejeq&QS2=?s6_JaMJ%D&QcxAD!! zE&Gwub)b0ey5Dq{mG+X-UhEwEShzo3zA#g|Fta!Q!{XTNpBIZAuRT)KgZ9&V?Sq@X z{Xn=J7%c@x?@gCu@lq_l7r40Tdd|z)-&^;`e)qF`=AT|Ic3gg>On>fe*}73~9W1pD z?t4R9>-*mDPJ8*}`O?Yr``(W2@p9*IsdJdBZkOBoN^O0VwJSbXk9$+D1W;4!rte>0 zcv=sUD7ioP2Dg5`liayl3`MD3t|AFd?yr&1;C_0jAaAH{^~TOmcT&aB>F2E~0=W6c zItxd3XX@ptQS@Nc6CV;DyrRUt!b6z@{-M`PX$Rw9P~!cOCW!yn9*9ofcIr! zzC$qst7@y3D%=1gqAVHk>XNHE_P{X8VG8HaQ3S1)CwBF$OfW+4+oHHZYef?m{)&=! zgB9An7i1$$!S$IML0=LJ^&R#|phnax0D@|u0^X|p7}Y~N&n37VpA!ySOYZdrFIa*x2&ehs%svKCqqkdn6BI~R;!m3IeU>tufp*P z#|{`Peh07adGWt{RfViLAp#z=-r(xE>LDC1NI83HXdJ#z6y1d61|*xk6y=Ewa0pmP zyt--SunwOSDZ{`W5L%(>5d`v`^GC30f7}HV%@8=BFwlFQ$lhjom6kSBjWOb8> zRKf6rh&Qc_Nu+dcHEWS(I%_s#U!oovl;Q7DrxG57j&Ms1%y2&pa1@9|Z=2y%6{Xx& zl%fwCkAsT6*ayH+bn;8#17Gu2|Mt{wU)eYE7vIRe*qz)REru8rQCvAbjUr6=DaDTPH`WB~<4)^j zKiY1d3`y?~0QX7AGkL1vlZZn3{?k(~;nNm{(r%8@6DOv4;nNG0f51yXqdrXY7t{Q~ z-9U(J=d*A_N1v#e9x3rxKOJAH7YJ6s0yXKes>JFQyGeH7?<0l))!R@mtY*@=75x?b ueeg-s-Z}-OB5)k{8FGJyJV5`3dP=D0A86pO=*-tLe04tg0S9=(-25BVTBW4` 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..db6cf20b220eaf31020b2f5e090af39162032e8f GIT binary patch literal 266 zcmdPqcnTkOAF^B^LOi;#WAt0lHA&xPK(UZZ0v4l~cA&5ztL6i9y#NyNv@K;hz;tkU9= z${7E=RF|U6vecLhQ!ebgu0)?N;-ci3g2W=Ai9l8HiNz%`iBKoR#K&jmWtPOp>lIYq;;_lhPbtkw bwJYKPx&h?qVi4m4Gb1D8eFnuMHXsK8XQxn- 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..4721ff61f6992bd70b7efae764343352d0340895 GIT binary patch literal 16113 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 LTGW3+ZP$MR3xaT= literal 0 HcmV?d00001 diff --git a/src/parser/tk_ast/analyzer/__pycache__/context.cpython-314.pyc b/src/parser/tk_ast/analyzer/__pycache__/context.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6cba67611f79ee9ca854529df3580bf800392df6 GIT binary patch literal 1235 zcmb_bL2DCH5T3W$rm;<9t4P4sqAAn_siO8!MC_qO?4{O)Mk-6gvU#ldyWpV_Iws3aLF+VBV#= zpdKZvW`dsmDN;=+!m=^-5}-`J5qQ8>nZ<01M6wVgh#mC4MwMH}o{EH~AC!5)2osKw zVQQx02rn1Xy*XP+fBelJv|idr@!{_LI7h>-K%0&!X4hcKdbvL;LOSi}t(r zoA&$N=ew`$;^HHF(ymR~@~P)bjuT}ryvIHn4dGJ5iN7YPqV~4qJC*fk z^z8<8XiZ+Nt%vDS$?^R_I+B-4*iSIj)kT(66Mh0?m%l5uo!CilrM0A}9MyLkkeRiwG32Ap;<%T z_@jLvU-LRRQ4LAsOOc_wANBq3`-CR-C>YqQVD;av`>yl?U#}9LB44yedPJH|6LVSRay2O9kOFs{z)VEoO6LkVLJ1mZhr`j_ zrnliK4u`wo>(1dY)x>irI}d2?jv9|rTrG*{&^MZkTNZlsob`HXmNiIJ2U?&(MZ1`~ pT@A|h3crIFDWo_T>NejJjIl4!cWBa|^CQVv?vPaefYg{i{RjVG* 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..84a98e529124cc420a48f09c824035518b94bb96 GIT binary patch literal 4816 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 X{08o)(EJw+{A0s+`*|}WnD6`-uH%9| literal 0 HcmV?d00001 diff --git a/src/parser/tk_ast/analyzer/__pycache__/imports.cpython-314.pyc b/src/parser/tk_ast/analyzer/__pycache__/imports.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fe3aee6c3d2133d1b49e227944797f114746de92 GIT binary patch literal 1638 zcmd6n&1(}u6u{r?=A-$lZM7ELXvJddCbm^jsIhpdEs9v{LW73Iuq11ZO*Ub6TWAYX zKfsCyJrs;5DHKXhJqap){0k|H3F}4h)SD$rufCa0#D*vc9(=HGzTUif@6B&E+#K)% zO19LRY8C*#ut805%%#$c%_3X_nVbg`WT69Ou>;aACXpT3ck;dw1=ERMTK*9{OgY9*HQEhDHtL=8R#^DVKEZEKSX7N^!aPw)kq}Zt->T zdGXE0osEY|bTX!#P;w^}b2_D)8tqSNMrumO&a9EZ|WE_W_D%f6OJkq43dzK^~`<#N6LyR3DnKv_SzVZ(QHN{)Pqwthde7z z0as0Ip@xEsj3Ek4uNrH&R&LpSJKBaQ%p=wY|w3BnSo!t!Sf z0?bD@!=(WPhz3idP8!>CT!yQ-2tB`xoG?uP@5p70UJ_|DrakYg5yK zY-zL{KDItw7%qiJi{a51C*MV0Mt+h$`7HJ{_TGP_BEsH-pFM#`T@SkQfe)U;f4_T6 z1aB`#5YuI3^C!q7EC8Rk-)YmZo%I>IAC1^A*Vu#Vt4Ij>1m5os_MhAKGPwD!AA40O AO#lD@ 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..397fa184bd3ce88358cb60acbaaabfac0f8414de GIT binary patch literal 2006 zcmZ{l&u`;I6vxN0<0P(=#?23>fo{u&{z!jPsGzJ69EvI`#L`;3S+uemnZ%ohrb*Uz ze`MiM0f!(iJ?tN_$8hTbB!mD735grB@u-Jbs?}%*@yC zd7fuNK@NdcnnLk`7ooq&L)SbV=X?U3L$rw`Y7H4^laf4Z)O#MoGm0cyL=q#SQrMs+ zDuvSCr)1L2G=)X=z|fSOkt-KqTzs}rd-LZm<2ss zm3*4Uv~FmaR&)b1Dgp9v)Z`D#>bqKTPnFGw z=FjF2))(fF=6B{#*5}sOa_073c}cD=$;NiE46i(wSM_3{49=dOgPrI07G+(_$q*A* z5vVL|Eh}ZERC@r=)sm7^chs_>&sS@h&t{c!xnd}Wn$2n=af zDY^Zc`fci$DaSpzb;lVw2OQ=4!xe=NC+NRJcn*<-s!Rrv1UzqHv>HRG>dkm1YBqT} znxU3ILa5**>HP>ox4czm6n)?cplc|@Om@KbxP($jCc8Oy$FtN)VU^9$l83C5e@K!o z!IQ*f*md%H&zZz*qV7D&>%w^Z7}kYhmpUJwJ;Luo_ChWq5H>qCi1s6u>V}@Lqp}xH$ zE=;74iMlXRDc04q;O~zOiBCFy69?g(7nP4oYPJj|2Ya3Kr1>F&m^#2z4SV;Cd1DK+ zTWYbe1!rDUO9xI{!+!AFqXFe-C5Bs}EUnAvE^O44p%eHAu?c;DyL%Wpf zI>9Eu#>U39AX!b^t>#IUXiv)aCTKG->J*Ky;Ojp`t)OtY+6u;+!I6{TNS$g8@b&B; z10#=;e-6yFV)6RwV_rD(q0qGp4)H^e{h@!v`Nn}Qu9!^pi7$90p7;_^_~=og#Sfao zOHE3p8jH3tcV$<^jEy&AX)Bg)Oxm$mTYS`vPc-AxR(!g#Y{!>c{E!)axfz|Z zqEn5y9ew4Bj<}haY9?l_#BAfPomg)1qB)#y4$oP`bB%3#_{M338|CV3{Zrfj!f70F z;iK120>eb+y%sN+kyJA>X+K@QW-uK%r_N`AMbzOBu}1X9NIFf^w$MNxkt?q9TU!N3o|nejilL$>Mw 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..c70a592ba77645f91ca16f561d22ab1a8dab96e8 GIT binary patch literal 11515 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 bqVLP$FTL-#z&|11A0+|*UAYt^ml*vwZvdQ= literal 0 HcmV?d00001 diff --git a/src/parser/tk_ast/analyzer/__pycache__/widget_creation.cpython-314.pyc b/src/parser/tk_ast/analyzer/__pycache__/widget_creation.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..322127c0b4eccddd76339c5c34cd778a4ca3a5e9 GIT binary patch literal 7137 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 literal 0 HcmV?d00001 diff --git a/src/parser/tk_ast/analyzer/base.py b/src/parser/tk_ast/analyzer/base.py index ec39738..c796fce 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,139 @@ class TkinterAnalyzer(ast.NodeVisitor): def visit_ClassDef(self, node: ast.ClassDef): prev = self.current_class enter_class(self, node) + + # Determine main application class + # Logic: + # 1. Prefer class named 'Application' + # 2. Else prefer class inheriting from tk.Tk or Tk + # 3. Fallback to first class found if nothing else set + + 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: + # Calculate start index based on 'def' keyword and colon + # We need to find the colon that ends the definition + def_start_idx = self.line_offsets[node.lineno - 1] + node.col_offset + + # Scan tokens to find the colon after the function definition + # We start tokenizing from the def line + # We use a safe approach to find the end of the header + + header_end_idx = -1 + + try: + # Create a byte stream for tokenize, starting from def line + # We need the full source code for context + # But tokenize expects bytes + + # Optimization: only tokenize the relevant chunk to avoid perf hit on large files + # We can guess the header won't be longer than, say, 10 lines or 1000 chars? + # Let's take a generous chunk + + chunk_start = def_start_idx + chunk_end = min(len(self.source_code), def_start_idx + 2000) + chunk = self.source_code[chunk_start:chunk_end] + + # Better approach: Just find the ':' char that is not enclosed in brackets/parens/quotes + # Manually parsing characters is safer than partial tokenize if we want to be quick + + depth = 0 + in_quote = False + quote_char = '' + + for i, char in enumerate(chunk): + if in_quote: + if char == quote_char: + # Check for escaping? Simplified check + if i > 0 and chunk[i-1] != '\\': + in_quote = False + continue + + if char in ['"', "'"]: + in_quote = True + quote_char = char + elif char in ['(', '[', '{']: + depth += 1 + elif char in [')', ']', '}']: + depth -= 1 + elif char == ':' and depth == 0: + # Found the colon! + # Add 1 to include the colon in signature, but body starts AFTER it + header_end_idx = chunk_start + i + 1 + break + + except Exception: + # Fallback to old method if manual parsing fails + start_node = node.body[0] + header_end_idx = self.line_offsets[start_node.lineno - 1] + start_node.col_offset + + if header_end_idx != -1: + start_idx = header_end_idx + + # End index: logic remains using the last node end + # Ideally we should include trailing comments, but that's hard without full tokenization + # For now, sticking to last node end is safe enough for "body" logic + end_node = node.body[-1] + end_idx = self.line_offsets[end_node.end_lineno - 1] + end_node.end_col_offset + + if start_idx < end_idx: + body_text = self.source_code[start_idx:end_idx] + + # Dedent logic needs care because body_text now starts with newline(s) and comments + # We should remove leading empty lines, then determine indentation from the first non-empty line + + lines = body_text.splitlines(keepends=True) + first_code_line_idx = 0 + for idx, line in enumerate(lines): + if line.strip(): + first_code_line_idx = idx + break + + # Calculate indentation of the first non-empty line + first_line = lines[first_code_line_idx] + indent_len = len(first_line) - len(first_line.lstrip()) + + # Construct body: Keep the leading newlines/comments, but dedent them? + # Actually, textwrap.dedent works on the whole block. + # But if the first line is blank, dedent might fail to detect common indent? + # textwrap.dedent removes common leading whitespace from every line. + + dedented_body = textwrap.dedent(body_text) + + # Signature: everything before start_idx + sig_raw = self.source_code[def_start_idx : start_idx].strip() + # Remove trailing colon if present (it should be) + if sig_raw.endswith(':'): + sig_raw = sig_raw[:-1].strip() + + self.methods[node.name] = { + 'body': dedented_body.strip(), + 'signature': sig_raw + } + except Exception as e: + # print(f"Error extracting method {node.name}: {e}") + pass + self.generic_visit(node) exit_function(self, prev) 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..d941e94 100644 --- a/src/parser/tk_ast/analyzer/widget_creation.py +++ b/src/parser/tk_ast/analyzer/widget_creation.py @@ -19,10 +19,14 @@ 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'] + + # Check if module_name resolves to ttk + 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 +35,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 +53,12 @@ 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 + # Allow ttk widgets + # if isinstance(call_node.func.value, ast.Name): + # module_name = call_node.func.value.id + # resolved = analyzer.imports.get(module_name, module_name) + # if resolved in ['ttk', 'tkinter.ttk']: + # 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/webview/TkinterDesignerProvider.ts b/src/webview/TkinterDesignerProvider.ts index 1118f89..c382013 100644 --- a/src/webview/TkinterDesignerProvider.ts +++ b/src/webview/TkinterDesignerProvider.ts @@ -1,10 +1,11 @@ import * as vscode from 'vscode'; import * as path from 'path'; import { Uri } from 'vscode'; +import { runPythonAst } from '../parser/pythonRunner'; export class TkinterDesignerProvider implements vscode.WebviewViewProvider { public static readonly viewType = 'tkinter-designer'; - public static _instance: TkinterDesignerProvider | undefined; + // Removed singleton _instance private _view?: vscode.WebviewPanel; private _designData: any = { widgets: [], @@ -14,21 +15,13 @@ export class TkinterDesignerProvider implements vscode.WebviewViewProvider { 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; - } - + // Factory method to create a new panel + public static createNew(extensionUri: vscode.Uri) { console.log('[Webview] Creating new panel'); const panel = vscode.window.createWebviewPanel( TkinterDesignerProvider.viewType, 'Tkinter Designer', - column || vscode.ViewColumn.One, + vscode.ViewColumn.Beside, // Open beside the current editor { enableScripts: true, retainContextWhenHidden: true, @@ -40,24 +33,18 @@ 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( @@ -114,16 +101,81 @@ export class TkinterDesignerProvider implements vscode.WebviewViewProvider { console.log('[Webview] Generating code from webview'); this.handleGenerateCode(message.data); break; + case 'applyChanges': + console.log('[Webview] Applying changes from webview'); + this.handleApplyChanges(message.data); + break; case 'showInfo': vscode.window.showInformationMessage(message.text); break; case 'showError': vscode.window.showErrorMessage(message.text); break; + case 'exportProject': + this.handleExportProject(message.data); + break; + case 'importProject': + this.handleImportProject(); + break; } }, undefined); } + private async handleExportProject(data: any): 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: any) { + vscode.window.showErrorMessage(`Export failed: ${error.message}`); + } + } + + private async handleImportProject(): 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); + + if (this._view) { + // Check if the imported file has the expected structure + const designData = data.data || data; // Handle both wrapped and unwrapped data + + this._view.webview.postMessage({ + type: 'loadDesign', + data: designData + }); + // Also update internal state + this._designData = designData; + vscode.window.showInformationMessage('Project imported successfully!'); + } + } + } catch (error: any) { + vscode.window.showErrorMessage(`Import failed: ${error.message}`); + } + } + public async getDesignData(): Promise { return this._designData; } @@ -145,54 +197,62 @@ export class TkinterDesignerProvider implements vscode.WebviewViewProvider { 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' - ); - } 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; - } - const fileName = `app_${Date.now()}.py`; - const filePath = path.join( - workspaceFolder.uri.fsPath, - fileName - ); - const fileUri = 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}` + const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; + if (!workspaceFolder) { + vscode.window.showErrorMessage( + 'No workspace folder is open. Please open a folder first.' ); + 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; + } + }; + + 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}` + ); + + // Update form name in UI if changed + const newFormName = path.basename(fileName, '.py'); + if (newFormName !== formName) { + if (this._view) { + this._view.webview.postMessage({ + type: 'updateFormName', + name: newFormName, + }); + } + } + console.log('[GenerateCode] Done'); } catch (error) { console.error('[GenerateCode] Error:', error); @@ -200,6 +260,228 @@ export class TkinterDesignerProvider implements vscode.WebviewViewProvider { } } + private async handleApplyChanges(designData: any): Promise { + try { + console.log('[ApplyChanges] Start'); + + const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; + if (!workspaceFolder) { + vscode.window.showErrorMessage( + 'No workspace folder is open. Please open a folder first.' + ); + return; + } + + const formName = designData.form.name || 'Form1'; + const fileName = `${formName}.py`; + const fileUri = vscode.Uri.joinPath(workspaceFolder.uri, fileName); + + // Try to read existing file to preserve method bodies + let existingMethods: any = {}; + let oldClassName = ''; + let fileContent = ''; + let fileExists = false; + + try { + // We check if file exists first to avoid error in openTextDocument if not exists + await vscode.workspace.fs.stat(fileUri); + fileExists = true; + + const doc = await vscode.workspace.openTextDocument(fileUri); + fileContent = doc.getText(); + const astResult = await runPythonAst(fileContent); + + if (astResult) { + if (astResult.methods) { + existingMethods = astResult.methods; + if (designData.events) { + for (const event of designData.events) { + // Always prefer the existing method body from disk if it exists + // This ensures we don't overwrite user edits in the Python file + if (existingMethods[event.name]) { + event.code = + existingMethods[event.name].body; + event.signature = + existingMethods[event.name].signature; + } + } + } + } + if (astResult.window && astResult.window.className) { + oldClassName = astResult.window.className; + } + } + } catch (e) { + console.log( + '[ApplyChanges] Could not read existing file for methods preservation', + e + ); + } + + const { CodeGenerator } = await import( + '../generator/codeGenerator' + ); + const generator = new CodeGenerator(); + + if (!fileExists) { + // New file: Generate full code + const pythonCode = generator.generateTkinterCode(designData); + const encoder = new TextEncoder(); + await vscode.workspace.fs.writeFile( + fileUri, + encoder.encode(pythonCode) + ); + vscode.window.showInformationMessage( + `Created new file: ${fileName}` + ); + } else { + // Smart Update: Replace only create_widgets and inject new methods + const newCreateWidgets = + generator.generateCreateWidgetsBody(designData); + + // 1. Update create_widgets method body + // Improved regex to handle arguments, return types, and spacing + 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 { + // Fallback if create_widgets not found (weird), just append or warn + vscode.window.showWarningMessage( + 'Could not find create_widgets method to update. Regenerating full file might be needed if structure is broken.' + ); + // Fallback to full generation if structure is too broken? + // For now let's try to proceed or maybe just replace the whole file if critical? + // Let's stick to safe update. If not found, we don't touch. + } + + // 2. Inject new event handlers that are not in existingMethods + 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) { + // Insert before run() or at the end of class + 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 { + // Append to end of class (before if __name__) + 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')}`; + } + } + } + } + + // 3. Update window title/geometry in __init__ + // self.root.title("...") + const titleRegex = /self\.root\.title\s*\(\s*["'].*?["']\s*\)/; + newFileContent = newFileContent.replace( + titleRegex, + `self.root.title("${designData.form.title}")` + ); + + // self.root.geometry("...") + const geoRegex = /self\.root\.geometry\s*\(\s*["'].*?["']\s*\)/; + newFileContent = newFileContent.replace( + geoRegex, + `self.root.geometry("${designData.form.size.width}x${designData.form.size.height}")` + ); + + // 4. Update class name if changed + if ( + oldClassName && + designData.form.className && + oldClassName !== designData.form.className + ) { + // Replace class definition + // Be careful to match exact class name to avoid partial replacements + const classDefRegex = new RegExp( + `class\\s+${oldClassName}\\s*:` + ); + newFileContent = newFileContent.replace( + classDefRegex, + `class ${designData.form.className}:` + ); + + // Replace instantiation in if __name__ == "__main__": + // app = 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) { + // Use validateRange to ensure we cover the entire document safely + // regardless of potential external changes (though we should be careful) + 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('[ApplyChanges] Done'); + } catch (error) { + console.error('[ApplyChanges] Error:', error); + vscode.window.showErrorMessage(`Error applying changes: ${error}`); + } + } + private _getHtmlForWebview(webview: vscode.Webview): string { const styleUri = webview.asWebviewUri( vscode.Uri.joinPath( 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..d907c2d 100644 --- a/src/webview/react/components/Canvas.tsx +++ b/src/webview/react/components/Canvas.tsx @@ -2,6 +2,76 @@ 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 ( + + ); + case 'Entry': + return ( + + ); + case 'Text': + return ( + + ); + case 'Checkbutton': + return ( + + ); + case 'Radiobutton': + return ( + + ); + default: + return w.properties?.text || w.type; + } +} + export function Canvas() { const dispatch = useAppDispatch(); const { design, selectedWidgetId } = useAppState(); @@ -38,10 +108,13 @@ export function Canvas() { 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 } }, }); + // Auto-expand removed: user wants clipping behavior }; const onUp = () => { window.removeEventListener('mousemove', onMove); @@ -52,43 +125,244 @@ export function Canvas() { 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; + + console.log('[Canvas] Resize start', id, direction); + + const onMove = (ev: MouseEvent) => { + const dx = ev.clientX - startX; + const dy = ev.clientY - startY; + const patch: any = {}; + + 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); + console.log('[Canvas] Resize end', id); + }; + 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; + + console.log('[Canvas] Window resize start', direction); + + 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); + } + + // If user requested to optimize/remove binding, we can skip complex calculations here + // But currently we just set form size which is fast enough usually. + // The main lag comes from React re-rendering all widgets. + + 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); + console.log('[Canvas] Window resize end'); + }; + window.addEventListener('mousemove', onMove); + window.addEventListener('mouseup', onUp); + }; + 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') + } + /> + + )} +
+ ))} +
+ {/* Window Resize Handles */} +
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..40d5538 --- /dev/null +++ b/src/webview/react/components/ErrorBoundary.tsx @@ -0,0 +1,46 @@ +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) { + console.error('[ErrorBoundary] Uncaught error:', error, 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..43396b2 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', // Default body }, }); 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,42 @@ export function EventsPanel() { - setEventType(e.target.value)} - /> +
+ + {!commonEvents.includes(eventType) && ( + + setEventType(e.target.value) + } + /> + )} +
@@ -75,14 +129,6 @@ export function EventsPanel() { value={eventName} onChange={(e) => setEventName(e.target.value)} /> - -