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,