diff --git a/src/App.js b/src/App.js
index 10ac57d..249d370 100644
--- a/src/App.js
+++ b/src/App.js
@@ -125,7 +125,6 @@ function App() {
const widgetCenterX = (TkMainWindow.initialSize.width - canvasBoundingBox.left) / 2
const widgetCenterY = (TkMainWindow.initialSize.height - canvasBoundingBox.top) / 2
-
canvasRef?.current?.createWidget(TkMainWindow, {x: canvasCenterX - widgetCenterX, y: canvasCenterY - widgetCenterY}, ({id, widgetRef}) => {
// center the widget when adding to canvas
@@ -142,7 +141,12 @@ function App() {
})
}else if (UIFramework === FrameWorks.CUSTOMTK){
- canvasRef?.current?.createWidget(CTkMainWindow, ({id, widgetRef}) => {
+
+ const widgetCenterX = (TkMainWindow.initialSize.width - canvasBoundingBox.left) / 2
+ const widgetCenterY = (TkMainWindow.initialSize.height - canvasBoundingBox.top) / 2
+
+
+ canvasRef?.current?.createWidget(CTkMainWindow, {x: canvasCenterX - widgetCenterX, y: canvasCenterY - widgetCenterY}, ({id, widgetRef}) => {
// center the widget when adding to canvas
if (!widgetRef.current){
@@ -154,7 +158,7 @@ function App() {
const widgetCenterY = (widgetBoundingBox.height - widgetBoundingBox.top) / 2
- widgetRef.current?.setPos(canvasCenterX-widgetCenterX, canvasCenterY-widgetCenterY)
+ // widgetRef.current?.setPos(canvasCenterX-widgetCenterX, canvasCenterY-widgetCenterY)
})
}
diff --git a/src/frameworks/customtk/constants/styling.js b/src/frameworks/customtk/constants/styling.js
index 12f132e..c399af2 100644
--- a/src/frameworks/customtk/constants/styling.js
+++ b/src/frameworks/customtk/constants/styling.js
@@ -20,5 +20,25 @@ export const ANCHOR = [
"s",
"e",
"w",
- "center"
-]
\ No newline at end of file
+ "center",
+ "ne",
+ "se",
+ "sw",
+ "nw",
+]
+
+
+export const GRID_STICKY = {
+ N: "n",
+ S: "s",
+ E: "e",
+ W: "w",
+ WE: "we",
+ NS: "ns",
+ NW: "nw",
+ NE: "ne",
+ SW: "sw",
+ SE: "se",
+ NEWS: "news",
+ NONE: "",
+}
\ No newline at end of file
diff --git a/src/frameworks/customtk/widgets/base.js b/src/frameworks/customtk/widgets/base.js
index 3b74899..6c9b88a 100644
--- a/src/frameworks/customtk/widgets/base.js
+++ b/src/frameworks/customtk/widgets/base.js
@@ -1,10 +1,11 @@
import { Layouts, PosType } from "../../../canvas/constants/layouts"
import Tools from "../../../canvas/constants/tools"
import Widget from "../../../canvas/widgets/base"
+import { DynamicGridWeightInput } from "../../../components/inputs"
import { convertObjectToKeyValueString, removeKeyFromObject } from "../../../utils/common"
import { Tkinter_TO_WEB_CURSOR_MAPPING } from "../constants/cursor"
import { Tkinter_To_GFonts } from "../constants/fontFamily"
-import { JUSTIFY, RELIEF } from "../constants/styling"
+import { ANCHOR, GRID_STICKY, JUSTIFY, RELIEF } from "../constants/styling"
@@ -18,11 +19,42 @@ export class CustomTkBase extends Widget {
super(props)
this.getLayoutCode = this.getLayoutCode.bind(this)
+
+ this.state = {
+ ...this.state,
+ packAttrs: { // This is required as during flex layout change remount happens and the state updates my not function as expected
+ side: "top",
+ anchor: "n",
+ }
+ }
+
+ this.renderTkinterLayout = this.renderTkinterLayout.bind(this) // this must be called if droppableTags is not set to null
+
+ const originalRenderContent = this.renderContent.bind(this);
+ this.renderContent = () => {
+ this._calledRenderTkinterLayout = false
+
+ const content = originalRenderContent()
+
+ if (
+ this.droppableTags !== null &&
+ this.droppableTags !== undefined &&
+ !this._calledRenderTkinterLayout
+ ) {
+ throw new Error(
+ `class ${this.constructor.name}: renderTkinterLayout() must be called inside renderContent() when droppableTags is not null`
+ )
+ }
+
+ return content
+ }
}
+
getLayoutCode(){
const {layout: parentLayout, direction, gap, align="start"} = this.getParentLayout()
+
const absolutePositioning = this.getAttrValue("positioning")
let layoutManager = `pack()`
@@ -50,23 +82,61 @@ export class CustomTkBase extends Widget {
}else if (parentLayout === Layouts.FLEX){
- const config = {
- side: direction === "row" ? "ctk.LEFT" : "ctk.TOP",
+ const packSide = this.getAttrValue("flexManager.side")
+
+ const marginX = this.getAttrValue("margin.marginX")
+ const marginY = this.getAttrValue("margin.marginY")
+
+ const paddingX = this.getAttrValue("padding.padX")
+ const paddingY = this.getAttrValue("padding.padY")
+
+ const config = {}
+
+ if (packSide === "" || packSide === "top"){
+
+ config['side'] = `tk.TOP`
+
+ }else if (packSide === "left"){
+
+ config['side'] = `tk.LEFT`
+
+ }else if (packSide === "right"){
+
+ config['side'] = `tk.RIGHT`
+
+ }else{
+ config['side'] = `tk.BOTTOM`
}
- if (gap > 0){
- config["padx"] = gap
- config["pady"] = gap
+ // if (gap > 0){
+ // config["padx"] = gap
+ // config["pady"] = gap
+ // }
+
+ if (marginX){
+ config["padx"] = marginX
}
- if (align === "start"){
- config["anchor"] = "'nw'"
- }else if (align === "center"){
- config["anchor"] = "'center'"
- }else if (align === "end"){
- config["anchor"] = "'se'"
+ if (marginY){
+ config["pady"] = marginY
}
+ if (paddingX){
+ config["ipadx"] = paddingX
+ }
+
+ if (paddingY){
+ config["ipady"] = paddingY
+ }
+
+ // if (align === "start"){
+ // config["anchor"] = "'nw'"
+ // }else if (align === "center"){
+ // config["anchor"] = "'center'"
+ // }else if (align === "end"){
+ // config["anchor"] = "'se'"
+ // }
+
const fillX = this.getAttrValue("flexManager.fillX")
const fillY = this.getAttrValue("flexManager.fillY")
const expand = this.getAttrValue("flexManager.expand")
@@ -91,32 +161,130 @@ export class CustomTkBase extends Widget {
}else if (parentLayout === Layouts.GRID){
const row = this.getAttrValue("gridManager.row")
- const col = this.getAttrValue("gridManager.column")
- layoutManager = `grid(row=${row}, column=${col})`
+ const column = this.getAttrValue("gridManager.column")
+ const rowSpan = this.getAttrValue("gridManager.rowSpan")
+ const columnSpan = this.getAttrValue("gridManager.columnSpan")
+
+ const sticky = this.getAttrValue("gridManager.sticky")
+
+ const config = {
+ row: row-1, // unlike css grid tkinter grid starts from 0
+ column: column-1, // unlike css grid tkinter grid starts from 0
+ }
+
+ if (rowSpan > 1){
+ config['rowspan'] = rowSpan
+ }
+
+ if (columnSpan > 1){
+ config['columnspan'] = columnSpan
+ }
+
+ if (sticky !== ""){
+ config['sticky'] = `"${sticky}"`
+ }
+
+ layoutManager = `grid(${convertObjectToKeyValueString(config)})`
}
return layoutManager
}
+
+ getGridLayoutConfigurationCode = (variableName) => {
+ const {layout: currentLayout} = this.getLayout()
+
+ let columnConfigure = []
+ let rowConfigure = []
+
+ if (currentLayout === Layouts.GRID){
+
+ const rowWeights = this.getAttrValue("gridWeights.rowWeights")
+ const colWeights = this.getAttrValue("gridWeights.colWeights")
+
+ if (rowWeights){
+ const correctedRowWeight = Object.fromEntries(
+ Object.entries(rowWeights).map(([_, { gridNo, weight }]) => [gridNo-1, weight]) // tkinter grid starts from 0 unlike css grid
+ );// converts the format : {index: {gridNo, weight}} to {gridNo: weight}
+
+ const groupByWeight = Object.entries(correctedRowWeight).reduce((acc, [gridNo, weight]) => {
+ if (!acc[weight])
+ acc[weight] = []; // Initialize array if it doesn't exist
+
+ acc[weight].push(Number(gridNo)); // Convert key to number and add it to the array
+ return acc;
+ }, {})
+
+ Object.entries(groupByWeight).forEach(([weight, indices]) => {
+ rowConfigure.push(`${variableName}.grid_rowconfigure(index=[${indices.join(",")}], weight=${weight})`)
+ })
+ }
+
+ if (colWeights){
+ const correctedColWeight = Object.fromEntries(
+ Object.entries(colWeights).map(([_, { gridNo, weight }]) => [gridNo-1, weight]) // tkinter grid starts from 0, so -1
+ ) // converts the format : {index: {gridNo, weight}} to {gridNo: weight}
+
+ const groupByWeight = Object.entries(correctedColWeight).reduce((acc, [gridNo, weight]) => {
+ if (!acc[weight])
+ acc[weight] = [] // Initialize array if it doesn't exist
+
+ acc[weight].push(Number(gridNo)) // Convert key to number and add it to the array
+ return acc
+ }, {})
+
+ Object.entries(groupByWeight).forEach(([weight, indices]) => {
+ columnConfigure.push(`${variableName}.grid_columnconfigure(index=[${indices.join(",")}], weight=${weight})`)
+ })
+ }
+
+ }
+
+ return [...rowConfigure, ...columnConfigure]
+
+ }
+
+ getPackAttrs = () => {
+ return ({
+ side: this.state.packAttrs.side,
+ anchor: this.state.packAttrs.anchor,
+ expand: this.getAttrValue("flexManager.expand"),
+ })
+ }
+
+ /**
+ * A simple function that returns a mapping for grid sticky tkinter
+ */
+ getGridStickyStyling(sticky){
+
+ const styleMapping = {
+ [GRID_STICKY.N]: { alignSelf: "start", justifySelf: "center" },
+ [GRID_STICKY.S]: { alignSelf: "end", justifySelf: "center" },
+ [GRID_STICKY.E]: { alignSelf: "center", justifySelf: "end" },
+ [GRID_STICKY.W]: { alignSelf: "center", justifySelf: "start" },
+ [GRID_STICKY.NS]: { alignSelf: "stretch", justifySelf: "center" }, // Stretch vertically
+ [GRID_STICKY.EW]: { alignSelf: "center", justifySelf: "stretch" }, // Stretch horizontally
+ [GRID_STICKY.NE]: { alignSelf: "start", justifySelf: "end" }, // Top-right
+ [GRID_STICKY.SE]: { alignSelf: "end", justifySelf: "end" }, // Bottom-right
+ [GRID_STICKY.NW]: { alignSelf: "start", justifySelf: "start" }, // Top-left
+ [GRID_STICKY.SW]: { alignSelf: "end", justifySelf: "start" }, // Bottom-left
+ [GRID_STICKY.NEWS]: { placeSelf: "stretch" } // Stretch in all directions
+ };
+
+ return styleMapping[sticky]
+
+ }
+
setParentLayout(layout){
if (!layout){
return {}
}
-
- super.setParentLayout(layout)
+ let updates = super.setParentLayout(layout)
const {layout: parentLayout, direction, gap} = layout
// show attributes related to the layout manager
- let updates = {
- parentLayout: layout,
- }
-
- // this.removeAttr("gridManager")
- // this.removeAttr("flexManager")
- // this.removeAttr("positioning")
-
// remove gridManager, flexManager positioning
const {gridManager, flexManager, positioning, ...restAttrs} = this.state.attrs
@@ -124,6 +292,7 @@ export class CustomTkBase extends Widget {
updates = {
...updates,
+ // pos: pos,
positionType: PosType.NONE,
}
// Allow optional absolute positioning if the parent layout is flex or grid
@@ -150,7 +319,7 @@ export class CustomTkBase extends Widget {
attrs: {
...updateAttrs,
flexManager: {
- label: "Flex Manager",
+ label: "Pack Manager",
display: "horizontal",
fillX: {
label: "Fill X",
@@ -158,14 +327,13 @@ export class CustomTkBase extends Widget {
value: false,
onChange: (value) => {
this.setAttrValue("flexManager.fillX", value)
- const widgetStyle = {
- ...this.state.widgetOuterStyling,
- flexGrow: value ? 1 : 0,
- }
- this.updateState({
- widgetOuterStyling: widgetStyle,
- })
+ this.updateState((prevState) => ({
+ widgetOuterStyling: {
+ ...prevState.widgetOuterStyling,
+ width: "100%"
+ }
+ }))
}
},
@@ -176,13 +344,33 @@ export class CustomTkBase extends Widget {
onChange: (value) => {
this.setAttrValue("flexManager.fillY", value)
- const widgetStyle = {
- ...this.state.widgetOuterStyling,
- flexGrow: value ? 1 : 0,
- }
- this.updateState({
- widgetOuterStyling: widgetStyle,
+ this.updateState((prevState) => ({
+ widgetOuterStyling: {
+ ...prevState.widgetOuterStyling,
+ height: "100%"
+ }
+ }))
+ }
+ },
+ side: {
+ label: "Align Side",
+ tool: Tools.SELECT_DROPDOWN,
+ options: ["left", "right", "top", "bottom", ""].map(val => ({value: val, label: val})),
+ value: this.state.packAttrs.side,
+ onChange: (value) => {
+ this.setAttrValue("flexManager.side", value, () => {
+ this.updateState((prevState) => ({packAttrs: {...prevState.packAttrs, side: value}}), () => {
+
+ // this.props.parentWidgetRef.current.forceRerender()
+ this.props.requestWidgetDataUpdate(this.props.parentWidgetRef.current.__id)
+ this.stateChangeSubscriberCallback() // call this to notify the toolbar that the widget has changed state
+ })
+
+
})
+
+
+ // console.log("updateing state: ", value, this.props.parentWidgetRef.current)
}
},
expand: {
@@ -191,13 +379,26 @@ export class CustomTkBase extends Widget {
value: false,
onChange: (value) => {
this.setAttrValue("flexManager.expand", value)
-
- const widgetStyle = {
- ...this.state.widgetOuterStyling,
- flexGrow: value ? 1 : 0,
- }
- this.updateState({
- widgetOuterStyling: widgetStyle,
+
+ // this.setWidgetOuterStyle(value ? 1 : 0)
+ }
+ },
+ anchor: {
+ label: "Anchor",
+ tool: Tools.SELECT_DROPDOWN,
+ options: ANCHOR.map(val => ({value: val, label: val})),
+ value: this.state.packAttrs.anchor,
+ onChange: (value) => {
+ this.setAttrValue("flexManager.anchor", value, () => {
+ // this.props.parentWidgetRef.current.forceRerender()
+ })
+ this.updateState((prevState) => ({packAttrs: {...prevState.packAttrs, anchor: value}}), () => {
+
+ // this.props.requestWidgetDataUpdate(this.props.parentWidgetRef.current.__id)
+
+ // this.props.requestWidgetDataUpdate(this.__id)
+ // this.stateChangeSubscriberCallback() // call this to notify the toolbar that the widget has changed state
+ // this.props.parentWidgetRef.current.forceRerender()
})
}
},
@@ -219,83 +420,108 @@ export class CustomTkBase extends Widget {
row: {
label: "Row",
tool: Tools.NUMBER_INPUT,
- toolProps: { placeholder: "width", max: 1000, min: 1 },
- value: 1,
+ toolProps: { placeholder: "row", max: 1000, min: 1 },
+ value: 0,
onChange: (value) => {
- const previousRow = this.getWidgetOuterStyle("gridRow") || "1/1"
-
- let [_row=1, rowSpan=1] = previousRow.replace(/\s+/g, '').split("/").map(Number)
-
- if (value > rowSpan){
- // rowSpan should always be greater than or eq to row
- rowSpan = value
- this.setAttrValue("gridManager.rowSpan", rowSpan)
- }
+ const previousRow = this.getWidgetOuterStyle("gridRow") || "1 / span 1"
+ let [_row=1, rowSpan=1] = previousRow.replace(/\s+/g, '').split("/span").map(Number)
this.setAttrValue("gridManager.row", value)
- this.setWidgetOuterStyle("gridRow", `${value+' / '+rowSpan}`)
+ this.setWidgetOuterStyle("gridRow", `${value+' / span '+rowSpan}`)
}
},
rowSpan: {
label: "Row span",
tool: Tools.NUMBER_INPUT,
- toolProps: { placeholder: "height", max: 1000, min: 1 },
+ toolProps: { placeholder: "row span", max: 1000, min: 1 },
value: 1,
onChange: (value) => {
- const previousRow = this.getWidgetOuterStyle("gridRow") || "1/1"
+ const previousRow = this.getWidgetOuterStyle("gridRow") || "1 / span 1"
- const [row=1, _rowSpan=1] = previousRow.replace(/\s+/g, '').split("/").map(Number)
+ // const [row=1, _rowSpan=1] = previousRow.replace(/\s+/g, '').split("/").map(Number)
+ const [row=1, rowSpan=1] = previousRow.replace(/\s+/g, '').split("/span").map(Number)
- if (value < row){
- value = row + 1
+ // if (value < row){
+ // value = row + 1
+ // }
+ if (value < 1){
+ value = 1
}
this.setAttrValue("gridManager.rowSpan", value)
- this.setWidgetOuterStyle("gridRow", `${row + ' / ' +value}`)
+ this.setWidgetOuterStyle("gridRow", `${(row || 1) + ' / span ' +value}`)
}
},
column: {
label: "Column",
tool: Tools.NUMBER_INPUT,
- toolProps: { placeholder: "height", max: 1000, min: 1 },
- value: 1,
+ toolProps: { placeholder: "column", max: 1000, min: 1 },
+ value: 0,
onChange: (value) => {
- const previousRow = this.getWidgetOuterStyle("gridColumn") || "1/1"
+ const previousRow = this.getWidgetOuterStyle("gridColumn") || "1 / span 1"
- let [_col=1, colSpan=1] = previousRow.replace(/\s+/g, '').split("/").map(Number)
+ let [_col=1, colSpan=1] = previousRow.replace(/\s+/g, '').split("/span").map(Number)
- if (value > colSpan){
- // The colSpan has always be equal or greater than col
- colSpan = value
- this.setAttrValue("gridManager.columnSpan", colSpan)
- }
+ // if (value > colSpan){
+ // // The colSpan has always be equal or greater than col
+ // colSpan = value
+ // this.setAttrValue("gridManager.columnSpan", colSpan)
+ // }
this.setAttrValue("gridManager.column", value)
- this.setWidgetOuterStyle("gridColumn", `${value +' / ' + colSpan}`)
+ this.setWidgetOuterStyle("gridColumn", `${value +' / span ' + colSpan}`)
}
},
columnSpan: {
label: "Column span",
tool: Tools.NUMBER_INPUT,
- toolProps: { placeholder: "height", max: 1000, min: 1 },
+ toolProps: { placeholder: "column span", max: 1000, min: 1 },
value: 1,
onChange: (value) => {
- const previousCol = this.getWidgetOuterStyle("gridColumn") || "1/1"
+ const previousCol = this.getWidgetOuterStyle("gridColumn") || "1 / span 1"
- const [col=1, _colSpan=1] = previousCol.replace(/\s+/g, '').split("/").map(Number)
+ const [col=1, _colSpan=1] = previousCol.replace(/\s+/g, '').split("/span").map(Number)
- if (value < col){
- value = col + 1
+
+ if (value < 1){
+ value = 1
}
this.setAttrValue("gridManager.columnSpan", value)
- this.setWidgetOuterStyle("gridColumn", `${col + ' / ' + value}`)
+ this.setWidgetOuterStyle("gridColumn", `${(col || 1) + ' / span ' + value}`)
}
},
+ sticky: {
+ label: "Sticky",
+ tool: Tools.SELECT_DROPDOWN,
+ toolProps: { placeholder: "Sticky", },
+ options: Object.values(GRID_STICKY).map((val) => ({value: val, label: val})),
+ value: GRID_STICKY.NONE,
+ onChange: (value) => {
+
+ this.setAttrValue("gridManager.sticky", value)
+
+
+ this.updateState((prev) => {
+
+ const { alignSelf, justifySelf, placeSelf, ...restStates } = prev.widgetOuterStyling; // Remove these properties
+
+ return ({
+ widgetOuterStyling: {
+ ...restStates,
+ ...this.getGridStickyStyling(value)
+ }
+ })
+ })
+
+ // this.setW
+
+ }
+ }
}
}
}
@@ -309,29 +535,313 @@ export class CustomTkBase extends Widget {
}
}
- this.updateState(updates)
+
+ this.updateState((prevState) => ({...prevState, ...updates}))
+
return updates
}
- getInnerRenderStyling(){
- let {width, height, minWidth, minHeight} = this.getRenderSize()
-
- const {layout: parentLayout, direction, gap} = this.getParentLayout() || {}
-
- if (parentLayout === Layouts.FLEX){
- const fillX = this.getAttrValue("flexManager.fillX")
- const fillY = this.getAttrValue("flexManager.fillY")
-
- // This is needed if fillX or fillY is true, as the parent is applied flex-grow
-
- if (fillX || fillY){
- width = "100%"
- height = "100%"
- }
+
+ /**
+ * adds the layout to achieve the pack from tkinter refer: https://www.youtube.com/watch?v=rbW1iJO1psk
+ * @param {*} widgets
+ * @param {*} index
+ * @returns
+ */
+ renderPackWidgetsRecursively = (widgets, index = 0, lastSide = "", previousExpandValue = 0) => {
+ if (index >= widgets.length) return null;
+
+ const widget = widgets[index];
+ const widgetRef = widget.ref?.current;
+ if (!widgetRef) return null; // Ensure ref exists before accessing
+
+ const { side = "top", expand = false, anchor } = widgetRef.getPackAttrs() || {};
+
+ // console.log("rerendering:", side, expand);
+
+ const directionMap = {
+ top: "column",
+ bottom: "column-reverse",
+ left: "row",
+ right: "row-reverse",
+ }
+
+ const currentWidgetDirection = directionMap[side] || "column"; // Default to "column"
+ const isSameSide = lastSide === side;
+ const isVertical = ["top", "bottom"].includes(side);
+
+ let expandValue = expand ? (isSameSide ? previousExpandValue : widgets.length - index) : 1;
+
+ if ((expand && previousExpandValue === 0) || (expand && expandValue === 0)){
+ expandValue = 1 // if there is expand it should expand
}
+ if (expand && !isSameSide) previousExpandValue = expandValue;
+
+ lastSide = side; // Update last side for recursion
+
+ const anchorStyles = {
+ n: { alignItems: "flex-start", justifyContent: "center" }, // Top-center
+ s: { alignItems: "flex-end", justifyContent: "center" }, // Bottom-center
+ e: { alignItems: "center", justifyContent: "flex-end" }, // Right-center
+ w: { alignItems: "center", justifyContent: "flex-start" }, // Left-center
+ ne: { alignItems: "flex-start", justifyContent: "flex-end" }, // Top-right
+ nw: { alignItems: "flex-start", justifyContent: "flex-start" }, // Top-left
+ se: { alignItems: "flex-end", justifyContent: "flex-end" }, // Bottom-right
+ sw: { alignItems: "flex-end", justifyContent: "flex-start" }, // Bottom-left
+ center: { alignItems: "center", justifyContent: "center" }, // Fully centered
+ };
+ const { justifyContent, alignItems } = anchorStyles[anchor] || anchorStyles["n"]
+
+
+ const stretchClass = isVertical ? "tw-flex-grow" : "tw-h-full"; // Allow only horizontal growth for top/bottom
+
+ if (isSameSide) {
+ return (
+ <>
+
+ {widget}
+
+
+ {this.renderPackWidgetsRecursively(widgets, index + 1, side, previousExpandValue)}
+ >
+ );
+ }
+
+ return (
+
+
+ {widget}
+
+
+ {this.renderPackWidgetsRecursively(widgets, index + 1, side, previousExpandValue)}
+
+ );
+ }
+
+
+
+ /**
+ *
+ * Helps with pack layout manager and grid manager
+ */
+ renderTkinterLayout(){
+ this._calledRenderTkinterLayout = true // NOTE: this is set so subclass are forced to call this method if droppable tags are not null
+ const {layout, direction, gap} = this.getLayout()
+
+ if (layout === Layouts.FLEX){
+
+
+ return (
+ <>
+ {this.renderPackWidgetsRecursively(this.props.children)}
+ >
+ )
+ }
+ return (<>{this.props.children}>)
+ }
+
+
+ setLayout(value) {
+ const { layout, direction, grid = { rows: 1, cols: 1 }, gap = 10, align } = value
+
+
+ if (layout === Layouts.GRID){
+
+
+ const {...restAttrs} = this.state.attrs
+
+ const updates = {
+ attrs: {
+ ...restAttrs,
+ gridConfig: {
+ label: "Grid Configure",
+ display: "horizontal",
+ noOfRows: {
+ label: "No of rows",
+ tool: Tools.NUMBER_INPUT,
+ toolProps: { placeholder: "no of rows", max: 1000, min: 1 },
+ value: 3,
+ onChange: (value) => {
+ this.setAttrValue("gridConfig.noOfRows", value)
+
+ const gridWeights = this.getAttrValue("gridConfig.rowWeights")
+
+ let gridTemplateRows = `repeat(${value}, max-content)`
+
+ if (gridWeights){
+ gridTemplateRows = Array.from({ length: value }, (_, i) =>
+ `${gridWeights[i + 1]}fr` || "max-content"
+ ).join(" ") // creates "max-content max-content 1fr 3fr" depending on value
+ }
+
+ this.updateState((prev) => ({
+ widgetInnerStyling: {
+ ...prev.widgetInnerStyling,
+ gridTemplateRows: gridTemplateRows
+ }
+ }))
+ }
+ },
+ noOfCols: {
+ label: "No of cols",
+ tool: Tools.NUMBER_INPUT,
+ toolProps: { placeholder: "no of cols", max: 1000, min: 1 },
+ value: 3,
+ onChange: (value) => {
+ this.setAttrValue("gridConfig.noOfCols", value)
+
+ this.updateState((prev) => ({
+ widgetInnerStyling: {
+ ...prev.widgetInnerStyling,
+ gridTemplateColumns: `repeat(${value}, max-content)`
+ }
+ }))
+ }
+ },
+
+ },
+ gridWeights: {
+ label: "Grid weights",
+ display: "vertical",
+ rowWeights: {
+ label: "Row weights",
+ tool: Tools.CUSTOM,
+ toolProps: {
+ // placeholder: "weight",
+ // defaultWeightMapping: this.getAttrValue("gridWeights.rowWeights"),
+ },
+ value: undefined,
+ Component: DynamicGridWeightInput,
+ onChange: (value) => {
+
+ if (!value) return
+
+
+ this.setAttrValue("gridWeights.rowWeights", value)
+
+ const noOfRows = this.getAttrValue("gridConfig.noOfRows")
+
+
+ const gridTemplateRows = Array.from({ length: noOfRows }, (_, i) => {
+ const row = value[i] // Get the row object
+ return row ? `${row.weight}fr` : "max-content"; // Use weight if available
+ }).join(" ") // creates "max-content max-content 1fr 3fr" depending on value
+
+
+ this.setWidgetInnerStyle("gridTemplateRows", gridTemplateRows)
+ }
+ },
+ colWeights: {
+ label: "Column weights",
+ tool: Tools.CUSTOM,
+ toolProps: {
+ // placeholder: "weight",
+ // defaultWeightMapping: {0: {weight: 0, gridNo: 0}}
+ },
+ value: undefined,
+ Component: DynamicGridWeightInput,
+ onChange: (value) => {
+
+ if (!value) return
+
+ this.setAttrValue("gridWeights.colWeights", value)
+
+ const noOfCols = this.getAttrValue("gridConfig.noOfCols")
+
+
+ const gridTemplateCol = Array.from({ length: noOfCols }, (_, i) => {
+ const col = value[i] // Get the row object
+ return col ? `${col.weight}fr` : "max-content"; // Use weight if available
+ }).join(" ") // creates "max-content max-content 1fr 3fr" depending on value
+
+
+ this.setWidgetInnerStyle("gridTemplateColumns", gridTemplateCol)
+ // this.setW
+
+ }
+ }
+ }
+
+ }
+ }
+ this.updateState((prevState) => ({...prevState, ...updates}))
+
+ }else if (layout === Layouts.FLEX){
+ const {gridConfig, gridWeights, ...restAttrs} = this.state.attrs
+
+ console.log("Flex: ", restAttrs)
+
+ this.updateState((prevState) => ({attrs: {...restAttrs}}))
+ }
+
+ this.props.onLayoutUpdate({parentId: this.__id, parentLayout: value})// inform children about the layout update
+ this.setAttrValue("layout", value)
+
+ this.updateState((prev) => ({
+ widgetInnerStyling: {
+ ...prev.widgetInnerStyling,
+ display: layout !== Layouts.PLACE ? (layout === "grid" ? "grid" : "flex" ) : "block",
+ flexDirection: "column",
+ // flexDirection: direction,
+ gap: `${gap}px`,
+ gridTemplateColumns: "repeat(3, max-content)",
+ gridTemplateRows: "repeat(3, max-content)",
+ // gridTemplateColumns: "repeat(auto-fill, minmax(100px, auto))",
+ // gridTemplateRows: "repeat(auto-fill, minmax(100px, auto))",
+ }
+ }))
+
+
+ }
+
+ getInnerRenderStyling(){
+ let {width, height, minWidth, minHeight} = this.getRenderSize()
+
+ const {layout: parentLayout, direction, gap} = this.getParentLayout() || {}
+
+
const styling = {
...this.state.widgetInnerStyling,
width,
@@ -342,17 +852,64 @@ export class CustomTkBase extends Widget {
return styling
}
+ getRenderSize(){
+
+ let {width, height, minWidth, minHeight} = super.getRenderSize()
+
+ let fillX = this.getAttrValue("flexManager.fillX") || false
+ let fillY = this.getAttrValue("flexManager.fillY") || false
+
+ const {layout: parentLayout} = (this.getParentLayout() || {})
+
+ if (fillX){
+ width = "100%"
+ }
+
+ if (fillY){
+ height = "100%"
+ }
+
+ if (parentLayout && parentLayout === Layouts.GRID){
+ const sticky = this.getAttrValue("gridManager.sticky")
+
+ if (sticky === GRID_STICKY.NEWS){
+ width = "100%"
+ height = "100%"
+ }else if (sticky === GRID_STICKY.WE){
+ width = "100%"
+ }else if (sticky === GRID_STICKY.NS){
+ height = "100%"
+ }
+ }
+
+ return {width, height, minWidth, minHeight}
+
+ }
+
+
+ serialize(){
+ return ({
+ ...super.serialize(),
+ attrs: this.serializeAttrsValues(), // makes sure that functions are not serialized
+ packAttrs: this.state.packAttrs,
+ })
+ }
+
/**
* loads the data
* @param {object} data
*/
load(data, callback=null){
+ // TODO: call the base widget
+
if (Object.keys(data).length === 0) return // no data to load
data = {...data} // create a shallow copy
- const {attrs, parentLayout=null, ...restData} = data
+ const {attrs={}, selected, pos={x: 0, y: 0}, ...restData} = data
+
+ const parentLayout = this.props.parentWidgetRef?.current?.getLayout()
let layoutUpdates = {
@@ -377,7 +934,8 @@ export class CustomTkBase extends Widget {
const newData = {
...restData,
- ...layoutUpdates
+ ...layoutUpdates,
+ pos
}
@@ -386,7 +944,6 @@ export class CustomTkBase extends Widget {
// UPdates attrs
let newAttrs = { ...this.state.attrs, ...layoutAttrs }
-
// Iterate over each path in the updates object
Object.entries(attrs).forEach(([path, value]) => {
const keys = path.split('.')
@@ -410,14 +967,17 @@ export class CustomTkBase extends Widget {
// TODO: find a better way to apply innerStyles
this.setWidgetInnerStyle("backgroundColor", newAttrs.styling.backgroundColor.value)
}
-
this.updateState({ attrs: newAttrs }, callback)
+ // FIXME: when changing layouts all the widgets are being selected
+ if (selected){
+ this.select()
+ }
})
- }
+ }
}
@@ -527,10 +1087,49 @@ export class CustomTkWidgetBase extends CustomTkBase{
widgetInnerStyling: widgetStyle
})
- this.setAttrValue("padding.padX", value)
+ this.setAttrValue("padding.padY", value)
}
},
},
+ margin: {
+ label: "Margin",
+ marginX: {
+ label: "Margin X",
+ tool: Tools.NUMBER_INPUT,
+ toolProps: {min: 0, max: 140},
+ value: null,
+ onChange: (value) => {
+
+
+ this.updateState((prev) => ({
+ widgetOuterStyling: {
+ ...prev.widgetOuterStyling,
+ marginLeft: `${value}px`,
+ marginRight: `${value}px`
+ },
+ }))
+ this.setAttrValue("margin.marginX", value)
+ }
+ },
+ marginY: {
+ label: "Margin Y",
+ tool: Tools.NUMBER_INPUT,
+ toolProps: {min: 0, max: 140},
+ value: null,
+ onChange: (value) => {
+
+ this.updateState((prev) => ({
+ widgetOuterStyling: {
+ ...prev.widgetOuterStyling,
+ marginTop: `${value}px`,
+ marginBottom: `${value}px`
+ },
+ }))
+
+ this.setAttrValue("margin.marginY", value)
+ }
+ },
+ },
font: {
label: "font",
fontFamily: {
@@ -601,21 +1200,31 @@ export class CustomTkWidgetBase extends CustomTkBase{
config["cursor"] = `"${this.getAttrValue("cursor")}"`
if (this.getAttrValue("padding.padX")){
- config["padx"] = this.getAttrValue("padding.padX")
+ // inner padding
+ config["ipadx"] = this.getAttrValue("padding.padX")
}
if (this.getAttrValue("padding.padY")){
- config["pady"] = this.getAttrValue("padding.padY")
+ config["ipady"] = this.getAttrValue("padding.padY")
+ }
+
+
+ if (this.getAttrValue("margin.marginX")){
+ config["padx"] = this.getAttrValue("margin.marginX")
+ }
+
+ if (this.getAttrValue("margin.marginY")){
+ config["pady"] = this.getAttrValue("margin.marginY")
}
// FIXME: add width and height, the scales may not be correct as the width and height are based on characters in pack and grid not pixels
- if (!this.state.fitContent.width){
- config["width"] = this.state.size.width
- }
- if (!this.state.fitContent.height){
- config["height"] = this.state.size.height
- }
+ // if (!this.state.fitContent.width){
+ // config["width"] = this.state.size.width
+ // }
+ // if (!this.state.fitContent.height){
+ // config["height"] = this.state.size.height
+ // }
return config
diff --git a/src/frameworks/customtk/widgets/frame.js b/src/frameworks/customtk/widgets/frame.js
index f1f813d..26261d1 100644
--- a/src/frameworks/customtk/widgets/frame.js
+++ b/src/frameworks/customtk/widgets/frame.js
@@ -1,3 +1,5 @@
+import { Layouts } from "../../../canvas/constants/layouts"
+import Tools from "../../../canvas/constants/tools"
import Widget from "../../../canvas/widgets/base"
import { CustomTkBase } from "./base"
@@ -17,7 +19,99 @@ class Frame extends CustomTkBase{
this.state = {
...this.state,
fitContent: {width: true, height: true},
- widgetName: "Frame"
+ widgetName: "Frame",
+ attrs: {
+ ...this.state.attrs,
+ padding: {
+ label: "padding",
+ padX: {
+ label: "Pad X",
+ tool: Tools.NUMBER_INPUT,
+ toolProps: {min: 0, max: 140},
+ value: null,
+ onChange: (value) => {
+ // this.setWidgetInnerStyle("paddingLeft", `${value}px`)
+ // this.setWidgetInnerStyle("paddingRight", `${value}px`)
+
+ // const widgetStyle = {
+
+ // }
+ this.setState((prevState) => ({
+
+ widgetInnerStyling: {
+ ...prevState.widgetInnerStyling,
+ paddingLeft: `${value}px`,
+ paddingRight: `${value}px`
+ }
+ }))
+
+
+ this.setAttrValue("padding.padX", value)
+ }
+ },
+ padY: {
+ label: "Pad Y",
+ tool: Tools.NUMBER_INPUT,
+ toolProps: {min: 0, max: 140},
+ value: null,
+ onChange: (value) => {
+
+ this.setState((prevState) => ({
+
+ widgetInnerStyling: {
+ ...prevState.widgetInnerStyling,
+ paddingTop: `${value}px`,
+ paddingBottom: `${value}px`
+ }
+ }))
+ // this.setState({
+
+ // widgetInnerStyling: widgetStyle
+ // })
+ this.setAttrValue("padding.padY", value)
+ }
+ },
+ },
+ margin: {
+ label: "Margin",
+ marginX: {
+ label: "Margin X",
+ tool: Tools.NUMBER_INPUT,
+ toolProps: {min: 0, max: 140},
+ value: null,
+ onChange: (value) => {
+
+
+ this.updateState((prev) => ({
+ widgetOuterStyling: {
+ ...prev.widgetOuterStyling,
+ marginLeft: `${value}px`,
+ marginRight: `${value}px`
+ },
+ }))
+ this.setAttrValue("margin.marginX", value)
+ }
+ },
+ marginY: {
+ label: "Margin Y",
+ tool: Tools.NUMBER_INPUT,
+ toolProps: {min: 0, max: 140},
+ value: null,
+ onChange: (value) => {
+
+ this.updateState((prev) => ({
+ widgetOuterStyling: {
+ ...prev.widgetOuterStyling,
+ marginTop: `${value}px`,
+ marginBottom: `${value}px`
+ },
+ }))
+
+ this.setAttrValue("margin.marginY", value)
+ }
+ },
+ },
+ }
}
}
@@ -27,6 +121,34 @@ class Frame extends CustomTkBase{
this.setAttrValue("styling.backgroundColor", "#EDECEC")
}
+ getConfigCode(){
+ const bg = this.getAttrValue("styling.backgroundColor")
+
+ const fitWidth = this.state.fitContent.width
+ const fitHeight = this.state.fitContent.height
+
+ const {width, height} = this.getSize()
+
+ const {layout} = this.getParentLayout()
+
+ const config = {
+ bg: `"${bg}"`
+ }
+
+ if (layout !== Layouts.PLACE){
+ if (!fitWidth){
+ config['width'] = width
+ }
+
+ if (!fitHeight){
+ config['height'] = height
+ }
+ }
+
+ return config
+ }
+
+
generateCode(variableName, parent){
const bg = this.getAttrValue("styling.backgroundColor")
@@ -34,12 +156,12 @@ class Frame extends CustomTkBase{
return [
`${variableName} = ctk.CTkFrame(master=${parent})`,
`${variableName}.configure(fg_color="${bg}")`,
- `${variableName}.${this.getLayoutCode()}`
+ `${variableName}.${this.getLayoutCode()}`,
+ ...this.getGridLayoutConfigurationCode(variableName)
]
}
-
renderContent(){
// console.log("bounding rect: ", this.getBoundingRect())
@@ -49,7 +171,7 @@ class Frame extends CustomTkBase{
- {this.props.children}
+ {this.renderTkinterLayout()}
)
diff --git a/src/frameworks/customtk/widgets/label.js b/src/frameworks/customtk/widgets/label.js
index 3a11d6d..7cbb211 100644
--- a/src/frameworks/customtk/widgets/label.js
+++ b/src/frameworks/customtk/widgets/label.js
@@ -116,6 +116,22 @@ class Label extends CustomTkWidgetBase{
})
}
+ getAnchorStyle = (anchor) => {
+ const anchorStyles = {
+ n: { justifyContent: 'center', alignItems: 'flex-start' },
+ s: { justifyContent: 'center', alignItems: 'flex-end' },
+ e: { justifyContent: 'flex-end', alignItems: 'center' },
+ w: { justifyContent: 'flex-start', alignItems: 'center' },
+ ne: { justifyContent: 'flex-end', alignItems: 'flex-start' },
+ se: { justifyContent: 'flex-end', alignItems: 'flex-end' },
+ nw: { justifyContent: 'flex-start', alignItems: 'flex-start' },
+ sw: { justifyContent: 'flex-start', alignItems: 'flex-end' },
+ center: { justifyContent: 'center', alignItems: 'center' }
+ }
+
+ return anchorStyles[anchor] || anchorStyles["w"];
+ }
+
renderContent(){
const image = this.getAttrValue("imageUpload")
@@ -137,7 +153,10 @@ class Label extends CustomTkWidgetBase{
)
}
-
+
{this.getAttrValue("labelWidget")}
diff --git a/src/frameworks/customtk/widgets/mainWindow.js b/src/frameworks/customtk/widgets/mainWindow.js
index 77e9784..f66c5d1 100644
--- a/src/frameworks/customtk/widgets/mainWindow.js
+++ b/src/frameworks/customtk/widgets/mainWindow.js
@@ -1,6 +1,7 @@
import Widget from "../../../canvas/widgets/base"
import Tools from "../../../canvas/constants/tools"
import { CustomTkBase } from "./base"
+import { getPythonAssetPath } from "../../utils/pythonFilePath"
class MainWindow extends CustomTkBase{
@@ -8,6 +9,13 @@ class MainWindow extends CustomTkBase{
static widgetType = "main_window"
static displayName = "Main Window"
+
+ static initialSize = {
+ width: 700,
+ height: 400
+ }
+
+
constructor(props) {
super(props)
@@ -27,6 +35,13 @@ class MainWindow extends CustomTkBase{
toolProps: {placeholder: "Window title", maxLength: 40},
value: "Main Window",
onChange: (value) => this.setAttrValue("title", value)
+ },
+ logo: {
+ label: "Window Logo",
+ tool: Tools.UPLOADED_LIST,
+ toolProps: {filterOptions: ["image/jpg", "image/jpeg", "image/png"]},
+ value: "",
+ onChange: (value) => this.setAttrValue("logo", value)
}
}
@@ -43,12 +58,48 @@ class MainWindow extends CustomTkBase{
generateCode(variableName, parent){
const backgroundColor = this.getAttrValue("styling.backgroundColor")
+ const logo = this.getAttrValue("logo")
- return [
- `${variableName} = ctk.CTk()`,
- `${variableName}.configure(fg_color="${backgroundColor}")`,
- `${variableName}.title("${this.getAttrValue("title")}")`
- ]
+ const {width, height} = this.getSize()
+
+ const code = [
+ `${variableName} = ctk.CTk()`,
+ `${variableName}.configure(fg_color="${backgroundColor}")`,
+ `${variableName}.title("${this.getAttrValue("title")}")`,
+ `${variableName}.geometry("${width}x${height}")`,
+ ...this.getGridLayoutConfigurationCode(variableName)
+ ]
+
+ if (logo?.name){
+
+ // code.push(`\n`)
+ code.push(`${variableName}_img = Image.open(${getPythonAssetPath(logo.name, "image")})`)
+ code.push(`${variableName}_img = ImageTk.PhotoImage(${variableName}_img)`)
+ code.push(`${variableName}.iconphoto(False, ${variableName}_img)`)
+ // code.push("\n")
+ }
+
+ return code
+ }
+
+ getImports(){
+ const imports = super.getImports()
+
+ if (this.getAttrValue("logo"))
+ imports.push("import os", "from PIL import Image, ImageTk", )
+
+ return imports
+ }
+
+
+ getRequirements(){
+ const requirements = super.getRequirements()
+
+
+ if (this.getAttrValue("logo"))
+ requirements.push("pillow")
+
+ return requirements
}
getToolbarAttrs(){
@@ -58,8 +109,8 @@ class MainWindow extends CustomTkBase{
id: this.__id,
widgetName: toolBarAttrs.widgetName,
title: this.state.attrs.title,
+ logo: this.state.attrs.logo,
size: toolBarAttrs.size,
-
...this.state.attrs,
})
@@ -83,7 +134,7 @@ class MainWindow extends CustomTkBase{
- {this.props.children}
+ {this.renderTkinterLayout()}
)
diff --git a/src/frameworks/customtk/widgets/toplevel.js b/src/frameworks/customtk/widgets/toplevel.js
index a379abe..cc20e42 100644
--- a/src/frameworks/customtk/widgets/toplevel.js
+++ b/src/frameworks/customtk/widgets/toplevel.js
@@ -1,8 +1,10 @@
import Widget from "../../../canvas/widgets/base"
import Tools from "../../../canvas/constants/tools"
+import { getPythonAssetPath } from "../../utils/pythonFilePath"
+import { CustomTkBase } from "./base"
-class TopLevel extends Widget{
+class TopLevel extends CustomTkBase{
static widgetType = "toplevel"
static displayName = "Top Level"
@@ -27,6 +29,13 @@ class TopLevel extends Widget{
toolProps: {placeholder: "Window title", maxLength: 40},
value: "Top level",
onChange: (value) => this.setAttrValue("title", value)
+ },
+ logo: {
+ label: "Toplevel Logo",
+ tool: Tools.UPLOADED_LIST,
+ toolProps: {filterOptions: ["image/jpg", "image/jpeg", "image/png"]},
+ value: "",
+ onChange: (value) => this.setAttrValue("logo", value)
}
}
@@ -42,11 +51,49 @@ class TopLevel extends Widget{
const backgroundColor = this.getAttrValue("styling.backgroundColor")
- return [
- `${variableName} = ctk.CTkToplevel(master=${parent})`,
- `${variableName}.configure(fg_color="${backgroundColor}")`,
- `${variableName}.title("${this.getAttrValue("title")}")`
- ]
+ const logo = this.getAttrValue("logo")
+
+ const {width, height} = this.getSize()
+
+ const code = [
+ `${variableName} = ctk.CTkToplevel(master=${parent})`,
+ `${variableName}.configure(fg_color="${backgroundColor}")`,
+ `${variableName}.title("${this.getAttrValue("title")}")`,
+ `${variableName}.geometry("${width}x${height}")`,
+
+ ...this.getGridLayoutConfigurationCode(variableName)
+ ]
+
+ if (logo?.name){
+
+ // code.push(`\n`)
+ code.push(`${variableName}_img = Image.open(${getPythonAssetPath(logo.name, "image")})`)
+ code.push(`${variableName}_img = ImageTk.PhotoImage(${variableName}_img)`)
+ code.push(`${variableName}.iconphoto(False, ${variableName}_img)`)
+ // code.push("\n")
+ }
+
+ return code
+ }
+
+ getImports(){
+ const imports = super.getImports()
+
+ if (this.getAttrValue("logo"))
+ imports.push("import os", "from PIL import Image, ImageTk", )
+
+ return imports
+ }
+
+
+ getRequirements(){
+ const requirements = super.getRequirements()
+
+
+ if (this.getAttrValue("logo"))
+ requirements.push("pillow")
+
+ return requirements
}
getToolbarAttrs(){
@@ -55,8 +102,8 @@ class TopLevel extends Widget{
return ({
widgetName: toolBarAttrs.widgetName,
title: this.state.attrs.title,
+ logo: this.state.attrs.logo,
size: toolBarAttrs.size,
-
...this.state.attrs,
})
@@ -80,7 +127,7 @@ class TopLevel extends Widget{
- {this.props.children}
+ {this.renderTkinterLayout()}
)
diff --git a/src/frameworks/tkinter/widgets/base.js b/src/frameworks/tkinter/widgets/base.js
index ac7804e..437ea9c 100644
--- a/src/frameworks/tkinter/widgets/base.js
+++ b/src/frameworks/tkinter/widgets/base.js
@@ -659,7 +659,7 @@ export class TkinterBase extends Widget {
{this.renderPackWidgetsRecursively(widgets, index + 1, side, previousExpandValue)}
);
- };
+ }
@@ -842,18 +842,6 @@ export class TkinterBase extends Widget {
const {layout: parentLayout, direction, gap} = this.getParentLayout() || {}
- // if (parentLayout === Layouts.FLEX){
- // const fillX = this.getAttrValue("flexManager.fillX")
- // const fillY = this.getAttrValue("flexManager.fillY")
-
- // // This is needed if fillX or fillY is true, as the parent is applied flex-grow
-
- // if (fillX || fillY){
- // width = "100%"
- // height = "100%"
- // }
-
- // }
const styling = {
...this.state.widgetInnerStyling,