fixed code generation and added asset generation

This commit is contained in:
paul
2024-09-28 11:29:37 +05:30
parent 15bed19d57
commit 1c9938398a
11 changed files with 220 additions and 166 deletions

View File

@@ -6,4 +6,4 @@
## F\*CK Javascript, why the fu\*k, this.bind(this), class sucks in js, you can also use arrow functions, but you won't be able to override it in the subclass, because arrow functions, inherits from the surrounding context.
### F\*CK Javascript, why the fu\*k, this.bind(this), classes sucks in js, you can also use arrow functions, but you won't be able to override it in the subclass, because arrow functions, inherits from the surrounding context.

View File

@@ -33,7 +33,9 @@ function App() {
// const [uploadedAssets, setUploadedAssets] = useState([]) // a global storage for assets, since redux can't store files(serialize files)
const [sidebarWidgets, setSidebarWidgets] = useState(TkinterWidgets || [])
const {uploadedAssets} = useFileUploadContext()
// NOTE: the below reference is no longer required
const [canvasWidgets, setCanvasWidgets] = useState([]) // contains the reference to the widgets inside the canvas
@@ -140,7 +142,7 @@ function App() {
const handleCodeGen = () => {
if (UIFramework === FrameWorks.TKINTER){
generateTkinterCode(projectName, canvasRef.current.getWidgets() || [], canvasRef.current.widgetRefs || [])
generateTkinterCode(projectName, canvasRef.current.getWidgets() || [], canvasRef.current.widgetRefs || [], uploadedAssets)
}
}

View File

@@ -1,6 +1,6 @@
import React, { createContext, Component, useContext, useState, createRef, useMemo } from 'react'
// NOT IN USE
// NOTE: using this context provider causes many re-rendering when the canvas is panned the toolbar updates unnecessarily
// use draggable widgetcontext

View File

@@ -15,7 +15,7 @@ import { AudioOutlined, FileImageOutlined, FileTextOutlined, VideoCameraOutlined
// FIXME: Maximum recursion error
// FIXME: Every time the parent attrs are changed a remount happens, which causes input cursor to go to the end
/**
*
* @param {boolean} isOpen

View File

@@ -7,9 +7,7 @@ import Cursor from "../constants/cursor"
import { toSnakeCase } from "../utils/utils"
import EditableDiv from "../../components/editableDiv"
import { ActiveWidgetContext } from "../activeWidgetContext"
import { DragWidgetProvider } from "./draggableWidgetContext"
import WidgetDraggable from "./widgetDragDrop"
import WidgetContainer from "../constants/containers"
import { DragContext } from "../../components/draggable/draggableContext"
import { isNumeric, removeKeyFromObject } from "../../utils/common"
@@ -66,12 +64,6 @@ class Widget extends React.Component {
// This indicates if the draggable can be dropped on this widget, set this to null to disable drops
this.droppableTags = {}
// this.boundingRect = {
// x: 0,
// y: 0,
// height: 100,
// width: 100
// }
this.state = {
zIndex: 0,
@@ -192,6 +184,9 @@ class Widget extends React.Component {
this.load = this.load.bind(this)
this.serialize = this.serialize.bind(this)
this.serializeAttrsValues = this.serializeAttrsValues.bind(this)
this.hideDroppableIndicator = this.hideDroppableIndicator.bind(this)
}
componentDidMount() {
@@ -206,7 +201,6 @@ class Widget extends React.Component {
this.load(this.props.initialData || {}) // load the initial data
}
componentWillUnmount() {
@@ -425,6 +419,17 @@ class Widget extends React.Component {
return this.elementRef.current
}
hideDroppableIndicator(){
console.log("hide drop indicator")
this.setState({
showDroppableStyle: {
allow: false,
show: false
}
}, () => {
console.log("hidden the drop indicator")
})
}
/**
*
@@ -1073,135 +1078,137 @@ class Widget extends React.Component {
<DragContext.Consumer>
{
({ draggedElement, widgetClass, onDragStart, onDragEnd, overElement, setOverElement }) => (
({ draggedElement, widgetClass, onDragStart, onDragEnd, overElement, setOverElement }) => {
<div data-widget-id={this.__id}
ref={this.elementRef}
className="tw-shadow-xl tw-w-fit tw-h-fit"
style={outerStyle}
data-draggable-type={this.getWidgetType()} // helps with droppable
data-container={this.state.widgetContainer} // indicates how the canvas should handle dragging, one is sidebar other is canvas
return ( <div data-widget-id={this.__id}
ref={this.elementRef}
className="tw-shadow-xl tw-w-fit tw-h-fit"
style={outerStyle}
data-draggable-type={this.getWidgetType()} // helps with droppable
data-container={this.state.widgetContainer} // indicates how the canvas should handle dragging, one is sidebar other is canvas
data-drag-start-within // this attribute indicates that the drag is occurring from within the project and not a outside file drop
data-drag-start-within // this attribute indicates that the drag is occurring from within the project and not a outside file drop
draggable={this.state.dragEnabled}
draggable={this.state.dragEnabled}
onDragOver={(e) => this.handleDragOver(e, draggedElement)}
onDrop={(e) => this.handleDropEvent(e, draggedElement, widgetClass)}
onDragOver={(e) => this.handleDragOver(e, draggedElement)}
onDrop={(e) => {this.handleDropEvent(e, draggedElement, widgetClass); onDragEnd()}}
onDragEnter={(e) => this.handleDragEnter(e, draggedElement, setOverElement)}
onDragLeave={(e) => this.handleDragLeave(e, draggedElement, overElement)}
onDragEnter={(e) => this.handleDragEnter(e, draggedElement, setOverElement)}
onDragLeave={(e) => this.handleDragLeave(e, draggedElement, overElement)}
onDragStart={(e) => this.handleDragStart(e, onDragStart)}
onDragEnd={(e) => this.handleDragEnd(onDragEnd)}
>
{/* FIXME: Swappable when the parent layout is flex/grid and gap is more, this trick won't work, add bg color to check */}
{/* FIXME: Swappable, when the parent layout is gap is 0, it doesn't work well */}
<div className="tw-relative tw-w-full tw-h-full tw-top-0 tw-left-0"
onDragStart={(e) => this.handleDragStart(e, onDragStart)}
onDragEnd={(e) => this.handleDragEnd(onDragEnd)}
>
<div className={`tw-absolute tw-top-[-5px] tw-left-[-5px]
tw-border-1 tw-opacity-0 tw-border-solid tw-border-black
tw-w-full tw-h-full tw-bg-red-400
tw-scale-[1.1] tw-opacity-1 tw-z-[-1] `}
style={{
width: "calc(100% + 10px)",
height: "calc(100% + 10px)",
}}
ref={this.swappableAreaRef}
// swapable area
{/* FIXME: Swappable when the parent layout is flex/grid and gap is more, this trick won't work, add bg color to check */}
{/* FIXME: Swappable, when the parent layout is gap is 0, it doesn't work well */}
<div className="tw-relative tw-w-full tw-h-full tw-top-0 tw-left-0"
>
{/* helps with swappable: if the mouse is in this area while hovering/dropping, then swap */}
</div>
<div className="tw-relative tw-w-full tw-h-full" ref={this.innerAreaRef}>
{this.renderContent()}
</div>
{
// show drop style on drag hover
this.state.showDroppableStyle.show &&
<div className={`${this.state.showDroppableStyle.allow ? "tw-border-blue-600" : "tw-border-red-600"}
tw-absolute tw-top-[-5px] tw-left-[-5px] tw-w-full tw-h-full tw-z-[2]
tw-border-2 tw-border-dashed tw-rounded-lg tw-pointer-events-none
`}
<div className={`tw-absolute tw-top-[-5px] tw-left-[-5px]
tw-border-1 tw-opacity-0 tw-border-solid tw-border-black
tw-w-full tw-h-full tw-bg-red-400
tw-scale-[1.1] tw-opacity-1 tw-z-[-1] `}
style={{
width: "calc(100% + 10px)",
height: "calc(100% + 10px)",
}}
ref={this.swappableAreaRef}
// swapable area
>
{/* helps with swappable: if the mouse is in this area while hovering/dropping, then swap */}
</div>
}
{/* FIXME: the resize handles get clipped in parent container */}
<div className={`tw-absolute tw-z-[-1] tw-bg-transparent tw-top-[-10px] tw-left-[-10px] tw-opacity-100
tw-w-full tw-h-full
${this.state.selected ? 'tw-border-2 tw-border-solid tw-border-blue-500' : 'tw-hidden'}`}
style={{
width: "calc(100% + 20px)",
height: "calc(100% + 20px)",
zIndex: -1,
}}
>
<div className="tw-relative tw-w-full tw-h-full" ref={this.innerAreaRef}>
{this.renderContent()}
</div>
{
// show drop style on drag hover
draggedElement && this.state.showDroppableStyle.show &&
<div className={`${this.state.showDroppableStyle.allow ? "tw-border-blue-600" : "tw-border-red-600"}
tw-absolute tw-top-[-5px] tw-left-[-5px] tw-w-full tw-h-full tw-z-[2]
tw-border-2 tw-border-dashed tw-rounded-lg tw-pointer-events-none
<div className={`"tw-relative tw-w-full tw-h-full"`}> {/* ${this.state.isDragging ? "tw-pointer-events-none" : "tw-pointer-events-auto"} */}
<EditableDiv value={this.state.widgetName} onChange={this.setWidgetName}
maxLength={40}
openEdit={this.state.enableRename}
className="tw-text-sm tw-w-fit tw-max-w-[160px] tw-text-clip tw-min-w-[150px]
tw-overflow-hidden tw-absolute tw--top-6 tw-h-6"
/>
`}
style={{
width: "calc(100% + 10px)",
height: "calc(100% + 10px)",
}}
>
</div>
}
{/* FIXME: the resize handles get clipped in parent container */}
<div className={`tw-absolute tw-z-[-1] tw-bg-transparent tw-top-[-10px] tw-left-[-10px] tw-opacity-100
tw-w-full tw-h-full
${this.state.selected ? 'tw-border-2 tw-border-solid tw-border-blue-500' : 'tw-hidden'}`}
style={{
width: "calc(100% + 20px)",
height: "calc(100% + 20px)",
zIndex: -1,
}}
>
<div
className="tw-w-2 tw-h-2 tw-absolute tw--left-1 tw--top-1 tw-bg-blue-500"
style={{ cursor: Cursor.NW_RESIZE }}
onMouseDown={(e) => {
e.stopPropagation()
e.preventDefault()
this.props.onWidgetResizing("nw")
this.setState({ dragEnabled: false })
}}
onMouseUp={() => this.setState({ dragEnabled: true })}
/>
<div
className="tw-w-2 tw-h-2 tw-absolute tw--right-1 tw--top-1 tw-bg-blue-500"
style={{ cursor: Cursor.SW_RESIZE }}
onMouseDown={(e) => {
e.stopPropagation()
e.preventDefault()
this.props.onWidgetResizing("ne")
this.setState({ dragEnabled: false })
}}
onMouseUp={() => this.setState({ dragEnabled: true })}
/>
<div
className="tw-w-2 tw-h-2 tw-absolute tw--left-1 tw--bottom-1 tw-bg-blue-500"
style={{ cursor: Cursor.SW_RESIZE }}
onMouseDown={(e) => {
e.stopPropagation()
e.preventDefault()
this.props.onWidgetResizing("sw")
this.setState({ dragEnabled: false })
}}
onMouseUp={() => this.setState({ dragEnabled: true })}
/>
<div
className="tw-w-2 tw-h-2 tw-absolute tw--right-1 tw--bottom-1 tw-bg-blue-500"
style={{ cursor: Cursor.SE_RESIZE }}
onMouseDown={(e) => {
e.stopPropagation()
e.preventDefault()
this.props.onWidgetResizing("se")
this.setState({ dragEnabled: false })
}}
onMouseUp={() => this.setState({ dragEnabled: true })}
/>
<div className={`"tw-relative tw-w-full tw-h-full"`}> {/* ${this.state.isDragging ? "tw-pointer-events-none" : "tw-pointer-events-auto"} */}
<EditableDiv value={this.state.widgetName} onChange={this.setWidgetName}
maxLength={40}
openEdit={this.state.enableRename}
className="tw-text-sm tw-w-fit tw-max-w-[160px] tw-text-clip tw-min-w-[150px]
tw-overflow-hidden tw-absolute tw--top-6 tw-h-6"
/>
<div
className="tw-w-2 tw-h-2 tw-absolute tw--left-1 tw--top-1 tw-bg-blue-500"
style={{ cursor: Cursor.NW_RESIZE }}
onMouseDown={(e) => {
e.stopPropagation()
e.preventDefault()
this.props.onWidgetResizing("nw")
this.setState({ dragEnabled: false })
}}
onMouseUp={() => this.setState({ dragEnabled: true })}
/>
<div
className="tw-w-2 tw-h-2 tw-absolute tw--right-1 tw--top-1 tw-bg-blue-500"
style={{ cursor: Cursor.SW_RESIZE }}
onMouseDown={(e) => {
e.stopPropagation()
e.preventDefault()
this.props.onWidgetResizing("ne")
this.setState({ dragEnabled: false })
}}
onMouseUp={() => this.setState({ dragEnabled: true })}
/>
<div
className="tw-w-2 tw-h-2 tw-absolute tw--left-1 tw--bottom-1 tw-bg-blue-500"
style={{ cursor: Cursor.SW_RESIZE }}
onMouseDown={(e) => {
e.stopPropagation()
e.preventDefault()
this.props.onWidgetResizing("sw")
this.setState({ dragEnabled: false })
}}
onMouseUp={() => this.setState({ dragEnabled: true })}
/>
<div
className="tw-w-2 tw-h-2 tw-absolute tw--right-1 tw--bottom-1 tw-bg-blue-500"
style={{ cursor: Cursor.SE_RESIZE }}
onMouseDown={(e) => {
e.stopPropagation()
e.preventDefault()
this.props.onWidgetResizing("se")
this.setState({ dragEnabled: false })
}}
onMouseUp={() => this.setState({ dragEnabled: true })}
/>
</div>
</div>
</div>
</div>
</div>
)
)
}
}
</DragContext.Consumer>

