import { Layouts, PosType } from "../../../canvas/constants/layouts" import Tools from "../../../canvas/constants/tools" import Widget from "../../../canvas/widgets/base" import { convertObjectToKeyValueString, isNumeric, removeKeyFromObject } from "../../../utils/common" import { randomArrayChoice } from "../../../utils/random" import { Tkinter_TO_WEB_CURSOR_MAPPING } from "../constants/cursor" import { Tkinter_To_GFonts } from "../constants/fontFamily" import { JUSTIFY, RELIEF } from "../constants/styling" export class TkinterBase extends Widget { static requiredImports = ['import tkinter as tk'] constructor(props) { super(props) this.getLayoutCode = this.getLayoutCode.bind(this) console.log("constructor 1: ", this.__id, this.state) this.state = { ...this.state, packAttrs: { side: "left", anchor: "nw" } } this.getPackSide = this.getPackSide.bind(this) this.renderTkinterLayout = this.renderTkinterLayout.bind(this) } getLayoutCode(){ const {layout: parentLayout, direction, gap, align="start"} = this.getParentLayout() const absolutePositioning = this.getAttrValue("positioning") let layoutManager = `pack()` if (parentLayout === Layouts.PLACE || absolutePositioning){ const config = { x: Math.trunc(this.state.pos.x), 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 config = { side: direction === "row" ? "tk.LEFT" : "tk.TOP", } 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 col = this.getAttrValue("gridManager.column") layoutManager = `grid(row=${row}, column=${col})` } return layoutManager } getPackAttrs = () => { // NOTE: tis returns (creates) a new object everytime causing unncessary renders return ({ side: this.state.packAttrs.side, anchor: this.state.packAttrs.anchor, }) } getPackSide(){ return this.state.packAttrs.side } setParentLayout(layout){ if (!layout){ return {} } 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 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", 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) => { // FIXME: force parent rerender because, here only child get rerendered, if only parent get rerendered the widget would move this.setAttrValue("flexManager.side", value, () => { this.updateState((prevState) => ({packAttrs: {...prevState.packAttrs, side: value}}), () => { this.props.requestWidgetDataUpdate(this.__id) this.stateChangeSubscriberCallback() // call this to notify the toolbar that the widget has changed state // this.props.parentWidgetRef.current.forceRerender() }) }) // console.log("updateing state: ", value, this.props.parentWidgetRef.current) } }, anchor: { label: "Anchor", tool: Tools.SELECT_DROPDOWN, options: ["nw", "ne", "sw", "se", "center"].map(val => ({value: val, label: val})), value: this.state.packAttrs.anchor, onChange: (value) => { this.setAttrValue("flexManager.anchor", value, () => { console.log("anchor updated") this.updateState((prevState) => ({packAttrs: {...prevState.packAttrs, anchor: value}}), () => { this.props.requestWidgetDataUpdate(this.__id) this.stateChangeSubscriberCallback() // call this to notify the toolbar that the widget has changed state // this.props.parentWidgetRef.current.forceRerender() }) // this.props.parentWidgetRef.current.forceRerender() }) } }, 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%" } })) } }, expand: { label: "Expand", tool: Tools.CHECK_BUTTON, value: false, onChange: (value) => { this.setAttrValue("flexManager.expand", value) const widgetStyle = { ...this.state.widgetOuterStyling, flexGrow: value ? 1 : 0, } this.updateState({ widgetOuterStyling: widgetStyle, }) } }, } } } } 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: "width", max: 1000, min: 1 }, value: 1, 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) } this.setAttrValue("gridManager.row", value) this.setWidgetOuterStyle("gridRow", `${value+' / '+rowSpan}`) } }, rowSpan: { label: "Row span", tool: Tools.NUMBER_INPUT, toolProps: { placeholder: "height", max: 1000, min: 1 }, value: 1, onChange: (value) => { const previousRow = this.getWidgetOuterStyle("gridRow") || "1/1" const [row=1, _rowSpan=1] = previousRow.replace(/\s+/g, '').split("/").map(Number) if (value < row){ value = row + 1 } this.setAttrValue("gridManager.rowSpan", value) this.setWidgetOuterStyle("gridRow", `${row + ' / ' +value}`) } }, column: { label: "Column", tool: Tools.NUMBER_INPUT, toolProps: { placeholder: "height", max: 1000, min: 1 }, value: 1, onChange: (value) => { const previousRow = this.getWidgetOuterStyle("gridColumn") || "1/1" let [_col=1, colSpan=1] = previousRow.replace(/\s+/g, '').split("/").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 +' / ' + colSpan}`) } }, columnSpan: { label: "Column span", tool: Tools.NUMBER_INPUT, toolProps: { placeholder: "height", max: 1000, min: 1 }, value: 1, onChange: (value) => { const previousCol = this.getWidgetOuterStyle("gridColumn") || "1/1" const [col=1, _colSpan=1] = previousCol.replace(/\s+/g, '').split("/").map(Number) if (value < col){ value = col + 1 } this.setAttrValue("gridManager.columnSpan", value) this.setWidgetOuterStyle("gridColumn", `${col + ' / ' + value}`) } }, } } } } } else if (parentLayout === Layouts.PLACE) { updates = { ...updates, positionType: PosType.ABSOLUTE } } this.updateState((prevState) => ({...prevState, ...updates})) return updates } getFlexLayoutStyle = (side, anchor) => { // let baseStyle = { display: "flex", width: "100%", height: "100%", ...this.getPackAnchorStyle(anchor) } let baseStyle = { } const rowStyle = { display: "flex", gap: "10px" } const columnStyle = { display: "flex", flexDirection: "column", gap: "10px" } switch (side) { case "top": return { gridColumn: "1 / -1", alignSelf: "stretch", width: "100%", ...baseStyle, ...columnStyle }; case "bottom": return { gridColumn: "1 / -1", alignSelf: "stretch", width: "100%", ...baseStyle, ...columnStyle }; case "left": return { gridRow: "2", gridColumn: "1", justifySelf: "stretch", height: "100%", ...baseStyle, ...rowStyle }; case "right": return { gridRow: "2", gridColumn: "3", justifySelf: "stretch", height: "100%", ...baseStyle, ...rowStyle }; case "center": return { gridRow: "2", gridColumn: "2", alignSelf: "center", justifySelf: "center", ...baseStyle, }; default: return {}; } } /** * Pack manager has anchor parameter * @param {*} anchor */ getPackAnchorStyle = (anchor, isColumn) => { const styleMap = { nw: { justifyContent: "flex-start", alignItems: "flex-start" }, ne: { justifyContent: "flex-end", alignItems: "flex-start" }, sw: { justifyContent: "flex-start", alignItems: "flex-end" }, se: { justifyContent: "flex-end", alignItems: "flex-end" }, center: { justifyContent: "center", alignItems: "center" } } // return styleMap[anchor] || {} const baseStyle = styleMap[anchor] || {}; const fillX = this.getAttrValue("flexManager.fillX") const fillY = this.getAttrValue("flexManager.fillY") if (fillX) { return { ...baseStyle, width: "100%", flexGrow: isColumn ? 0 : 1 }; } if (fillY) { return { ...baseStyle, height: "100%", flexGrow: isColumn ? 1 : 0 }; } if (fillX && fillY) { return { ...baseStyle, width: "100%", height: "100%", flexGrow: 1 }; } return { ...baseStyle, alignSelf: "stretch", // Forces stretching in grid rows justifySelf: "stretch", // Forces stretching in grid columns // flexGrow: fill ? 1 : 0 }; } /** * * Helps with pack layout manager and grid manager */ renderTkinterLayout(){ const {layout, direction, gap} = this.getLayout() if (layout === Layouts.FLEX){ return ( <> {(this.props.children.length > 0) && ["top", "bottom", "left", "right"].map((pos) => { const filteredChildren = this.props.children.filter((item) => { const widgetRef = item.ref?.current if (!widgetRef) return false // Ensure ref exists before accessing const packAttrs = widgetRef.getPackAttrs()// Cache value return (packAttrs.side || "left") === pos }) const isColumn = pos === "top" || pos === "bottom"; return (