added MainWindow widget and updated readme

This commit is contained in:
paul
2024-09-22 19:23:06 +05:30
parent d942663b39
commit 65d7aec1a2
14 changed files with 210 additions and 33 deletions

View File

@@ -1,8 +1,21 @@
# PyUIBuilder - The only Python GUI builder you'll ever need
<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" />
</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" />
</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" />
</a>
<a href="https://youtube.com/">
<img src="./assets/share/3.png" height="30" />
</a>
</p>
Build Python GUI's with the ease of Canva
@@ -10,9 +23,82 @@
* Framework agnostic - Can outputs code in multiple frameworks.
* Easy to use.
* Plugins to extend 3rd party UI libraries
* Generate Code.
* Generates Code.
## Roadmap
Here are some of the upcoming features.
* Treeview on the sidebar
* Kivy Framework support
* Pyqt/PySide Support
* **Downloadable Electron app** and more.
To learn more/ see upcoming features visit [roadmap](./roadmap.md)
### All code generated by the builder tools are under MIT license and can be used commercially
## License
To support open-source and development of this tool, consider buying a one-time license.
License will give you access to upcoming features, early access and more.
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 | ✅ | ✅ |
| **Lifetime license** (one-time purchase) | 👍️ | ✅ | ✅ |
| **Early access** to upcoming features | ❌ | ✅ | ✅ |
| **Downloadable Electron App** (upcoming) | ❌ | ✅ | ✅ |
| **Run Preview live**(upcoming) | ❌ | ✅ | ✅ |
| **Save and Load UI files** (upcoming) | ❌ | ✅ | ✅ |
| **Load 3rd party plugins locally** | ❌ | ✅ | ✅ |
| **Dark theme** (upcoming) | ❌ | ✅ | ✅ |
| **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) |
| Pre-order now! | | [Get license]() | [Get license]() |
## FAQ
1. **Why do I need a GUI builder?**
**A.** GUI builders assist you quickly create GUI without learning too much about GUI frameworks. It can also help you quickly prototype and see things visually.
2. **Do I need to purchase a license to use this?**
**A.** Webbased editor will remain free to use. To support open-source development, If you want a downloadable exe for local development and additional features, you'll need to purchase license based on your needs (hobbiest / commercial)
3. **How does this compare to other UI builders?**
**A.**
* Most GUI builders out there are framework specific, this UI Builder tool is framework independent.
* This outputs code in Python, not in XML or other formats which can be hard to debug. So its easier to modify even after downloading the code.
* Support for 3rd party UI libraries. Many GUI builders don't come with support for 3rd party libraries.
## License Information
To support development of this project, license differ depending on the usecase.
#### Webbased Editor
* All code generated by the builder tools are licensed under MIT and can be used commercially
#### Electron App - Hobbyist License
This is meant for students and hobbiest's
* All code generated by the builder tools are free to use for non-commercial purposes. If you are using
this for a startup or your business you'll need to get a business license.
#### Electron App - Commercial License
This is meant for business usecases, you can use the code even for commercial use.
* 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.
## Author
* Paul
* Github: PaulleDemon

BIN
assets/share/1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

BIN
assets/share/2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
assets/share/3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
assets/share/4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -1 +1,2 @@
### State management in react is a f*king mess
### State management in react is a f*king mess
### Update to TypeScript

View File

