From 1533888f07655ba7b41ea63781bd745bc33efb14 Mon Sep 17 00:00:00 2001 From: paul Date: Mon, 23 Sep 2024 18:25:40 +0530 Subject: [PATCH] fix: widget initialData load. feat: added more widgets --- README.md | 18 ++- src/canvas/canvas.js | 15 +-- src/canvas/constants/tools.js | 2 + src/canvas/toolbar.js | 15 ++- src/canvas/widgets/base.js | 61 ++++++--- src/components/inputs.js | 124 ++++++++++++++++++ src/frameworks/tkinter/sidebarWidgets.js | 16 ++- .../tkinter/widgets/ checkButton.js | 110 +++++++++++++++- src/frameworks/tkinter/widgets/button.js | 2 +- src/frameworks/tkinter/widgets/input.js | 2 +- src/frameworks/tkinter/widgets/slider.js | 100 ++++++++++++++ 11 files changed, 421 insertions(+), 44 deletions(-) create mode 100644 src/components/inputs.js create mode 100644 src/frameworks/tkinter/widgets/slider.js diff --git a/README.md b/README.md index 50e0539..599c7a0 100644 --- a/README.md +++ b/README.md @@ -3,16 +3,16 @@

- + - + - + - +

@@ -61,7 +61,7 @@ The discount's will be available for limited time only on pre-orders. | Type | Free | Premium - Hobbyist / Per user | Premium - Commercial / Per user | |-------------------------------------------------------------------|-------------------|----------------------------------------------------------|------------------------------------------------------------| | **Support open-source development** | 👍️ | 😎 | 🚀 | -| **Priority support** - (priorities your feature requests, issues) | community support | ✅ | ✅ | +| **Priority support** - (prioritize your feature requests, issues) | community support | ✅ | ✅ | | **Lifetime license** (one-time purchase) | 👍️ | ✅ | ✅ | | **Early access** to upcoming features | ❌ | ✅ | ✅ | | **Downloadable Electron App** (upcoming) | ❌ | ✅ | ✅ | @@ -72,7 +72,7 @@ The discount's will be available for limited time only on pre-orders. | **Commercial Use** | ✅ | ❌ | ✅ | | **Support for PyQt/PySide frameworks** (upcoming) | ❌ | ❌ | ✅ | | **More upcoming features and support** | ❓️ | ✅ | ✅ | -| **Price** | - | ~~$129~~ $29 (save 77.52% for limited time on pre-order) | ~~180~~ $49 (Save 72.78% for a limited time on pre-orders) | +| **Price** | - | ~~$129~~ **$29** (save 77.52% for limited time on pre-order) | ~~180~~ **$49** (Save 72.78% for a limited time on pre-orders) | | Pre-order now! | | [Get license]() | [Get license]() | ## Newsletter @@ -105,6 +105,12 @@ To keep up with the latest developments considering starting ⭐️ this repo * Support for 3rd party UI libraries. Many GUI builders don't come with support for 3rd party libraries. +4. **Why doesn't the theme of the GUI builder match the theme of Tkinter?** + + **A.** Tkinter is a OS-dependent library, so it would render differently on different OS. Having a common UI the the GUI builder makes it simpler for development. + + If you want a live preview before generating the code you can get a premium license and you'll be notified when that feature releases. + ## License Information To support development of this project, license differ depending on the usecase. diff --git a/src/canvas/canvas.js b/src/canvas/canvas.js index 5cf3bc8..261db6f 100644 --- a/src/canvas/canvas.js +++ b/src/canvas/canvas.js @@ -1,11 +1,11 @@ import React from "react" -import { DndContext } from '@dnd-kit/core' +// import { DndContext } from '@dnd-kit/core' -import { CloseOutlined, DeleteOutlined, EditOutlined, FullscreenOutlined, ReloadOutlined } from "@ant-design/icons" +import { DeleteOutlined, EditOutlined, ReloadOutlined } from "@ant-design/icons" import { Button, Tooltip, Dropdown } from "antd" -import Droppable from "../components/utils/droppableDnd" +// import Droppable from "../components/utils/droppableDnd" import Widget from "./widgets/base" import Cursor from "./constants/cursor" @@ -14,15 +14,12 @@ import CanvasToolBar from "./toolbar" import { UID } from "../utils/uid" import { removeDuplicateObjects } from "../utils/common" -import { WidgetContext } from './context/widgetContext' -// import {ReactComponent as DotsBackground} from "../assets/background/dots.svg" // import DotsBackground from "../assets/background/dots.svg" import { ReactComponent as DotsBackground } from "../assets/background/dots.svg" import DroppableWrapper from "../components/draggable/droppable" -import { ActiveWidgetContext, ActiveWidgetProvider, withActiveWidget } from "./activeWidgetContext" -import { DragWidgetProvider } from "./widgets/draggableWidgetContext" + import { PosType } from "./constants/layouts" import WidgetContainer from "./constants/containers" import { isSubClassOfWidget } from "../utils/widget" @@ -629,7 +626,6 @@ class Canvas extends React.Component { */ handleAddWidgetChild = ({ parentWidgetId, dragElementID, swap = false }) => { - // TODO: creation of the child widget if its not created // widgets data structure { id, widgetType: widgetComponentType, children: [], parent: "" } const dropWidgetObj = this.findWidgetFromListById(parentWidgetId) // Find the dragged widget object @@ -846,7 +842,6 @@ class Canvas extends React.Component { throw new Error("WidgetClass has to be passed for widgets dropped from sidebar") } - // TODO: handle drop from sidebar // if the widget is being dropped from the sidebar, use the info to create the widget first this.createWidget(widgetClass, ({ id, widgetRef }) => { widgetRef.current.setPos(finalPosition.x, finalPosition.y) @@ -878,8 +873,6 @@ class Canvas extends React.Component { // remove child from current position - // console.log("pre updated widgets: ", updatedWidgets) - const updatedChildWidget = { ...childWidgetObj, parent: "", diff --git a/src/canvas/constants/tools.js b/src/canvas/constants/tools.js index 632211c..c7e4af9 100644 --- a/src/canvas/constants/tools.js +++ b/src/canvas/constants/tools.js @@ -6,6 +6,8 @@ const Tools = { COLOR_PICKER: "color_picker", EVENT_HANDLER: "event_handler", // shows a event handler with all the possible function in the dropdown CHECK_BUTTON: "check_button", + INPUT_LIST: "input_list", + INPUT_RADIO_LIST: "input_radio_list", LAYOUT_MANAGER: "layout_manager" } diff --git a/src/canvas/toolbar.js b/src/canvas/toolbar.js index 51b353b..64d8eb4 100644 --- a/src/canvas/toolbar.js +++ b/src/canvas/toolbar.js @@ -6,6 +6,7 @@ import { capitalize } from "../utils/common" import Tools from "./constants/tools.js" import { useActiveWidget } from "./activeWidgetContext.js" import { Layouts } from "./constants/layouts.js" +import { DynamicRadioInputList } from "../components/inputs.js" // FIXME: Maximum recursion error @@ -188,12 +189,20 @@ const CanvasToolBar = memo(({ isOpen, widgetType, attrs = {} }) => { {val.tool === Tools.CHECK_BUTTON && ( handleChange(e.target.checked, val.onChange)} >{val.label} )} + {val.tool === Tools.INPUT_RADIO_LIST && ( + handleChange({inputs, selectedRadio}, val.onChange)} + /> + )} + { val.tool === Tools.LAYOUT_MANAGER && ( renderLayoutManager(val) @@ -207,7 +216,7 @@ const CanvasToolBar = memo(({ isOpen, widgetType, attrs = {} }) => { // Handle nested objects and horizontal display for inner elements if (typeof val === "object") { const containerClass = val.display === "horizontal" - ? "tw-flex tw-flex-row tw-gap-4" + ? "tw-flex tw-flex-row tw-flex-wrap tw-content-start tw-gap-4" : "tw-flex tw-flex-col tw-gap-2" return ( @@ -227,7 +236,7 @@ const CanvasToolBar = memo(({ isOpen, widgetType, attrs = {} }) => {

