Files
PyUIBuilder/src/frameworks/tkinter/widgets/base.js

1275 lines
50 KiB
JavaScript
Raw Normal View History

2025-03-24 15:50:46 +05:30
import lo from 'lodash'
2024-09-25 17:27:12 +05:30
import { Layouts, PosType } from "../../../canvas/constants/layouts"
2024-09-26 11:59:24 +05:30
import Tools from "../../../canvas/constants/tools"
import Widget from "../../../canvas/widgets/base"
2025-03-24 15:50:46 +05:30
import { DynamicGridWeightInput } from "../../../components/inputs"
2025-03-16 10:01:35 +05:30
import { convertObjectToKeyValueString, isNumeric, removeKeyFromObject } from "../../../utils/common"
2025-03-18 18:08:25 +05:30
import { randomArrayChoice } from "../../../utils/random"
2024-09-27 16:04:03 +05:30
import { Tkinter_TO_WEB_CURSOR_MAPPING } from "../constants/cursor"
import { Tkinter_To_GFonts } from "../constants/fontFamily"
2025-03-26 15:08:25 +05:30
import { ANCHOR, GRID_STICKY, JUSTIFY, RELIEF } from "../constants/styling"
2024-09-25 17:27:12 +05:30
// FIXME: grid sticky may clash with flex sticky when changing layout, check it once
// FIXME: widget items should add width and height in tkinter code especially in frame
2025-03-24 21:04:49 +05:30
// TODO: width and height aren't really fixed
2024-09-27 16:04:03 +05:30
export class TkinterBase extends Widget {
2024-09-26 11:59:24 +05:30
static requiredImports = ['import tkinter as tk']
constructor(props) {
super(props)
this.getLayoutCode = this.getLayoutCode.bind(this)
2025-03-18 18:08:25 +05:30
this.state = {
...this.state,
2025-03-25 15:34:18 +05:30
packAttrs: { // This is required as during flex layout change remount happens and the state updates my not function as expected
2025-03-20 11:25:07 +05:30
side: "top",
2025-03-26 15:08:25 +05:30
anchor: "n",
2025-03-18 21:29:41 +05:30
}
}
2025-03-18 18:08:25 +05:30
this.renderTkinterLayout = this.renderTkinterLayout.bind(this)
2024-09-26 11:59:24 +05:30
}
2025-03-18 21:29:41 +05:30
2024-09-26 11:59:24 +05:30
getLayoutCode(){
2024-09-30 15:30:46 +05:30
const {layout: parentLayout, direction, gap, align="start"} = this.getParentLayout()
2024-09-26 11:59:24 +05:30
2025-03-24 15:50:46 +05:30
const absolutePositioning = this.getAttrValue("positioning")
2024-09-26 11:59:24 +05:30
let layoutManager = `pack()`
if (parentLayout === Layouts.PLACE || absolutePositioning){
const config = {
2025-03-10 10:37:57 +05:30
x: Math.trunc(this.state.pos.x),
y: Math.trunc(this.state.pos.y),
}
2025-03-10 10:37:57 +05:30
config["width"] = Math.trunc(this.state.size.width)
config["height"] = Math.trunc(this.state.size.height)
2024-09-30 15:30:46 +05:30
// 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})`
2024-09-30 15:30:46 +05:30
}else if (parentLayout === Layouts.FLEX){
2025-03-22 11:11:19 +05:30
const packSide = this.getAttrValue("flexManager.side")
2025-03-26 15:08:25 +05:30
const marginX = this.getAttrValue("margin.marginX")
const marginY = this.getAttrValue("margin.marginY")
const paddingX = this.getAttrValue("padding.padX")
const paddingY = this.getAttrValue("padding.padY")
2025-03-22 11:11:19 +05:30
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`
}
2025-03-26 15:08:25 +05:30
// if (gap > 0){
// config["padx"] = gap
// config["pady"] = gap
// }
if (marginX){
config["padx"] = marginX
}
if (marginY){
config["pady"] = marginY
}
if (paddingX){
config["ipadx"] = paddingX
2024-09-30 15:30:46 +05:30
}
2025-03-26 15:08:25 +05:30
if (paddingY){
config["ipady"] = paddingY
}
2025-03-16 10:01:35 +05:30
// if (align === "start"){
// config["anchor"] = "'nw'"
// }else if (align === "center"){
// config["anchor"] = "'center'"
// }else if (align === "end"){
// config["anchor"] = "'se'"
// }
2024-09-30 15:30:46 +05:30
const fillX = this.getAttrValue("flexManager.fillX")
const fillY = this.getAttrValue("flexManager.fillY")
const expand = this.getAttrValue("flexManager.expand")
if (fillX){
2024-09-29 20:57:10 +05:30
config['fill'] = `"x"`
}
if (fillY){
2024-09-29 20:57:10 +05:30
config['fill'] = `"y"`
}
if (fillX && fillY){
2024-09-29 20:57:10 +05:30
config['fill'] = `"both"`
}
if (expand){
2024-09-29 20:57:10 +05:30
config['expand'] = "True"
}
layoutManager = `pack(${convertObjectToKeyValueString(config)})`
2024-09-26 11:59:24 +05:30
}else if (parentLayout === Layouts.GRID){
const row = this.getAttrValue("gridManager.row")
2025-03-24 21:04:49 +05:30
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 !== ""){
2025-03-25 05:24:37 +05:30
config['sticky'] = `"${sticky}"`
2025-03-24 21:04:49 +05:30
}
layoutManager = `grid(${convertObjectToKeyValueString(config)})`
2024-09-26 11:59:24 +05:30
}
return layoutManager
}
2025-03-24 15:50:46 +05:30
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
2025-03-24 15:50:46 +05:30
);// 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
2025-03-25 05:24:37 +05:30
) // converts the format : {index: {gridNo, weight}} to {gridNo: weight}
2025-03-24 15:50:46 +05:30
const groupByWeight = Object.entries(correctedColWeight).reduce((acc, [gridNo, weight]) => {
if (!acc[weight])
2025-03-25 05:24:37 +05:30
acc[weight] = [] // Initialize array if it doesn't exist
2025-03-24 15:50:46 +05:30
2025-03-25 05:24:37 +05:30
acc[weight].push(Number(gridNo)) // Convert key to number and add it to the array
return acc
2025-03-24 15:50:46 +05:30
}, {})
Object.entries(groupByWeight).forEach(([weight, indices]) => {
columnConfigure.push(`${variableName}.grid_columnconfigure(index=[${indices.join(",")}], weight=${weight})`)
})
}
}
return [...rowConfigure, ...columnConfigure]
}
2025-03-16 10:01:35 +05:30
getPackAttrs = () => {
2025-03-18 18:08:25 +05:30
return ({
2025-03-18 21:29:41 +05:30
side: this.state.packAttrs.side,
anchor: this.state.packAttrs.anchor,
2025-03-25 15:34:18 +05:30
expand: this.getAttrValue("flexManager.expand"),
2025-03-18 18:08:25 +05:30
})
}
2025-03-24 15:50:46 +05:30
/**
* A simple function that returns a mapping for grid sticky tkinter
*/
getGridStickyStyling(sticky){
const styleMapping = {
2025-03-24 21:04:49 +05:30
[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
2025-03-24 15:50:46 +05:30
};
return styleMapping[sticky]
}
setParentLayout(layout){
2024-09-26 11:59:24 +05:30
if (!layout){
2024-09-27 16:04:03 +05:30
return {}
}
2025-03-22 11:11:19 +05:30
let updates = super.setParentLayout(layout)
2024-09-27 16:04:03 +05:30
const {layout: parentLayout, direction, gap} = layout
2024-09-26 11:59:24 +05:30
2024-09-25 17:27:12 +05:30
// show attributes related to the layout manager
2024-09-29 23:34:26 +05:30
// remove gridManager, flexManager positioning
const {gridManager, flexManager, positioning, ...restAttrs} = this.state.attrs
if (parentLayout === Layouts.FLEX || parentLayout === Layouts.GRID) {
2024-09-25 17:27:12 +05:30
updates = {
...updates,
2025-03-08 12:02:00 +05:30
// pos: pos,
positionType: PosType.NONE,
}
// Allow optional absolute positioning if the parent layout is flex or grid
const updateAttrs = {
2024-09-29 23:34:26 +05:30
...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,
})
}
}
2024-09-25 17:27:12 +05:30
}
if (parentLayout === Layouts.FLEX){
updates = {
...updates,
attrs: {
...updateAttrs,
flexManager: {
2025-03-16 10:01:35 +05:30
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%"
}
}))
}
},
2025-03-21 09:10:48 +05:30
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) => {
2025-03-21 09:10:48 +05:30
this.setAttrValue("flexManager.side", value, () => {
this.updateState((prevState) => ({packAttrs: {...prevState.packAttrs, side: value}}), () => {
2025-03-25 15:34:18 +05:30
// this.props.parentWidgetRef.current.forceRerender()
2025-03-21 09:10:48 +05:30
this.props.requestWidgetDataUpdate(this.props.parentWidgetRef.current.__id)
this.stateChangeSubscriberCallback() // call this to notify the toolbar that the widget has changed state
})
2025-03-25 15:34:18 +05:30
})
2025-03-21 09:10:48 +05:30
// console.log("updateing state: ", value, this.props.parentWidgetRef.current)
}
},
2025-03-25 15:34:18 +05:30
expand: {
label: "Expand",
tool: Tools.CHECK_BUTTON,
value: false,
onChange: (value) => {
this.setAttrValue("flexManager.expand", value)
// this.setWidgetOuterStyle(value ? 1 : 0)
}
},
2025-03-26 15:08:25 +05:30
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, () => {
console.log("set anchor: ", this.state.attrs.flexManager)
// 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) {
2024-09-25 17:27:12 +05:30
// Set attributes related to grid layout manager
updates = {
...updates,
attrs: {
...updateAttrs,
2024-09-25 17:27:12 +05:30
gridManager: {
label: "Grid manager",
display: "horizontal",
row: {
label: "Row",
tool: Tools.NUMBER_INPUT,
toolProps: { placeholder: "row", max: 1000, min: 1 },
2025-03-23 19:02:54 +05:30
value: 0,
2024-09-25 17:27:12 +05:30
onChange: (value) => {
const previousRow = this.getWidgetOuterStyle("gridRow") || "1 / span 1"
2025-03-23 19:02:54 +05:30
let [_row=1, rowSpan=1] = previousRow.replace(/\s+/g, '').split("/span").map(Number)
this.setAttrValue("gridManager.row", value)
2025-03-23 19:02:54 +05:30
this.setWidgetOuterStyle("gridRow", `${value+' / span '+rowSpan}`)
2024-09-25 17:27:12 +05:30
}
},
rowSpan: {
label: "Row span",
tool: Tools.NUMBER_INPUT,
2025-03-23 19:02:54 +05:30
toolProps: { placeholder: "row span", max: 1000, min: 1 },
2024-09-25 17:27:12 +05:30
value: 1,
onChange: (value) => {
2025-03-23 19:02:54 +05:30
const previousRow = this.getWidgetOuterStyle("gridRow") || "1 / span 1"
2025-03-23 19:02:54 +05:30
// 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)
2025-03-23 19:02:54 +05:30
// if (value < row){
// value = row + 1
// }
if (value < 1){
value = 1
}
this.setAttrValue("gridManager.rowSpan", value)
2025-03-23 19:02:54 +05:30
this.setWidgetOuterStyle("gridRow", `${(row || 1) + ' / span ' +value}`)
2024-09-25 17:27:12 +05:30
}
},
column: {
label: "Column",
tool: Tools.NUMBER_INPUT,
toolProps: { placeholder: "column", max: 1000, min: 1 },
2025-03-23 19:02:54 +05:30
value: 0,
2024-09-25 17:27:12 +05:30
onChange: (value) => {
const previousRow = this.getWidgetOuterStyle("gridColumn") || "1 / span 1"
2024-09-25 17:27:12 +05:30
2025-03-23 19:02:54 +05:30
let [_col=1, colSpan=1] = previousRow.replace(/\s+/g, '').split("/span").map(Number)
2025-03-23 19:02:54 +05:30
// 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)
2025-03-23 19:02:54 +05:30
this.setWidgetOuterStyle("gridColumn", `${value +' / span ' + colSpan}`)
2024-09-25 17:27:12 +05:30
}
},
columnSpan: {
label: "Column span",
tool: Tools.NUMBER_INPUT,
2025-03-23 19:02:54 +05:30
toolProps: { placeholder: "column span", max: 1000, min: 1 },
2024-09-25 17:27:12 +05:30
value: 1,
onChange: (value) => {
2025-03-23 19:02:54 +05:30
const previousCol = this.getWidgetOuterStyle("gridColumn") || "1 / span 1"
2024-09-25 17:27:12 +05:30
2025-03-23 19:02:54 +05:30
const [col=1, _colSpan=1] = previousCol.replace(/\s+/g, '').split("/span").map(Number)
2024-09-25 17:27:12 +05:30
2025-03-23 19:02:54 +05:30
if (value < 1){
value = 1
}
this.setAttrValue("gridManager.columnSpan", value)
2025-03-23 19:02:54 +05:30
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})),
2025-03-23 19:02:54 +05:30
value: GRID_STICKY.NONE,
onChange: (value) => {
this.setAttrValue("gridManager.sticky", value)
2025-03-24 21:04:49 +05:30
this.updateState((prev) => {
const { alignSelf, justifySelf, placeSelf, ...restStates } = prev.widgetOuterStyling; // Remove these properties
return ({
widgetOuterStyling: {
...restStates,
...this.getGridStickyStyling(value)
}
})
2025-03-24 15:50:46 +05:30
})
2025-03-24 15:50:46 +05:30
// this.setW
2024-09-25 17:27:12 +05:30
}
2025-03-24 15:50:46 +05:30
}
2024-09-25 17:27:12 +05:30
}
}
}
}
} else if (parentLayout === Layouts.PLACE) {
2024-09-25 17:27:12 +05:30
updates = {
...updates,
positionType: PosType.ABSOLUTE
}
}
2025-03-16 10:01:35 +05:30
2025-03-18 21:29:41 +05:30
this.updateState((prevState) => ({...prevState, ...updates}))
2025-03-16 10:01:35 +05:30
2024-09-25 17:27:12 +05:30
return updates
}
2025-03-18 21:29:41 +05:30
getFlexLayoutStyle = (side, anchor) => {
2025-03-21 09:10:48 +05:30
// NOTE: may no longer be required
2025-03-18 21:29:41 +05:30
// let baseStyle = { display: "flex", width: "100%", height: "100%", ...this.getPackAnchorStyle(anchor) }
let baseStyle = { }
2025-03-16 10:01:35 +05:30
const rowStyle = {
display: "flex",
2025-03-20 11:25:07 +05:30
gap: "10px",
2025-03-16 10:01:35 +05:30
}
const columnStyle = {
display: "flex",
flexDirection: "column",
2025-03-20 11:25:07 +05:30
gap: "10px",
2025-03-16 10:01:35 +05:30
}
switch (side) {
case "top":
2025-03-20 11:25:07 +05:30
return { gridColumn: "1 / -1", alignSelf: "stretch", ...baseStyle, ...columnStyle };
case "bottom":
2025-03-20 11:25:07 +05:30
return { gridColumn: "1 / -1", alignSelf: "stretch", ...baseStyle, ...columnStyle };
case "left":
2025-03-20 11:25:07 +05:30
return { gridRow: "2", gridColumn: "1", justifySelf: "stretch", ...baseStyle, ...rowStyle };
case "right":
2025-03-20 11:25:07 +05:30
return { gridRow: "2", gridColumn: "3", justifySelf: "stretch", ...baseStyle, ...rowStyle };
case "center":
2025-03-18 21:29:41 +05:30
return { gridRow: "2", gridColumn: "2", alignSelf: "center", justifySelf: "center", ...baseStyle, };
default:
return {};
2025-03-16 10:01:35 +05:30
}
}
2025-03-18 21:29:41 +05:30
/**
* Pack manager has anchor parameter
* @param {*} anchor
*/
getPackAnchorStyle = (anchor, isColumn) => {
2025-03-21 09:10:48 +05:30
// NOTE: may no longer be required
2025-03-18 21:29:41 +05:30
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
2025-03-20 11:25:07 +05:30
flexGrow: (fillX || fillY) ? 1 : 0
2025-03-25 15:34:18 +05:30
}
2025-03-18 21:29:41 +05:30
}
2025-03-20 11:25:07 +05:30
/**
* adds the layout to achieve the pack from tkinter refer: https://www.youtube.com/watch?v=rbW1iJO1psk
* @param {*} widgets
* @param {*} index
* @returns
*/
2025-03-26 15:08:25 +05:30
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",
2025-03-20 11:25:07 +05:30
}
2025-03-26 15:08:25 +05:30
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 && !isSameSide) previousExpandValue = expandValue;
lastSide = side; // Update last side for recursion
2025-03-25 15:34:18 +05:30
2025-03-26 15:08:25 +05:30
// 🟢 Mapping Tkinter anchors to Flexbox styles
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"]
2025-03-20 11:25:07 +05:30
2025-03-26 15:08:25 +05:30
const stretchClass = isVertical ? "tw-flex-grow" : "tw-h-full"; // Allow only horizontal growth for top/bottom
2025-03-20 11:25:07 +05:30
if (isSameSide) {
return (
<>
2025-03-26 15:08:25 +05:30
<div
className={`tw-flex tw-justify-center tw-items-center ${stretchClass}`}
2025-03-25 15:34:18 +05:30
style={{
2025-03-26 15:08:25 +05:30
flexGrow: expand ? expandValue : 0, // Prevent vertical stretching
flexShrink: expand ? 0 : 1, // Prevent collapse when expanding
flexBasis: "auto",
minWidth: isVertical ? "0" : "auto",
minHeight: !isVertical ? "0" : "auto",
// alignSelf,
// justifySelf,
alignItems,
justifyContent
2025-03-25 15:34:18 +05:30
}}
2025-03-26 15:08:25 +05:30
>
2025-03-20 11:25:07 +05:30
{widget}
</div>
2025-03-26 15:08:25 +05:30
2025-03-25 15:34:18 +05:30
{this.renderPackWidgetsRecursively(widgets, index + 1, side, previousExpandValue)}
2025-03-20 11:25:07 +05:30
</>
2025-03-26 15:08:25 +05:30
);
}
return (
<div
data-pack-container={side}
className={`tw-flex ${isVertical ? "!tw-h-full" : "!tw-w-full"}`}
style={{
display: "flex",
flexDirection: currentWidgetDirection,
flexGrow: expand ? expandValue : 1, //((widgets.length - 1) === index) ? 1 : 0, // last index will always have flex-grow 1
flexShrink: expand ? 0 : 1,
flexBasis: "auto",
minWidth: isVertical ? "0" : "auto",
minHeight: !isVertical ? "0" : "auto",
}}
>
<div
className={`tw-flex ${isVertical ? "tw-flex-grow" : "tw-h-full"}
tw-justify-center tw-items-center`}
2025-03-25 15:34:18 +05:30
style={{
2025-03-26 15:08:25 +05:30
flexGrow: expand ? expandValue : 0,
flexShrink: expand ? 0 : 1,
flexBasis: "auto",
minWidth: isVertical ? "0" : "auto",
minHeight: !isVertical ? "0" : "auto",
// alignSelf,
// justifySelf,
alignItems,
justifyContent
}}
>
{widget}
2025-03-20 11:25:07 +05:30
</div>
2025-03-26 15:08:25 +05:30
{this.renderPackWidgetsRecursively(widgets, index + 1, side, previousExpandValue)}
</div>
);
};
2025-03-20 11:25:07 +05:30
2025-03-18 21:29:41 +05:30
/**
*
* Helps with pack layout manager and grid manager
*/
renderTkinterLayout(){
const {layout, direction, gap} = this.getLayout()
2025-03-24 15:50:46 +05:30
2025-03-18 21:29:41 +05:30
if (layout === Layouts.FLEX){
2025-03-20 11:25:07 +05:30
2025-03-18 21:29:41 +05:30
return (
<>
2025-03-20 11:25:07 +05:30
{this.renderPackWidgetsRecursively(this.props.children)}
2025-03-18 21:29:41 +05:30
</>
)
}
return (<>{this.props.children}</>)
}
2025-03-16 10:01:35 +05:30
setLayout(value) {
const { layout, direction, grid = { rows: 1, cols: 1 }, gap = 10, align } = value
2025-03-26 15:08:25 +05:30
// FIXME: when the layout changes the data is lost
2025-03-23 19:02:54 +05:30
if (layout === Layouts.GRID){
const {gridManager, flexManager, positioning, ...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)
2025-03-24 15:50:46 +05:30
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
}
2025-03-26 15:08:25 +05:30
this.updateState((prev) => ({
widgetInnerStyling: {
...prev.widgetInnerStyling,
2025-03-24 15:50:46 +05:30
gridTemplateRows: gridTemplateRows
2025-03-26 15:08:25 +05:30
}
}))
2025-03-23 19:02:54 +05:30
}
},
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)
2025-03-26 15:08:25 +05:30
this.updateState((prev) => ({
widgetInnerStyling: {
...prev.widgetInnerStyling,
gridTemplateColumns: `repeat(${value}, max-content)`
}
}))
2025-03-23 19:02:54 +05:30
}
},
2025-03-24 15:50:46 +05:30
2025-03-23 19:02:54 +05:30
},
2025-03-24 15:50:46 +05:30
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
}
}
}
2025-03-23 19:02:54 +05:30
}
}
this.updateState((prevState) => ({...prevState, ...updates}))
2025-03-20 11:25:07 +05:30
2025-03-16 10:01:35 +05:30
}
this.props.onLayoutUpdate({parentId: this.__id, parentLayout: value})// inform children about the layout update
2025-03-26 15:08:25 +05:30
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))",
}
}))
2025-03-16 10:01:35 +05:30
2025-03-26 15:08:25 +05:30
2025-03-16 10:01:35 +05:30
}
getInnerRenderStyling(){
let {width, height, minWidth, minHeight} = this.getRenderSize()
2025-03-20 11:25:07 +05:30
const {layout: parentLayout, direction, gap} = this.getParentLayout() || {}
2025-03-16 10:01:35 +05:30
// if (parentLayout === Layouts.FLEX){
// const fillX = this.getAttrValue("flexManager.fillX")
// const fillY = this.getAttrValue("flexManager.fillY")
2025-03-16 10:01:35 +05:30
// // This is needed if fillX or fillY is true, as the parent is applied flex-grow
2025-03-16 10:01:35 +05:30
// if (fillX || fillY){
// width = "100%"
// height = "100%"
// }
2025-03-16 10:01:35 +05:30
// }
2025-03-20 11:25:07 +05:30
const styling = {
...this.state.widgetInnerStyling,
width,
height,
minWidth,
minHeight
}
return styling
}
2025-03-16 10:01:35 +05:30
getRenderSize(){
let {width, height, minWidth, minHeight} = super.getRenderSize()
let fillX = this.getAttrValue("flexManager.fillX") || false
let fillY = this.getAttrValue("flexManager.fillY") || false
2025-03-24 21:04:49 +05:30
const {layout: parentLayout} = (this.getParentLayout() || {})
2025-03-16 10:01:35 +05:30
if (fillX){
width = "100%"
}
if (fillY){
height = "100%"
}
2025-03-24 21:04:49 +05:30
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%"
}
}
2025-03-16 10:01:35 +05:30
return {width, height, minWidth, minHeight}
}
serialize(){
return ({
...super.serialize(),
2025-03-18 18:08:25 +05:30
attrs: this.serializeAttrsValues(), // makes sure that functions are not serialized
2025-03-18 21:29:41 +05:30
packAttrs: this.state.packAttrs,
})
}
2024-09-25 17:27:12 +05:30
/**
* loads the data
* @param {object} data
*/
load(data, callback=null){
2024-09-25 17:27:12 +05:30
2025-03-06 19:20:40 +05:30
// TODO: call the base widget
2024-09-25 17:27:12 +05:30
if (Object.keys(data).length === 0) return // no data to load
data = {...data} // create a shallow copy
2025-03-22 11:11:19 +05:30
const {attrs={}, selected, pos={x: 0, y: 0}, ...restData} = data
const parentLayout = this.props.parentWidgetRef?.current?.getLayout()
2024-09-25 17:27:12 +05:30
let layoutUpdates = {
parentLayout: parentLayout
}
2024-09-27 16:04:03 +05:30
if (parentLayout){
if (parentLayout.layout === Layouts.FLEX || parentLayout.layout === Layouts.GRID){
2024-09-25 17:27:12 +05:30
2024-09-27 16:04:03 +05:30
layoutUpdates = {
...layoutUpdates,
positionType: PosType.NONE
}
2024-09-25 17:27:12 +05:30
2024-09-27 16:04:03 +05:30
}else if (parentLayout.layout === Layouts.PLACE){
layoutUpdates = {
...layoutUpdates,
positionType: PosType.ABSOLUTE
}
2024-09-25 17:27:12 +05:30
}
}
const newData = {
...restData,
...layoutUpdates,
pos
2024-09-25 17:27:12 +05:30
}
2024-09-27 16:04:03 +05:30
2024-09-25 17:27:12 +05:30
this.setState(newData, () => {
let layoutAttrs = this.setParentLayout(parentLayout).attrs || {}
2024-09-25 17:27:12 +05:30
// 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('.')
const lastKey = keys.pop()
// Traverse the nested object within attrs
let nestedObject = newAttrs
keys.forEach(key => {
nestedObject[key] = { ...nestedObject[key] } // Ensure immutability for each nested level
nestedObject = nestedObject[key]
})
// Set the value at the last key
if (nestedObject[lastKey])
nestedObject[lastKey].value = value
})
if (newAttrs?.styling?.backgroundColor){
// TODO: find a better way to apply innerStyles
this.setWidgetInnerStyle("backgroundColor", newAttrs.styling.backgroundColor.value)
}
this.updateState({ attrs: newAttrs }, callback)
2024-09-25 17:27:12 +05:30
2025-03-26 15:08:25 +05:30
// FIXME: when changing layouts all the widgets are being selected
2025-03-18 18:08:25 +05:30
if (selected){
this.select()
}
2024-09-25 17:27:12 +05:30
})
2025-03-06 19:20:40 +05:30
2024-09-25 17:27:12 +05:30
}
}
2024-09-27 16:04:03 +05:30
// base for widgets that have common base properties such as bg, fg, cursor etc
export class TkinterWidgetBase extends TkinterBase{
constructor(props) {
super(props)
this.droppableTags = null // disables drops
const newAttrs = removeKeyFromObject("layout", this.state.attrs)
this.state = {
...this.state,
attrs: {
...newAttrs,
styling: {
...newAttrs.styling,
foregroundColor: {
label: "Foreground Color",
tool: Tools.COLOR_PICKER,
value: "#000",
onChange: (value) => {
this.setWidgetInnerStyle("color", value)
this.setAttrValue("styling.foregroundColor", value)
}
},
borderWidth: {
label: "Border thickness",
tool: Tools.NUMBER_INPUT,
toolProps: {min: 0, max: 10},
value: 0,
onChange: (value) => {
this.setWidgetInnerStyle("border", `${value}px solid black`)
this.setAttrValue("styling.borderWidth", value)
}
},
relief: {
label: "Relief",
tool: Tools.SELECT_DROPDOWN,
options: RELIEF.map((val) => ({value: val, label: val})),
value: "",
2024-09-27 16:04:03 +05:30
onChange: (value) => {
// this.setWidgetInnerStyle("fontFamily", Tkinter_To_GFonts[value])
this.setAttrValue("styling.relief", value)
2024-09-27 16:04:03 +05:30
}
},
// justify: {
// label: "Justify",
// tool: Tools.SELECT_DROPDOWN,
// options: JUSTIFY.map((val) => ({value: val, label: val})),
// value: "",
// onChange: (value) => {
// this.setWidgetInnerStyle("text-align", value)
// this.setAttrValue("styling.justify", value)
// }
// }
},
padding: {
label: "padding",
padX: {
label: "Pad X",
tool: Tools.NUMBER_INPUT,
2025-03-14 16:42:27 +05:30
toolProps: {min: 0, max: 140},
value: null,
onChange: (value) => {
// this.setWidgetInnerStyle("paddingLeft", `${value}px`)
// this.setWidgetInnerStyle("paddingRight", `${value}px`)
2025-03-16 10:01:35 +05:30
// const widgetStyle = {
// }
this.setState((prevState) => ({
2025-03-16 10:01:35 +05:30
widgetInnerStyling: {
...prevState.widgetInnerStyling,
paddingLeft: `${value}px`,
paddingRight: `${value}px`
}
}))
this.setAttrValue("padding.padX", value)
}
},
padY: {
label: "Pad Y",
tool: Tools.NUMBER_INPUT,
2025-03-14 16:42:27 +05:30
toolProps: {min: 0, max: 140},
value: null,
onChange: (value) => {
2025-03-16 10:01:35 +05:30
this.setState((prevState) => ({
widgetInnerStyling: {
...prevState.widgetInnerStyling,
paddingTop: `${value}px`,
paddingBottom: `${value}px`
}
}))
// this.setState({
// widgetInnerStyling: widgetStyle
// })
this.setAttrValue("padding.padX", value)
}
},
2024-09-27 16:04:03 +05:30
},
2025-03-14 16:42:27 +05:30
margin: {
label: "Margin",
marginX: {
label: "Margin X",
tool: Tools.NUMBER_INPUT,
toolProps: {min: 0, max: 140},
value: null,
onChange: (value) => {
2025-03-26 15:08:25 +05:30
this.updateState((prev) => ({
widgetOuterStyling: {
...prev.widgetOuterStyling,
marginLeft: `${value}px`,
marginRight: `${value}px`
},
}))
2025-03-14 16:42:27 +05:30
this.setAttrValue("margin.marginX", value)
}
},
marginY: {
label: "Margin Y",
tool: Tools.NUMBER_INPUT,
toolProps: {min: 0, max: 140},
value: null,
onChange: (value) => {
2025-03-26 15:08:25 +05:30
this.updateState((prev) => ({
widgetOuterStyling: {
...prev.widgetOuterStyling,
marginTop: `${value}px`,
marginBottom: `${value}px`
},
}))
2025-03-14 16:42:27 +05:30
this.setAttrValue("margin.marginY", value)
}
},
2025-03-16 10:01:35 +05:30
},
2024-09-27 16:04:03 +05:30
font: {
label: "font",
fontFamily: {
label: "font family",
tool: Tools.SELECT_DROPDOWN,
options: Object.keys(Tkinter_To_GFonts).map((val) => ({value: val, label: val})),
value: "",
2024-09-27 16:04:03 +05:30
onChange: (value) => {
this.setWidgetInnerStyle("fontFamily", Tkinter_To_GFonts[value])
this.setAttrValue("font.fontFamily", value)
}
},
fontSize: {
label: "font size",
tool: Tools.NUMBER_INPUT,
toolProps: {min: 3, max: 140},
value: null,
2024-09-27 16:04:03 +05:30
onChange: (value) => {
this.setWidgetInnerStyle("fontSize", `${value}px`)
this.setAttrValue("font.fontSize", value)
}
}
},
cursor: {
label: "Cursor",
tool: Tools.SELECT_DROPDOWN,
toolProps: {placeholder: "select cursor"},
value: "",
2024-09-27 16:04:03 +05:30
options: Object.keys(Tkinter_TO_WEB_CURSOR_MAPPING).map((val) => ({value: val, label: val})),
onChange: (value) => {
this.setWidgetInnerStyle("cursor", Tkinter_TO_WEB_CURSOR_MAPPING[value])
this.setAttrValue("cursor", value)
}
},
}
}
this.getConfigCode = this.getConfigCode.bind(this)
}
getConfigCode(){
2024-09-30 15:54:09 +05:30
const config = {
bg: `"${this.getAttrValue("styling.backgroundColor")}"`,
fg: `"${this.getAttrValue("styling.foregroundColor")}"`,
}
if (this.getAttrValue("styling.borderWidth"))
2024-09-30 15:54:09 +05:30
config["bd"] = this.getAttrValue("styling.borderWidth")
if (this.getAttrValue("styling.relief"))
2025-01-01 16:43:25 +05:30
config["relief"] = `tk.${this.getAttrValue("styling.relief")}`
if (this.getAttrValue("font.fontFamily") || this.getAttrValue("font.fontSize")){
2024-09-30 15:54:09 +05:30
config["font"] = `("${this.getAttrValue("font.fontFamily")}", ${this.getAttrValue("font.fontSize") || 12}, )`
}
if (this.getAttrValue("cursor"))
2024-09-30 15:54:09 +05:30
config["cursor"] = `"${this.getAttrValue("cursor")}"`
if (this.getAttrValue("padding.padX")){
2025-03-14 16:42:27 +05:30
// inner padding
config["ipadx"] = this.getAttrValue("padding.padX")
}
if (this.getAttrValue("padding.padY")){
2025-03-14 16:42:27 +05:30
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")
}
2024-09-30 15:54:09 +05:30
// 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
// }
return config
2024-09-27 16:04:03 +05:30
}
}