View File

@@ -109,7 +109,7 @@ export function DraggableAssetCard({file, onDelete}){
return (
<div draggable="false" className="tw-w-full tw-h-[240px] tw-p-1 tw-flex tw-flex-col tw-rounded-md tw-overflow-hidden
<div draggable="false" className="tw-w-full tw-h-[220px] tw-flex-shrink-0 tw-p-1 tw-flex tw-flex-col tw-rounded-md tw-overflow-hidden
tw-gap-2 tw-text-gray-600 tw-bg-[#ffffff44] tw-border-solid tw-border-[1px]
tw-border-blue-500 tw-shadow-md ">
<div className="tw-h-[200px] tw-pointer-events-none tw-w-full tw-flex tw-place-content-center tw-p-1 tw-text-3xl tw-overflow-hidden">

View File

@@ -1,4 +1,4 @@
import { memo, useState } from "react"
import { memo, useEffect, useState } from "react"
import { useDragContext } from "./draggableContext"
@@ -15,11 +15,19 @@ const DroppableWrapper = memo(({onDrop, droppableTags={}, ...props}) => {
allow: false
})
useEffect(() => {
if (draggedElement === null){
setShowDroppable({
show: false,
allow: false
})
}
}, [draggedElement])
const handleDragEnter = (e) => {
console.log("Drag enter")
if (!draggedElement || !draggedElement.getAttribute("data-drag-start-within")){
// if the drag is starting from outside (eg: file drop) or if drag doesn't exist
return
@@ -71,6 +79,11 @@ const DroppableWrapper = memo(({onDrop, droppableTags={}, ...props}) => {
const handleDropEvent = (e) => {
setShowDroppable({
allow: false,
show: false
})
if (!draggedElement || !draggedElement.getAttribute("data-drag-start-within")){
// if the drag is starting from outside (eg: file drop) or if drag doesn't exist
return
@@ -78,10 +91,7 @@ const DroppableWrapper = memo(({onDrop, droppableTags={}, ...props}) => {
e.stopPropagation()
setShowDroppable({
allow: false,
show: false
})
const dragElementType = draggedElement.getAttribute("data-draggable-type")

View File

@@ -7,39 +7,46 @@ import TopLevel from "../widgets/toplevel"
// FIXME: if the toplevel comes first, before the MainWindow in widgetlist the root may become null
// Recursive function to generate the code list, imports, requirements, and track mainVariable
function generateTkinterCodeList(widgetList = [], widgetRefs = [], parentVariable = null, mainVariable = "") {
let variableNames = [] // {widgetId: "", name: "", count: 1}
function generateTkinterCodeList(widgetList = [], widgetRefs = [], parentVariable = null, mainVariable = "", usedVariableNames = new Set()) {
let variableMapping = new Map() // Map widget to variable { widgetId: variableName }
let imports = new Set([])
let requirements = new Set([])
let code = []
for (let widget of widgetList) {
// console.log("Key: ", widget)
const widgetRef = widgetRefs[widget.id].current
let varName = widgetRef.getVariableName()
imports.add(widgetRef.getImports())
requirements.add(widgetRef.getRequirements())
// Add imports and requirements to sets
widgetRef.getImports().forEach(importItem => imports.add(importItem));
widgetRef.getRequirements().forEach(requirementItem => requirements.add(requirementItem));
// Set main variable if the widget is MainWindow
if (widget.widgetType === MainWindow) {
mainVariable = varName
}
if (!variableNames.find((val) => val.variable === varName)) {
variableNames.push({ widgetId: widgetRef.id, name: varName, count: 1 })
} else {
// Avoid duplicate names
const existingVariable = variableNames.find((val) => val.variable === varName)
const existingCount = existingVariable.count
existingVariable.count += 1
varName = `${existingVariable.name}${existingCount}`
variableNames.push({ widgetId: widgetRef.id, name: varName, count: 1 })
// Ensure unique variable names across recursion
let originalVarName = varName
let count = 1;
// Check for uniqueness and update varName
while (usedVariableNames.has(varName)) {
varName = `${originalVarName}${count}`
count++
}
const currentParentVariable = variableNames.find(val => val.widgetId === widget.id)?.name || parentVariable
usedVariableNames.add(varName)
variableMapping.set(widget.id, varName) // Store the variable name by widget ID
// Determine the current parent variable from variableNames or fallback to parentVariable
let currentParentVariable = parentVariable || (variableMapping.get(widget.id) || null)
if (widget.widgetType === TopLevel){
// for top level set it to the main variable
// TODO: the toplevels parent should be determined other ways, suppose the top level has another toplevel
currentParentVariable = mainVariable
}
let widgetCode = widgetRef.generateCode(varName, currentParentVariable)
@@ -53,32 +60,32 @@ function generateTkinterCodeList(widgetList = [], widgetRefs = [], parentVariabl
code.push(...widgetCode)
code.push("\n\n")
// Recursively handle child widgets, which are full widget objects
// Recursively handle child widgets
if (widget.children && widget.children.length > 0) {
const childResult = generateTkinterCodeList(widget.children, widgetRefs, varName, mainVariable)
// Pass down the unique names for children to prevent duplication
const childResult = generateTkinterCodeList(widget.children, widgetRefs, varName, mainVariable, usedVariableNames)
// Merge child imports, requirements, and code
imports = new Set([...imports, ...childResult.imports])
requirements = new Set([...requirements, ...childResult.requirements])
code.push(...childResult.code)
// Track the main variable returned by the child
mainVariable = childResult.mainVariable || mainVariable
mainVariable = childResult.mainVariable || mainVariable // the main variable is the main window variable
}
}
return {
imports: Array.from(imports), // Convert Set to Array
imports: Array.from(imports),
code: code,
requirements: Array.from(requirements), // Convert Set to Array
requirements: Array.from(requirements),
mainVariable
}
}
async function generateTkinterCode(projectName, widgetList=[], widgetRefs=[]){
async function generateTkinterCode(projectName, widgetList=[], widgetRefs=[], assetFiles){
console.log("widgetList and refs", projectName, widgetList, widgetRefs)
console.log("widgetList and refs", projectName, widgetList, widgetRefs, assetFiles)
let mainWindowCount = 0
@@ -102,38 +109,37 @@ async function generateTkinterCode(projectName, widgetList=[], widgetRefs=[]){
return
}
// let code = [`# This code is generated by PyUIbuilder: https://github.com/paulledemon \n`]
// code.push("\n", "import tkinter as tk", "\n\n")
// widget - {id, widgetType: widgetComponentType, children: [], parent: "", initialData: {}}
const generatedObject = generateTkinterCodeList(filteredWidgetList, widgetRefs, "", "")
const {code: codeLines, imports, requirements, mainVariable} = generatedObject
// TODO: avoid adding \n inside the list instead rewrite using code.join("\n")
const code = [
"# This code is generated by PyUIbuilder: https://github.com/paulledemon",
"\n\n",
...imports,
...imports.flatMap((item, index) => index < imports.length - 1 ? [item, "\n"] : [item]), //add \n to every line
"\n\n",
...codeLines,
"\n",
`${mainVariable}.mainloop()`,
]
// TODO: create requirements.txt file
console.log("Code: ", code.join(""))
console.log("Code: ", code.join(""), "\n", requirements.join("\n"))
message.info("starting zipping files, download will start in a few seconds")
const createFileList = [
{
fileData: code.join(""),
fileData: code.join("\n"),
fileName: "main.py",
folder: ""
}
]
// TODO: Zip asset files
if (requirements.length > 0){
createFileList.push({
fileData: requirements.join("\n"),
@@ -142,6 +148,36 @@ async function generateTkinterCode(projectName, widgetList=[], widgetRefs=[]){
})
}
for (let asset of assetFiles){
if (asset.fileType === "image"){
createFileList.push({
fileData: asset.originFileObj,
fileName: asset.name,
folder: "assets/images"
})
}else if (asset.fileType === "video"){
createFileList.push({
fileData: asset.originFileObj,
fileName: asset.name,
folder: "assets/videos"
})
}else if (asset.fileType === "audio"){
createFileList.push({
fileData: asset.originFileObj,
fileName: asset.name,
folder: "assets/audio"
})
}else{
createFileList.push({
fileData: asset.originFileObj,
fileName: asset.name,
folder: "assets/media"
})
}
}
// createFilesAndDownload(createFileList, projectName).then(() => {
// message.success("Download complete")
// }).catch(() => {

View File

@@ -255,8 +255,6 @@ export class TkinterWidgetBase extends TkinterBase{
const newAttrs = removeKeyFromObject("layout", this.state.attrs)
console.log("new attrs: ", newAttrs)
this.state = {
...this.state,
attrs: {

View File

@@ -17,6 +17,7 @@ class MainWindow extends TkinterBase{
this.state = {
...this.state,
size: { width: 700, height: 400 },
widgetName: "main",
attrs: {
...this.state.attrs,
widgetName: "main",

View File

@@ -92,7 +92,7 @@ function UploadsContainer() {
<SearchComponent onSearch={onSearch} searchValue={searchValue}
onClear={() => setSearchValue("")} />
<div className="tw-flex tw-relative tw-flex-col tw-gap-2 tw-h-full tw-p-1 tw-pb-4">
<div className="tw-flex tw-relative tw-flex-col tw-place-items-center tw-gap-2 tw-h-full tw-p-1 tw-pb-4">
<Dragger className={`${dragEnter && "!tw-h-[80vh] !tw-opacity-100 !tw-bg-[#fafafa] tw-absolute tw-top-0 tw-z-10"} tw-w-full !tw-min-w-[250px]`}
{...props}
ref={fileUploadRef}