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 { ANCHOR, GRID_STICKY, JUSTIFY, RELIEF } from "../constants/styling" export class CustomTkBase extends Widget { static requiredImports = ['import customtkinter as ctk'] static requirements = ['customtkinter'] constructor(props) { 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()` const marginX = this.getAttrValue("margin.marginX") const marginY = this.getAttrValue("margin.marginY") const paddingX = this.getAttrValue("padding.padX") const paddingY = this.getAttrValue("padding.padY") let config = {} if (marginX){ config["padx"] = marginX } if (marginY){ config["pady"] = marginY } if (paddingX){ config["ipadx"] = paddingX } if (paddingY){ config["ipady"] = paddingY } if (parentLayout === Layouts.PLACE || absolutePositioning){ config['x'] = Math.trunc(this.state.pos.x) config['y'] = Math.trunc(this.state.pos.y) // config["width"] = Math.trunc(this.state.size.width) // config["height"] = Math.trunc(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 // } const configStr = convertObjectToKeyValueString(config) layoutManager = `place(${configStr})` }else if (parentLayout === Layouts.FLEX){ const packSide = this.getAttrValue("flexManager.side") const config = {} if (packSide === "" || packSide === "top"){ config['side'] = `ctk.TOP` }else if (packSide === "left"){ config['side'] = `ctk.LEFT` }else if (packSide === "right"){ config['side'] = `ctk.RIGHT` }else{ config['side'] = `ctk.BOTTOM` } // if (gap > 0){ // config["padx"] = gap // config["pady"] = gap // } // 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") if (fillX){ config['fill'] = `"x"` } if (fillY){ config['fill'] = `"y"` } if (fillX && fillY){ config['fill'] = `"both"` } if (expand){ config['expand'] = "True" } layoutManager = `pack(${convertObjectToKeyValueString(config)})` }else if (parentLayout === Layouts.GRID){ const row = this.getAttrValue("gridManager.row") const column = this.getAttrValue("gridManager.column") const rowSpan = this.getAttrValue("gridManager.rowSpan") const columnSpan = this.getAttrValue("gridManager.columnSpan") const sticky = this.getAttrValue("gridManager.sticky") config['row'] = row - 1 config['column'] = column - 1 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 {} } let updates = super.setParentLayout(layout) const {layout: parentLayout, direction, gap} = layout // show attributes related to the layout manager // remove gridManager, flexManager positioning const {gridManager, flexManager, positioning, ...restAttrs} = this.state.attrs if (parentLayout === Layouts.FLEX || parentLayout === Layouts.GRID) { updates = { ...updates, // pos: pos, positionType: PosType.NONE, } // Allow optional absolute positioning if the parent layout is flex or grid const updateAttrs = { ...restAttrs, positioning: { label: "Absolute positioning", tool: Tools.CHECK_BUTTON, value: false, onChange: (value) => { this.setAttrValue("positioning", value) this.updateState({ positionType: value ? PosType.ABSOLUTE : PosType.NONE, }) } } } if (parentLayout === Layouts.FLEX){ updates = { ...updates, attrs: { ...updateAttrs, flexManager: { label: "Pack Manager", display: "horizontal", fillX: { label: "Fill X", tool: Tools.CHECK_BUTTON, value: false, onChange: (value) => { this.setAttrValue("flexManager.fillX", value) this.updateState((prevState) => ({ widgetOuterStyling: { ...prevState.widgetOuterStyling, width: "100%" } })) } }, fillY: { label: "Fill Y", tool: Tools.CHECK_BUTTON, value: false, onChange: (value) => { this.setAttrValue("flexManager.fillY", value) 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: { label: "Expand", tool: Tools.CHECK_BUTTON, value: false, onChange: (value) => { this.setAttrValue("flexManager.expand", value) // 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() }) } }, } } } } else if (parentLayout === Layouts.GRID) { // Set attributes related to grid layout manager updates = { ...updates, attrs: { ...updateAttrs, gridManager: { label: "Grid manager", display: "horizontal", row: { label: "Row", tool: Tools.NUMBER_INPUT, toolProps: { placeholder: "row", max: 1000, min: 1 }, value: 0, onChange: (value) => { 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+' / span '+rowSpan}`) } }, rowSpan: { label: "Row span", tool: Tools.NUMBER_INPUT, toolProps: { placeholder: "row span", max: 1000, min: 1 }, value: 1, onChange: (value) => { 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("/span").map(Number) // if (value < row){ // value = row + 1 // } if (value < 1){ value = 1 } this.setAttrValue("gridManager.rowSpan", value) this.setWidgetOuterStyle("gridRow", `${(row || 1) + ' / span ' +value}`) } }, column: { label: "Column", tool: Tools.NUMBER_INPUT, toolProps: { placeholder: "column", max: 1000, min: 1 }, value: 0, onChange: (value) => { const previousRow = this.getWidgetOuterStyle("gridColumn") || "1 / span 1" 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) // } this.setAttrValue("gridManager.column", value) this.setWidgetOuterStyle("gridColumn", `${value +' / span ' + colSpan}`) } }, columnSpan: { label: "Column span", tool: Tools.NUMBER_INPUT, toolProps: { placeholder: "column span", max: 1000, min: 1 }, value: 1, onChange: (value) => { const previousCol = this.getWidgetOuterStyle("gridColumn") || "1 / span 1" const [col=1, _colSpan=1] = previousCol.replace(/\s+/g, '').split("/span").map(Number) if (value < 1){ value = 1 } this.setAttrValue("gridManager.columnSpan", 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 } } } } } } } else if (parentLayout === Layouts.PLACE) { updates = { ...updates, positionType: PosType.ABSOLUTE } } this.updateState((prevState) => ({...prevState, ...updates})) return updates } /** * 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 && expandValue === 0){ expandValue = 1 // if there is expand it should expand } if ((expand && !isSameSide) || (expand && previousExpandValue === 0)){ 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 ( <>