diff --git a/README.md b/README.md index 63539ee..3f28b91 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/src/App.js b/src/App.js index 7338698..53395cc 100644 --- a/src/App.js +++ b/src/App.js @@ -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 (
-
+
+ {/* +

Are you sure you want to change the framework? This will clear the canvas.

+
*/} +
diff --git a/src/canvas/canvas.js b/src/canvas/canvas.js index c69fe43..9f14355 100644 --- a/src/canvas/canvas.js +++ b/src/canvas/canvas.js @@ -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 diff --git a/src/canvas/widgets/base.js b/src/canvas/widgets/base.js index f374c25..d274495 100644 --- a/src/canvas/widgets/base.js +++ b/src/canvas/widgets/base.js @@ -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: { diff --git a/src/components/header.js b/src/components/header.js index c48dca4..7529a5d 100644 --- a/src/components/header.js +++ b/src/components/header.js @@ -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 (
+ ${className||''}`}> setProjectName(e.target.value)} placeholder="project name"/> -
diff --git a/src/components/modals.js b/src/components/modals.js index 505f369..a0e5ff0 100644 --- a/src/components/modals.js +++ b/src/components/modals.js @@ -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 (
{children} diff --git a/src/constants/frameworks.js b/src/constants/frameworks.js new file mode 100644 index 0000000..ea365d4 --- /dev/null +++ b/src/constants/frameworks.js @@ -0,0 +1,13 @@ + + +const FrameWorks = { + + TKINTER: 'tkinter', + CUSTOMTK: 'customTk', + KIVY: 'kivy', + PYSIDE: 'PySide', + +} + + +export default FrameWorks \ No newline at end of file diff --git a/src/frameworks/tkinter/engine/code.js b/src/frameworks/tkinter/engine/code.js new file mode 100644 index 0000000..ee6c10a --- /dev/null +++ b/src/frameworks/tkinter/engine/code.js @@ -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 \ No newline at end of file diff --git a/src/frameworks/tkinter/widgets/base.js b/src/frameworks/tkinter/widgets/base.js index bb07317..fbc526f 100644 --- a/src/frameworks/tkinter/widgets/base.js +++ b/src/frameworks/tkinter/widgets/base.js @@ -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 diff --git a/src/frameworks/tkinter/widgets/frame.js b/src/frameworks/tkinter/widgets/frame.js index d058ea7..ac13418 100644 --- a/src/frameworks/tkinter/widgets/frame.js +++ b/src/frameworks/tkinter/widgets/frame.js @@ -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 = { diff --git a/src/frameworks/tkinter/widgets/label.js b/src/frameworks/tkinter/widgets/label.js index eca85e6..da66a54 100644 --- a/src/frameworks/tkinter/widgets/label.js +++ b/src/frameworks/tkinter/widgets/label.js @@ -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") diff --git a/src/frameworks/tkinter/widgets/mainWindow.js b/src/frameworks/tkinter/widgets/mainWindow.js index 0dd9762..815c365 100644 --- a/src/frameworks/tkinter/widgets/mainWindow.js +++ b/src/frameworks/tkinter/widgets/mainWindow.js @@ -36,6 +36,13 @@ class MainWindow extends Widget{ this.setWidgetName("main") } + generateCode(parent){ + + return (` + ${this.getWidgetName()} = tk.Tk() + `) + } + getToolbarAttrs(){ const toolBarAttrs = super.getToolbarAttrs() diff --git a/src/sidebar/utils/premium.js b/src/sidebar/utils/premium.js index a5239bf..d2daebd 100644 --- a/src/sidebar/utils/premium.js +++ b/src/sidebar/utils/premium.js @@ -23,7 +23,7 @@ function Premium({ children, className = "" }) {
{children} Buy Pre-order one Time License} + title={

Fund development. Pre-order one Time License

} style={{ zIndex: 14000, gap: '10px', maxWidth: '80vw', placeItems: "center" }} onCancel={onClose} centered @@ -33,8 +33,8 @@ function Premium({ children, className = "" }) { open={premiumModalOpen} >
- 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 one time license. + 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 one time license.

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 = "" }) { Load plugins locally +
  • + + Load local UI templates +
  • Dark theme @@ -146,6 +150,10 @@ function Premium({ children, className = "" }) { Load plugins locally
  • +
  • + + Load local UI templates +
  • Dark theme @@ -221,6 +229,10 @@ function Premium({ children, className = "" }) { Load plugins locally
  • +
  • + + Load local UI templates +
  • Dark theme