fix: widget initialData load. feat: added more widgets

This commit is contained in:
paul
2024-09-23 18:25:40 +05:30
parent 5d40894a40
commit 1533888f07
11 changed files with 421 additions and 44 deletions

View File

@@ -3,16 +3,16 @@
<p align="center">
<a href="https://twitter.com/share?url=https://github.com/PaulleDemon/tkbuilder&text=Check out PyUIBuilder tool">
<img src="./assets/share/1.png" height="30" />
<img src="./assets/share/1.png" height="35" />
</a>
<a href="https://www.reddit.com/submit?url=https://github.com/PaulleDemon/tkbuilder&title=Check out PyUIBuilder tool">
<img src="./assets/share/4.png" height="30" />
<img src="./assets/share/4.png" height="35" />
</a>
<a href="https://www.linkedin.com/shareArticle?mini=true&url=https://github.com/PaulleDemon/tkbuilder&title=check out PyUIBuilder tool">
<img src="./assets/share/2.png" height="30" />
<img src="./assets/share/2.png" height="35" />
</a>
<a href="https://youtube.com/">
<img src="./assets/share/3.png" height="30" />
<img src="./assets/share/3.png" height="35" />
</a>
</p>
@@ -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.

View File

@@ -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: "",

View File

@@ -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"
}

View File

@@ -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 && (
<Checkbox
value={val.value}
checked={val.value}
defaultChecked={val.value}
onChange={(e) => handleChange(e.target.checked, val.onChange)}
>{val.label}</Checkbox>
)}
{val.tool === Tools.INPUT_RADIO_LIST && (
<DynamicRadioInputList
defaultInputs={val.value.inputs}
defaultSelected={val.value.selectedRadio}
onChange={({inputs, selectedRadio}) => 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 = {} }) => {
<div
className={`tw-absolute tw-top-20 tw-right-5 tw-bg-white ${toolbarOpen ? "tw-w-[280px]" : "tw-w-0"
} tw-px-4 tw-p-2 tw-h-[600px] tw-rounded-md tw-z-[1000] tw-shadow-lg
tw-transition-transform tw-duration-75
tw-transition-transform tw-duration-75 tw-overflow-x-hidden
tw-flex tw-flex-col tw-gap-2 tw-overflow-y-auto`}
>
<h3 className="tw-text-xl tw-text-center">

View File

@@ -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)

124
src/components/inputs.js Normal file
View File

@@ -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 (
<div>
{inputs.map((input, index) => (
<Space key={index} style={{ display: "flex", marginBottom: 8 }} align="baseline">
<Input
value={input}
onChange={(e) => handleInputChange(e.target.value, index)}
placeholder={`Input ${index + 1}`}
/>
{index !== 0 && ( // Do not show delete button for the first input
<MinusCircleOutlined onClick={() => removeInput(index)} />
)}
</Space>
))}
<Button type="dashed" onClick={addInput} icon={<PlusOutlined />}>
Add Input
</Button>
</div>
)
}
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 (
<div>
<Radio.Group onChange={handleRadioChange} value={selectedRadio}>
{inputs.map((input, index) => (
<Space key={index} style={{ display: "flex", marginBottom: 8 }} align="baseline">
<Radio value={index} defaultChecked={ index === selectedRadio}/>
<Input
value={input}
onChange={(e) => handleInputChange(e.target.value, index)}
placeholder={`Input ${index + 1}`}
/>
{index !== 0 && ( // Do not show delete button for the first input
<div>
<MinusCircleOutlined className="tw-text-xl tw-text-red-500"
onClick={() => removeInput(index)} />
</div>
)}
</Space>
))}
</Radio.Group>
<Button type="dashed" onClick={addInput} icon={<PlusOutlined />}>
Add Input
</Button>
</div>
)
})

View File

@@ -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
},
]

View File

@@ -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 (
<div className="tw-flex tw-p-1 tw-w-full tw-h-full tw-rounded-md tw-overflow-hidden"
style={this.state.widgetStyling}
>
{
inputs.map((value, index) => {
return (
<div key={index} className="tw-flex tw-gap-2 tw-w-full tw-h-full tw-place-items-center ">
<div className="tw-border-solid tw-border-[#D9D9D9] tw-border-2
tw-min-w-[20px] tw-min-h-[20px] tw-w-[20px] tw-h-[20px]
tw-text-blue-600 tw-flex tw-items-center tw-justify-center
tw-rounded-full tw-overflow-hidden tw-p-1">
{
selectedRadio === index &&
<div className="tw-rounded-full tw-bg-blue-600 tw-w-full tw-h-full">
</div>
}
</div>
<span className="tw-text-base" style={{color: this.state.widgetStyling.foregroundColor}}>
{value}
</span>
</div>
)
})
}
</div>
)
}
}

View File

@@ -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,

View File

@@ -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,

View File

@@ -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 (
<div className="tw-w-flex tw-flex-col tw-w-full tw-h-full tw-rounded-md tw-overflow-hidden">
<div className="tw-p-2 tw-w-full tw-h-full tw-flex tw-place-items-center" style={this.state.widgetStyling}>
<div className="tw-text-sm tw-text-gray-300">
{this.getAttrValue("placeHolder")}
</div>
</div>
</div>
)
}
}
export default Slider