@@ -9,6 +9,7 @@ Any feature that has 👑 beside it, is meant only for [premium users](./readme.
- [ ] UI fixes and enhancement
- [ ] Tree view for elements on the canvas
- [ ] Add text editor to support event handlers
- [ ] Support more widgets
- [ ] Rewrite DND for better feedback - (swappy/react-dnd-kit/ GSAP draggable)
- [ ] Duplicate widgets
@@ -23,6 +24,7 @@ Any feature that has 👑 beside it, is meant only for [premium users](./readme.
### 2.0.0
- [ ] Support for more third party plugins
- [ ] Support more templates
- [ ] Allow creating components
- [ ] Support for Kivy
- [ ] Sharable Templates
- [ ] Dark theme 👑

View File

@@ -713,6 +713,8 @@ class Canvas extends React.Component {
throw new Error("widgetComponentType must be a subclass of Widget class")
}
console.log("componete: ", widgetComponentType)
const widgetRef = React.createRef()
const id = `${widgetComponentType.widgetType}_${UID()}`
@@ -842,7 +844,7 @@ class Canvas extends React.Component {
// TODO: handle drop from sidebar
// if the widget is being dropped from the sidebar, use the info to create the widget first
this.createWidget(Widget, ({ id, widgetRef }) => {
this.createWidget(widgetClass, ({ id, widgetRef }) => {
widgetRef.current.setPos(finalPosition.x, finalPosition.y)
})
@@ -968,6 +970,7 @@ class Canvas extends React.Component {
{/* <ActiveWidgetProvider> */}
<DroppableWrapper id="canvas-droppable"
droppableTags={{exclude: ["image", "video"]}}
className="tw-w-full tw-h-full"
onDrop={this.handleDropEvent}>
{/* <DragWidgetProvider> */}

View File

@@ -55,7 +55,7 @@ class Widget extends React.Component {
"load": { "args1": "number", "args2": "string" }
}
this.droppableTags = ["widget"] // This indicates if the draggable can be dropped on this widget
this.droppableTags = {} // This indicates if the draggable can be dropped on this widget
this.boundingRect = {
x: 0,
y: 0,
@@ -143,7 +143,6 @@ class Widget extends React.Component {
},
}
this.mousePress = this.mousePress.bind(this)
this.getElement = this.getElement.bind(this)
this.getId = this.getId.bind(this)
@@ -169,7 +168,6 @@ class Widget extends React.Component {
}
componentDidMount() {
this.elementRef.current?.addEventListener("click", this.mousePress)
// FIXME: initial layout is not set properly
console.log("prior layout: ", this.state.attrs.layout.value)
@@ -182,7 +180,6 @@ class Widget extends React.Component {
}
componentWillUnmount() {
this.elementRef.current?.removeEventListener("click", this.mousePress)
}
updateState = (newState, callback) => {
@@ -273,12 +270,6 @@ class Widget extends React.Component {
return this.__id
}
mousePress(event) {
// event.preventDefault()
if (!this._disableSelection) {
}
}
select() {
this.setState({
selected: true
@@ -642,7 +633,12 @@ class Widget extends React.Component {
show: true
}
if (this.droppableTags.length === 0 || this.droppableTags.includes(dragEleType)) {
const allowDrop = (this.droppableTags && (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) {
showDrop = {
allow: true,
show: true
@@ -670,7 +666,12 @@ class Widget extends React.Component {
// console.log("Drag over: ", e.dataTransfer.getData("text/plain"), e.dataTransfer)
const dragEleType = draggedElement.getAttribute("data-draggable-type")
if (this.droppableTags.length === 0 || this.droppableTags.includes(dragEleType)) {
const allowDrop = (this.droppableTags && (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) {
e.preventDefault() // NOTE: this is necessary to allow drop to take place
}
@@ -692,7 +693,12 @@ class Widget extends React.Component {
const dragEleType = draggedElement.getAttribute("data-draggable-type")
if (this.droppableTags.length > 0 && !this.droppableTags.includes(dragEleType)) {
const allowDrop = (this.droppableTags && (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
}
@@ -771,11 +777,14 @@ class Widget extends React.Component {
this.elementRef.current.style.pointerEvents = "auto"
}
// FIXME: children outside the bounding box, add tw-overflow-hidden
renderContent() {
/**
* Note: you must implement this method in subclass, if you want children make sure to pass
* {this.props.children}, to modify the style add this.state.widgetStyling
*/
renderContent() {
// throw new NotImplementedError("render method has to be implemented")
return (
<div className="tw-w-full tw-h-full tw-p-2 tw-content-start tw-rounded-md" style={this.state.widgetStyling}>
<div className="tw-w-full tw-h-full tw-p-2 tw-content-start tw-rounded-md tw-overflow-hidden" style={this.state.widgetStyling}>
{this.props.children}
</div>
)

View File

@@ -14,7 +14,7 @@ import { useDragContext } from "../../components/draggable/draggableContext"
*/
const WidgetDraggable = memo(({ widgetRef, enableDrag=true, dragElementType="widget",
onDragEnter, onDragLeave, onDrop, style={},
droppableTags = ["widget"], ...props }) => {
droppableTags = {}, ...props }) => {
// const { draggedElement, onDragStart, onDragEnd } = useDragWidgetContext()
const { draggedElement, onDragStart, onDragEnd, overElement, setOverElement } = useDragContext()
@@ -71,8 +71,12 @@ const WidgetDraggable = memo(({ widgetRef, enableDrag=true, dragElementType="wid
allow: true,
show: true
}
const allowDrop = (Object.keys(droppableTags).length === 0 ||
(droppableTags.include.length > 0 && droppableTags.include.includes(dragEleType)) ||
(droppableTags.exclude.length > 0 && !droppableTags.exclude.includes(dragEleType))
)
if (droppableTags.length === 0 || droppableTags.includes(dragEleType)) {
if (allowDrop) {
showDrop = {
allow: true,
show: true
@@ -99,7 +103,12 @@ const WidgetDraggable = memo(({ widgetRef, enableDrag=true, dragElementType="wid
// console.log("Drag over: ", e.dataTransfer.getData("text/plain"), e.dataTransfer)
const dragEleType = draggedElement.getAttribute("data-draggable-type")
if (droppableTags.length === 0 || droppableTags.includes(dragEleType)) {
const allowDrop = (Object.keys(droppableTags).length === 0 ||
(droppableTags.include.length > 0 && droppableTags.include.includes(dragEleType)) ||
(droppableTags.exclude.length > 0 && !droppableTags.exclude.includes(dragEleType))
)
if (allowDrop) {
e.preventDefault() // this is necessary to allow drop to take place
}

View File

@@ -2,7 +2,10 @@ import { memo, useState } from "react"
import { useDragContext } from "./draggableContext"
const DroppableWrapper = memo(({onDrop, droppableTags=["widget"], ...props}) => {
/**
* @param {{include: [], exclude: []} || {}} - droppableTags - if empty object, allows everything to be dropped, define include to allow only included widgets, define exclude to exclude
*/
const DroppableWrapper = memo(({onDrop, droppableTags={}, ...props}) => {
const { draggedElement, overElement, setOverElement, widgetClass } = useDragContext()
@@ -17,11 +20,16 @@ const DroppableWrapper = memo(({onDrop, droppableTags=["widget"], ...props}) =>
const dragElementType = draggedElement.getAttribute("data-draggable-type")
// console.log("Current target: ", e.currentTarget)
console.log("Current target: ", droppableTags, Object.keys(droppableTags))
setOverElement(e.currentTarget)
if (droppableTags.length === 0 || droppableTags.includes(dragElementType)){
const allowDrop = (droppableTags && (Object.keys(droppableTags).length === 0 ||
(droppableTags.include?.length > 0 && droppableTags.include?.includes(dragElementType)) ||
(droppableTags.exclude?.length > 0 && !droppableTags.exclude?.includes(dragElementType))
))
if (allowDrop){
setShowDroppable({
allow: true,
show: true
@@ -37,8 +45,13 @@ const DroppableWrapper = memo(({onDrop, droppableTags=["widget"], ...props}) =>
const handleDragOver = (e) => {
// console.log("Drag over: ", e.dataTransfer.getData("text/plain"), e.dataTransfer)
const dragElementType = draggedElement.getAttribute("data-draggable-type")
if (droppableTags.length === 0 || droppableTags.includes(dragElementType)){
const allowDrop = (droppableTags && (Object.keys(droppableTags).length === 0 ||
(droppableTags.include?.length > 0 && droppableTags.include?.includes(dragElementType)) ||
(droppableTags.exclude?.length > 0 && !droppableTags.exclude?.includes(dragElementType))
))
if (allowDrop){
e.preventDefault() // this is necessary to allow drop to take place
}
@@ -51,8 +64,15 @@ const DroppableWrapper = memo(({onDrop, droppableTags=["widget"], ...props}) =>
allow: false,
show: false
})
const dragElementType = draggedElement.getAttribute("data-draggable-type")
if(onDrop){
const allowDrop = (droppableTags && (Object.keys(droppableTags).length === 0 ||
(droppableTags.include?.length > 0 && droppableTags.include?.includes(dragElementType)) ||
(droppableTags.exclude?.length > 0 && !droppableTags.exclude?.includes(dragElementType))
))
if(onDrop && allowDrop){
onDrop(e, draggedElement, widgetClass)
}
}

View File

@@ -2,6 +2,7 @@
import Widget from "../../canvas/widgets/base"
import ButtonWidget from "./assets/widgets/button.png"
import MainWindow from "./widgets/mainWindow"
const TkinterSidebar = [
@@ -9,7 +10,7 @@ const TkinterSidebar = [
name: "Main window",
img: ButtonWidget,
link: "https://github.com",
widgetClass: Widget
widgetClass: MainWindow
},
{
name: "Top Level",

View File

@@ -0,0 +1,46 @@
import { CloseCircleFilled, ExpandOutlined, MinusCircleFilled } from "@ant-design/icons"
import Widget from "../../../canvas/widgets/base"
class MainWindow extends Widget{
static widgetType = "main_window"
constructor(props) {
super(props)
this.droppableTags = {
exclude: ["image", "video", "media"]
}
this.state = {
...this.state,
}
this.setAttrValue("styling.backgroundColor", "#E4E2E2")
}
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-flex tw-w-full tw-h-[25px] tw-bg-[#adadad] tw-shadow-md">
<div className="tw-ml-auto tw-flex tw-gap-1 tw-p-1 tw-place-items-center">
<div className="tw-bg-yellow-400 tw-rounded-full tw-w-[15px] tw-h-[15px]">
</div>
<div className="tw-bg-blue-400 tw-rounded-full tw-w-[15px] tw-h-[15px]">
</div>
<div className="tw-bg-red-400 tw-rounded-full tw-w-[15px] tw-h-[15px]">
</div>
</div>
</div>
<div className="tw-p-2 tw-w-full tw-h-full tw-content-start" style={this.state.widgetStyling}>
{this.props.children}
</div>
</div>
)
}
}
export default MainWindow

View File

@@ -11,7 +11,7 @@ function Share({children, className=""}){
const shareInfo = useMemo(() => {
return {
url: encodeURI("https://github.com/PaulleDemon/font-tester-chrome"),
url: encodeURI("https://github.com/PaulleDemon/tkbuilder"),
text: "Check out Framework agnostic GUI builder for python"
}
}, [])