working on label image resize fix

This commit is contained in:
paul
2025-03-31 20:38:25 +05:30
parent bdd3bab3a5
commit a38cd90c16
10 changed files with 3611 additions and 1315 deletions

4591
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -63,8 +63,26 @@
]
},
"devDependencies": {
"@babel/core": "^7.26.10",
"@babel/preset-env": "^7.26.9",
"@babel/preset-react": "^7.26.3",
"ajv": "^7.2.4",
"babel-loader": "^10.0.0",
"clean-webpack-plugin": "^4.0.0",
"css-loader": "^7.1.2",
"css-minimizer-webpack-plugin": "^7.0.2",
"docsify-cli": "^4.4.4",
"typescript": "^4.9.5"
"file-loader": "^6.2.0",
"html-webpack-plugin": "^5.6.3",
"mini-css-extract-plugin": "^2.9.2",
"raw-loader": "^4.0.2",
"react-app-rewired": "^2.2.1",
"sass-loader": "^16.0.5",
"style-loader": "^4.0.0",
"terser-webpack-plugin": "^5.3.14",
"typescript": "^4.9.5",
"webpack": "^5.98.0",
"webpack-cli": "^6.0.1",
"webpack-dev-server": "^5.2.1"
}
}

View File

@@ -41,6 +41,8 @@ class Widget extends React.Component {
static requirements = [] // requirements for the widgets (libraries) eg: tkvideoplayer, tktimepicker
static requiredImports = [] // import statements
static requiredCustomPyFiles = [] // custom widgets inside of pythonWidgets don't add .py at the end
// static contextType = ActiveWidgetContext
constructor(props) {
@@ -185,6 +187,7 @@ class Widget extends React.Component {
this.getImports = this.getImports.bind(this)
this.getRequirements = this.getRequirements.bind(this)
this.getRequiredCustomPyFiles = this.getRequiredCustomPyFiles.bind(this)
// this.openRenaming = this.openRenaming.bind(this)
@@ -214,8 +217,6 @@ class Widget extends React.Component {
this.stateUpdateCallback = null // allowing other components such as toolbar to subscribe to changes in this widget
this.resizeObserver = null
}
@@ -428,6 +429,10 @@ class Widget extends React.Component {
return this.constructor.requiredImports
}
getRequiredCustomPyFiles(){
return this.constructor.requiredCustomPyFiles
}
generateCode(){
throw new NotImplementedError("generateCode() must be implemented by the subclass")
}
@@ -918,6 +923,8 @@ class Widget extends React.Component {
pos
}
const {layout} = attrs
this.setState(newData, () => {
// Updates attrs
let newAttrs = { ...this.state.attrs }
@@ -949,6 +956,10 @@ class Widget extends React.Component {
if (selected){
this.select()
}
if (layout){
this.setLayout(layout)
}
})

View File

@@ -574,11 +574,13 @@ export class CustomTkBase extends Widget {
let expandValue = expand ? (isSameSide ? previousExpandValue : widgets.length - index) : 1;
if ((expand && previousExpandValue === 0) || (expand && expandValue === 0)){
if (expand && expandValue === 0){
expandValue = 1 // if there is expand it should expand
}
if (expand && !isSameSide) previousExpandValue = expandValue;
if ((expand && !isSameSide) || (expand && previousExpandValue === 0)){
previousExpandValue = expandValue;
}
lastSide = side; // Update last side for recursion

View File

@@ -4,6 +4,8 @@ import MainWindow from "../widgets/mainWindow"
import { message } from "antd"
import TopLevel from "../widgets/toplevel"
const pythonFiles = require.context("../pythonWidgets", false, /\.py$/)
// 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
@@ -13,6 +15,8 @@ function generateTkinterCodeList(widgetList = [], widgetRefs = [], parentVariabl
let requirements = new Set([])
let code = []
let customPythonWidgets = new Set([])
for (let widget of widgetList) {
const widgetRef = widgetRefs[widget.id].current
let varName = widgetRef.getVariableName()
@@ -20,6 +24,7 @@ function generateTkinterCodeList(widgetList = [], widgetRefs = [], parentVariabl
// Add imports and requirements to sets
widgetRef.getImports().forEach(importItem => imports.add(importItem))
widgetRef.getRequirements().forEach(requirementItem => requirements.add(requirementItem))
widgetRef.getRequiredCustomPyFiles().forEach(customFile => customPythonWidgets.add(customFile))
// Set main variable if the widget is MainWindow
if (widget.widgetType === MainWindow) {
@@ -68,6 +73,8 @@ function generateTkinterCodeList(widgetList = [], widgetRefs = [], parentVariabl
// Merge child imports, requirements, and code
imports = new Set([...imports, ...childResult.imports])
requirements = new Set([...requirements, ...childResult.requirements])
customPythonWidgets = new Set([...customPythonWidgets, ...childResult.customPythonWidgets])
code.push(...childResult.code)
mainVariable = childResult.mainVariable || mainVariable // the main variable is the main window variable
@@ -78,6 +85,7 @@ function generateTkinterCodeList(widgetList = [], widgetRefs = [], parentVariabl
imports: Array.from(imports),
code: code,
requirements: Array.from(requirements),
customPythonWidgets: Array.from(customPythonWidgets),
mainVariable
}
}
@@ -113,9 +121,13 @@ async function generateTkinterCode(projectName, widgetList=[], widgetRefs=[], as
const generatedObject = generateTkinterCodeList(filteredWidgetList, widgetRefs.current, "", "")
const {code: codeLines, imports, requirements, mainVariable} = generatedObject
const {code: codeLines, imports, requirements, mainVariable, customPythonWidgets} = generatedObject
console.log("custom python widgets: ", customPythonWidgets)
// TODO: avoid adding \n inside the list instead rewrite using code.join("\n")
// TODO: import customWidgets
const code = [
"# This code is generated by PyUIbuilder: https://pyuibuilder.com",
"\n\n",
@@ -147,6 +159,26 @@ async function generateTkinterCode(projectName, widgetList=[], widgetRefs=[], as
})
}
// TODO: add empty __init__ file
for (let customWidget of customPythonWidgets){
let [fileName, extension] = customWidget.split(".")
if (!extension){
fileName = `${fileName}.py`
}
const fileContent = pythonFiles(`./${fileName}`);
console.log("file name: ", fileContent.default, pythonFiles(`./${fileName}`))
createFileList.push({
fileData: new Blob([fileContent], { type: "text/plain" }),
fileName: fileName,
folder: "customWidgets"
})
}
for (let asset of assetFiles){
if (asset.fileType === "image"){

View File

@@ -0,0 +1,67 @@
import tkinter as tk
from PIL import Image, ImageTk
class ImageLabel(tk.Label):
def __init__(self, master, image_path, mode="fit", *args, **kwargs):
"""
mode:
- "fit" -> Keeps aspect ratio, fits inside label
- "cover" -> Covers label fully, cropping excess
"""
super().__init__(master, *args, **kwargs)
self.parent = master # Store parent reference
self.image_path = image_path
self.mode = mode
self.original_image = Image.open(image_path)
self.photo = None
self.resize_job = None # Debounce job reference
self.force_resize() # Initial resize
self.after(100, self.init_events) # Delay event binding slightly
def init_events(self):
self.parent.bind("<Configure>", self.on_resize) # Bind resize to parent
def on_resize(self, event=None):
"""Debounce resizing to prevent rapid execution."""
if self.resize_job:
self.after_cancel(self.resize_job)
self.resize_job = self.after(50, self.force_resize) # Debounce
def force_resize(self):
"""Resize image using actual widget size."""
width = self.winfo_width()
height = self.winfo_height()
if width < 5 or height < 5:
return
aspect_ratio = self.original_image.width / self.original_image.height
if self.mode == "fit":
if width / height > aspect_ratio:
new_width = int(height * aspect_ratio)
new_height = height
else:
new_width = width
new_height = int(width / aspect_ratio)
resized = self.original_image.resize((new_width, new_height), Image.LANCZOS)
elif self.mode == "cover":
if width / height > aspect_ratio:
new_width = width
new_height = int(width / aspect_ratio)
else:
new_width = int(height * aspect_ratio)
new_height = height
resized = self.original_image.resize((new_width, new_height), Image.LANCZOS)
# Crop excess
left = (new_width - width) // 2
top = (new_height - height) // 2
right = left + width
bottom = top + height
resized = resized.crop((left, top, right, bottom))
# Update image
self.photo = ImageTk.PhotoImage(resized)
self.config(image=self.photo)

View File

@@ -0,0 +1 @@
# contains python coded custom widgets

View File

@@ -570,17 +570,29 @@ export class TkinterBase extends Widget {
}
const currentWidgetDirection = directionMap[side] || "column"; // Default to "column"
const isSameSide = lastSide === side;
const isVertical = ["top", "bottom"].includes(side);
let expandValue = expand ? (isSameSide ? previousExpandValue : widgets.length - index) : 1;
if ((expand && previousExpandValue === 0) || (expand && expandValue === 0)){
const isSameSide = lastSide === side
const isOppositeSide = ((lastSide === "top" && side === "bottom") || (lastSide === "left" && side === "right"))
const isDiagonal = (!isSameSide && !isOppositeSide && lastSide !== "") // bottom and right, top and left
let expandValue = expand ? ((isSameSide || isOppositeSide) ? previousExpandValue : (widgets.length - index) + 1) : (previousExpandValue > 0) ? 0 : 1
if (expand && expandValue === 0){
expandValue = 1 // if there is expand it should expand
}
if (expand && !isSameSide) previousExpandValue = expandValue;
if (expand && isDiagonal){
expandValue = 1
}
// TODO: if previous expand value is greater than 0 and current doesn't have expand then it should be 0
if ((expand && !isSameSide) || (expand && previousExpandValue === 0)){
previousExpandValue = expandValue;
}
lastSide = side; // Update last side for recursion
const anchorStyles = {
@@ -627,11 +639,12 @@ export class TkinterBase extends Widget {
return (
<div
data-pack-container={side}
className={`tw-flex ${isVertical ? "!tw-h-full" : "!tw-w-full"}`}
className={`tw-flex tw-flex-auto`}
// 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
flexGrow: expandValue, //((widgets.length - 1) === index) ? 1 : 0, // last index will always have flex-grow 1
flexShrink: expand ? 0 : 1,
flexBasis: "auto",
minWidth: isVertical ? "0" : "auto",

View File

@@ -1,3 +1,5 @@
import { useEffect, useState } from "react"
import { Layouts } from "../../../canvas/constants/layouts"
import Tools from "../../../canvas/constants/tools"
import { convertObjectToKeyValueString } from "../../../utils/common"
import { getPythonAssetPath } from "../../utils/pythonFilePath"
@@ -10,6 +12,7 @@ class Label extends TkinterWidgetBase{
static widgetType = "label"
static displayName = "Label"
static requiredCustomPyFiles = ["imageLabel"]
constructor(props) {
super(props)
@@ -55,6 +58,34 @@ class Label extends TkinterWidgetBase{
value: "",
onChange: (value) => this.setAttrValue("imageUpload", value)
},
imageSize: {
label: "Image size",
display: "horizontal",
// width: {
// label: "Width",
// tool: Tools.NUMBER_INPUT, // the tool to display, can be either HTML ELement or a constant string
// toolProps: { placeholder: "width", max: 3000, min: 1 },
// value: 150,
// onChange: (value) => this.setWidgetSize(value, null)
// },
// height: {
// label: "Height",
// tool: Tools.NUMBER_INPUT,
// toolProps: { placeholder: "height", max: 3000, min: 1 },
// value: 150,
// onChange: (value) => this.setWidgetSize(null, value)
// },
mode: {
label: "Image mode",
tool: Tools.SELECT_DROPDOWN,
options: ["fit", "cover"].map((val) => ({value: val, label: val})),
value: "cover",
onChange: (value) => {
this.setAttrValue("imageSize.mode", value)
}
}
},
}
}
@@ -92,10 +123,28 @@ class Label extends TkinterWidgetBase{
const config = super.getConfigCode()
const anchor = this.getAttrValue("styling.anchor")
const fitWidth = this.state.fitContent.width
const fitHeight = this.state.fitContent.height
const {width, height} = this.getSize()
const {layout} = this.getParentLayout()
if (anchor)
config['anchor'] = `"${anchor}"`
// LABEL width and height are not pixel based instead its character based
// if (layout !== Layouts.PLACE){
// if (!fitWidth){
// config['width'] = width
// }
// if (!fitHeight){
// config['height'] = height
// }
// }
return config
}
@@ -155,36 +204,53 @@ class Label extends TkinterWidgetBase{
center: { justifyContent: 'center', alignItems: 'center' }
}
return anchorStyles[anchor] || anchorStyles["w"];
return anchorStyles[anchor] || anchorStyles["center"];
}
renderContent(){
//FIXME: label image causing issues
const image = this.getAttrValue("imageUpload")
const imageMode = this.getAttrValue("imageSize.mode") || "cover"
const imgClassName = imageMode === "fit" ? "tw-object-contain" : (imageMode === "cover" ? "tw-object-cover" : "")
return (
<div className="tw-w-flex tw-flex-col tw-w-full tw-content-start tw-h-full tw-rounded-md tw-overflow-hidden"
style={{
flexGrow: 1, // Ensure the content grows to fill the parent
minWidth: '100%', // Force the width to 100% of the parent
minHeight: '100%', // Force the height to 100% of the parent
}}
<div className="tw-flex tw-flex-col tw-w-full tw-relative tw-content-start tw-h-full tw-rounded-md tw-overflow-hidden"
// style={{
// flexGrow: 1, // Ensure the content grows to fill the parent
// minWidth: '100%', // Force the width to 100% of the parent
// minHeight: '100%', // Force the height to 100% of the parent
// }}
>
<div className="tw-p-2 tw-w-full tw-h-full tw-flex tw-place-content-center tw-place-items-center "
<div className="tw-p-2 tw-w-full tw-h-full tw-overflow-hidden tw-flex tw-place-content-center tw-place-items-center"
ref={this.styleAreaRef}
style={this.getInnerRenderStyling()}>
{/* {this.props.children} */}
{
image && (
<img src={image.previewUrl} className="tw-bg-contain tw-w-full tw-h-full" />
)
}
<div className={`tw-flex ${!image ? "tw-w-full tw-h-full" : ""}`} style={{
color: this.getAttrValue("styling.foregroundColor"),
...this.getAnchorStyle(this.getAttrValue("styling.anchor"))
}}>
{this.getAttrValue("labelWidget")}
</div>
{/* {this.props.children} */}
{
image ? (
<div className="tw-relative tw-w-full tw-h-full tw-overflow-hidden">
<img src={image.previewUrl}
className={`${imgClassName}`}
alt={this.getAttrValue("widgetName")}
style={{
width: "100%",
height: "100%",
position: "absolute",
top: '0px',
left: '0px',
}}
// className="tw-object-cover"
/>
</div>
) : null
}
<div className={`tw-flex ${!image ? "tw-w-full tw-h-full" : ""}`} style={{
color: this.getAttrValue("styling.foregroundColor"),
...this.getAnchorStyle(this.getAttrValue("styling.anchor"))
}}>
{this.getAttrValue("labelWidget")}
</div>
</div>
</div>
)
@@ -193,4 +259,44 @@ class Label extends TkinterWidgetBase{
}
function LabelImage({imageSrc, alt, styleAreaRef}){
const [size, setSize] = useState({
width: 0,
height: 0
})
useEffect(() => {
if (!styleAreaRef.current) return;
// Function to update size
const updateSize = () => {
const boundingBox = styleAreaRef.current.getBoundingClientRect();
setSize({ width: boundingBox.width, height: boundingBox.height });
};
// Initial size update
updateSize();
// Observe size changes
const resizeObserver = new ResizeObserver(updateSize);
resizeObserver.observe(styleAreaRef.current);
return () => resizeObserver.disconnect();
}, [styleAreaRef])
return (
<img src={imageSrc} alt={alt} className="tw-object-cover"
style={{
position: "absolute",
top: '0px',
left: '0px',
width: "100%", // Prevent overflow
height: "100%", // Prevent overflow
}}
/>
)
}
export default Label

11
webpack.config.js Normal file
View File

@@ -0,0 +1,11 @@
module.exports = {
module: {
rules: [
{
test: /\.py$/,
use: "raw-loader",
},
],
},
};