working on code generation

This commit is contained in:
paul
2024-09-26 11:59:24 +05:30
parent 37e7bea0fa
commit e34751c20c
13 changed files with 252 additions and 108 deletions

View File

@@ -37,20 +37,23 @@ Build Python GUI's with the ease of Canva
## Features
* Framework agnostic - Can outputs code in multiple frameworks.
* Easy to use.
* Pre-built UI components
* Plugins to extend 3rd party UI libraries
* Generates Code.
## Roadmap
Here are some of the upcoming features.
* Treeview on the sidebar
* Support for Event Handlers
* Kivy Framework support
* Pyqt/PySide Support
* **Downloadable Electron app** and more.
To learn more/ see upcoming features visit [roadmap](./roadmap.md)
To learn more/ see upcoming features visit [roadmap](./roadmap.md)
To stay in loop, subscribe to the free [newsletter](https://paulfreeman.substack.com/subscribe?utm_source=Github-Pybuilder)
## License
## License - Fund the development
To support open-source and development of this tool and upcoming free open-source tools and libraries, consider buying a one-time license.
@@ -128,6 +131,13 @@ This is meant for business usecases, you can use the code even for commercial us
* All code generated by the builder tools are free to use for commercial and non-commercial purposes. If you are using this for a startup or your business you'll need to get a commercial license.
## Some of my other open-source
* [Awesome Landing pages](https://github.com/PaulleDemon/awesome-landing-pages)
* [Hover Preview](https://github.com/PaulleDemon/Hover-Preview)
* [Font Tester](https://github.com/PaulleDemon/font-tester-chrome)
* [Django SaaS Boilerplate](https://github.com/PaulleDemon/Django-SAAS-Boilerplate)
## Author
* Paul

View File

@@ -1,8 +1,8 @@
import { useRef, useState } from 'react'
import { LayoutFilled, ProductFilled, CloudUploadOutlined } from "@ant-design/icons"
import { DndContext, useSensors, useSensor, PointerSensor, closestCorners, DragOverlay, rectIntersection } from '@dnd-kit/core'
import { snapCenterToCursor } from '@dnd-kit/modifiers'
// import { DndContext, useSensors, useSensor, PointerSensor, closestCorners, DragOverlay, rectIntersection } from '@dnd-kit/core'
// import { snapCenterToCursor } from '@dnd-kit/modifiers'
import Canvas from './canvas/canvas'
import Header from './components/header'
@@ -10,13 +10,12 @@ import Sidebar from './sidebar/sidebar'
import UploadsContainer from './sidebar/uploadsContainer'
import WidgetsContainer from './sidebar/widgetsContainer'
import Widget from './canvas/widgets/base'
import { DraggableWidgetCard } from './components/cards'
import { DragProvider } from './components/draggable/draggableContext'
import { ActiveWidgetProvider } from './canvas/activeWidgetContext'
import TkinterWidgets from './frameworks/tkinter/sidebarWidgets'
import PluginsContainer from './sidebar/pluginsContainer'
import TkinterPluginWidgets from './frameworks/tkinter/sidebarPlugins'
import FrameWorks from './constants/frameworks'
import generateTkinterCode from './frameworks/tkinter/engine/code'
function App() {
@@ -25,22 +24,16 @@ function App() {
* @type {Canvas | null>}
*/
const canvasRef = useRef()
const widgetOverlayRef = useRef()
const [initialPosition, setInitialPosition] = useState({ x: 0, y: 0 })
const [projectName, setProjectName] = useState('untitled project')
const [UIFramework, setUIFramework] = useState(FrameWorks.TKINTER)
const [uploadedAssets, setUploadedAssets] = useState([]) // a global storage for assets, since redux can't store files(serialize files)
const [dropAnimation, setDropAnimation] = useState(null)
const [sidebarWidgets, setSidebarWidgets] = useState(TkinterWidgets || [])
const [canvasWidgets, setCanvasWidgets] = useState([]) // contains the reference to the widgets inside the canvas
const [activeSidebarWidget, setActiveSidebarWidget] = useState(null) // helps with the dnd overlay
const sensors = useSensors(
useSensor(PointerSensor)
)
const [canvasWidgets, setCanvasWidgets] = useState([]) // contains the reference to the widgets inside the canvas
const [canvasWidgetRefs, setCanvasWidgetRefs] = useState([])
const sidebarTabs = [
{
@@ -61,87 +54,112 @@ function App() {
}
]
const handleDragStart = (event) => {
console.log("Drag start: ", event)
const draggedItem = sidebarWidgets.find((item) => item.name === event.active.id)
setActiveSidebarWidget(draggedItem)
// const handleDragStart = (event) => {
// console.log("Drag start: ", event)
// const draggedItem = sidebarWidgets.find((item) => item.name === event.active.id)
// setActiveSidebarWidget(draggedItem)
const activeItemElement = widgetOverlayRef.current
// const activeItemElement = widgetOverlayRef.current
if (activeItemElement) {
const rect = activeItemElement.getBoundingClientRect()
// if (activeItemElement) {
// const rect = activeItemElement.getBoundingClientRect()
// Store the initial position of the dragged element
setInitialPosition({
x: rect.left,
y: rect.top,
})
}
}
// // Store the initial position of the dragged element
// setInitialPosition({
// x: rect.left,
// y: rect.top,
// })
// }
// }
const handleDragMove = (event) => {
// const handleDragMove = (event) => {
// console.log("drag move: ", event)
}
// // console.log("drag move: ", event)
// }
const handleDragEnd = (event) => {
// add items to canvas from sidebar
// const handleDragEnd = (event) => {
// // add items to canvas from sidebar
const {active, over, delta, activatorEvent} = event
// const {active, over, delta, activatorEvent} = event
const widgetItem = active.data.current?.title
const activeItemElement = widgetOverlayRef.current
// const widgetItem = active.data.current?.title
// const activeItemElement = widgetOverlayRef.current
console.log("ended: ", activatorEvent.clientX, activatorEvent.clientY)
// console.log("over: ", active, over, activeItemElement)
if (over?.id !== "canvas-droppable" || !widgetItem) {
setDropAnimation({ duration: 250, easing: "ease" })
return
}
setDropAnimation(null)
// console.log("ended: ", activatorEvent.clientX, activatorEvent.clientY)
// // console.log("over: ", active, over, activeItemElement)
// if (over?.id !== "canvas-droppable" || !widgetItem) {
// setDropAnimation({ duration: 250, easing: "ease" })
// return
// }
// setDropAnimation(null)
// Get widget dimensions (assuming you have a way to get these)
const widgetWidth = activeItemElement.offsetWidth; // Adjust this based on how you get widget size
const widgetHeight = activeItemElement.offsetHeight; // Adjust this based on how you get widget size
// // Get widget dimensions (assuming you have a way to get these)
// const widgetWidth = activeItemElement.offsetWidth; // Adjust this based on how you get widget size
// const widgetHeight = activeItemElement.offsetHeight; // Adjust this based on how you get widget size
const canvasContainerRect = canvasRef.current.getCanvasContainerBoundingRect()
const canvasTranslate = canvasRef.current.getCanvasTranslation()
const zoom = canvasRef.current.getZoom()
// const canvasContainerRect = canvasRef.current.getCanvasContainerBoundingRect()
// const canvasTranslate = canvasRef.current.getCanvasTranslation()
// const zoom = canvasRef.current.getZoom()
let finalPosition = {
x: (initialPosition.x + delta.x - canvasContainerRect.x - canvasTranslate.x) / zoom - (widgetWidth / 2),
y: (initialPosition.y + delta.y - canvasContainerRect.y - canvasTranslate.y) / zoom - (widgetHeight / 2),
}
// let finalPosition = {
// x: (initialPosition.x + delta.x - canvasContainerRect.x - canvasTranslate.x) / zoom - (widgetWidth / 2),
// y: (initialPosition.y + delta.y - canvasContainerRect.y - canvasTranslate.y) / zoom - (widgetHeight / 2),
// }
// find the center of the active widget then set the final position
// // find the center of the active widget then set the final position
// finalPosition = {
// finalPosition
// }
// // finalPosition = {
// // finalPosition
// // }
console.log("drop position: ", "delta: ", delta, "activator", finalPosition, canvasTranslate,)
// console.log("drop position: ", "delta: ", delta, "activator", finalPosition, canvasTranslate,)
canvasRef.current.addWidget(Widget, ({id, widgetRef}) => {
widgetRef.current.setPos(finalPosition.x, finalPosition.y)
// widgetRef.current.setPos(10, 10)
})
// canvasRef.current.addWidget(Widget, ({id, widgetRef}) => {
// widgetRef.current.setPos(finalPosition.x, finalPosition.y)
// // widgetRef.current.setPos(10, 10)
// })
setActiveSidebarWidget(null)
// setActiveSidebarWidget(null)
}
// }
const handleWidgetAddedToCanvas = (widgets) => {
console.log("canvas ref: ", canvasRef)
setCanvasWidgets(widgets)
}
const handleCodeGen = () => {
if (UIFramework === FrameWorks.TKINTER){
generateTkinterCode(projectName, canvasRef.current.getWidgets() || [], canvasRef.current.widgetRefs || [])
}
}
const handleFrameworkChange = (framework) => {
if (framework === UIFramework) return
canvasRef?.current?.clearCanvas()
setUIFramework(framework)
}
return (
<div className="tw-w-full tw-h-[100vh] tw-flex tw-flex-col tw-bg-primaryBg">
<Header className="tw-h-[6vh]"/>
<Header className="tw-h-[6vh]" onExportClick={handleCodeGen}
projectName={projectName} onProjectNameChange={setProjectName}
framework={UIFramework} onFrameworkChange={handleFrameworkChange}/>
{/* <Modal title={"FrameWork Change"} open={isModalOpen} onClose={handleCancel}
okText={"Yes"}
onOk={handleOk} okType={okButtonType} onCancel={handleCancel}>
<p>Are you sure you want to change the framework? This will clear the canvas.</p>
</Modal> */}
<DragProvider>
<div className="tw-w-full tw-h-[94vh] tw-flex">
<Sidebar tabs={sidebarTabs}/>

View File

@@ -823,7 +823,7 @@ class Canvas extends React.Component {
initialData: {
...dragData,
positionType: parentLayout === Layouts.PLACE ? PosType.ABSOLUTE : PosType.NONE,
parentLayout: parentLayout,
parentLayout: parentWidget.getLayout() || null, // pass everything about the parent layout
zIndex: 0,
pos: {x: finalPosition.x, y: finalPosition.y},
widgetContainer: WidgetContainer.WIDGET

View File

@@ -14,10 +14,11 @@ import WidgetContainer from "../constants/containers"
import { DragContext } from "../../components/draggable/draggableContext"
import { isNumeric, removeKeyFromObject } from "../../utils/common"
// FIXME: make it possible to have fit-width and height
// TODO: make it possible to apply widgetInnerStyle on load
// FIXME: the drag drop indicator is not going invisible if the drop happens on the child
const ATTRS_KEYS = ['value', 'label', 'tool', 'onChange', 'toolProps'] // these are attrs keywords, don't use these keywords as keys while defining the attrs property
@@ -162,10 +163,14 @@ class Widget extends React.Component {
this.getWidgetType = this.getWidgetType.bind(this)
this.getBoundingRect = this.getBoundingRect.bind(this)
this.getAttrValue = this.getAttrValue.bind(this)
this.getLayout = this.getLayout.bind(this)
this.getParentLayout = this.getParentLayout.bind(this)
this.getAttrValue = this.getAttrValue.bind(this)
this.getToolbarAttrs = this.getToolbarAttrs.bind(this)
this.generateCode = this.generateCode.bind(this)
// this.openRenaming = this.openRenaming.bind(this)
this.isSelected = this.isSelected.bind(this)
@@ -337,7 +342,7 @@ class Widget extends React.Component {
return this.constructor.requiredImports
}
getCode = () => {
generateCode(){
throw new NotImplementedError("Get Code must be implemented by the subclass")
}
@@ -534,12 +539,15 @@ class Widget extends React.Component {
/**
* inform the child about the parent layout changes
* @param {Layouts} layout
* @param {Layouts} parentLayout
*/
setParentLayout(layout){
setParentLayout(parentLayout){
const {layout, direction, gap} = parentLayout
let updates = {
parentLayout: layout,
parentLayout: parentLayout,
}
if (layout === Layouts.FLEX || layout === Layouts.GRID){
@@ -556,16 +564,14 @@ class Widget extends React.Component {
}
}
console.log("Parent layout updated: ", updates)
this.setState(updates)
}
getParentLayout = () => {
getParentLayout(){
return this.state.parentLayout
}
getLayout = () => {
getLayout(){
return this.state?.attrs?.layout?.value || Layouts.FLEX
}
@@ -726,17 +732,17 @@ class Widget extends React.Component {
let layoutUpdates = {
parentLayout: parentLayout
parentLayout: parentLayout.layout || null
}
if (parentLayout === Layouts.FLEX || parentLayout === Layouts.GRID){
if (parentLayout?.layout === Layouts.FLEX || parentLayout?.layout === Layouts.GRID){
layoutUpdates = {
...layoutUpdates,
positionType: PosType.NONE
}
}else if (parentLayout === Layouts.PLACE){
}else if (parentLayout?.layout === Layouts.PLACE){
layoutUpdates = {
...layoutUpdates,
positionType: PosType.ABSOLUTE
@@ -987,7 +993,7 @@ class Widget extends React.Component {
// if (!e.currentTarget.contains(draggedElement)) {
if (!isInBoundingBox) {
// FIXME: if the mouse pointer is over this widget's child, then droppable from here
// FIXME: if the mouse pointer is over this widget's child, then droppable style should be invisible
// only if the dragging element is not within the rect of this element remove the dragging rect
this.setState({
showDroppableStyle: {

View File

@@ -1,37 +1,42 @@
import { useState } from "react"
import { useEffect, useState } from "react"
import { Select, Input, Button } from "antd"
import { DownloadOutlined, DownOutlined } from "@ant-design/icons"
import FrameWorks from "../constants/frameworks"
const items = [
{
key: 'tkinter',
value: FrameWorks.TKINTER,
label: 'tkinter',
},
{
key: 'customtk',
value: FrameWorks.CUSTOMTK,
label: 'customtk',
},
]
function Header(props){
function Header({projectName, onProjectNameChange, framework, onFrameworkChange,
onExportClick, className=''}){
const [projectName, setProjectName] = useState("project")
return (
<div className={`tw-w-full tw-bg-primaryBg tw-p-2 tw-flex tw-place-items-center
${props.className||''}`}>
${className||''}`}>
<Select
defaultValue={"tkinter"}
// defaultValue={framework}
value={framework}
options={items}
// onSelect={(key) => {console.log("value: ", key); onFrameworkChange(key); }}
onChange={(key) => {onFrameworkChange(key)}}
className="tw-min-w-[150px]"
/>
<div className="tw-ml-auto tw-flex tw-gap-2 tw-place-content-center">
<Input value={projectName} onChange={(e) => setProjectName(e.target.value)} placeholder="project name"/>
<Button icon={<DownloadOutlined />} >
<Input value={projectName} onChange={(e) => onProjectNameChange(e.target.value)} placeholder="project name"/>
<Button icon={<DownloadOutlined />} onClick={onExportClick}>
Export code
</Button>
</div>

View File

@@ -20,7 +20,6 @@ export const ButtonModal = ({ message, title, okText="OK", onOk, onCancel, okBut
const handleOk = () => {
setIsModalOpen(false)
console.log("Ok pressed")
if (onOk){
onOk()
}
@@ -30,12 +29,12 @@ export const ButtonModal = ({ message, title, okText="OK", onOk, onCancel, okBut
e.stopPropagation()
setIsModalOpen(false)
console.log("cancel pressed")
if (onCancel){
onCancel()
}
}
return (
<div onClick={showModal}>
{children}

View File

@@ -0,0 +1,13 @@
const FrameWorks = {
TKINTER: 'tkinter',
CUSTOMTK: 'customTk',
KIVY: 'kivy',
PYSIDE: 'PySide',
}
export default FrameWorks

View File

@@ -0,0 +1,33 @@
import MainWindow from "../widgets/mainWindow"
import { message } from "antd"
async function generateTkinterCode(projectName, widgetList=[], widgetRefs=[]){
console.log("widgetList and refs", projectName, widgetList, widgetRefs)
let mainWindowCount = 0
for (let widget of widgetList){
if (widget.widgetType === MainWindow){
mainWindowCount += 1
}
if (mainWindowCount > 1){
message.error("Multiple instances of Main window found, delete one and try again.")
return
}
}
if (mainWindowCount === 0){
message.error("Aborting. No instances of Main window found. Add one and try again")
}
}
export default generateTkinterCode

View File

@@ -1,16 +1,46 @@
import { Layouts, PosType } from "../../../canvas/constants/layouts"
import Tools from "../../../canvas/constants/tools";
import Widget from "../../../canvas/widgets/base";
import Tools from "../../../canvas/constants/tools"
import Widget from "../../../canvas/widgets/base"
class TkinterBase extends Widget {
setParentLayout(layout){
static requiredImports = ['import tkinter as tk']
constructor(props) {
super(props)
this.getLayoutCode = this.getLayoutCode.bind(this)
}
getLayoutCode(){
const {layout: parentLayout, direction, gap} = this.getParentLayout()
let layoutManager = `pack()`
if (parentLayout === Layouts.FLEX){
layoutManager = `pack(${direction === "horizontal"? "tk.LEFT" : "tk.TOP"})`
}else if (parentLayout === Layouts.GRID){
const row = this.getAttrValue("gridManager.row")
const col = this.getAttrValue("gridManager.col")
layoutManager = `grid(row=${row}, col=${col})`
}else{
// FIXME: position may not be correct
layoutManager = `place(x=${this.state.pos.x}, y=${this.state.pos.y})`
}
return layoutManager
}
setParentLayout(parentLayout){
console.log("parent layout: ", parentLayout)
const {layout, direction, gap} = parentLayout
// show attributes related to the layout manager
let updates = {
parentLayout: layout,
parentLayout: parentLayout,
}
this.removeAttr("gridManager")
@@ -21,7 +51,7 @@ class TkinterBase extends Widget {
positionType: PosType.NONE
}
if (layout === Layouts.GRID) {
if (parentLayout === Layouts.GRID) {
// Set attributes related to grid layout manager
updates = {
...updates,
@@ -145,14 +175,14 @@ class TkinterBase extends Widget {
parentLayout: parentLayout
}
if (parentLayout === Layouts.FLEX || parentLayout === Layouts.GRID){
if (parentLayout.layout === Layouts.FLEX || parentLayout.layout === Layouts.GRID){
layoutUpdates = {
...layoutUpdates,
positionType: PosType.NONE
}
}else if (parentLayout === Layouts.PLACE){
}else if (parentLayout.layout === Layouts.PLACE){
layoutUpdates = {
...layoutUpdates,
positionType: PosType.ABSOLUTE

View File

@@ -10,7 +10,7 @@ class Frame extends TkinterBase{
super(props)
this.droppableTags = {
exclude: ["image", "video", "media", "toplevel"]
exclude: ["image", "video", "media", "toplevel", "main_window"]
}
this.state = {

View File

@@ -2,6 +2,7 @@ import Widget from "../../../canvas/widgets/base"
import Tools from "../../../canvas/constants/tools"
import { removeKeyFromObject } from "../../../utils/common"
import TkinterBase from "./base"
import { Layouts } from "../../../canvas/constants/layouts"
class Label extends TkinterBase{
@@ -45,6 +46,16 @@ class Label extends TkinterBase{
}
}
generateCode(parent){
const label = this.getAttrValue("labelWidget")
return (`
${this.getWidgetName()} = tk.Label(master=${parent}, text="${label}")
${this.getWidgetName()}.${this.getLayoutCode()}
`)
}
componentDidMount(){
super.componentDidMount()
this.setAttrValue("styling.backgroundColor", "#fff0")

View File

@@ -36,6 +36,13 @@ class MainWindow extends Widget{
this.setWidgetName("main")
}
generateCode(parent){
return (`
${this.getWidgetName()} = tk.Tk()
`)
}
getToolbarAttrs(){
const toolBarAttrs = super.getToolbarAttrs()

View File

@@ -23,7 +23,7 @@ function Premium({ children, className = "" }) {
<div onClick={onClick} className={`${className}`}>
{children}
<Modal
title={<h3 className="tw-text-xl tw-font-medium">Buy Pre-order one Time License</h3>}
title={<h3 className="tw-text-xl tw-font-medium">Fund development. Pre-order one Time License</h3>}
style={{ zIndex: 14000, gap: '10px', maxWidth: '80vw', placeItems: "center" }}
onCancel={onClose}
centered
@@ -33,8 +33,8 @@ function Premium({ children, className = "" }) {
open={premiumModalOpen}
>
<div className="tw-mt-5 tw-text-lg tw-max-w-[850px] tw-w-full ">
I am Paul, an open-source dev, funding open-source projects by providing custom works.
If you find this tool useful and want to support its development, consider buying a <b>one time license</b>.
I am Paul, a self-funded open-source dev.
If you find this tool useful and want to fund and support it's development, consider buying a <b>one time license</b>.
<br />
<br />
By buying pre-order license, you get advance features, priority support, early access, upcoming features, and more.
@@ -89,6 +89,10 @@ function Premium({ children, className = "" }) {
<i className="bi bi-x-circle-fill tw-text-red-600 tw-text-base"></i>
<span>Load plugins locally</span>
</li>
<li className="tw-flex tw-place-items-center tw-gap-2">
<i className="bi bi-x-circle-fill tw-text-red-600 tw-text-base"></i>
<span>Load local UI templates</span>
</li>
<li className="tw-flex tw-place-items-center tw-gap-2">
<i className="bi bi-x-circle-fill tw-text-red-600 tw-text-base"></i>
<span>Dark theme</span>
@@ -146,6 +150,10 @@ function Premium({ children, className = "" }) {
<i className="bi bi-check-circle-fill tw-text-green-600 tw-text-base"></i>
<span>Load plugins locally</span>
</li>
<li className="tw-flex tw-place-items-center tw-gap-2">
<i className="bi bi-x-circle-fill tw-text-green-600 tw-text-base"></i>
<span>Load local UI templates</span>
</li>
<li className="tw-flex tw-place-items-center tw-gap-2">
<i className="bi bi-check-circle-fill tw-text-green-600 tw-text-base"></i>
<span>Dark theme</span>
@@ -221,6 +229,10 @@ function Premium({ children, className = "" }) {
<i className="bi bi-check-circle-fill tw-text-green-600 tw-text-base"></i>
<span>Load plugins locally</span>
</li>
<li className="tw-flex tw-place-items-center tw-gap-2">
<i className="bi bi-x-circle-fill tw-text-green-600 tw-text-base"></i>
<span>Load local UI templates</span>
</li>
<li className="tw-flex tw-place-items-center tw-gap-2">
<i className="bi bi-check-circle-fill tw-text-green-600 tw-text-base"></i>
<span>Dark theme</span>