diff --git a/src/canvas/widgets/base.js b/src/canvas/widgets/base.js index 5349d9d..25408d3 100644 --- a/src/canvas/widgets/base.js +++ b/src/canvas/widgets/base.js @@ -569,18 +569,39 @@ class Widget extends React.Component { if (Object.keys(data).length === 0) return // no data to load - for (let [key, value] of Object.entries(data.attrs | {})) - this.setAttrValue(key, value) + data = {...data} // create a shallow copy - delete data.attrs + const {attrs, ...restData} = data - /** - * const obj = { a: 1, b: 2, c: 3 } - * const { b, ...newObj } = obj - * console.log(newObj) // { a: 1, c: 3 } - */ + // for (let [key, value] of Object.entries(attrs | {})) + // this.setAttrValue(key, value) - this.setState(data) + // delete data.attrs + + this.setState(restData, () => { + // UPdates attrs + let newAttrs = { ...this.state.attrs } + + // Iterate over each path in the updates object + Object.entries(attrs).forEach(([path, value]) => { + const keys = path.split('.') + const lastKey = keys.pop() + + // Traverse the nested object within attrs + let nestedObject = newAttrs + + keys.forEach(key => { + nestedObject[key] = { ...nestedObject[key] } // Ensure immutability for each nested level + nestedObject = nestedObject[key] + }) + + // Set the value at the last key + nestedObject[lastKey].value = value + }) + + this.updateState({ attrs: newAttrs }) + + }) } @@ -698,17 +719,6 @@ class Widget extends React.Component { }) - const dragEleType = draggedElement.getAttribute("data-draggable-type") - - const allowDrop = (this.droppableTags && this.droppableTags !== null && (Object.keys(this.droppableTags).length === 0 || - (this.droppableTags.include?.length > 0 && this.droppableTags.include?.includes(dragEleType)) || - (this.droppableTags.exclude?.length > 0 && !this.droppableTags.exclude?.includes(dragEleType)) - )) - - if (!allowDrop) { - return // prevent drop if the draggable element doesn't match - } - if (draggedElement === this.elementRef.current){ // prevent drop on itself, since the widget is invisible when dragging, if dropped on itself, it may consume itself return @@ -731,6 +741,17 @@ class Widget extends React.Component { // If swaparea is true, then it swaps instead of adding it as a child, also make sure that the parent widget(this widget) is on the widget and not on the canvas const swapArea = (this.swappableAreaRef.current.contains(e.target) && !this.innerAreaRef.current.contains(e.target) && thisContainer === WidgetContainer.WIDGET) + const dragEleType = draggedElement.getAttribute("data-draggable-type") + + const allowDrop = (this.droppableTags && this.droppableTags !== null && (Object.keys(this.droppableTags).length === 0 || + (this.droppableTags.include?.length > 0 && this.droppableTags.include?.includes(dragEleType)) || + (this.droppableTags.exclude?.length > 0 && !this.droppableTags.exclude?.includes(dragEleType)) + )) + + if (!allowDrop && !swapArea) { + // only if both swap and drop is not allowed return, if swap is allowed continue + return + } // TODO: check if the drop is allowed if ([WidgetContainer.CANVAS, WidgetContainer.WIDGET].includes(container)) { // console.log("Dropped on meee: ", swapArea, this.swappableAreaRef.current.contains(e.target), thisContainer) diff --git a/src/components/inputs.js b/src/components/inputs.js new file mode 100644 index 0000000..5b67347 --- /dev/null +++ b/src/components/inputs.js @@ -0,0 +1,124 @@ +import React, { useEffect, useState } from "react" +import { Input, Button, Space, Radio } from "antd" +import { MinusCircleOutlined, PlusOutlined } from "@ant-design/icons" + + +export const DynamicInputList = () => { + const [inputs, setInputs] = useState([""]) // Initialize with one input + + const addInput = () => { + setInputs([...inputs, ""]) + } + + const removeInput = (index) => { + setInputs(inputs.filter((_, i) => i !== index)) + } + + const handleInputChange = (value, index) => { + const newInputs = [...inputs] + newInputs[index] = value + setInputs(newInputs) + } + + return ( +
+ {inputs.map((input, index) => ( + + handleInputChange(e.target.value, index)} + placeholder={`Input ${index + 1}`} + /> + {index !== 0 && ( // Do not show delete button for the first input + removeInput(index)} /> + )} + + ))} + + +
+ ) +} + + +export const DynamicRadioInputList = React.memo(({defaultInputs=[""], defaultSelected=null, onChange}) => { + const [inputs, setInputs] = useState([""]) // Initialize with one input + const [selectedRadio, setSelectedRadio] = useState(null) // Tracks selected radio button + + useEffect(() => { + + setInputs(defaultInputs) + + }, [defaultInputs]) + + useEffect(() => { + + setSelectedRadio(defaultSelected) + }, [defaultSelected]) + + useEffect(() => { + + if(onChange){ + onChange({inputs, selectedRadio}) + } + + }, [selectedRadio, inputs]) + + // Add a new input + const addInput = () => { + setInputs([...inputs, ""]) + } + + // Remove an input by index, but keep the first one + const removeInput = (index) => { + const newInputs = inputs.filter((_, i) => i !== index) + setInputs(newInputs) + + // Adjust selected radio if necessary + if (selectedRadio >= newInputs.length) { + setSelectedRadio(newInputs.length - 1) + } + } + + // Update input value + const handleInputChange = (value, index) => { + const newInputs = [...inputs] + newInputs[index] = value + setInputs(newInputs) + } + + // Handle radio button selection + const handleRadioChange = (e) => { + setSelectedRadio(e.target.value) + } + + return ( +
+ + {inputs.map((input, index) => ( + + + handleInputChange(e.target.value, index)} + placeholder={`Input ${index + 1}`} + /> + + {index !== 0 && ( // Do not show delete button for the first input +
+ removeInput(index)} /> +
+ )} +
+ ))} +
+ + +
+ ) +}) \ No newline at end of file diff --git a/src/frameworks/tkinter/sidebarWidgets.js b/src/frameworks/tkinter/sidebarWidgets.js index e452b18..faa5582 100644 --- a/src/frameworks/tkinter/sidebarWidgets.js +++ b/src/frameworks/tkinter/sidebarWidgets.js @@ -2,12 +2,13 @@ import Widget from "../../canvas/widgets/base" import ButtonWidget from "./assets/widgets/button.png" -import { CheckBox } from "./widgets/ checkButton" +import { CheckBox, RadioButton } from "./widgets/ checkButton" import Button from "./widgets/button" import Frame from "./widgets/frame" import { Input, Text } from "./widgets/input" import Label from "./widgets/label" import MainWindow from "./widgets/mainWindow" +import Slider from "./widgets/slider" import TopLevel from "./widgets/toplevel" @@ -60,6 +61,19 @@ const TkinterSidebar = [ link: "https://github.com", widgetClass: CheckBox }, + { + name: "Radio button", + img: ButtonWidget, + link: "https://github.com", + widgetClass: RadioButton + }, + { + name: "Scale", + img: ButtonWidget, + link: "https://github.com", + widgetClass: Slider + }, + ] diff --git a/src/frameworks/tkinter/widgets/ checkButton.js b/src/frameworks/tkinter/widgets/ checkButton.js index 6c0b86f..88ebfa2 100644 --- a/src/frameworks/tkinter/widgets/ checkButton.js +++ b/src/frameworks/tkinter/widgets/ checkButton.js @@ -48,7 +48,6 @@ export class CheckBox extends Widget{ defaultChecked: { label: "Checked", tool: Tools.CHECK_BUTTON, // the tool to display, can be either HTML ELement or a constant string - toolProps: {placeholder: "text", maxLength: 100}, value: true, onChange: (value) => this.setAttrValue("defaultChecked", value) } @@ -103,4 +102,113 @@ export class CheckBox extends Widget{ ) } +} + + +export class RadioButton extends Widget{ + + static widgetType = "radio_button" + // FIXME: the radio buttons are not visible because of the default heigh provided + constructor(props) { + super(props) + + this.droppableTags = null // disables drops + + // const {layout, ...newAttrs} = this.state.attrs // Removes the layout attribute + + let newAttrs = removeKeyFromObject("layout", this.state.attrs) + newAttrs = removeKeyFromObject("styling.backgroundColor", newAttrs) + + this.minSize = {width: 50, height: 30} + + + this.state = { + ...this.state, + size: { width: 120, height: 'fit' }, + attrs: { + ...newAttrs, + styling: { + foregroundColor: { + label: "Foreground Color", + tool: Tools.COLOR_PICKER, // the tool to display, can be either HTML ELement or a constant string + value: "#000", + onChange: (value) => { + this.setWidgetStyling("color", value) + this.setAttrValue("styling.foregroundColor", value) + } + } + }, + + + radios: { + label: "Radio Group", + tool: Tools.INPUT_RADIO_LIST, + value: {inputs: ["default"], selectedRadio: -1}, + onChange: ({inputs, selectedRadio}) => { + this.setAttrValue("radios", {inputs, selectedRadio}) + } + } + + } + } + } + + componentDidMount(){ + super.componentDidMount() + // this.setAttrValue("styling.backgroundColor", "#fff") + this.setWidgetName("Checkbox") + this.setWidgetStyling("backgroundColor", "#fff0") + } + + getToolbarAttrs(){ + + const toolBarAttrs = super.getToolbarAttrs() + + const attrs = this.state.attrs + return ({ + id: this.__id, + widgetName: toolBarAttrs.widgetName, + checkLabel: attrs.checkLabel, + size: toolBarAttrs.size, + ...attrs, + }) + } + + renderContent(){ + + const {inputs, selectedRadio} = this.getAttrValue("radios") + + return ( +
+ + { + inputs.map((value, index) => { + return ( +
+
+ + { + selectedRadio === index && +
+ +
+ } +
+ + {value} + +
+ ) + }) + + } +
+ ) + } + } \ No newline at end of file diff --git a/src/frameworks/tkinter/widgets/button.js b/src/frameworks/tkinter/widgets/button.js index 79d9ec3..8b0d41a 100644 --- a/src/frameworks/tkinter/widgets/button.js +++ b/src/frameworks/tkinter/widgets/button.js @@ -62,7 +62,7 @@ class Button extends Widget{ id: this.__id, widgetName: toolBarAttrs.widgetName, buttonLabel: this.state.attrs.buttonLabel, - size: toolBarAttrs.widgetName, + size: toolBarAttrs.size, ...this.state.attrs, diff --git a/src/frameworks/tkinter/widgets/input.js b/src/frameworks/tkinter/widgets/input.js index 9b306b9..926d754 100644 --- a/src/frameworks/tkinter/widgets/input.js +++ b/src/frameworks/tkinter/widgets/input.js @@ -57,7 +57,7 @@ export class Input extends Widget{ id: this.__id, widgetName: toolBarAttrs.widgetName, placeHolder: this.state.attrs.placeHolder, - size: toolBarAttrs.widgetName, + size: toolBarAttrs.size, ...this.state.attrs, diff --git a/src/frameworks/tkinter/widgets/slider.js b/src/frameworks/tkinter/widgets/slider.js new file mode 100644 index 0000000..30c7e90 --- /dev/null +++ b/src/frameworks/tkinter/widgets/slider.js @@ -0,0 +1,100 @@ +import Widget from "../../../canvas/widgets/base" +import Tools from "../../../canvas/constants/tools" +import { removeKeyFromObject } from "../../../utils/common" + + +class Slider extends Widget{ + + static widgetType = "scale" + + constructor(props) { + super(props) + + this.droppableTags = null // disables drops + + const newAttrs = removeKeyFromObject("layout", this.state.attrs) + + this.state = { + ...this.state, + size: { width: 120, height: 40 }, + attrs: { + ...newAttrs, + styling: { + ...newAttrs.styling, + foregroundColor: { + label: "Foreground Color", + tool: Tools.COLOR_PICKER, // the tool to display, can be either HTML ELement or a constant string + value: "#000", + onChange: (value) => { + this.setWidgetStyling("color", value) + this.setAttrValue("styling.foregroundColor", value) + } + } + }, + scale: { + label: "Scale", + display: "horizontal", + min: { + label: "Min", + tool: Tools.NUMBER_INPUT, // the tool to display, can be either HTML ELement or a constant string + toolProps: { placeholder: "min" }, + value: 0, + onChange: (value) => this.setAttrValue("scale.min", value) + }, + max: { + label: "Max", + tool: Tools.NUMBER_INPUT, + toolProps: { placeholder: "max"}, + value: 100, + onChange: (value) => this.setAttrValue("scale.max", value) + }, + step: { + label: "Step", + tool: Tools.NUMBER_INPUT, + toolProps: { placeholder: "max", stringMode: true, step: "0.1"}, + value: 1, + onChange: (value) => this.setAttrValue("scale.step", value) + } + }, + + } + } + } + + componentDidMount(){ + super.componentDidMount() + this.setAttrValue("styling.backgroundColor", "#fff") + this.setWidgetName("Scale") + } + + getToolbarAttrs(){ + + const toolBarAttrs = super.getToolbarAttrs() + + return ({ + id: this.__id, + widgetName: toolBarAttrs.widgetName, + placeHolder: this.state.attrs.placeHolder, + size: toolBarAttrs.size, + + ...this.state.attrs, + + }) + } + + renderContent(){ + return ( +
+
+
+ {this.getAttrValue("placeHolder")} +
+
+
+ ) + } + +} + + +export default Slider \ No newline at end of file