diff --git a/src/frameworks/customtk/assets/widgets/main/Toplevel.png b/src/frameworks/customtk/assets/widgets/main/Toplevel.png new file mode 100644 index 0000000..86bbdbb Binary files /dev/null and b/src/frameworks/customtk/assets/widgets/main/Toplevel.png differ diff --git a/src/frameworks/customtk/assets/widgets/main/button.png b/src/frameworks/customtk/assets/widgets/main/button.png new file mode 100644 index 0000000..ad0d3a7 Binary files /dev/null and b/src/frameworks/customtk/assets/widgets/main/button.png differ diff --git a/src/frameworks/customtk/assets/widgets/main/check.png b/src/frameworks/customtk/assets/widgets/main/check.png new file mode 100644 index 0000000..85f1d61 Binary files /dev/null and b/src/frameworks/customtk/assets/widgets/main/check.png differ diff --git a/src/frameworks/customtk/assets/widgets/main/dropdown.png b/src/frameworks/customtk/assets/widgets/main/dropdown.png new file mode 100644 index 0000000..16f27ec Binary files /dev/null and b/src/frameworks/customtk/assets/widgets/main/dropdown.png differ diff --git a/src/frameworks/customtk/assets/widgets/main/frame.png b/src/frameworks/customtk/assets/widgets/main/frame.png new file mode 100644 index 0000000..6c78833 Binary files /dev/null and b/src/frameworks/customtk/assets/widgets/main/frame.png differ diff --git a/src/frameworks/customtk/assets/widgets/main/frame2.png b/src/frameworks/customtk/assets/widgets/main/frame2.png new file mode 100644 index 0000000..91433d4 Binary files /dev/null and b/src/frameworks/customtk/assets/widgets/main/frame2.png differ diff --git a/src/frameworks/customtk/assets/widgets/main/input.png b/src/frameworks/customtk/assets/widgets/main/input.png new file mode 100644 index 0000000..0cb4dbf Binary files /dev/null and b/src/frameworks/customtk/assets/widgets/main/input.png differ diff --git a/src/frameworks/customtk/assets/widgets/main/label.png b/src/frameworks/customtk/assets/widgets/main/label.png new file mode 100644 index 0000000..6dbfacf Binary files /dev/null and b/src/frameworks/customtk/assets/widgets/main/label.png differ diff --git a/src/frameworks/customtk/assets/widgets/main/mainwindow.jpg b/src/frameworks/customtk/assets/widgets/main/mainwindow.jpg new file mode 100644 index 0000000..72a62a0 Binary files /dev/null and b/src/frameworks/customtk/assets/widgets/main/mainwindow.jpg differ diff --git a/src/frameworks/customtk/assets/widgets/main/mainwindow.png b/src/frameworks/customtk/assets/widgets/main/mainwindow.png new file mode 100644 index 0000000..45270a0 Binary files /dev/null and b/src/frameworks/customtk/assets/widgets/main/mainwindow.png differ diff --git a/src/frameworks/customtk/assets/widgets/main/radio.png b/src/frameworks/customtk/assets/widgets/main/radio.png new file mode 100644 index 0000000..3b1b794 Binary files /dev/null and b/src/frameworks/customtk/assets/widgets/main/radio.png differ diff --git a/src/frameworks/customtk/assets/widgets/main/slider.png b/src/frameworks/customtk/assets/widgets/main/slider.png new file mode 100644 index 0000000..9916b8e Binary files /dev/null and b/src/frameworks/customtk/assets/widgets/main/slider.png differ diff --git a/src/frameworks/customtk/assets/widgets/main/spinbox.png b/src/frameworks/customtk/assets/widgets/main/spinbox.png new file mode 100644 index 0000000..de9d58c Binary files /dev/null and b/src/frameworks/customtk/assets/widgets/main/spinbox.png differ diff --git a/src/frameworks/customtk/assets/widgets/main/textarea.png b/src/frameworks/customtk/assets/widgets/main/textarea.png new file mode 100644 index 0000000..8b79b65 Binary files /dev/null and b/src/frameworks/customtk/assets/widgets/main/textarea.png differ diff --git a/src/frameworks/customtk/assets/widgets/main/widget.png b/src/frameworks/customtk/assets/widgets/main/widget.png new file mode 100644 index 0000000..2d45182 Binary files /dev/null and b/src/frameworks/customtk/assets/widgets/main/widget.png differ diff --git a/src/frameworks/customtk/assets/widgets/plugins/clock.png b/src/frameworks/customtk/assets/widgets/plugins/clock.png new file mode 100644 index 0000000..f78a281 Binary files /dev/null and b/src/frameworks/customtk/assets/widgets/plugins/clock.png differ diff --git a/src/frameworks/customtk/assets/widgets/plugins/map.png b/src/frameworks/customtk/assets/widgets/plugins/map.png new file mode 100644 index 0000000..66de3f7 Binary files /dev/null and b/src/frameworks/customtk/assets/widgets/plugins/map.png differ diff --git a/src/frameworks/customtk/assets/widgets/plugins/tables.png b/src/frameworks/customtk/assets/widgets/plugins/tables.png new file mode 100644 index 0000000..0b92acb Binary files /dev/null and b/src/frameworks/customtk/assets/widgets/plugins/tables.png differ diff --git a/src/frameworks/customtk/assets/widgets/plugins/video.png b/src/frameworks/customtk/assets/widgets/plugins/video.png new file mode 100644 index 0000000..2a85da9 Binary files /dev/null and b/src/frameworks/customtk/assets/widgets/plugins/video.png differ diff --git a/src/frameworks/customtk/constants/cursor.js b/src/frameworks/customtk/constants/cursor.js new file mode 100644 index 0000000..64a4e41 --- /dev/null +++ b/src/frameworks/customtk/constants/cursor.js @@ -0,0 +1,43 @@ + + +export const Tkinter_TO_WEB_CURSOR_MAPPING = { + + "arrow": "default", + "circle": "wait", + "clock": "wait", + "cross": "crosshair", + "dotbox": "not-allowed", + "exchange": "alias", + "fleur": "move", + "heart": "default", // mapping doesn't exist + "man": "default", + "mouse": "default", + "pirate": "default", + "plus": "zoom-in", + "shuttle": "default", + "sizing": "se-resize", + "spider": "default", + "spraycan": "default", + "star": "default", + "target": "cell", + "tcross": "crosshair", + "trek": "default", + "watch": "wait", + "X_cursor": "not-allowed", + "bottom_left_corner": "sw-resize", + "bottom_right_corner": "se-resize", + "bottom_side": "s-resize", + "top_left_corner": "nw-resize", + "top_right_corner": "ne-resize", + "top_side": "n-resize", + "left_side": "w-resize", + "right_side": "e-resize", + "hand1": "pointer", + "hand2": "pointer", + "sb_h_double_arrow": "ew-resize", + "sb_v_double_arrow": "ns-resize", + "center_ptr": "default", + "question_arrow": "help", + "umbrella": "default", + "pencil": "copy", +} diff --git a/src/frameworks/customtk/constants/fontFamily.js b/src/frameworks/customtk/constants/fontFamily.js new file mode 100644 index 0000000..3b95f86 --- /dev/null +++ b/src/frameworks/customtk/constants/fontFamily.js @@ -0,0 +1,20 @@ + + + +export const Tkinter_To_GFonts= { + "Arial": "Roboto", + "Courier": "Courier Prime", + "Comic Sans MS": "Comic Neue", + "Fixedsys": "Cousine", + "Helvetica": "Roboto", + "Times": "Merriweather", + "System": "Noto Sans", + "Verdana": "Open Sans", + "Symbol": "Noto Sans Symbols", + "TkDefaultFont": "Noto Sans", + "TkFixedFont": "Source Code Pro", + "TkMenuFont": "Roboto", + "TkHeadingFont": "Montserrat", + "TkTextFont": "Roboto", + "TkTooltipFont": "Lato", +} diff --git a/src/frameworks/customtk/constants/styling.js b/src/frameworks/customtk/constants/styling.js new file mode 100644 index 0000000..12f132e --- /dev/null +++ b/src/frameworks/customtk/constants/styling.js @@ -0,0 +1,24 @@ + +export const RELIEF = [ + "FLAT", + "RAISED", + "SUNKEN", + "GROOVE", + "RIDGE" +] + + +export const JUSTIFY = [ + "LEFT", + "CENTER", + "RIGHT" +] + + +export const ANCHOR = [ + "n", + "s", + "e", + "w", + "center" +] \ No newline at end of file diff --git a/src/frameworks/customtk/engine/code.js b/src/frameworks/customtk/engine/code.js new file mode 100644 index 0000000..76ad508 --- /dev/null +++ b/src/frameworks/customtk/engine/code.js @@ -0,0 +1,191 @@ +import { createFilesAndDownload } from "../../../codeEngine/utils" +import MainWindow from "../widgets/mainWindow" + +import { message } from "antd" +import TopLevel from "../widgets/toplevel" + + +// FIXME: if the toplevel comes first, before the MainWindow in widgetlist the root may become null +// Recursive function to generate the code list, imports, requirements, and track mainVariable +function generateTkinterCodeList(widgetList = [], widgetRefs = [], parentVariable = null, mainVariable = "", usedVariableNames = new Set()) { + let variableMapping = new Map() // Map widget to variable { widgetId: variableName } + let imports = new Set([]) + let requirements = new Set([]) + let code = [] + + for (let widget of widgetList) { + const widgetRef = widgetRefs[widget.id].current + let varName = widgetRef.getVariableName() + + // Add imports and requirements to sets + widgetRef.getImports().forEach(importItem => imports.add(importItem)) + widgetRef.getRequirements().forEach(requirementItem => requirements.add(requirementItem)) + + // Set main variable if the widget is MainWindow + if (widget.widgetType === MainWindow) { + mainVariable = varName + } + + // Ensure unique variable names across recursion + let originalVarName = varName + let count = 1; + + // Check for uniqueness and update varName + while (usedVariableNames.has(varName)) { + varName = `${originalVarName}${count}` + count++ + } + + usedVariableNames.add(varName) + variableMapping.set(widget.id, varName) // Store the variable name by widget ID + + // Determine the current parent variable from variableNames or fallback to parentVariable + let currentParentVariable = parentVariable || (variableMapping.get(widget.id) || null) + + if (widget.widgetType === TopLevel){ + // for top level set it to the main variable + // TODO: the toplevels parent should be determined other ways, suppose the top level has another toplevel + currentParentVariable = mainVariable + } + + let widgetCode = widgetRef.generateCode(varName, currentParentVariable) + + if (!(widgetCode instanceof Array)) { + throw new Error("generateCode() function should return array, each new line should be a new item") + } + + // Add \n after every line + widgetCode = widgetCode.flatMap((item, index) => index < widgetCode.length - 1 ? [item, "\n"] : [item]) + + code.push(...widgetCode) + code.push("\n\n") + + // Recursively handle child widgets + if (widget.children && widget.children.length > 0) { + // Pass down the unique names for children to prevent duplication + const childResult = generateTkinterCodeList(widget.children, widgetRefs, varName, mainVariable, usedVariableNames) + + // Merge child imports, requirements, and code + imports = new Set([...imports, ...childResult.imports]) + requirements = new Set([...requirements, ...childResult.requirements]) + code.push(...childResult.code) + + mainVariable = childResult.mainVariable || mainVariable // the main variable is the main window variable + } + } + + return { + imports: Array.from(imports), + code: code, + requirements: Array.from(requirements), + mainVariable + } +} + + +async function generateTkinterCode(projectName, widgetList=[], widgetRefs=[], assetFiles){ + + console.log("widgetList and refs", projectName, widgetList, widgetRefs, assetFiles) + + let mainWindowCount = 0 + + // only MainWindow and/or the TopLevel should be on the canvas + const filteredWidgetList = widgetList.filter(widget => widget.widgetType === MainWindow || widget.widgetType === TopLevel) + + for (let widget of filteredWidgetList){ + if (widget.widgetType === MainWindow){ + mainWindowCount += 1 + } + + if (mainWindowCount > 1){ + message.error("Multiple instances of Main window found, delete one and try again.") + return + } + + } + + if (mainWindowCount === 0){ + message.error("Aborting. No instances of Main window found. Add one and try again") + return + } + + // widget - {id, widgetType: widgetComponentType, children: [], parent: "", initialData: {}} + + const generatedObject = generateTkinterCodeList(filteredWidgetList, widgetRefs, "", "") + + const {code: codeLines, imports, requirements, mainVariable} = generatedObject + + // TODO: avoid adding \n inside the list instead rewrite using code.join("\n") + const code = [ + "# This code is generated by PyUIbuilder: https://github.com/PaulleDemon/PyUIBuilder", + "\n\n", + ...imports.flatMap((item, index) => index < imports.length - 1 ? [item, "\n"] : [item]), //add \n to every line + "\n\n", + ...codeLines, + "\n", + `${mainVariable}.mainloop()`, + ] + + console.log("Code: ", code.join(""), "\n\n requirements:", requirements.join("\n")) + + message.info("starting zipping files, download will start in a few seconds") + + + const createFileList = [ + { + fileData: code.join(""), + fileName: "main.py", + folder: "" + } + ] + + + if (requirements.length > 0){ + createFileList.push({ + fileData: requirements.join("\n"), + fileName: "requirements.txt", + folder: "" + }) + } + + for (let asset of assetFiles){ + + if (asset.fileType === "image"){ + createFileList.push({ + fileData: asset.originFileObj, + fileName: asset.name, + folder: "assets/images" + }) + }else if (asset.fileType === "video"){ + createFileList.push({ + fileData: asset.originFileObj, + fileName: asset.name, + folder: "assets/videos" + }) + }else if (asset.fileType === "audio"){ + createFileList.push({ + fileData: asset.originFileObj, + fileName: asset.name, + folder: "assets/audio" + }) + }else{ + createFileList.push({ + fileData: asset.originFileObj, + fileName: asset.name, + folder: "assets/others" + }) + } + + } + + createFilesAndDownload(createFileList, projectName).then(() => { + message.success("Download complete") + }).catch(() => { + message.error("Error while downloading") + }) + + +} + + +export default generateTkinterCode \ No newline at end of file diff --git a/src/frameworks/customtk/engine/componentCodes/entry.py b/src/frameworks/customtk/engine/componentCodes/entry.py new file mode 100644 index 0000000..0d76a9e --- /dev/null +++ b/src/frameworks/customtk/engine/componentCodes/entry.py @@ -0,0 +1,27 @@ +import tkinter as tk + +class EntryWithPlaceholder(tk.Entry): + def __init__(self, master=None, placeholder="placeholder", *args, **kwargs): + super().__init__(master, *args, **kwargs) + + self.placeholder = placeholder + self.placeholder_color = color + self.default_fg_color = self['fg'] + + self.bind("", self.foc_in) + self.bind("", self.foc_out) + + self.put_placeholder() + + def put_placeholder(self): + self.insert(0, self.placeholder) + self['fg'] = self.placeholder_color + + def foc_in(self, *args): + if self['fg'] == self.placeholder_color: + self.delete('0', 'end') + self['fg'] = self.default_fg_color + + def foc_out(self, *args): + if not self.get(): + self.put_placeholder() \ No newline at end of file diff --git a/src/frameworks/customtk/plugins/analogTimepicker.js b/src/frameworks/customtk/plugins/analogTimepicker.js new file mode 100644 index 0000000..41a30fc --- /dev/null +++ b/src/frameworks/customtk/plugins/analogTimepicker.js @@ -0,0 +1,265 @@ +import React from "react" + +import { timePicker } from 'analogue-time-picker' + +import Widget from "../../../canvas/widgets/base" +import Tools from "../../../canvas/constants/tools" +import { convertObjectToKeyValueString, removeKeyFromObject } from "../../../utils/common" +import { TkinterBase } from "../widgets/base" + +import "./styles/timepickerStyle.css" + + +const Themes = { + NAVY_BLUE: "Navy Blue", + DRACULA: "Dracula", + PURPLE: "Purple", + NONE: "" +} + +class AnalogTimePicker extends TkinterBase{ + + static widgetType = "analog_timepicker" + + static requiredImports = [ + ...TkinterBase.requiredImports, + 'from tktimepicker import AnalogPicker, AnalogThemes, constants' + ] + + static requirements = ["tkTimePicker"] + + constructor(props) { + super(props) + + this.droppableTags = null + + const newAttrs = removeKeyFromObject("layout", this.state.attrs) + + this.timePicker = null + + this.timePickerRef = React.createRef() + + this.minSize = {width: 100, height: 100} + + this.state = { + ...this.state, + widgetName: "Timepicker", + size: { width: 250, height: 350 }, + attrs: { + ...newAttrs, + styling: { + theme:{ + label: "Theme", + tool: Tools.SELECT_DROPDOWN, + toolProps: {placeholder: "select theme"}, + value: "", + options: Object.values(Themes).map(val => ({value: val, label: val})), + onChange: (value) => this.handleThemeChange(value) + }, + ...newAttrs.styling, + clockColor: { + label: "Clock Color", + tool: Tools.COLOR_PICKER, // the tool to display, can be either HTML ELement or a constant string + value: "#EEEEEE", + onChange: (value) => { + this.setAttrValue("styling.clockColor", value) + } + }, + displayColor: { + label: "Display Color", + tool: Tools.COLOR_PICKER, // the tool to display, can be either HTML ELement or a constant string + value: "#000", + onChange: (value) => { + this.setAttrValue("styling.displayColor", value) + } + }, + numberColor: { + label: "Numbers Color", + tool: Tools.COLOR_PICKER, // the tool to display, can be either HTML ELement or a constant string + value: "#000", + onChange: (value) => { + this.setAttrValue("styling.numberColor", value) + } + }, + handleColor: { + label: "Handle Color", + tool: Tools.COLOR_PICKER, // the tool to display, can be either HTML ELement or a constant string + value: "#000000", + onChange: (value) => { + this.setAttrValue("styling.handleColor", value) + } + }, + + }, + clockMode:{ + label: "Clock Mode", + tool: Tools.SELECT_DROPDOWN, + toolProps: {placeholder: "select mode", defaultValue: 12}, + value: 12, + options: [12, 24].map(val => ({value: val, label: val})), + onChange: (value) => { + this.setAttrValue("clockMode", value) + if (value === 24){ + // FIXME: the timepicker for 24 hrs also shows 12 hrs time + + this.timePicker.set24h() + + }else{ + this.timePicker.set12h() + } + } + }, + + } + } + + this.handleThemeChange = this.handleThemeChange.bind(this) + } + + componentDidMount(){ + super.componentDidMount() + this.timePicker = timePicker({ + element: this.timePickerRef.current, + mode: "12", + width: this.state.size.width, + // height: this.state.size.height + }) + + // used to remove ok and cancel buttons + const timePickerBtns = this.timePickerRef.current.getElementsByClassName("atp-clock-btn") + for (let i = 0; i < timePickerBtns.length; i++) { + timePickerBtns[i].remove() + } + } + + componentWillUnmount(){ + this.timePicker.dispose() + } + + setResize(pos, size){ + super.setResize(pos, size) + this.timePicker.setWidth(size.width) + } + + handleThemeChange(value){ + this.setAttrValue("styling.theme", value) + + if (value === Themes.NAVY_BLUE){ + this.setAttrValue("styling.handleColor", "#009688") + this.setAttrValue("styling.displayColor", "#009688") + this.setAttrValue("styling.backgroundColor", "#fff") + this.setAttrValue("styling.clockColor", "#EEEEEE") + this.setAttrValue("styling.numberColor", "#000") + }else if (value === Themes.DRACULA){ + this.setAttrValue("styling.handleColor", "#863434") + this.setAttrValue("styling.displayColor", "#404040") + this.setAttrValue("styling.backgroundColor", "#404040") + this.setAttrValue("styling.clockColor", "#363636") + this.setAttrValue("styling.numberColor", "#fff") + }else if (value === Themes.PURPLE){ + this.setAttrValue("styling.handleColor", "#EE333D") + this.setAttrValue("styling.displayColor", "#71135C") + this.setAttrValue("styling.backgroundColor", "#4E0D3A") + this.setAttrValue("styling.clockColor", "#71135C") + this.setAttrValue("styling.numberColor", "#fff") + } + + } + + generateCode(variableName, parent){ + + const theme = this.getAttrValue("styling.theme") + + const mode = this.getAttrValue("clockMode") + const bgColor = this.getAttrValue("styling.backgroundColor") + const clockColor = this.getAttrValue("styling.clockColor") + const displayColor = this.getAttrValue("styling.displayColor") + const numColor = this.getAttrValue("styling.numberColor") + const handleColor = this.getAttrValue("styling.handleColor") + + const code = [ + `${variableName} = AnalogPicker(parent=${parent}, type=${mode===12 ? "constants.HOURS12" : "constants.HOURS24"})`, + ] + + if (theme){ + + code.push(`${variableName}_theme = AnalogThemes(${variableName})`) + if (theme === Themes.NAVY_BLUE){ + code.push(`${variableName}_theme.setNavyBlue()`) + }else if (theme === Themes.DRACULA){ + code.push(`${variableName}_theme.setDracula()`) + }else if (theme === Themes.PURPLE){ + code.push(`${variableName}_theme.setPurple()`) + } + + }else{ + + const configAnalog = { + "canvas_bg": `"${bgColor}"`, + "textcolor": `"${numColor}"`, + "bg": `"${clockColor}"`, + "handcolor": `"${handleColor}"`, + "headcolor": `"${handleColor}"` + } + + const displayConfig = { + bg: `"${displayColor}"` + } + + code.push(`${variableName}.configAnalog(${convertObjectToKeyValueString(configAnalog)})`) + code.push(`${variableName}.configSpin(${convertObjectToKeyValueString(displayConfig)})`) + + + // code.push(`configAnalog(canvas_bg="${bgColor}", textcolor="${numColor}", + // bg="${clockColor}", handcolor="${handleColor}", headcolor="${handleColor}")`) + + // code.push(`configSpin(bg="${displayColor}"`) + } + + return [ + ...code, + `${variableName}.${this.getLayoutCode()}` + ] + } + + getToolbarAttrs(){ + + const toolBarAttrs = super.getToolbarAttrs() + + + return ({ + id: this.__id, + widgetName: toolBarAttrs.widgetName, + size: toolBarAttrs.size, + + ...this.state.attrs, + + }) + } + + + renderContent(){ + const timePickerStyling = { + '--bg': this.getAttrValue("styling.backgroundColor"), + '--clock-color': this.getAttrValue("styling.clockColor"), + '--number-color': this.getAttrValue("styling.numberColor"), + '--time-display': this.getAttrValue("styling.displayColor"), + '--main-handle-color': this.getAttrValue("styling.handleColor"), + } + + return ( +
+
+ +
+
+ ) + } + +} + + +export default AnalogTimePicker \ No newline at end of file diff --git a/src/frameworks/customtk/plugins/assets/map.png b/src/frameworks/customtk/plugins/assets/map.png new file mode 100644 index 0000000..fcfd2c0 Binary files /dev/null and b/src/frameworks/customtk/plugins/assets/map.png differ diff --git a/src/frameworks/customtk/plugins/assets/video.jpg b/src/frameworks/customtk/plugins/assets/video.jpg new file mode 100644 index 0000000..d0692e6 Binary files /dev/null and b/src/frameworks/customtk/plugins/assets/video.jpg differ diff --git a/src/frameworks/customtk/plugins/mapview.js b/src/frameworks/customtk/plugins/mapview.js new file mode 100644 index 0000000..a2cd063 --- /dev/null +++ b/src/frameworks/customtk/plugins/mapview.js @@ -0,0 +1,96 @@ +import React from "react" + + +import Widget from "../../../canvas/widgets/base" +import Tools from "../../../canvas/constants/tools" +import { removeKeyFromObject } from "../../../utils/common" + +import MapImage from "./assets/map.png" +import { MinusOutlined, PlayCircleFilled, PlusOutlined } from "@ant-design/icons" +import { TkinterBase } from "../widgets/base" + + +class MapView extends TkinterBase{ + + static widgetType = "map_view" + + static requiredImports = [ + ...TkinterBase.requiredImports, + "import tkintermapview" + ] + + static requirements = ["tkintermapview"] + + constructor(props) { + super(props) + + this.droppableTags = null + + const newAttrs = removeKeyFromObject("layout", this.state.attrs) + + this.state = { + ...this.state, + widgetName: "Map viewer", + size: { width: 400, height: 250 }, + } + } + + // componentDidMount(){ + // super.componentDidMount() + // } + + generateCode(variableName, parent){ + + + return [ + `${variableName} = tkintermapview.TkinterMapView(master=${parent})`, + `${variableName}.${this.getLayoutCode()}` + ] + } + + + getToolbarAttrs(){ + + const toolBarAttrs = super.getToolbarAttrs() + + + return ({ + id: this.__id, + widgetName: toolBarAttrs.widgetName, + size: toolBarAttrs.size, + + ...this.state.attrs, + + }) + } + + renderContent(){ + return ( +
+
+
+
+
+ +
+
+ +
+
+ +
+
+
+ ) + } + +} + + +export default MapView \ No newline at end of file diff --git a/src/frameworks/customtk/plugins/pandasTable.js b/src/frameworks/customtk/plugins/pandasTable.js new file mode 100644 index 0000000..14ef746 --- /dev/null +++ b/src/frameworks/customtk/plugins/pandasTable.js @@ -0,0 +1,237 @@ + + + + +import React, { useEffect, useRef, useState } from "react" + +import Widget from "../../../canvas/widgets/base" +import { removeKeyFromObject } from "../../../utils/common" + +import MapImage from "./assets/map.png" +import { MinusOutlined, PlusOutlined } from "@ant-design/icons" +import { TkinterBase } from "../widgets/base" +import Tools from "../../../canvas/constants/tools" +import { getPythonAssetPath } from "../../utils/pythonFilePath" + + +const ResizableTable = ({minRows=5, minCols=5}) => { + const [rows, setRows] = useState(minRows) + const [cols, setCols] = useState(minCols) + const containerRef = useRef(null) + + + + useEffect(() => { + const resizeObserver = new ResizeObserver((entries) => { + for (let entry of entries) { + const { width, height } = entry.contentRect + + // Set number of columns and rows based on widget width and height + const newCols = Math.max(minCols, Math.floor(width / 100)) // each column is 100px wide + const newRows = Math.max(minRows, Math.floor(height / 50)) // each row is 50px high + + setCols(newCols) + setRows(newRows) + } + }) + + if (containerRef.current) { + resizeObserver.observe(containerRef.current); // Start observing the widget + } + + return () => { + if (containerRef.current) { + resizeObserver.unobserve(containerRef.current); // Stop observing when component unmounts + } + } + }, [containerRef]) + + + return ( +
+ + + {Array.from({ length: rows }).map((_, rowIndex) => ( + + {Array.from({ length: cols }).map((_, colIndex) => ( + + ))} + + ))} + +
+ {/* Row {rowIndex + 1}, Col {colIndex + 1} */} +
+
+ ) +} + + +class PandasTable extends TkinterBase{ + + static widgetType = "pandas_table" + + static requiredImports = [ + ...TkinterBase.requiredImports, + "import os", + "from pandastable import Table", + ] + + static requirements = ["pandastable"] + + + constructor(props) { + super(props) + + this.droppableTags = null + + const newAttrs = removeKeyFromObject("layout", this.state.attrs) + + this.state = { + ...this.state, + widgetName: "Pandas Table", + size: { width: 400, height: 250 }, + attrs: { + ...newAttrs, + styling: { + ...newAttrs.styling, + textColor: { + label: "Text Color", + tool: Tools.COLOR_PICKER, // the tool to display, can be either HTML ELement or a constant string + value: "", + onChange: (value) => { + this.setAttrValue("styling.textColor", value) + } + }, + cellBg: { + label: "Cell background", + tool: Tools.COLOR_PICKER, // the tool to display, can be either HTML ELement or a constant string + value: "", + onChange: (value) => { + this.setAttrValue("styling.textColor", value) + } + }, + outlineColor: { + label: "Box outline color", + tool: Tools.COLOR_PICKER, // the tool to display, can be either HTML ELement or a constant string + value: "", + onChange: (value) => { + this.setAttrValue("styling.textColor", value) + } + }, + }, + defaultTable: { + label: "Default table", + tool: Tools.UPLOADED_LIST, + toolProps: {filterOptions: ["text/csv"]}, + value: "", + onChange: (value) => this.setAttrValue("defaultTable", value) + }, + enableEdit: { + label: "Enable editing", + tool: Tools.CHECK_BUTTON, + value: false, + onChange: (value) => { + this.setAttrValue("enableEdit", value) + } + + }, + } + } + } + + // componentDidMount(){ + // super.componentDidMount() + // this.setWidgetName("Pandas Table") + // this.setAttrValue("styling.backgroundColor", "#E4E2E2") + // } + + generateCode(variableName, parent){ + + const defaultTable = this.getAttrValue("defaultTable") + + const textColor = this.getAttrValue("styling.textColor") + const cellBg = this.getAttrValue("styling.cellBg") + const outlineColor = this.getAttrValue("styling.outlineColor") + + const enableEdit = this.getAttrValue("enableEdit") + + const code = [ + `${variableName}_table_frame = tk.Frame(master=${parent})`, + `${variableName}_table_frame.${this.getLayoutCode()}`, + + `${variableName} = Table(parent=${variableName}_table_frame)`, + `${variableName}.editable = ${enableEdit ? "True" : "False"}`, + ] + + if (textColor){ + code.push(`${variableName}.textColor = "${textColor}"`) + } + if (cellBg){ + code.push(`${variableName}.cellbackgr = "${cellBg}"`) + } + + if (outlineColor){ + code.push(`${variableName}.boxoutlinecolor = "${outlineColor}"`) + } + + if (defaultTable){ + code.push(`${variableName}.importCSV(${getPythonAssetPath(defaultTable.name, "text/csv")})`) + } + + return [ + ...code, + `${variableName}.show()`, + ] + } + + getToolbarAttrs(){ + + const toolBarAttrs = super.getToolbarAttrs() + + + return ({ + id: this.__id, + widgetName: toolBarAttrs.widgetName, + size: toolBarAttrs.size, + + ...this.state.attrs, + + }) + } + + renderContent(){ + return ( +
+
+ +
+
+ ) + } + +} + + +export default PandasTable + + + + +/** + * {'align': 'w', + 'cellbackgr': '#F4F4F3', + 'cellwidth': 80, + 'floatprecision': 2, + 'thousandseparator': '', + 'font': 'Arial', + 'fontsize': 12, + 'fontstyle': '', + 'grid_color': '#ABB1AD', + 'linewidth': 1, + 'rowheight': 22, + 'rowselectedcolor': '#E4DED4', + 'textcolor': 'black'} + */ \ No newline at end of file diff --git a/src/frameworks/customtk/plugins/styles/timepickerStyle.css b/src/frameworks/customtk/plugins/styles/timepickerStyle.css new file mode 100644 index 0000000..977997a --- /dev/null +++ b/src/frameworks/customtk/plugins/styles/timepickerStyle.css @@ -0,0 +1,28 @@ + +.atp-time { + background-color: var(--time-display, black) !important; + text-align: center !important; + font-size: 35px; +} + +.atp-clock-cnt { + background-color: var(--bg, white) !important; +} + +.atp-face-color { + background-color: var(--clock-color, #EEEEEE) !important; +} + +.atp-n { + color: var(--number-color, #000) !important; +} + +/* .atp-color--primary { + background-color: var(--main-handle-color, #000000c0) !important; +} */ + + +.atp-h-cnt-cnt .atp-h-ctn, .atp-h, .atp-b, .atp-h-dot{ + /* affects only the handle*/ + background-color: var(--main-handle-color, #000000c0) !important; +} \ No newline at end of file diff --git a/src/frameworks/customtk/plugins/videoPlayer.js b/src/frameworks/customtk/plugins/videoPlayer.js new file mode 100644 index 0000000..15e2dcf --- /dev/null +++ b/src/frameworks/customtk/plugins/videoPlayer.js @@ -0,0 +1,126 @@ +import React from "react" + +import Tools from "../../../canvas/constants/tools" +import { removeKeyFromObject } from "../../../utils/common" + +import VideoImage from "./assets/video.jpg" +import { PlayCircleFilled } from "@ant-design/icons" +import { TkinterBase } from "../widgets/base" +import { getPythonAssetPath } from "../../utils/pythonFilePath" + + +class VideoPlayer extends TkinterBase{ + + static widgetType = "video_player" + + static requiredImports = [ + ...TkinterBase.requiredImports, + "import os", + "from tkVideoPlayer import TkinterVideo" + ] + + static requirements = ["tkvideoplayer"] + + + constructor(props) { + super(props) + + this.droppableTags = null + + const newAttrs = removeKeyFromObject("layout", this.state.attrs) + + this.state = { + ...this.state, + size: { width: 350, height: 200 }, + widgetName: "Video player", + attrs: { + ...newAttrs, + play: { + label: "Start playing", + tool: Tools.CHECK_BUTTON, + value: false, + onChange: (value) => { + this.setAttrValue("play", value) + } + + }, + defaultVideo: { + label: "Video", + tool: Tools.UPLOADED_LIST, + toolProps: {filterOptions: ["video/mp4", "video/webm", "video/m4v"]}, + value: "", + onChange: (value) => this.setAttrValue("defaultVideo", value) + }, + } + } + } + + componentDidMount(){ + super.componentDidMount() + // this.setAttrValue("styling.backgroundColor", "#E4E2E2") + } + + generateCode(variableName, parent){ + + + const defaultVideo = this.getAttrValue("defaultVideo") + const play = this.getAttrValue("play") + + const code = [ + `${variableName} = TkinterVideo(master=${parent}, scaled=True)`, + ] + + if (defaultVideo){ + code.push(`${variableName}.load(${getPythonAssetPath(defaultVideo.name, "video")})`) + } + + if (play){ + code.push(`${variableName}.play()`) + } + + return [ + ...code, + `${variableName}.${this.getLayoutCode()}` + ] + } + + getToolbarAttrs(){ + + const toolBarAttrs = super.getToolbarAttrs() + + + return ({ + id: this.__id, + widgetName: toolBarAttrs.widgetName, + size: toolBarAttrs.size, + + ...this.state.attrs, + + }) + } + + renderContent(){ + + // const defaultVideo = this.getAttrValue("defaultVideo") + + return ( +
+
+
+
+ +
+ +
+
+
+ ) + } + +} + + +export default VideoPlayer \ No newline at end of file diff --git a/src/frameworks/customtk/sidebarPlugins.js b/src/frameworks/customtk/sidebarPlugins.js new file mode 100644 index 0000000..2c21077 --- /dev/null +++ b/src/frameworks/customtk/sidebarPlugins.js @@ -0,0 +1,59 @@ + +import ClockImage from "./assets/widgets/plugins/clock.png" +import VideoImage from "./assets/widgets/plugins/video.png" +import MapImage from "./assets/widgets/plugins/map.png" +import DataTableImage from "./assets/widgets/plugins/tables.png" + +import AnalogTimePicker from "./plugins/analogTimepicker" +import VideoPlayer from "./plugins/videoPlayer" +import MapView from "./plugins/mapview" +import PandasTable from "./plugins/pandasTable" + +// TODO: add license for 3rd party plugins + +const TkinterPluginWidgets = [ + { + name: "Analog TimePicker", + img: ClockImage, + link: "https://github.com/PaulleDemon/tkTimePicker", + widgetClass: AnalogTimePicker, + license: { + name: "MIT", + url: "" + } + }, + { + name: "Video Player", + img: VideoImage, + link: "https://github.com/PaulleDemon/tkVideoPlayer", + widgetClass: VideoPlayer, + license: { + name: "MIT", + url: "" + } + }, + { + name: "Map viewer", + img: MapImage, + link: "https://github.com/TomSchimansky/TkinterMapView", + widgetClass: MapView, + license: { + name: "CC0 1.0", + url: "https://github.com/TomSchimansky/TkinterMapView/blob/main/LICENSE.txt" + } + }, + { + name: "Pandas Table", + img: DataTableImage, + link: "https://github.com/dmnfarrell/pandastable", + widgetClass: PandasTable, + license: { + name: "GPL v3", + url: "https://github.com/dmnfarrell/pandastable/blob/master/LICENSE" + } + }, + +] + + +export default TkinterPluginWidgets diff --git a/src/frameworks/customtk/sidebarWidgets.js b/src/frameworks/customtk/sidebarWidgets.js new file mode 100644 index 0000000..2a36fae --- /dev/null +++ b/src/frameworks/customtk/sidebarWidgets.js @@ -0,0 +1,131 @@ + + +import MainWindow from "./widgets/mainWindow" +import TopLevel from "./widgets/toplevel" +import Frame from "./widgets/frame" +import Label from "./widgets/label" +import Button from "./widgets/button" +import OptionMenu from "./widgets/optionMenu" +import Slider from "./widgets/slider" +import { CheckBox, RadioButton } from "./widgets/ checkButton" +import { Input, Text } from "./widgets/input" +import SpinBox from "./widgets/spinBox" + +import MainWindowImage from "./assets/widgets/main/mainwindow.png" +import TopLevelImage from "./assets/widgets/main/Toplevel.png" +import FrameImage from "./assets/widgets/main/frame2.png" +import LabelImage from "./assets/widgets/main/label.png" +import ButtonImage from "./assets/widgets/main/button.png" +import InputImage from "./assets/widgets/main/input.png" +import TextAreaImage from "./assets/widgets/main/textarea.png" +import SliderImage from "./assets/widgets/main/slider.png" +import DropDownImage from "./assets/widgets/main/dropdown.png" +import CheckButtonImage from "./assets/widgets/main/check.png" +import RadioButtonImage from "./assets/widgets/main/radio.png" +import SpinBoxImage from "./assets/widgets/main/spinbox.png" + + +const TkinterWidgets = [ + { + name: "Main window", + img: MainWindowImage, + link: "https://github.com", + widgetClass: MainWindow + }, + { + name: "Top Level", + img: TopLevelImage, + link: "https://github.com", + widgetClass: TopLevel + }, + { + name: "Frame", + img: FrameImage, + link: "https://github.com", + widgetClass: Frame + }, + { + name: "Label", + img: LabelImage, + link: "https://github.com", + widgetClass: Label + }, + { + name: "Button", + img: ButtonImage, + link: "https://github.com", + widgetClass: Button + }, + { + name: "Entry", + img: InputImage, + link: "https://github.com", + widgetClass: Input + }, + { + name: "Text", + img: TextAreaImage, + link: "https://github.com", + widgetClass: Text + }, + { + name: "CheckBox", + img: CheckButtonImage, + link: "https://github.com", + widgetClass: CheckBox + }, + { + name: "Radio button", + img: RadioButtonImage, + link: "https://github.com", + widgetClass: RadioButton + }, + { + name: "Scale", + img: SliderImage, + link: "https://github.com", + widgetClass: Slider + }, + { + name: "Option Menu", + img: DropDownImage, + link: "https://github.com", + widgetClass: OptionMenu + }, + { + name: "Spinbox", + img: SpinBoxImage, + link: "https://github.com", + widgetClass: SpinBox + }, + +] + + +export default TkinterWidgets + + +/** + * widgets = { + "Tk": set(), + "Label": set(), + "Button": set(), + "Entry": set(), + "CheckButton": set(), + "RadioButton": set(), + "Scale": set(), + "ListBox": set(), + "Frame": set(), + "LabelFrame": set(), + "PanedWindow": set(), + "SpinBox": set(), + "OptionMenu": set(), + "Canvas": set(), + "TopLevel": set(), + "Message": set(), + "Menu": set(), + "MenuButton": set(), + "ScrollBar": set(), + "Text": set() + } + */ \ No newline at end of file diff --git a/src/frameworks/customtk/widgets/ checkButton.js b/src/frameworks/customtk/widgets/ checkButton.js new file mode 100644 index 0000000..0b103bc --- /dev/null +++ b/src/frameworks/customtk/widgets/ checkButton.js @@ -0,0 +1,240 @@ + +import Tools from "../../../canvas/constants/tools" +import { convertObjectToKeyValueString, removeKeyFromObject } from "../../../utils/common" +import { CheckSquareFilled } from "@ant-design/icons" +import { TkinterWidgetBase } from "./base" + + +export class CheckBox extends TkinterWidgetBase{ + + static widgetType = "check_button" + constructor(props) { + super(props) + + // const {layout, ...newAttrs} = this.state.attrs // Removes the layout attribute + + let newAttrs = removeKeyFromObject("layout", this.state.attrs) + + this.minSize = {width: 50, height: 30} + + this.state = { + ...this.state, + size: { width: 120, height: 30 }, + widgetName: "Check box", + attrs: { + ...newAttrs, + styling: { + ...newAttrs.styling, + foregroundColor: { + label: "Foreground Color", + tool: Tools.COLOR_PICKER, // the tool to display, can be either HTML ELement or a constant string + value: "#000", + onChange: (value) => { + this.setWidgetInnerStyle("color", value) + this.setAttrValue("styling.foregroundColor", value) + } + } + }, + checkLabel: { + label: "Check Label", + tool: Tools.INPUT, // the tool to display, can be either HTML ELement or a constant string + toolProps: {placeholder: "Button label", maxLength: 100}, + value: "Checkbox", + onChange: (value) => this.setAttrValue("checkLabel", value) + }, + defaultChecked: { + label: "Checked", + tool: Tools.CHECK_BUTTON, // the tool to display, can be either HTML ELement or a constant string + value: true, + onChange: (value) => this.setAttrValue("defaultChecked", value) + } + + } + } + } + + componentDidMount(){ + super.componentDidMount() + // this.setAttrValue("styling.backgroundColor", "#fff") + this.setWidgetInnerStyle("backgroundColor", "#fff0") + } + + generateCode(variableName, parent){ + + const labelText = this.getAttrValue("checkLabel") + const config = convertObjectToKeyValueString(this.getConfigCode()) + + const code = [ + `${variableName} = tk.Checkbutton(master=${parent}, text="${labelText}")`, + `${variableName}.config(${config})`, + ] + + if (this.getAttrValue("defaultChecked")){ + code.push(`${variableName}.select()`) + } + + code.push(`${variableName}.${this.getLayoutCode()}`) + + return code + } + + getToolbarAttrs(){ + + const toolBarAttrs = super.getToolbarAttrs() + + const attrs = this.state.attrs + return ({ + id: this.__id, + widgetName: toolBarAttrs.widgetName, + checkLabel: attrs.checkLabel, + size: toolBarAttrs.size, + ...attrs, + }) + } + + renderContent(){ + return ( +
+ +
+
+ { + this.getAttrValue("defaultChecked") === true && + + } +
+ + + {this.getAttrValue("checkLabel")} +
+ +
+ ) + } + +} + + +export class RadioButton extends TkinterWidgetBase{ + // FIXME: the radio buttons are not visible because of the default heigh provided + + static widgetType = "radio_button" + + constructor(props) { + super(props) + + this.minSize = {width: 50, height: 30} + + this.state = { + ...this.state, + size: { width: 80, height: 30 }, + fitContent: { width: true, height: true }, + widgetName: "Radio button", + attrs: { + ...this.state.attrs, + radios: { + label: "Radio Group", + tool: Tools.INPUT_RADIO_LIST, + value: {inputs: ["default"], selectedRadio: -1}, + onChange: ({inputs, selectedRadio}) => { + this.setAttrValue("radios", {inputs, selectedRadio}) + } + } + + } + } + } + + componentDidMount(){ + super.componentDidMount() + // this.setAttrValue("styling.backgroundColor", "#fff") + this.setWidgetInnerStyle("backgroundColor", "#fff0") + } + + generateCode(variableName, parent){ + + const config = convertObjectToKeyValueString(this.getConfigCode()) + + + const code = [ + `${variableName}_var = tk.IntVar()`, + ] + const radios = this.getAttrValue("radios") + + radios.inputs.forEach((radio_text, idx) => { + + const radioBtnVariable = `${variableName}_${idx}` + code.push(`\n`) + code.push(`${radioBtnVariable} = tk.Radiobutton(master=${parent}, variable=${variableName}_var, text="${radio_text}")`) + code.push(`${radioBtnVariable}.config(${config}, value=${idx})`) + code.push(`${radioBtnVariable}.${this.getLayoutCode()}`) + }) + + const defaultSelected = radios.selectedRadio + + if (defaultSelected !== -1){ + code.push(`${variableName}_var.set(${defaultSelected})`) + } + + + return code + } + + getToolbarAttrs(){ + + const toolBarAttrs = super.getToolbarAttrs() + + const attrs = this.state.attrs + return ({ + id: this.__id, + widgetName: toolBarAttrs.widgetName, + checkLabel: attrs.checkLabel, + size: toolBarAttrs.size, + ...attrs, + }) + } + + renderContent(){ + + const {inputs, selectedRadio} = this.getAttrValue("radios") + + return ( +
+
+ { + inputs.map((value, index) => { + return ( +
+
+ + { + selectedRadio === index && +
+ +
+ } +
+ + {value} + +
+ ) + }) + + } +
+
+ ) + } + +} \ No newline at end of file diff --git a/src/frameworks/customtk/widgets/base.js b/src/frameworks/customtk/widgets/base.js new file mode 100644 index 0000000..9dc05d6 --- /dev/null +++ b/src/frameworks/customtk/widgets/base.js @@ -0,0 +1,601 @@ +import { Layouts, PosType } from "../../../canvas/constants/layouts" +import Tools from "../../../canvas/constants/tools" +import Widget from "../../../canvas/widgets/base" +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" + + + +export class CustomTkBase extends Widget { + + static requiredImports = ['import tkinter as tk'] + + static requirements = ['customtkinter'] + + constructor(props) { + super(props) + + this.getLayoutCode = this.getLayoutCode.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: this.state.pos.x, + y: this.state.pos.y, + } + + config["width"] = this.state.size.width + 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 + // } + + 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 + } + + setParentLayout(layout){ + + if (!layout){ + return {} + } + + 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, + 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: "Flex Manager", + display: "horizontal", + fillX: { + label: "Fill X", + tool: Tools.CHECK_BUTTON, + value: false, + onChange: (value) => { + this.setAttrValue("flexManager.fillX", value) + const widgetStyle = { + ...this.state.widgetOuterStyling, + flexGrow: value ? 1 : 0, + } + + this.updateState({ + widgetOuterStyling: widgetStyle, + }) + + } + }, + fillY: { + label: "Fill Y", + tool: Tools.CHECK_BUTTON, + value: false, + onChange: (value) => { + this.setAttrValue("flexManager.fillY", value) + + const widgetStyle = { + ...this.state.widgetOuterStyling, + flexGrow: value ? 1 : 0, + } + this.updateState({ + widgetOuterStyling: widgetStyle, + }) + } + }, + 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(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%" + } + + } + + const styling = { + ...this.state.widgetInnerStyling, + width, + height, + minWidth, + minHeight + } + return styling + } + + /** + * loads the data + * @param {object} data + */ + load(data, callback=null){ + + if (Object.keys(data).length === 0) return // no data to load + + data = {...data} // create a shallow copy + + const {attrs, parentLayout=null, ...restData} = data + + + let layoutUpdates = { + parentLayout: parentLayout + } + + if (parentLayout){ + if (parentLayout.layout === Layouts.FLEX || parentLayout.layout === Layouts.GRID){ + + layoutUpdates = { + ...layoutUpdates, + positionType: PosType.NONE + } + + }else if (parentLayout.layout === Layouts.PLACE){ + layoutUpdates = { + ...layoutUpdates, + positionType: PosType.ABSOLUTE + } + } + } + + const newData = { + ...restData, + ...layoutUpdates + } + + + this.setState(newData, () => { + let layoutAttrs = this.setParentLayout(parentLayout).attrs || {} + + // 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) + + }) + + + } + + +} + + +// base for widgets that have common base properties such as bg, fg, cursor etc +export class CustomTkWidgetBase extends CustomTkBase{ + + 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) + } + }, + borderRadius: { + label: "Border radius", + tool: Tools.NUMBER_INPUT, + toolProps: {min: 0, max: 140}, + value: 0, + onChange: (value) => { + this.setWidgetInnerStyle("borderRadius", `${value}px`) + this.setAttrValue("styling.borderRadius", value) + } + } + // 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, + toolProps: {min: 1, max: 140}, + value: null, + onChange: (value) => { + // this.setWidgetInnerStyle("paddingLeft", `${value}px`) + // this.setWidgetInnerStyle("paddingRight", `${value}px`) + + const widgetStyle = { + ...this.state.widgetInnerStyling, + paddingLeft: `${value}px`, + paddingRight: `${value}px` + } + this.setState({ + + widgetInnerStyling: widgetStyle + }) + + + this.setAttrValue("padding.padX", value) + } + }, + padY: { + label: "Pad Y", + tool: Tools.NUMBER_INPUT, + toolProps: {min: 1, max: 140}, + value: null, + onChange: (value) => { + const widgetStyle = { + ...this.state.widgetInnerStyling, + paddingTop: `${value}px`, + paddingBottom: `${value}px` + } + this.setState({ + + widgetInnerStyling: widgetStyle + }) + this.setAttrValue("padding.padX", value) + } + }, + }, + font: { + label: "font", + fontFamily: { + label: "font family", + tool: Tools.SELECT_DROPDOWN, + options: Object.keys(Tkinter_To_GFonts).map((val) => ({value: val, label: val})), + value: "", + 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, + onChange: (value) => { + this.setWidgetInnerStyle("fontSize", `${value}px`) + this.setAttrValue("font.fontSize", value) + } + } + }, + cursor: { + label: "Cursor", + tool: Tools.SELECT_DROPDOWN, + toolProps: {placeholder: "select cursor"}, + value: "", + 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(){ + + const config = { + fg_color: `"${this.getAttrValue("styling.backgroundColor")}"`, + text_color: `"${this.getAttrValue("styling.foregroundColor")}"`, + } + + if (this.getAttrValue("styling.borderWidth")) + config["border_width"] = this.getAttrValue("styling.borderWidth") + + if (this.getAttrValue("font.fontFamily") || this.getAttrValue("font.fontSize")){ + config["font"] = `("${this.getAttrValue("font.fontFamily")}", ${this.getAttrValue("font.fontSize") || 12}, )` + } + + if (this.getAttrValue("cursor")) + config["cursor"] = `"${this.getAttrValue("cursor")}"` + + if (this.getAttrValue("padding.padX")){ + config["padx"] = this.getAttrValue("padding.padX") + } + + if (this.getAttrValue("padding.padY")){ + config["pady"] = this.getAttrValue("padding.padY") + } + + // 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 + } + +} \ No newline at end of file diff --git a/src/frameworks/customtk/widgets/button.js b/src/frameworks/customtk/widgets/button.js new file mode 100644 index 0000000..e5b6fb9 --- /dev/null +++ b/src/frameworks/customtk/widgets/button.js @@ -0,0 +1,84 @@ +import Tools from "../../../canvas/constants/tools" +import { convertObjectToKeyValueString } from "../../../utils/common" +import { TkinterWidgetBase } from "./base" + + +class Button extends TkinterWidgetBase{ + + static widgetType = "button" + + constructor(props) { + super(props) + + this.state = { + ...this.state, + size: { width: 80, height: 40 }, + widgetName: "Button", + attrs: { + ...this.state.attrs, + buttonLabel: { + label: "Button Label", + tool: Tools.INPUT, // the tool to display, can be either HTML ELement or a constant string + toolProps: {placeholder: "Button label", maxLength: 100}, + value: "Button", + onChange: (value) => this.setAttrValue("buttonLabel", value) + } + + } + } + } + + + componentDidMount(){ + super.componentDidMount() + this.setAttrValue("styling.backgroundColor", "#E4E2E2") + } + + generateCode(variableName, parent){ + + const labelText = this.getAttrValue("buttonLabel") + + const config = convertObjectToKeyValueString(this.getConfigCode()) + + return [ + `${variableName} = tk.Button(master=${parent}, text="${labelText}")`, + `${variableName}.config(${config})`, + `${variableName}.${this.getLayoutCode()}` + ] + } + + getToolbarAttrs(){ + + const toolBarAttrs = super.getToolbarAttrs() + + + return ({ + id: this.__id, + widgetName: toolBarAttrs.widgetName, + buttonLabel: this.state.attrs.buttonLabel, + size: toolBarAttrs.size, + + ...this.state.attrs, + + }) + } + + renderContent(){ + return ( +
+
+ {/* {this.props.children} */} +
+ {this.getAttrValue("buttonLabel")} +
+
+
+ ) + } + +} + + +export default Button \ No newline at end of file diff --git a/src/frameworks/customtk/widgets/frame.js b/src/frameworks/customtk/widgets/frame.js new file mode 100644 index 0000000..adf6c21 --- /dev/null +++ b/src/frameworks/customtk/widgets/frame.js @@ -0,0 +1,58 @@ +import Widget from "../../../canvas/widgets/base" +import {TkinterBase} from "./base" + + +class Frame extends TkinterBase{ + + static widgetType = "frame" + + constructor(props) { + super(props) + + this.droppableTags = { + exclude: ["image", "video", "media", "toplevel", "main_window"] + } + + this.state = { + ...this.state, + fitContent: {width: true, height: true}, + widgetName: "Frame" + } + + } + + componentDidMount(){ + super.componentDidMount() + this.setAttrValue("styling.backgroundColor", "#EDECEC") + } + + generateCode(variableName, parent){ + + const bg = this.getAttrValue("styling.backgroundColor") + + return [ + `${variableName} = tk.Frame(master=${parent})`, + `${variableName}.config(bg="${bg}")`, + `${variableName}.${this.getLayoutCode()}` + ] + } + + + + renderContent(){ + // console.log("bounding rect: ", this.getBoundingRect()) + + // console.log("widget styling: ", this.state.widgetInnerStyling) + return ( +
+
+ {this.props.children} +
+
+ ) + } + +} + + +export default Frame \ No newline at end of file diff --git a/src/frameworks/customtk/widgets/input.js b/src/frameworks/customtk/widgets/input.js new file mode 100644 index 0000000..1380204 --- /dev/null +++ b/src/frameworks/customtk/widgets/input.js @@ -0,0 +1,150 @@ +import Tools from "../../../canvas/constants/tools" +import { convertObjectToKeyValueString } from "../../../utils/common" +import { TkinterWidgetBase } from "./base" + + +export class Input extends TkinterWidgetBase{ + + static widgetType = "entry" + + constructor(props) { + super(props) + + this.state = { + ...this.state, + size: { width: 120, height: 40 }, + widgetName: "Entry", + attrs: { + ...this.state.attrs, + placeHolder: { + label: "PlaceHolder", + tool: Tools.INPUT, // the tool to display, can be either HTML ELement or a constant string + toolProps: {placeholder: "text", maxLength: 100}, + value: "placeholder text", + onChange: (value) => this.setAttrValue("placeHolder", value) + } + + } + } + } + + componentDidMount(){ + super.componentDidMount() + this.setAttrValue("styling.backgroundColor", "#fff") + } + + generateCode(variableName, parent){ + + const placeHolderText = this.getAttrValue("placeHolder") + + const config = convertObjectToKeyValueString(this.getConfigCode()) + + return [ + `${variableName} = tk.Entry(master=${parent}, text="${placeHolderText}")`, + `${variableName}.config(${config})`, + `${variableName}.${this.getLayoutCode()}` + ] + } + + getToolbarAttrs(){ + + const toolBarAttrs = super.getToolbarAttrs() + + return ({ + id: this.__id, + widgetName: toolBarAttrs.widgetName, + placeHolder: this.state.attrs.placeHolder, + size: toolBarAttrs.size, + + ...this.state.attrs, + + }) + } + + renderContent(){ + return ( +
+
+
+ {this.getAttrValue("placeHolder")} +
+
+
+ ) + } + +} + + +export class Text extends TkinterWidgetBase{ + + static widgetType = "Text" + + constructor(props) { + super(props) + + this.state = { + ...this.state, + size: { width: 120, height: 80 }, + attrs: { + ...this.state.attrs, + // placeHolder: { + // label: "PlaceHolder", + // tool: Tools.INPUT, // the tool to display, can be either HTML ELement or a constant string + // toolProps: {placeholder: "text", maxLength: 100}, + // value: "placeholder text", + // onChange: (value) => this.setAttrValue("placeHolder", value) + // } + + } + } + } + + componentDidMount(){ + super.componentDidMount() + this.setAttrValue("styling.backgroundColor", "#fff") + this.setWidgetName("text") + } + + generateCode(variableName, parent){ + + const placeHolderText = this.getAttrValue("placeHolder") + + const config = convertObjectToKeyValueString(this.getConfigCode()) + + return [ + `${variableName} = tk.Text(master=${parent})`, + `${variableName}.config(${config})`, + `${variableName}.${this.getLayoutCode()}` + ] + } + + getToolbarAttrs(){ + const toolBarAttrs = super.getToolbarAttrs() + + return ({ + id: this.__id, + widgetName: toolBarAttrs.widgetName, + placeHolder: this.state.attrs.placeHolder, + size: toolBarAttrs.size, + + ...this.state.attrs, + + }) + } + + renderContent(){ + return ( +
+
+
+ {this.getAttrValue("placeHolder")} +
+
+
+ ) + } + +} diff --git a/src/frameworks/customtk/widgets/label.js b/src/frameworks/customtk/widgets/label.js new file mode 100644 index 0000000..1a97c5a --- /dev/null +++ b/src/frameworks/customtk/widgets/label.js @@ -0,0 +1,142 @@ +import Tools from "../../../canvas/constants/tools" +import { convertObjectToKeyValueString } from "../../../utils/common" +import { getPythonAssetPath } from "../../utils/pythonFilePath" +import { TkinterWidgetBase } from "./base" + + +class Label extends TkinterWidgetBase{ + + static widgetType = "label" + + + constructor(props) { + super(props) + + + this.state = { + ...this.state, + widgetName: "Label", + size: { width: 80, height: 40 }, + attrs: { + ...this.state.attrs, + labelWidget: { + label: "Text", + tool: Tools.INPUT, + toolProps: {placeholder: "text", maxLength: 100}, + value: "Label", + onChange: (value) => this.setAttrValue("labelWidget", value) + }, + imageUpload: { + label: "Image", + tool: Tools.UPLOADED_LIST, + toolProps: {filterOptions: ["image/jpg", "image/jpeg", "image/png"]}, + value: "", + onChange: (value) => this.setAttrValue("imageUpload", value) + }, + } + } + + } + + componentDidMount(){ + super.componentDidMount() + + + this.setAttrValue("styling.backgroundColor", "#E4E2E2") + // this.setWidgetName("label") // Don't do this this causes issues while loading data + + } + + getImports(){ + const imports = super.getImports() + + if (this.getAttrValue("imageUpload")) + imports.push("import os", "from PIL import Image, ImageTk", ) + + return imports + } + + getRequirements(){ + const requirements = super.getRequirements() + + + if (this.getAttrValue("imageUpload")) + requirements.push("pillow") + + return requirements + } + + generateCode(variableName, parent){ + + + const labelText = this.getAttrValue("labelWidget") + const config = convertObjectToKeyValueString(this.getConfigCode()) + const image = this.getAttrValue("imageUpload") + + let labelInitialization = `${variableName} = tk.Label(master=${parent}, text="${labelText}")` + + const code = [] + + if (image?.name){ + code.push(`${variableName}_img = Image.open(${getPythonAssetPath(image.name, "image")})`) + code.push(`${variableName}_img = ImageTk.PhotoImage(${variableName}_img)`) + // code.push("\n") + labelInitialization = `${variableName} = tk.Label(master=${parent}, image=${variableName}_img, text="${labelText}", compound=tk.TOP)` + } + + // code.push("\n") + code.push(labelInitialization) + return [ + ...code, + `${variableName}.config(${config})`, + `${variableName}.${this.getLayoutCode()}` + ] + } + + + getToolbarAttrs(){ + const toolBarAttrs = super.getToolbarAttrs() + + return ({ + id: this.__id, + widgetName: toolBarAttrs.widgetName, + labelWidget: this.state.attrs.labelWidget, + size: toolBarAttrs.size, + + ...this.state.attrs, + + }) + } + + renderContent(){ + + const image = this.getAttrValue("imageUpload") + + return ( +
+
+ {/* {this.props.children} */} + { + image && ( + + ) + } +
+ {this.getAttrValue("labelWidget")} +
+
+
+ ) + } + +} + + +export default Label \ No newline at end of file diff --git a/src/frameworks/customtk/widgets/mainWindow.js b/src/frameworks/customtk/widgets/mainWindow.js new file mode 100644 index 0000000..bc4075f --- /dev/null +++ b/src/frameworks/customtk/widgets/mainWindow.js @@ -0,0 +1,93 @@ +import Widget from "../../../canvas/widgets/base" +import Tools from "../../../canvas/constants/tools" +import { CustomTkBase } from "./base" + + +class MainWindow extends CustomTkBase{ + + static widgetType = "main_window" + + constructor(props) { + super(props) + + this.droppableTags = { + exclude: ["image", "video", "media", "main_window", "toplevel"] + } + + this.state = { + ...this.state, + size: { width: 700, height: 400 }, + widgetName: "main", + attrs: { + ...this.state.attrs, + title: { + label: "Window Title", + tool: Tools.INPUT, // the tool to display, can be either HTML ELement or a constant string + toolProps: {placeholder: "Window title", maxLength: 40}, + value: "Main Window", + onChange: (value) => this.setAttrValue("title", value) + } + + } + } + + } + + componentDidMount(){ + super.componentDidMount() + this.setAttrValue("styling.backgroundColor", "#E4E2E2") + // this.setWidgetName("main") // Don't do this as this will cause conflicts while loading names + } + + generateCode(variableName, parent){ + + const backgroundColor = this.getAttrValue("styling.backgroundColor") + + return [ + `${variableName} = tk.Tk()`, + `${variableName}.config(bg="${backgroundColor}")`, + `${variableName}.title("${this.getAttrValue("title")}")` + ] + } + + getToolbarAttrs(){ + const toolBarAttrs = super.getToolbarAttrs() + + return ({ + id: this.__id, + widgetName: toolBarAttrs.widgetName, + title: this.state.attrs.title, + size: toolBarAttrs.size, + + ...this.state.attrs, + + }) + } + + renderContent(){ + return ( +
+
+
{this.getAttrValue("title")}
+
+
+
+
+
+
+
+
+
+
+ {this.props.children} +
+
+ ) + } + +} + + +export default MainWindow \ No newline at end of file diff --git a/src/frameworks/customtk/widgets/optionMenu.js b/src/frameworks/customtk/widgets/optionMenu.js new file mode 100644 index 0000000..5bc0f1c --- /dev/null +++ b/src/frameworks/customtk/widgets/optionMenu.js @@ -0,0 +1,139 @@ +import Tools from "../../../canvas/constants/tools" +import { DownOutlined } from "@ant-design/icons" +import { TkinterWidgetBase} from "./base" +import { convertObjectToKeyValueString } from "../../../utils/common" + + +class OptionMenu extends TkinterWidgetBase{ + + static widgetType = "option_menu" + + constructor(props) { + super(props) + + // const {layout, ...newAttrs} = this.state.attrs // Removes the layout attribute + + this.minSize = {width: 50, height: 30} + + this.state = { + ...this.state, + isDropDownOpen: false, + widgetName: "Option menu", + size: { width: 120, height: 'fit' }, + attrs: { + ...this.state.attrs, + defaultValue: { + label: "Default Value", + tool: Tools.INPUT, + value: "Select option", + onChange: ({inputs, selectedRadio}) => { + this.setAttrValue("options", {inputs, selectedRadio}) + } + }, + widgetOptions: { + label: "Options", + tool: Tools.INPUT_RADIO_LIST, + value: {inputs: ["option 1"], selectedRadio: -1}, + onChange: ({inputs, selectedRadio}) => { + this.setAttrValue("widgetOptions", {inputs, selectedRadio}) + } + } + + } + } + + } + + componentDidMount(){ + super.componentDidMount() + this.setWidgetInnerStyle("backgroundColor", "#fff") + } + + generateCode(variableName, parent){ + + + const config = convertObjectToKeyValueString(this.getConfigCode()) + + const defaultValue = this.getAttrValue("defaultValue") + const options = JSON.stringify(this.getAttrValue("widgetOptions").inputs) + + const code = [ + `${variableName}_options = ${options}`, + `${variableName}_var = tk.StringVar(value="${defaultValue || options.at(1) || ""}")`, + `${variableName} = tk.OptionMenu(${parent}, ${variableName}_var, *${variableName}_options)` + ] + + + return [ + ...code, + `${variableName}.config(${config})`, + `${variableName}.${this.getLayoutCode()}` + ] + } + + getToolbarAttrs(){ + + const toolBarAttrs = super.getToolbarAttrs() + + const attrs = this.state.attrs + + return ({ + id: this.__id, + widgetName: toolBarAttrs.widgetName, + checkLabel: attrs.checkLabel, + size: toolBarAttrs.size, + ...attrs, + }) + } + + toggleDropDownOpen = () => { + this.setState((prev) => ({ + isDropDownOpen: !prev.isDropDownOpen + })) + } + + renderContent(){ + + const {inputs, selectedRadio} = this.getAttrValue("widgetOptions") + + return ( +
+
+ {this.getAttrValue("defaultValue")} + +
+ +
+
+ {this.state.isDropDownOpen && +
+ { + inputs.map((value, index) => { + return ( +
+ + + {value} + +
+ ) + }) + } +
+ } + +
+ ) + } + +} + + +export default OptionMenu \ No newline at end of file diff --git a/src/frameworks/customtk/widgets/slider.js b/src/frameworks/customtk/widgets/slider.js new file mode 100644 index 0000000..3c9da59 --- /dev/null +++ b/src/frameworks/customtk/widgets/slider.js @@ -0,0 +1,159 @@ +import Widget from "../../../canvas/widgets/base" +import Tools from "../../../canvas/constants/tools" +import { convertObjectToKeyValueString, removeKeyFromObject } from "../../../utils/common" +import {TkinterBase, TkinterWidgetBase} from "./base" + + +class Slider extends TkinterWidgetBase{ + + static widgetType = "scale" + // FIXME: You provided a `value` prop to a form field without an `onChange` handler. This will render a read-only field. If the field should be mutable use + constructor(props) { + super(props) + + this.state = { + ...this.state, + widgetName: "Scale", + size: { width: 'fit', height: 'fit' }, + attrs: { + ...this.state.attrs, + styling: { + ...this.state.attrs.styling, + // TODO: trough color + troughColor: { + label: "Trough Color", + tool: Tools.COLOR_PICKER, + value: "#fff", + onChange: (value) => { + // this.setWidgetInnerStyle("color", value) + this.setAttrValue("styling.troughColor", value) + } + } + }, + scale: { + label: "Scale", + display: "horizontal", + min: { + label: "Min", + tool: Tools.NUMBER_INPUT, // the tool to display, can be either HTML ELement or a constant string + toolProps: { placeholder: "min" }, + value: 0, + onChange: (value) => this.setAttrValue("scale.min", value) + }, + max: { + label: "Max", + tool: Tools.NUMBER_INPUT, + toolProps: { placeholder: "max"}, + value: 100, + onChange: (value) => this.setAttrValue("scale.max", value) + }, + step: { + label: "Step", + tool: Tools.NUMBER_INPUT, + toolProps: { placeholder: "max", stringMode: true, step: "0.1"}, + value: 1, + onChange: (value) => this.setAttrValue("scale.step", value) + }, + default: { + label: "Default", + tool: Tools.NUMBER_INPUT, + toolProps: { placeholder: "max", stringMode: true, step: "0.1"}, + value: 0, + onChange: (value) => this.setAttrValue("scale.default", value) + } + }, + orientation: { + label: "Orientation", + tool: Tools.SELECT_DROPDOWN, + toolProps: {placeholder: "select orientation"}, + value: "", + options: [{value: "horizontal", label: "horizontal"}, {value: "vertical", label: "vertical"}], + onChange: (value) => { + + // const widgetStyling = { + // transformOrigin: "0 0", + // transform: value === "horizontal" ? "rotate(0deg)" : "rotate(90deg)" + // } + + // this.setState((prev) => ({ + // widgetOuterStyling: {...prev, ...widgetStyling} + // })) + // this.setWidgetOuterStyle("transform-origin", "0 0") + // this.setWidgetOuterStyle("transform", value === "horizontal" ? "rotate(0deg)" : "rotate(90deg)") + this.setAttrValue("orientation", value) + } + }, + + } + } + } + + componentDidMount(){ + super.componentDidMount() + this.setAttrValue("styling.backgroundColor", "#fff") + } + + generateCode(variableName, parent){ + // TODO: add orientation + + const config = this.getConfigCode() + + config["from_"] = this.getAttrValue("scale.min") + config["to"] = this.getAttrValue("scale.max") + config["resolution"] = this.getAttrValue("scale.step") + + if (this.getAttrValue("orientation")){ + config["orientation"] = this.getAttrValue("orientation") + } + + const defaultValue = this.getAttrValue("scale.default") + + return [ + `${variableName}_var = tk.DoubleVar(value=${defaultValue})`, + `${variableName} = tk.Scale(master=${parent}, variable=${variableName}_var)`, + `${variableName}.config(${convertObjectToKeyValueString(config)})`, + `${variableName}.${this.getLayoutCode()}` + ] + } + + getToolbarAttrs(){ + + const toolBarAttrs = super.getToolbarAttrs() + + return ({ + id: this.__id, + widgetName: toolBarAttrs.widgetName, + placeHolder: this.state.attrs.placeHolder, + size: toolBarAttrs.size, + + ...this.state.attrs, + + }) + } + + renderContent(){ + return ( +
+
+
+ + +
+
+
+ ) + } + +} + + +export default Slider \ No newline at end of file diff --git a/src/frameworks/customtk/widgets/spinBox.js b/src/frameworks/customtk/widgets/spinBox.js new file mode 100644 index 0000000..56cdf6d --- /dev/null +++ b/src/frameworks/customtk/widgets/spinBox.js @@ -0,0 +1,128 @@ +import Widget from "../../../canvas/widgets/base" +import Tools from "../../../canvas/constants/tools" +import { convertObjectToKeyValueString, removeKeyFromObject } from "../../../utils/common" +import { DownOutlined, UpOutlined } from "@ant-design/icons" +import {TkinterBase, TkinterWidgetBase} from "./base" + + +class SpinBox extends TkinterWidgetBase{ + + static widgetType = "spin_box" + + constructor(props) { + super(props) + + this.state = { + ...this.state, + size: { width: 70, height: 'fit' }, + widgetName: "Spin box", + attrs: { + ...this.state.attrs, + spinProps: { + label: "Properties", + display: "horizontal", + min: { + label: "Min", + tool: Tools.NUMBER_INPUT, // the tool to display, can be either HTML ELement or a constant string + toolProps: { placeholder: "min" }, + value: 0, + onChange: (value) => this.setAttrValue("spinProps.min", value) + }, + max: { + label: "Max", + tool: Tools.NUMBER_INPUT, + toolProps: { placeholder: "max"}, + value: 100, + onChange: (value) => this.setAttrValue("spinProps.max", value) + }, + step: { + label: "Step", + tool: Tools.NUMBER_INPUT, + toolProps: { placeholder: "max", stringMode: true, step: "0.1"}, + value: 1, + onChange: (value) => this.setAttrValue("spinProps.step", value) + }, + default: { + label: "Default", + tool: Tools.NUMBER_INPUT, + toolProps: { placeholder: "max", stringMode: true, step: "0.1"}, + value: 0, + onChange: (value) => this.setAttrValue("spinProps.default", value) + } + }, + + } + } + } + + componentDidMount(){ + super.componentDidMount() + this.setAttrValue("styling.backgroundColor", "#fff") + } + + generateCode(variableName, parent){ + + const min = this.getAttrValue("spinProps.min") + const max = this.getAttrValue("spinProps.max") + const step = this.getAttrValue("spinProps.step") + const defaultValue = this.getAttrValue("spinProps.default") + + const config = { + from_: min, + to: max, + increment: step, + value: defaultValue, + ...this.getConfigCode() + } + + const code = [] + let spinBox = `${variableName} = tk.Spinbox(master=${parent})` + if (defaultValue){ + code.push(`${variableName}_var = tk.IntVar(${defaultValue})`) + spinBox = `${variableName} = tk.Spinbox(master=${parent}, textvariable=${variableName}_var)` + } + code.push(spinBox) + + return [ + ...code, + `${variableName}.config(${convertObjectToKeyValueString(config)})`, + `${variableName}.${this.getLayoutCode()}` + ] + } + + getToolbarAttrs(){ + + const toolBarAttrs = super.getToolbarAttrs() + + return ({ + id: this.__id, + widgetName: toolBarAttrs.widgetName, + placeHolder: this.state.attrs.placeHolder, + size: toolBarAttrs.size, + + ...this.state.attrs, + + }) + } + + renderContent(){ + return ( +
+
+
+ {this.getAttrValue("spinProps.default")} +
+
+ + +
+
+
+ ) + } + +} + + +export default SpinBox \ No newline at end of file diff --git a/src/frameworks/customtk/widgets/toplevel.js b/src/frameworks/customtk/widgets/toplevel.js new file mode 100644 index 0000000..e312b74 --- /dev/null +++ b/src/frameworks/customtk/widgets/toplevel.js @@ -0,0 +1,89 @@ +import Widget from "../../../canvas/widgets/base" +import Tools from "../../../canvas/constants/tools" + + +class TopLevel extends Widget{ + + static widgetType = "toplevel" + + constructor(props) { + super(props) + + this.droppableTags = { + exclude: ["image", "video", "media", "main_window", "toplevel"] + } + this.maxSize = { width: 2000, height: 2000 } // disables resizing above this number + + this.state = { + ...this.state, + size: { width: 450, height: 200 }, + widgetName: "top level", + attrs: { + ...this.state.attrs, + title: { + label: "Window Title", + tool: Tools.INPUT, // the tool to display, can be either HTML ELement or a constant string + toolProps: {placeholder: "Window title", maxLength: 40}, + value: "Top level", + onChange: (value) => this.setAttrValue("title", value) + } + + } + } + } + + componentDidMount(){ + super.componentDidMount() + this.setAttrValue("styling.backgroundColor", "#E4E2E2") + } + + generateCode(variableName, parent){ + + const backgroundColor = this.getAttrValue("styling.backgroundColor") + + return [ + `${variableName} = tk.Toplevel(master=${parent})`, + `${variableName}.config(bg="${backgroundColor}")`, + `${variableName}.title("${this.getAttrValue("title")}")` + ] + } + + getToolbarAttrs(){ + const toolBarAttrs = super.getToolbarAttrs() + + return ({ + widgetName: toolBarAttrs.widgetName, + title: this.state.attrs.title, + size: toolBarAttrs.size, + + ...this.state.attrs, + + }) + } + + renderContent(){ + return ( +
+
+
{this.getAttrValue("title")}
+
+
+
+
+
+
+
+
+
+
+ {this.props.children} +
+
+ ) + } + +} + + +export default TopLevel \ No newline at end of file diff --git a/src/frameworks/tkinter/engine/code.js b/src/frameworks/tkinter/engine/code.js index 6a6ac93..a9644b1 100644 --- a/src/frameworks/tkinter/engine/code.js +++ b/src/frameworks/tkinter/engine/code.js @@ -130,8 +130,6 @@ async function generateTkinterCode(projectName, widgetList=[], widgetRefs=[], as message.info("starting zipping files, download will start in a few seconds") - return - const createFileList = [ { fileData: code.join(""), diff --git a/src/frameworks/tkinter/widgets/base.js b/src/frameworks/tkinter/widgets/base.js index 18e3682..bbb908e 100644 --- a/src/frameworks/tkinter/widgets/base.js +++ b/src/frameworks/tkinter/widgets/base.js @@ -560,33 +560,41 @@ export class TkinterWidgetBase extends TkinterBase{ getConfigCode(){ - const code = { + const config = { bg: `"${this.getAttrValue("styling.backgroundColor")}"`, fg: `"${this.getAttrValue("styling.foregroundColor")}"`, } if (this.getAttrValue("styling.borderWidth")) - code["bd"] = this.getAttrValue("styling.borderWidth") + config["bd"] = this.getAttrValue("styling.borderWidth") if (this.getAttrValue("styling.relief")) - code["relief"] = `"${this.getAttrValue("styling.relief")}"` + config["relief"] = `"${this.getAttrValue("styling.relief")}"` if (this.getAttrValue("font.fontFamily") || this.getAttrValue("font.fontSize")){ - code["font"] = `("${this.getAttrValue("font.fontFamily")}", ${this.getAttrValue("font.fontSize") || 12}, )` + config["font"] = `("${this.getAttrValue("font.fontFamily")}", ${this.getAttrValue("font.fontSize") || 12}, )` } if (this.getAttrValue("cursor")) - code["cursor"] = `"${this.getAttrValue("cursor")}"` + config["cursor"] = `"${this.getAttrValue("cursor")}"` if (this.getAttrValue("padding.padX")){ - code["padx"] = this.getAttrValue("padding.padX") + config["padx"] = this.getAttrValue("padding.padX") } if (this.getAttrValue("padding.padY")){ - code["pady"] = this.getAttrValue("padding.padY") + config["pady"] = this.getAttrValue("padding.padY") } - return code + // 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 } } \ No newline at end of file