#!/usr/bin/env python3
"""
Enhanced Image Studio GUI - Refactored Main Application.
A visual tool for developers to design images/icons and generate embedded code.
Features detachable left and right panels with fixed width.
"""
import base64
import gc
import json
import os
import tempfile
import tkinter as tk
from collections import Counter
from io import BytesIO
from tkinter import colorchooser, filedialog, messagebox, simpledialog, ttk
from typing import Dict, List, Optional, Tuple
# Optional memory monitoring
try:
import psutil
PSUTIL_AVAILABLE = True
except ImportError:
PSUTIL_AVAILABLE = False
from PIL import Image, ImageDraw, ImageFilter, ImageFont, ImageTk
from threepanewindows import ( # type: ignore[import]
EnhancedDockableThreePaneWindow,
PaneConfig,
)
from gui_image_studio.embedded_icons import cleanup_icon, get_icon_path
from .core.canvas_manager import CanvasManager
from .core.drawing_tools import DrawingToolsManager
# Import refactored components
from .core.image_manager import ImageManager
from .ui.dialogs import CodePreviewWindow, HelpWindow, ImageSizeDialog
from .ui.menu import MenuManager
from .ui.panels import PanelManager
[docs]
class EnhancedImageDesignerGUI:
"""Main GUI application for image design and code generation."""
def __init__(self) -> None:
self.root = tk.Tk()
self.root.title("GUI Image Studio - Enhanced")
self.root.geometry("1200x700")
self.root.minsize(800, 500)
# Application state - copied from original
self.current_images: Dict[str, Image.Image] = {}
self.original_images: Dict[str, Image.Image] = (
{}
) # Store originals for rotation
self.base_images: Dict[str, Image.Image] = (
{}
) # Store base images (before rotation)
self.current_rotations: Dict[str, int] = {} # Track current rotation angles
self.image_previews: Dict[str, ImageTk.PhotoImage] = {}
self.selected_image: Optional[str] = None
self.temp_dir = tempfile.mkdtemp()
# Canvas size (moved from drawing tools)
self.canvas_size = (300, 300)
# UI variables that will be created by panels
self.size_var = tk.IntVar(value=5)
self.width_var = tk.IntVar(value=300)
self.height_var = tk.IntVar(value=300)
self.name_var = tk.StringVar()
# UI widgets (will be set during UI setup)
self.canvas: Optional[tk.Canvas] = None
self.image_listbox: Optional[tk.Listbox] = None
self.color_button: Optional[tk.Button] = None
self.preview_canvas: Optional[tk.Canvas] = None
self.grid_var: Optional[tk.BooleanVar] = None
self.rotation_var: Optional[tk.IntVar] = None
# Drawing state variables
self.drawing = False
self.last_x = 0
self.last_y = 0
self.start_x = 0
self.start_y = 0
self.preview_shape = None
self.preview_active = False
self.pixel_highlight = None
self.last_highlight_pos = None
# Code generation variables
self.quality_var = tk.IntVar(value=95)
self.framework_var = tk.StringVar(value="tkinter")
self.usage_var = tk.StringVar(value="general")
# Add trace callbacks to update preview when settings change
self.framework_var.trace("w", lambda *args: self.update_preview())
self.usage_var.trace("w", lambda *args: self.update_preview())
self.quality_var.trace("w", lambda *args: self.update_preview())
# Icon paths for cleanup
self.icon_paths: List[str] = []
# Initialize managers first
self.image_manager = ImageManager()
self.drawing_tools = DrawingToolsManager()
self.canvas_manager = CanvasManager(self)
self.menu_manager = MenuManager(self)
self.panel_manager = PanelManager(self)
# Drawing state (initialize drawing tools grid setting)
self.drawing_tools.show_grid = False
# Cursor settings - copied from original
self.cursor_settings = {
"handedness": "right", # 'left' or 'right'
"brush": "crosshair",
"pencil": "crosshair",
"eraser": "dotbox",
"line": "crosshair",
"rectangle": "crosshair",
"circle": "crosshair",
"text": "xterm",
"fill": "spraycan",
"custom_cursors": {}, # Store custom cursor data
}
self.tool_buttons = {} # Dictionary to store tool button references
# Load cursor settings from file if exists
self.load_cursor_settings()
self.setup_ui()
self.setup_bindings()
self.setup_preview_bindings()
# Don't select any tool initially when no images are present
# Tools will be enabled when an image is created or loaded
# Initialize UI state
self.update_ui_state()
self.update_canvas() # Show initial instructions
self.update_preview() # Show initial preview
# Set initial button styles (prominent when no images)
self.update_button_styles()
# Center the window on the desktop
self.center_window()
# Set up proper cleanup on window close
self.root.protocol("WM_DELETE_WINDOW", self.on_closing)
[docs]
def center_window(self) -> None:
"""Center the window on the desktop."""
self.root.update_idletasks()
w = self.root.winfo_width()
h = self.root.winfo_height()
ws = self.root.winfo_screenwidth()
hs = self.root.winfo_screenheight()
# Reserve space for the Windows taskbar (typically ~40px, adjust if needed)
TASKBAR_HEIGHT = 80
if h > hs - TASKBAR_HEIGHT:
h = hs - TASKBAR_HEIGHT
x = max((ws // 2) - (w // 2), 0)
y = max((hs - TASKBAR_HEIGHT) // 2 - (h // 2), 0)
self.root.geometry(f"{w}x{h}+{x}+{y}")
[docs]
def setup_ui(self) -> None:
"""Setup the enhanced user interface with threepanewindows."""
# Create menu bar first
self.menu_manager.setup_menu()
# Configure custom button styles for prominence
self.setup_button_styles()
# Configure pane configurations with embedded icons
tools_icon = get_icon_path("tools")
canvas_icon = get_icon_path("canvas")
settings_icon = get_icon_path("settings")
# Track icon paths for cleanup
self.icon_paths.extend([tools_icon, canvas_icon, settings_icon])
left_config = PaneConfig(
title="Tools & Images",
icon="🛠️",
window_icon=tools_icon,
custom_titlebar=True,
custom_titlebar_shadow=True,
detached_height=600, # Fixed height for detached window
detached_scrollable=True, # Enable scrollbars if content is too tall
min_width=200,
max_width=200,
default_width=200,
fixed_width=200,
resizable=False,
detachable=True,
closable=False,
)
center_config = PaneConfig(
title="Canvas",
icon="🎨",
window_icon=canvas_icon,
resizable=True,
detachable=False,
closable=False,
)
right_config = PaneConfig(
title="Properties & Code",
icon="⚙️",
window_icon=settings_icon,
custom_titlebar=True,
custom_titlebar_shadow=False, # No shadow for this panel
detached_height=500, # Different height for this panel
detached_scrollable=True,
min_width=200,
max_width=200,
default_width=200,
fixed_width=200,
resizable=False,
detachable=True,
closable=False,
)
# Create the enhanced three-pane window
self.three_pane = EnhancedDockableThreePaneWindow(
master=self.root,
left_config=left_config,
center_config=center_config,
right_config=right_config,
left_builder=self.build_left_panel,
center_builder=self.build_center_panel,
right_builder=self.build_right_panel,
theme_name="light",
enable_animations=True,
menu_bar=None, # We'll handle menu separately
)
self.three_pane.pack(fill=tk.BOTH, expand=True)
# Store references to the pane frames
self.left_frame = self.three_pane.get_pane_frame("left")
self.center_frame = self.three_pane.get_pane_frame("center")
self.right_frame = self.three_pane.get_pane_frame("right")
[docs]
def build_left_panel(self, parent) -> None:
"""Build the left panel with tools and image management."""
self.panel_manager.setup_left_panel(parent)
# Update the image list and restore selection after the panel is rebuilt (e.g., after detach/reattach)
self.root.after_idle(self._restore_left_panel_state)
def _restore_left_panel_state(self) -> None:
"""Restore the left panel state after it's been rebuilt (detach/reattach)."""
# Update the image list
self.update_image_list()
# Restore the selected image if there was one
if self.selected_image and hasattr(self, "image_listbox"):
try:
items = list(self.current_images.keys())
if self.selected_image in items:
index = items.index(self.selected_image)
self.image_listbox.selection_clear(0, tk.END)
self.image_listbox.selection_set(index)
self.image_listbox.see(index)
except Exception as e:
print(f"Error restoring image selection: {e}")
[docs]
def build_center_panel(self, parent) -> None:
"""Build the center panel with the drawing canvas."""
self.panel_manager.setup_center_panel(parent)
[docs]
def build_right_panel(self, parent) -> None:
"""Build the right panel with properties and code generation."""
self.panel_manager.setup_right_panel(parent)
# Restore the right panel state after it's been rebuilt (detach/reattach)
self.root.after_idle(self._restore_right_panel_state)
def _restore_right_panel_state(self) -> None:
"""Restore the right panel state after it's been rebuilt (detach/reattach)."""
# Update the properties if there's a selected image
if self.selected_image and self.selected_image in self.current_images:
image = self.current_images[self.selected_image]
if hasattr(self, "width_var"):
self.width_var.set(image.width)
if hasattr(self, "height_var"):
self.height_var.set(image.height)
if hasattr(self, "name_var"):
self.name_var.set(self.selected_image)
# Update the preview
self.update_preview()
[docs]
def setup_bindings(self):
"""Setup keyboard and mouse bindings."""
# Keyboard shortcuts
self.root.bind("<Control-n>", lambda e: self.new_image())
self.root.bind("<Control-o>", lambda e: self.load_image())
self.root.bind("<Control-q>", lambda e: self.root.quit())
self.root.bind("<KeyPress-g>", lambda e: self.toggle_grid())
self.root.bind("<KeyPress-plus>", lambda e: self.zoom_in())
self.root.bind("<KeyPress-minus>", lambda e: self.zoom_out())
self.root.bind("<KeyPress-0>", lambda e: self.reset_zoom())
# Help system key bindings
self.root.bind("<F1>", lambda e: self.show_help())
self.root.bind("<Control-F1>", lambda e: self.start_tutorial())
# Tool selection key bindings
self.root.bind(
"<KeyPress-b>", lambda e: self.drawing_tools.select_tool("brush")
)
self.root.bind(
"<KeyPress-p>", lambda e: self.drawing_tools.select_tool("pencil")
)
self.root.bind(
"<KeyPress-e>", lambda e: self.drawing_tools.select_tool("eraser")
)
self.root.bind("<KeyPress-l>", lambda e: self.drawing_tools.select_tool("line"))
self.root.bind(
"<KeyPress-r>", lambda e: self.drawing_tools.select_tool("rectangle")
)
self.root.bind(
"<KeyPress-c>", lambda e: self.drawing_tools.select_tool("circle")
)
self.root.bind("<KeyPress-t>", lambda e: self.drawing_tools.select_tool("text"))
self.root.bind("<KeyPress-f>", lambda e: self.drawing_tools.select_tool("fill"))
self.root.bind(
"<KeyPress-s>", lambda e: self.drawing_tools.select_tool("spray")
)
self.root.bind(
"<KeyPress-m>", lambda e: self.drawing_tools.select_tool("marker")
)
self.root.bind(
"<KeyPress-h>", lambda e: self.drawing_tools.select_tool("highlighter")
)
# Image listbox binding is set up in panels.py
[docs]
def get_cursor_fallback_options(self, tool):
"""Get fallback cursor options based on tool, platform, and handedness."""
# Base options for each tool
base_options = {
"brush": ["crosshair", "pencil", "dotbox"],
"pencil": ["crosshair", "pencil", "dotbox"],
"eraser": ["dotbox", "crosshair"],
"line": ["crosshair", "plus"],
"rectangle": ["crosshair", "plus"],
"circle": ["crosshair", "plus"],
"text": ["xterm", "ibeam"],
"fill": ["spraycan", "crosshair"],
}
return base_options.get(tool, ["crosshair", "arrow"])
[docs]
def load_cursor_settings(self):
"""Load cursor settings from file."""
try:
settings_file = os.path.join(
os.path.expanduser("~"), ".gui_image_studio_cursors.json"
)
if os.path.exists(settings_file):
with open(settings_file, "r") as f:
saved_settings = json.load(f)
self.cursor_settings.update(saved_settings)
except (FileNotFoundError, PermissionError) as e:
print(f"Warning: Could not load cursor settings: {e}. Using defaults.")
except (json.JSONDecodeError, KeyError, ValueError) as e:
print(f"Warning: Invalid cursor settings file format: {e}. Using defaults.")
except Exception as e:
print(
f"Warning: Unexpected error loading cursor settings: {e}. "
"Using defaults."
)
[docs]
def save_cursor_settings(self):
"""Save cursor settings to file."""
try:
settings_file = os.path.join(
os.path.expanduser("~"), ".gui_image_studio_cursors.json"
)
with open(settings_file, "w") as f:
json.dump(self.cursor_settings, f, indent=2)
except (PermissionError, OSError) as e:
print(f"Warning: Could not save cursor settings: {e}")
except (TypeError, ValueError) as e:
print(f"Warning: Invalid cursor settings data: {e}")
except Exception as e:
print(f"Warning: Unexpected error saving cursor settings: {e}")
[docs]
def reset_cursor_settings(self):
"""Reset cursor settings to defaults."""
if messagebox.askyesno(
"Reset Cursor Settings",
"Are you sure you want to reset all cursor settings to defaults?",
):
self.cursor_settings = {
"handedness": "right",
"brush": "crosshair",
"pencil": "crosshair",
"eraser": "dotbox",
"line": "crosshair",
"rectangle": "crosshair",
"circle": "crosshair",
"text": "xterm",
"fill": "spraycan",
"custom_cursors": {},
}
self.save_cursor_settings()
# Update current tool cursor
self.update_tool_cursor(self.drawing_tools.get_current_tool())
messagebox.showinfo(
"Settings Reset", "Cursor settings have been reset to defaults."
)
def open_cursor_settings(self):
"""Open the cursor settings dialog."""
# Placeholder - would need to implement CursorSettingsDialog
messagebox.showinfo(
"Cursor Settings",
"Cursor settings dialog not yet implemented in refactored version.",
)
[docs]
def choose_color(self):
"""Open color chooser dialog."""
color = colorchooser.askcolor(color=self.drawing_tools.get_brush_color())
if color[1]:
self.drawing_tools.set_brush_color(color[1])
if hasattr(self, "color_button"):
self.color_button.configure(bg=self.drawing_tools.get_brush_color())
[docs]
def update_ui_state(self):
"""Update UI state based on current selection."""
# Placeholder for UI state updates
pass
[docs]
def update_canvas(self):
"""Update the canvas display."""
# Clear any active preview shapes and pixel highlights
if hasattr(self, "canvas_manager"):
self.canvas_manager.clear_preview()
self.canvas_manager.clear_pixel_highlight()
if not self.selected_image:
# Clear canvas and show instructions
if hasattr(self, "canvas"):
self.canvas.delete("all")
self.show_canvas_instructions()
return
image = self.current_images[self.selected_image]
# Check if image is too large to process safely
max_image_size = 4096 # Maximum original image dimension
if image.width > max_image_size or image.height > max_image_size:
# Show error and clear canvas
if hasattr(self, "canvas"):
self.canvas.delete("all")
self.canvas.create_text(
200,
200,
text=f"Image too large to display safely\n({image.width}x{image.height})\nMaximum size: {max_image_size}x{max_image_size}",
fill="red",
font=("Arial", 12),
anchor=tk.CENTER,
)
return
# Clean up old PhotoImage to prevent memory leaks
if self.selected_image in self.image_previews:
old_photo = self.image_previews[self.selected_image]
# Force garbage collection of the old PhotoImage
del old_photo
# Create display image with zoom
display_size = (
int(image.width * self.drawing_tools.get_zoom_level()),
int(image.height * self.drawing_tools.get_zoom_level()),
)
# Limit maximum display size to prevent memory issues
max_display_size = 2048 # Maximum dimension in pixels
if display_size[0] > max_display_size or display_size[1] > max_display_size:
# Calculate scaling factor to fit within max size
scale_factor = min(
max_display_size / display_size[0], max_display_size / display_size[1]
)
display_size = (
int(display_size[0] * scale_factor),
int(display_size[1] * scale_factor),
)
try:
display_image = image.resize(display_size, Image.NEAREST)
# Convert to PhotoImage
photo = ImageTk.PhotoImage(display_image)
self.image_previews[self.selected_image] = photo
# Clean up the temporary display_image to free memory
del display_image
except MemoryError:
# Handle memory error gracefully
if hasattr(self, "canvas"):
self.canvas.delete("all")
self.canvas.create_text(
200,
200,
text="Not enough memory to display image\nTry reducing zoom level or image size",
fill="red",
font=("Arial", 12),
anchor=tk.CENTER,
)
# Reset zoom to a safer level
self.drawing_tools.set_zoom_level(1.0)
return
except Exception as e:
# Handle other errors
if hasattr(self, "canvas"):
self.canvas.delete("all")
self.canvas.create_text(
200,
200,
text=f"Error displaying image:\n{str(e)}",
fill="red",
font=("Arial", 12),
anchor=tk.CENTER,
)
return
# Clear and update canvas
if hasattr(self, "canvas"):
self.canvas.delete("all")
self.canvas.create_image(10, 10, anchor=tk.NW, image=photo)
# Draw grid if enabled
if (
self.drawing_tools.show_grid
and self.drawing_tools.get_zoom_level() >= 4
):
self.draw_grid(display_size)
# Update scroll region
self.canvas.configure(
scrollregion=(0, 0, display_size[0] + 20, display_size[1] + 20)
)
# Update preview when canvas changes
if hasattr(self, "preview_canvas"):
self.root.after_idle(self.update_preview)
# Periodic memory cleanup (every 10th canvas update)
if not hasattr(self, "_cleanup_counter"):
self._cleanup_counter = 0
self._cleanup_counter += 1
if self._cleanup_counter >= 10:
self._cleanup_counter = 0
self.root.after_idle(self.cleanup_memory)
[docs]
def show_canvas_instructions(self):
"""Show instructions on empty canvas."""
if not hasattr(self, "canvas"):
return
canvas_width = self.canvas.winfo_width()
canvas_height = self.canvas.winfo_height()
if canvas_width <= 1 or canvas_height <= 1:
# Canvas not yet initialized, schedule for later
self.root.after(100, self.show_canvas_instructions)
return
center_x = canvas_width // 2
center_y = canvas_height // 2
# Add background rectangle first (so it's behind the text)
self.canvas.create_rectangle(
center_x - 200,
center_y - 100,
center_x + 200,
center_y + 100,
outline="#cccccc",
width=2,
fill="#f9f9f9",
tags="instructions",
)
# Main instruction text
self.canvas.create_text(
center_x,
center_y - 70,
text="🎨 Welcome to GUI Image Studio!",
font=("Arial", 16, "bold"),
fill="#333333",
tags="instructions",
)
self.canvas.create_text(
center_x,
center_y - 30,
text="To start drawing, you need to:",
font=("Arial", 12),
fill="#666666",
tags="instructions",
)
self.canvas.create_text(
center_x,
center_y,
text="• Click 'New' to make a new canvas",
font=("Arial", 11),
fill="#666666",
tags="instructions",
)
self.canvas.create_text(
center_x,
center_y + 20,
text="• Click 'Load' to open an existing file",
font=("Arial", 11),
fill="#666666",
tags="instructions",
)
self.canvas.create_text(
center_x,
center_y + 50,
text="Then select a tool and start creating!",
font=("Arial", 11, "italic"),
fill="#0066cc",
tags="instructions",
)
self.canvas.create_text(
center_x,
center_y + 80,
text="💡 Tip: Use the Pencil tool with Grid for pixel art!",
font=("Arial", 10),
fill="#888888",
tags="instructions",
)
[docs]
def draw_grid(self, display_size):
"""Draw grid on the canvas."""
if not hasattr(self, "canvas"):
return
grid_spacing = max(1, int(self.drawing_tools.get_zoom_level()))
# Draw vertical lines
for x in range(10, display_size[0] + 10, grid_spacing):
self.canvas.create_line(
x, 10, x, display_size[1] + 10, fill="#e0e0e0", width=1
)
# Draw horizontal lines
for y in range(10, display_size[1] + 10, grid_spacing):
self.canvas.create_line(
10, y, display_size[0] + 10, y, fill="#e0e0e0", width=1
)
def cleanup_memory(self):
"""Cleanup memory periodically."""
gc.collect()
[docs]
def on_image_select(self, event):
"""Handle image selection from listbox."""
if hasattr(self, "image_listbox"):
selection = self.image_listbox.curselection()
if selection:
name = self.image_listbox.get(selection[0])
self.select_image(name)
[docs]
def select_image(self, name):
"""Select an image."""
if name in self.current_images:
self.selected_image = name
self.update_canvas()
self.update_preview()
# Update properties
image = self.current_images[name]
if hasattr(self, "width_var"):
self.width_var.set(image.width)
if hasattr(self, "height_var"):
self.height_var.set(image.height)
if hasattr(self, "name_var"):
self.name_var.set(name)
# Update listbox selection
if hasattr(self, "image_listbox"):
try:
# Find the index of the selected image
items = list(self.current_images.keys())
if name in items:
index = items.index(name)
self.image_listbox.selection_clear(0, tk.END)
self.image_listbox.selection_set(index)
self.image_listbox.see(index)
except Exception as e:
print(f"Error updating listbox selection: {e}")
# Auto-select brush tool when an image is selected (if no tool is currently selected)
if self.drawing_tools.get_current_tool() is None:
self.select_tool("brush")
def on_name_change(self, event):
"""Handle image name change."""
if not self.selected_image:
return
new_name = self.name_var.get().strip()
if not new_name or new_name == self.selected_image:
return
if new_name in self.current_images:
messagebox.showwarning("Warning", "Name already exists")
self.name_var.set(self.selected_image)
return
# Rename image
image = self.current_images[self.selected_image]
del self.current_images[self.selected_image]
self.current_images[new_name] = image
old_name = self.selected_image
self.selected_image = new_name
# Update preview if exists
if old_name in self.image_previews:
self.image_previews[new_name] = self.image_previews[old_name]
del self.image_previews[old_name]
self.update_image_list()
self.select_image(new_name)
# Canvas event handlers are handled by CanvasManager
# Drawing methods
[docs]
def draw_on_image(self, x, y):
"""Draw on the current image using the new tool system."""
if not self.selected_image:
return
image = self.current_images[self.selected_image]
# Use the new tool system
kwargs = {
"size": self.size_var.get(),
"root": self.root, # For text tool dialog
}
self.drawing_tools.handle_click(image, x, y, **kwargs)
self.update_canvas()
[docs]
def draw_line_on_image(self, x1, y1, x2, y2):
"""Draw a line on the current image using the new tool system."""
if not self.selected_image:
return
image = self.current_images[self.selected_image]
# Use the new tool system
kwargs = {"size": self.size_var.get(), "root": self.root}
self.drawing_tools.handle_drag(image, x1, y1, x2, y2, **kwargs)
self.update_canvas()
[docs]
def draw_shape(self, x1, y1, x2, y2):
"""Draw a shape on the current image using the new tool system."""
if not self.selected_image:
return
image = self.current_images[self.selected_image]
# Use the new tool system
kwargs = {
"size": self.size_var.get(),
"width": self.size_var.get(), # For line width
"root": self.root,
}
self.drawing_tools.handle_release(image, x1, y1, x2, y2, **kwargs)
self.update_canvas()
def add_text(self, x, y):
"""Add text to the current image."""
if not self.selected_image:
return
# Simple text input dialog
text = simpledialog.askstring("Add Text", "Enter text:")
if text:
image = self.current_images[self.selected_image]
draw = ImageDraw.Draw(image)
try:
# Try to use a default font with size based on brush size
font_size = max(12, self.size_var.get() * 2)
font = ImageFont.load_default()
except (OSError, IOError, ImportError) as e:
# Font loading failed, use None (PIL will use built-in font)
print(f"Warning: Could not load default font: {e}")
font = None
draw.text(
(x, y), text, fill=self.drawing_tools.get_brush_color(), font=font
)
self.update_canvas()
# Preview and highlight methods
[docs]
def update_shape_preview(self, x1, y1, x2, y2):
"""Update the preview shape on canvas."""
# Clear existing preview
self.clear_preview()
# Convert image coordinates back to canvas coordinates for display
canvas_x1 = x1 * self.drawing_tools.get_zoom_level() + 10
canvas_y1 = y1 * self.drawing_tools.get_zoom_level() + 10
canvas_x2 = x2 * self.drawing_tools.get_zoom_level() + 10
canvas_y2 = y2 * self.drawing_tools.get_zoom_level() + 10
# Create preview shape using new tool system
preview_id = self.drawing_tools.create_preview(self.canvas, x1, y1, x2, y2)
if preview_id:
self.preview_shape = preview_id
else:
# Fallback for tools that don't support preview
self.preview_shape = None
self.preview_active = True
[docs]
def clear_preview(self):
"""Clear the preview shape from canvas."""
if not hasattr(self, "canvas") or self.canvas is None:
return
if hasattr(self, "preview_shape") and self.preview_shape:
self.canvas.delete(self.preview_shape)
self.preview_shape = None
self.preview_active = False
[docs]
def update_pixel_highlight(self, x, y):
"""Highlight the pixel that will be affected by drawing tools."""
# Check if canvas is available
if not hasattr(self, "canvas") or self.canvas is None:
return
# Only highlight if position changed
if hasattr(self, "last_highlight_pos") and self.last_highlight_pos == (x, y):
return
self.last_highlight_pos = (x, y)
# Clear existing highlight
self.clear_pixel_highlight()
# Check if coordinates are within image bounds
if not self.selected_image:
return
image = self.current_images[self.selected_image]
if x < 0 or y < 0 or x >= image.width or y >= image.height:
return
# Convert image coordinates to canvas coordinates
canvas_x = x * self.drawing_tools.get_zoom_level() + 10
canvas_y = y * self.drawing_tools.get_zoom_level() + 10
# Create highlight rectangle around the pixel
self.pixel_highlight = self.canvas.create_rectangle(
canvas_x,
canvas_y,
canvas_x + self.drawing_tools.get_zoom_level(),
canvas_y + self.drawing_tools.get_zoom_level(),
outline="#FF0000",
width=1,
fill="",
dash=(2, 2),
tags="pixel_highlight",
)
[docs]
def clear_pixel_highlight(self):
"""Clear the pixel highlight from canvas."""
if not hasattr(self, "canvas") or self.canvas is None:
return
if hasattr(self, "pixel_highlight") and self.pixel_highlight:
self.canvas.delete(self.pixel_highlight)
self.pixel_highlight = None
if hasattr(self, "last_highlight_pos"):
self.last_highlight_pos = None
[docs]
def update_preview(self, event=None):
"""Update the live preview based on current settings."""
if not hasattr(self, "preview_canvas"):
return
# Clear previous preview
self.preview_canvas.delete("all")
# Clear old image references to prevent memory leaks
if hasattr(self, "_preview_refs"):
self._preview_refs.clear()
else:
self._preview_refs = []
if not self.current_images:
# Show placeholder when no images
self._show_preview_placeholder("Create images to see preview")
return
# Get current settings
framework = getattr(self, "framework_var", tk.StringVar(value="tkinter")).get()
usage = getattr(self, "usage_var", tk.StringVar(value="general")).get()
try:
# Generate preview based on usage type
if usage == "buttons":
self.preview_buttons()
elif usage == "icons":
self.preview_icons()
elif usage == "backgrounds":
self.preview_backgrounds()
elif usage == "sprites":
self.preview_sprites()
elif usage == "ui_elements":
self.preview_ui_elements()
else:
self.preview_general()
# Update scrollbar visibility
self._update_preview_scrollbar()
except Exception as e:
print(f"Error generating preview: {e}")
self._show_preview_placeholder("Preview generation error", color="#ff0000")
def _show_preview_placeholder(self, text, color="#888888"):
"""Show placeholder text in preview canvas."""
try:
canvas_width = self.preview_canvas.winfo_width()
canvas_height = self.preview_canvas.winfo_height()
if canvas_width <= 1:
# Canvas not ready, try again later
self.root.after(
100, lambda: self._show_preview_placeholder(text, color)
)
return
center_x = canvas_width // 2
center_y = canvas_height // 2
self.preview_canvas.create_text(
center_x,
center_y,
text=text,
fill=color,
font=("Arial", 9),
justify=tk.CENTER,
)
except Exception as e:
print(f"Error showing placeholder: {e}")
def _update_preview_scrollbar(self):
"""Update scrollbar visibility based on content."""
try:
# Update scroll region
bbox = self.preview_canvas.bbox("all")
if bbox:
# Add some padding to the scroll region
padded_bbox = (bbox[0] - 5, bbox[1] - 5, bbox[2] + 5, bbox[3] + 5)
self.preview_canvas.configure(scrollregion=padded_bbox)
# Check if scrollbar is needed
content_height = bbox[3] - bbox[1]
canvas_height = self.preview_canvas.winfo_height()
if content_height > canvas_height - 10: # Account for padding
# Show scrollbar if it exists
if hasattr(self, "preview_scrollbar"):
self.preview_scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
else:
# Hide scrollbar if it exists
if hasattr(self, "preview_scrollbar"):
self.preview_scrollbar.pack_forget()
else:
# No content, reset scroll region and hide scrollbar
self.preview_canvas.configure(scrollregion=(0, 0, 0, 0))
if hasattr(self, "preview_scrollbar"):
self.preview_scrollbar.pack_forget()
except Exception as e:
print(f"Error updating scrollbar: {e}")
def _on_preview_mousewheel(self, event):
"""Handle mouse wheel scrolling in preview canvas."""
try:
if hasattr(self, "preview_canvas"):
# Check if there's content to scroll
bbox = self.preview_canvas.bbox("all")
if bbox:
content_height = bbox[3] - bbox[1]
canvas_height = self.preview_canvas.winfo_height()
if content_height > canvas_height:
# Scroll the canvas
self.preview_canvas.yview_scroll(
int(-1 * (event.delta / 120)), "units"
)
except Exception as e:
print(f"Error handling mousewheel: {e}")
[docs]
def setup_preview_bindings(self):
"""Set up preview canvas event bindings."""
try:
# Wait a bit for UI to be fully initialized
self.root.after(100, self._setup_preview_bindings_delayed)
except Exception as e:
print(f"Error setting up preview bindings: {e}")
def _setup_preview_bindings_delayed(self):
"""Set up preview canvas bindings after UI is ready."""
try:
if hasattr(self, "preview_canvas"):
# Bind mouse wheel scrolling
self.preview_canvas.bind("<MouseWheel>", self._on_preview_mousewheel)
self.preview_canvas.bind(
"<Button-4>",
lambda e: self._on_preview_mousewheel(
type("Event", (), {"delta": 120})()
),
)
self.preview_canvas.bind(
"<Button-5>",
lambda e: self._on_preview_mousewheel(
type("Event", (), {"delta": -120})()
),
)
# Make canvas focusable for keyboard events
self.preview_canvas.configure(highlightthickness=0)
print("Preview canvas bindings set up successfully")
else:
# Try again later if preview_canvas not ready
self.root.after(200, self._setup_preview_bindings_delayed)
except Exception as e:
print(f"Error in delayed preview bindings setup: {e}")
[docs]
def on_drawing_complete(self):
"""Called when a drawing operation is completed."""
try:
# Update preview after drawing operations
self.update_preview()
except Exception as e:
print(f"Error updating preview after drawing: {e}")
[docs]
def on_image_modified(self):
"""Called when an image is modified in any way."""
try:
# Update preview when image is modified
self.update_preview()
except Exception as e:
print(f"Error updating preview after image modification: {e}")
# Panel toggle methods
[docs]
def toggle_left_panel(self):
"""Toggle left panel visibility."""
if hasattr(self, "three_pane"):
self.three_pane.toggle_pane("left")
[docs]
def toggle_right_panel(self):
"""Toggle right panel visibility."""
if hasattr(self, "three_pane"):
self.three_pane.toggle_pane("right")
[docs]
def reset_panel_layout(self):
"""Reset panel layout to default."""
if hasattr(self, "three_pane"):
self.three_pane.reset_layout()
# Code generation methods
def generate_python_code(self):
"""Generate Python code for images."""
messagebox.showinfo(
"Code Generation",
"Python code generation not yet implemented in refactored version.",
)
def generate_tkinter_code(self):
"""Generate Tkinter code for images."""
messagebox.showinfo(
"Code Generation",
"Tkinter code generation not yet implemented in refactored version.",
)
[docs]
def preview_code(self):
"""Preview the generated embedded code."""
if not self.current_images:
messagebox.showwarning("Warning", "No images to generate code for")
return
try:
framework = self.framework_var.get()
usage = self.usage_var.get()
quality = self.quality_var.get()
# Generate embedded images dictionary
embedded_images = {}
for name, image in self.current_images.items():
# Convert to bytes
buffer = BytesIO()
image.save(buffer, format="PNG", quality=quality)
image_bytes = buffer.getvalue()
# Encode to base64
encoded = base64.b64encode(image_bytes).decode("utf-8")
embedded_images[name] = encoded
# Generate code based on framework and usage
code = self._generate_code_content(embedded_images, framework, usage)
# Show in preview window
CodePreviewWindow(self.root, f"Code Preview - {framework} ({usage})", code)
except Exception as e:
messagebox.showerror("Error", f"Failed to generate code preview: {str(e)}")
def _generate_code_content(self, embedded_images, framework, usage):
"""Generate the actual code content."""
code = f"# Generated {framework} code for {usage}\n"
code += "# Generated by GUI Image Studio\n\n"
if framework == "tkinter":
code += "import tkinter as tk\n"
code += "from tkinter import ttk\n"
else:
code += "import customtkinter as ctk\n"
code += "import base64\n"
code += "from io import BytesIO\n"
code += "from PIL import Image, ImageTk\n\n"
# Add embedded images dictionary
code += "# Embedded images dictionary\n"
code += "embedded_images = {\n"
for name, data in embedded_images.items():
code += f' "{name}": "{data}",\n'
code += "}\n\n"
# Add helper functions
code += "def load_image(name, size=None):\n"
code += ' """Load an embedded image by name."""\n'
code += " if name not in embedded_images:\n"
code += " return None\n"
code += " \n"
code += " # Decode base64 data\n"
code += " image_data = base64.b64decode(embedded_images[name])\n"
code += " image = Image.open(BytesIO(image_data))\n"
code += " \n"
code += " # Resize if requested\n"
code += " if size:\n"
code += " image = image.resize(size, Image.Resampling.LANCZOS)\n"
code += " \n"
code += " return ImageTk.PhotoImage(image)\n\n"
# Add usage-specific examples
if usage == "buttons":
code += self._generate_button_examples(framework)
elif usage == "icons":
code += self._generate_icon_examples(framework)
elif usage == "backgrounds":
code += self._generate_background_examples(framework)
else:
code += self._generate_general_examples(framework)
return code
def _generate_button_examples(self, framework):
"""Generate button usage examples."""
if framework == "tkinter":
return """# Example: Using images in buttons
def create_button_with_image(parent, image_name, text="", command=None):
image = load_image(image_name, size=(32, 32))
button = tk.Button(
parent,
text=text,
image=image,
compound=tk.LEFT,
command=command
)
button.image = image # Keep reference
return button
# Usage example:
# root = tk.Tk()
# button = create_button_with_image(root, "your_image_name", "Click me")
# button.pack()
"""
else:
return """# Example: Using images in CustomTkinter buttons
def create_button_with_image(parent, image_name, text="", command=None):
image = load_image(image_name, size=(32, 32))
button = ctk.CTkButton(
parent,
text=text,
image=image,
compound="left",
command=command
)
return button
# Usage example:
# root = ctk.CTk()
# button = create_button_with_image(root, "your_image_name", "Click me")
# button.pack()
"""
def _generate_icon_examples(self, framework):
"""Generate icon usage examples."""
return """# Example: Using images as icons
def create_icon_label(parent, image_name, size=(24, 24)):
image = load_image(image_name, size=size)
label = tk.Label(parent, image=image)
label.image = image # Keep reference
return label
# Usage example:
# icon = create_icon_label(parent, "your_icon_name")
# icon.pack()
"""
def _generate_background_examples(self, framework):
"""Generate background usage examples."""
return """# Example: Using images as backgrounds
def set_background_image(widget, image_name):
image = load_image(image_name)
widget.configure(image=image)
widget.image = image # Keep reference
# Usage example:
# canvas = tk.Canvas(root, width=400, height=300)
# bg_image = load_image("your_background_name")
# canvas.create_image(0, 0, anchor="nw", image=bg_image)
# canvas.image = bg_image # Keep reference
"""
def _generate_general_examples(self, framework):
"""Generate general usage examples."""
return """# Example: General image usage
def display_image(parent, image_name, size=None):
image = load_image(image_name, size=size)
label = tk.Label(parent, image=image)
label.image = image # Keep reference
return label
# Usage example:
# image_widget = display_image(parent, "your_image_name", size=(100, 100))
# image_widget.pack()
"""
[docs]
def generate_code_file(self):
"""Generate the embedded code file."""
if not self.current_images:
messagebox.showwarning("Warning", "No images to generate code for")
return
try:
framework = self.framework_var.get()
usage = self.usage_var.get()
quality = self.quality_var.get()
# Ask user for save location
filename = filedialog.asksaveasfilename(
title="Save Generated Code",
defaultextension=".py",
filetypes=[("Python files", "*.py"), ("All files", "*.*")],
)
if not filename:
return
# Generate embedded images dictionary
embedded_images = {}
for name, image in self.current_images.items():
buffer = BytesIO()
image.save(buffer, format="PNG", quality=quality)
image_bytes = buffer.getvalue()
encoded = base64.b64encode(image_bytes).decode("utf-8")
embedded_images[name] = encoded
# Generate code
code = self._generate_code_content(embedded_images, framework, usage)
# Save to file
with open(filename, "w", encoding="utf-8") as f:
f.write(code)
messagebox.showinfo(
"Code Generated", f"Code successfully saved to:\n{filename}"
)
except Exception as e:
messagebox.showerror("Error", f"Failed to generate code file: {str(e)}")
# Help and About methods
[docs]
def show_quick_start(self):
"""Show quick start guide."""
messagebox.showinfo(
"Help", "Quick start guide not yet implemented in refactored version."
)
def show_tools_help(self):
"""Show drawing tools help."""
messagebox.showinfo(
"Help", "Tools help not yet implemented in refactored version."
)
[docs]
def show_info_help(self):
"""Show image information help."""
messagebox.showinfo(
"Help", "Info help not yet implemented in refactored version."
)
[docs]
def show_transparency_help(self):
"""Show transparency features help."""
messagebox.showinfo(
"Help", "Transparency help not yet implemented in refactored version."
)
[docs]
def show_code_help(self):
"""Show code generation help."""
messagebox.showinfo(
"Help", "Code help not yet implemented in refactored version."
)
[docs]
def show_shortcuts(self):
"""Show keyboard shortcuts."""
messagebox.showinfo(
"Help", "Shortcuts help not yet implemented in refactored version."
)
[docs]
def show_tips(self):
"""Show tips and tricks."""
messagebox.showinfo("Help", "Tips not yet implemented in refactored version.")
[docs]
def show_troubleshooting(self):
"""Show troubleshooting guide."""
messagebox.showinfo(
"Help", "Troubleshooting not yet implemented in refactored version."
)
def show_about(self):
"""Show about dialog."""
messagebox.showinfo(
"About", "About dialog not yet implemented in refactored version."
)
# Image adjustment methods
[docs]
def apply_blur(self):
"""Apply blur filter to current image."""
if not self.selected_image:
messagebox.showwarning("Warning", "No image selected")
return
try:
image = self.current_images[self.selected_image]
blurred = image.filter(ImageFilter.GaussianBlur(radius=1))
self.current_images[self.selected_image] = blurred
self.update_canvas()
self.update_preview()
messagebox.showinfo("Filter Applied", "Blur filter applied successfully!")
except Exception as e:
messagebox.showerror("Error", f"Failed to apply blur filter: {str(e)}")
[docs]
def apply_sharpen(self):
"""Apply sharpen filter to current image."""
if not self.selected_image:
messagebox.showwarning("Warning", "No image selected")
return
try:
image = self.current_images[self.selected_image]
sharpened = image.filter(ImageFilter.SHARPEN)
self.current_images[self.selected_image] = sharpened
self.update_canvas()
self.update_preview()
messagebox.showinfo(
"Filter Applied", "Sharpen filter applied successfully!"
)
except Exception as e:
messagebox.showerror("Error", f"Failed to apply sharpen filter: {str(e)}")
[docs]
def apply_emboss(self):
"""Apply emboss filter to current image."""
if not self.selected_image:
messagebox.showwarning("Warning", "No image selected")
return
try:
image = self.current_images[self.selected_image]
embossed = image.filter(ImageFilter.EMBOSS)
self.current_images[self.selected_image] = embossed
self.update_canvas()
self.update_preview()
messagebox.showinfo("Filter Applied", "Emboss filter applied successfully!")
except Exception as e:
messagebox.showerror("Error", f"Failed to apply emboss filter: {str(e)}")
[docs]
def apply_transparent_background(self):
"""Make background transparent."""
if not self.selected_image:
messagebox.showwarning("Warning", "No image selected")
return
try:
image = self.current_images[self.selected_image]
# Convert to RGBA if not already
if image.mode != "RGBA":
image = image.convert("RGBA")
# Ask user to choose background color method
choice = messagebox.askyesnocancel(
"Background Color Selection",
"Choose background color method:\n\n"
"Yes - Use color picker to select color\n"
"No - Use top-left pixel color\n"
"Cancel - Cancel operation",
)
if choice is None: # Cancel
return
elif choice: # Yes - color picker
color = colorchooser.askcolor(title="Select Background Color")
if not color[0]: # User cancelled color picker
return
bg_color = tuple(int(c) for c in color[0])
else: # No - use top-left pixel
bg_color = image.getpixel((0, 0))[:3] # Get RGB only
# Ask for tolerance
tolerance = simpledialog.askinteger(
"Tolerance",
"Enter color tolerance (0-100):\n"
"Lower values = exact color match\n"
"Higher values = broader color range",
initialvalue=30,
minvalue=0,
maxvalue=100,
)
if tolerance is None:
return
# Apply transparency
data = image.getdata()
new_data = []
for pixel in data:
# Calculate color distance
if len(pixel) >= 3:
r, g, b = pixel[:3]
distance = (
(r - bg_color[0]) ** 2
+ (g - bg_color[1]) ** 2
+ (b - bg_color[2]) ** 2
) ** 0.5
# If within tolerance, make transparent
if distance <= tolerance * 2.55: # Scale tolerance to 0-255 range
new_data.append((r, g, b, 0)) # Transparent
else:
new_data.append(pixel if len(pixel) == 4 else (r, g, b, 255))
else:
new_data.append(pixel)
# Create new image with transparency
transparent_image = Image.new("RGBA", image.size)
transparent_image.putdata(new_data)
self.current_images[self.selected_image] = transparent_image
self.update_canvas()
self.update_preview()
messagebox.showinfo(
"Transparency Applied",
f"Background color {bg_color} made transparent with tolerance {tolerance}",
)
except Exception as e:
messagebox.showerror("Error", f"Failed to apply transparency: {str(e)}")
[docs]
def remove_background(self):
"""Remove background from image."""
if not self.selected_image:
messagebox.showwarning("Warning", "No image selected")
return
try:
image = self.current_images[self.selected_image]
# Convert to RGBA if not already
if image.mode != "RGBA":
image = image.convert("RGBA")
# Ask user for background detection method
choice = messagebox.askyesnocancel(
"Background Detection",
"Choose background detection method:\n\n"
"Yes - Auto-detect from image corners\n"
"No - Manual color selection\n"
"Cancel - Cancel operation",
)
if choice is None: # Cancel
return
elif choice: # Yes - auto-detect
# Sample corners to find most common background color
width, height = image.size
corner_pixels = [
image.getpixel((0, 0))[:3], # Top-left
image.getpixel((width - 1, 0))[:3], # Top-right
image.getpixel((0, height - 1))[:3], # Bottom-left
image.getpixel((width - 1, height - 1))[:3], # Bottom-right
]
# Find most common corner color
color_counts = Counter(corner_pixels)
bg_color = color_counts.most_common(1)[0][0]
messagebox.showinfo(
"Auto-detected Background",
f"Detected background color: RGB{bg_color}\n"
f"This color appeared in {color_counts[bg_color]} corners",
)
else: # No - manual selection
color = colorchooser.askcolor(title="Select Background Color to Remove")
if not color[0]: # User cancelled
return
bg_color = tuple(int(c) for c in color[0])
# Ask for tolerance
tolerance = simpledialog.askinteger(
"Removal Tolerance",
"Enter removal tolerance (0-100):\n"
"0 = Exact color match only\n"
"50 = Moderate color range\n"
"100 = Very broad color range",
initialvalue=40,
minvalue=0,
maxvalue=100,
)
if tolerance is None:
return
# Apply smart background removal
data = image.getdata()
new_data = []
pixels_removed = 0
for pixel in data:
if len(pixel) >= 3:
r, g, b = pixel[:3]
# Calculate Euclidean distance in RGB space
distance = (
(r - bg_color[0]) ** 2
+ (g - bg_color[1]) ** 2
+ (b - bg_color[2]) ** 2
) ** 0.5
# Scale tolerance (0-100 to 0-441, max RGB distance)
max_distance = tolerance * 4.41
if distance <= max_distance:
new_data.append((r, g, b, 0)) # Make transparent
pixels_removed += 1
else:
new_data.append(pixel if len(pixel) == 4 else (r, g, b, 255))
else:
new_data.append(pixel)
# Create new image with removed background
result_image = Image.new("RGBA", image.size)
result_image.putdata(new_data)
self.current_images[self.selected_image] = result_image
self.update_canvas()
self.update_preview()
# Calculate percentage removed
total_pixels = len(data)
percentage = (pixels_removed / total_pixels) * 100
messagebox.showinfo(
"Background Removed",
f"Background removal complete!\n\n"
f"Color removed: RGB{bg_color}\n"
f"Tolerance used: {tolerance}\n"
f"Pixels made transparent: {pixels_removed:,}\n"
f"Percentage of image: {percentage:.1f}%",
)
except Exception as e:
messagebox.showerror("Error", f"Failed to remove background: {str(e)}")
[docs]
def show_image_info(self):
"""Show comprehensive image information."""
if not self.selected_image:
messagebox.showwarning("Warning", "No image selected")
return
try:
image = self.current_images[self.selected_image]
# Basic image information
info = f"Image: {self.selected_image}\n"
info += f"Size: {image.width} x {image.height} pixels\n"
info += f"Mode: {image.mode}\n"
info += f"Format: {getattr(image, 'format', 'Unknown')}\n"
# Calculate approximate file size
buffer = BytesIO()
image.save(buffer, format="PNG")
size_bytes = buffer.tell()
size_kb = size_bytes / 1024
info += f"Estimated PNG size: {size_kb:.1f} KB\n"
# Color information
if image.mode == "RGBA":
info += "Has transparency: Yes\n"
else:
info += "Has transparency: No\n"
# Rotation info
if self.selected_image in self.current_rotations:
rotation = self.current_rotations[self.selected_image]
info += f"Current rotation: {rotation}°\n"
messagebox.showinfo("Image Information", info)
except Exception as e:
messagebox.showerror("Error", f"Failed to get image information: {str(e)}")
[docs]
def apply_rotation(self):
"""Apply rotation to current image."""
if not self.selected_image:
messagebox.showwarning("Warning", "No image selected")
return
try:
angle = self.rotation_var.get()
if angle == 0:
return
image = self.current_images[self.selected_image]
# Rotate with transparent background
rotated = image.rotate(angle, expand=True, fillcolor=(0, 0, 0, 0))
self.current_images[self.selected_image] = rotated
# Update current rotation tracking
if self.selected_image not in self.current_rotations:
self.current_rotations[self.selected_image] = 0
self.current_rotations[self.selected_image] = (
self.current_rotations[self.selected_image] + angle
) % 360
self.update_canvas()
self.update_preview()
messagebox.showinfo(
"Rotation Applied", f"Image rotated by {angle} degrees!"
)
except Exception as e:
messagebox.showerror("Error", f"Failed to apply rotation: {str(e)}")
[docs]
def reset_rotation(self):
"""Reset image rotation to 0 degrees."""
if not self.selected_image:
messagebox.showwarning("Warning", "No image selected")
return
try:
# Reset to original image
if self.selected_image in self.original_images:
self.current_images[self.selected_image] = self.original_images[
self.selected_image
].copy()
self.current_rotations[self.selected_image] = 0
# Reset rotation controls
if hasattr(self, "rotation_var"):
self.rotation_var.set(0)
self.update_rotation_display()
self.update_canvas()
self.update_preview()
messagebox.showinfo(
"Rotation Reset", "Image rotation reset to 0 degrees!"
)
except Exception as e:
messagebox.showerror("Error", f"Failed to reset rotation: {str(e)}")
def on_rotation_entry_change(self, event=None):
"""Handle rotation entry changes."""
pass # Placeholder
def update_rotation_display(self):
"""Update rotation display."""
pass # Placeholder
def update_base_image(self):
"""Update base image after transformations."""
pass # Placeholder
# Memory and performance methods
[docs]
def cleanup_memory(self):
"""Clean up memory usage."""
gc.collect()
[docs]
def check_memory_usage(self):
"""Check current memory usage."""
if PSUTIL_AVAILABLE:
process = psutil.Process()
memory_mb = process.memory_info().rss / 1024 / 1024
messagebox.showinfo(
"Memory Usage", f"Current memory usage: {memory_mb:.1f} MB"
)
else:
messagebox.showinfo(
"Memory Usage", "Memory monitoring not available (psutil not installed)"
)
# Cursor settings methods (implemented above)
# Export/Import methods
def export_images(self):
"""Export images to folder."""
if not self.current_images:
messagebox.showwarning("Warning", "No images to export")
return
try:
# Ask user to select export folder
folder = filedialog.askdirectory(title="Select Export Folder")
if not folder:
return
# Ask for export format
format_choice = messagebox.askyesnocancel(
"Export Format",
"Choose export format:\n\n"
"Yes - PNG (with transparency support)\n"
"No - JPEG (smaller file size)\n"
"Cancel - Cancel export",
)
if format_choice is None: # Cancel
return
export_format = "PNG" if format_choice else "JPEG"
file_extension = ".png" if format_choice else ".jpg"
# For JPEG, ask about quality
quality = 95 # Default quality
if export_format == "JPEG":
quality = simpledialog.askinteger(
"JPEG Quality",
"Enter JPEG quality (1-100):\n"
"Higher values = better quality, larger files",
initialvalue=95,
minvalue=1,
maxvalue=100,
)
if quality is None:
return
# Export all images
exported_count = 0
errors = []
for name, image in self.current_images.items():
try:
# Clean filename
safe_name = "".join(
c for c in name if c.isalnum() or c in (" ", "-", "_")
).rstrip()
if not safe_name:
safe_name = f"image_{exported_count + 1}"
filepath = os.path.join(folder, safe_name + file_extension)
# Handle file conflicts
counter = 1
original_filepath = filepath
while os.path.exists(filepath):
base_name = safe_name + f"_{counter}"
filepath = os.path.join(folder, base_name + file_extension)
counter += 1
# Convert image for export
export_image = image.copy()
if export_format == "JPEG":
# Convert RGBA to RGB for JPEG
if export_image.mode == "RGBA":
# Create white background
background = Image.new(
"RGB", export_image.size, (255, 255, 255)
)
background.paste(
export_image, mask=export_image.split()[-1]
) # Use alpha as mask
export_image = background
elif export_image.mode != "RGB":
export_image = export_image.convert("RGB")
export_image.save(
filepath,
format=export_format,
quality=quality,
optimize=True,
)
else: # PNG
export_image.save(filepath, format=export_format, optimize=True)
exported_count += 1
except Exception as e:
errors.append(f"{name}: {str(e)}")
# Show results
result_message = "Export completed!\n\n"
result_message += f"Successfully exported: {exported_count} images\n"
result_message += f"Format: {export_format}\n"
result_message += f"Location: {folder}\n"
if export_format == "JPEG":
result_message += f"Quality: {quality}%\n"
if errors:
result_message += f"\nErrors ({len(errors)}):\n"
result_message += "\n".join(errors[:5]) # Show first 5 errors
if len(errors) > 5:
result_message += f"\n... and {len(errors) - 5} more errors"
messagebox.showinfo("Export Complete", result_message)
except Exception as e:
messagebox.showerror("Export Error", f"Failed to export images: {str(e)}")
# Panel management methods (already implemented above)
# Right panel functionality
[docs]
def update_rotation_display(self, value=None):
"""Update rotation display when slider changes."""
if hasattr(self, "rotation_var") and hasattr(self, "rotation_entry"):
current_value = self.rotation_var.get()
self.rotation_entry.delete(0, tk.END)
self.rotation_entry.insert(0, str(current_value))
[docs]
def on_rotation_entry_change(self, event=None):
"""Handle rotation entry changes."""
if hasattr(self, "rotation_entry") and hasattr(self, "rotation_var"):
try:
value = int(self.rotation_entry.get())
value = max(0, min(360, value)) # Clamp to 0-360
self.rotation_var.set(value)
except ValueError:
# Reset to current slider value if input is invalid
current_value = self.rotation_var.get()
self.rotation_entry.delete(0, tk.END)
self.rotation_entry.insert(0, str(current_value))
[docs]
def preview_icons(self):
"""Preview images as icons."""
if not hasattr(self, "preview_canvas"):
return
x_offset = 15
y_offset = 15
icon_size = 24
spacing_x = 60
spacing_y = 45
cols = 3
for i, (name, image) in enumerate(
list(self.current_images.items())[:6]
): # Show first 6
try:
col = i % cols
row = i // cols
x = x_offset + col * spacing_x
y = y_offset + row * spacing_y
# Create icon
icon_img = image.resize(
(icon_size, icon_size), Image.Resampling.LANCZOS
)
photo = ImageTk.PhotoImage(icon_img)
# Create icon background
self.preview_canvas.create_rectangle(
x - 2,
y - 2,
x + icon_size + 2,
y + icon_size + 2,
fill="white",
outline="#dddddd",
width=1,
)
self.preview_canvas.create_image(x, y, image=photo, anchor="nw")
# Add icon name below
self.preview_canvas.create_text(
x + icon_size // 2,
y + icon_size + 5,
text=name[:8] + ("..." if len(name) > 8 else ""),
anchor="n",
font=("Arial", 7),
fill="#666666",
)
# Keep reference
self._preview_refs.append(photo)
except Exception as e:
print(f"Error creating icon preview for {name}: {e}")
continue
[docs]
def preview_backgrounds(self):
"""Preview images as backgrounds."""
if not hasattr(self, "preview_canvas"):
return
if not self.current_images:
return
try:
# Use selected image or first available
if self.selected_image and self.selected_image in self.current_images:
name = self.selected_image
image = self.current_images[name]
else:
name, image = next(iter(self.current_images.items()))
canvas_width = self.preview_canvas.winfo_width() or 200
bg_width = min(canvas_width - 20, 150)
bg_height = int(bg_width * 0.6) # 3:2 aspect ratio
# Create background preview
bg_img = image.resize((bg_width, bg_height), Image.Resampling.LANCZOS)
photo = ImageTk.PhotoImage(bg_img)
# Create frame around background
frame_x1, frame_y1 = 10, 10
frame_x2, frame_y2 = 10 + bg_width, 10 + bg_height
self.preview_canvas.create_rectangle(
frame_x1 - 2,
frame_y1 - 2,
frame_x2 + 2,
frame_y2 + 2,
fill="white",
outline="#333333",
width=2,
)
self.preview_canvas.create_image(
frame_x1, frame_y1, image=photo, anchor="nw"
)
# Add label
self.preview_canvas.create_text(
10,
frame_y2 + 10,
text=f"Background: {name[:20]}{'...' if len(name) > 20 else ''}",
anchor="w",
font=("Arial", 9),
fill="#333333",
)
# Add sample content overlay
self.preview_canvas.create_rectangle(
frame_x1 + 10,
frame_y1 + 10,
frame_x1 + 80,
frame_y1 + 30,
fill="white",
outline="#666666",
stipple="gray50",
)
self.preview_canvas.create_text(
frame_x1 + 45,
frame_y1 + 20,
text="Content",
font=("Arial", 8),
fill="#333333",
)
# Keep reference
self._preview_refs.append(photo)
except Exception as e:
print(f"Error creating background preview: {e}")
[docs]
def preview_sprites(self):
"""Preview images as game sprites."""
if not hasattr(self, "preview_canvas"):
return
canvas_width = self.preview_canvas.winfo_width() or 200
canvas_height = self.preview_canvas.winfo_height() or 100
# Get framework name
framework = self.framework_var.get()
self.preview_canvas.create_text(
10,
10,
text=f"{framework.title()} Sprites:",
anchor="nw",
font=("Arial", 10, "bold"),
fill="#333333",
)
if not self.current_images:
# Show message when no image is selected
self.preview_canvas.create_text(
canvas_width // 2,
canvas_height // 2,
text="Select an image to see sprite preview",
fill="#888888",
font=("Arial", 12),
anchor="center",
)
return
# Use selected image or first available
if self.selected_image and self.selected_image in self.current_images:
name = self.selected_image
image = self.current_images[name]
else:
name, image = next(iter(self.current_images.items()))
# Show current image name
self.preview_canvas.create_text(
10,
25,
text=f"Current Sprite: {name}",
anchor="nw",
font=("Arial", 9),
fill="#666666",
)
# Simulate a game scene with proper bounds
margin = 15
scene_x = margin
scene_y = 45 # Adjusted for the additional text
scene_width = canvas_width - (2 * margin)
scene_height = canvas_height - scene_y - margin
if scene_width > 50 and scene_height > 50:
# Draw scene background (sky)
self.preview_canvas.create_rectangle(
scene_x,
scene_y,
scene_x + scene_width,
scene_y + scene_height,
fill="#87CEEB",
outline="#4682B4",
width=1,
)
# Add ground (bottom 25% of scene)
ground_height = max(20, scene_height // 4)
ground_y = scene_y + scene_height - ground_height
self.preview_canvas.create_rectangle(
scene_x,
ground_y,
scene_x + scene_width,
scene_y + scene_height,
fill="#90EE90",
outline="#228B22",
width=1,
)
# Create sprite photo
try:
# Scale sprite appropriately for the scene
max_sprite_size = min(scene_width // 3, scene_height // 2, 64)
sprite_img = image.resize(
(max_sprite_size, max_sprite_size), Image.Resampling.LANCZOS
)
photo = ImageTk.PhotoImage(sprite_img)
# Place the selected sprite in the center of the scene
sprite_x = scene_x + (scene_width - photo.width()) // 2
sprite_y = ground_y - photo.height()
if sprite_y < scene_y + 10: # If sprite is too tall, place it lower
sprite_y = scene_y + 10
self.preview_canvas.create_image(
sprite_x, sprite_y, image=photo, anchor="nw"
)
# Keep reference
self._preview_refs.append(photo)
except Exception as e:
print(f"Error creating sprite preview for {name}: {e}")
[docs]
def preview_ui_elements(self):
"""Preview images as UI elements."""
if not hasattr(self, "preview_canvas"):
return
canvas_width = self.preview_canvas.winfo_width() or 200
canvas_height = self.preview_canvas.winfo_height() or 100
# Get framework name
framework = self.framework_var.get()
self.preview_canvas.create_text(
10,
10,
text=f"{framework.title()} UI Elements:",
anchor="nw",
font=("Arial", 10, "bold"),
fill="#333333",
)
# Create a mock UI layout with proper spacing
margin = 15
ui_x = margin
ui_y = 30
ui_width = canvas_width - (2 * margin)
if ui_width <= 50: # Not enough space
return
# Toolbar
toolbar_height = 35
self.preview_canvas.create_rectangle(
ui_x,
ui_y,
ui_x + ui_width,
ui_y + toolbar_height,
fill="#f0f0f0",
outline="#cccccc",
width=1,
)
# Place images as toolbar icons with proper spacing
icon_x = ui_x + 8
toolbar_images = 0
max_toolbar_images = max(4, min(8, len(self.current_images)))
try:
for i, (name, image) in enumerate(
list(self.current_images.items())[:max_toolbar_images]
):
if toolbar_images >= max_toolbar_images:
break
# Create toolbar icon (24x24)
icon_size = 24
icon_img = image.resize(
(icon_size, icon_size), Image.Resampling.LANCZOS
)
photo = ImageTk.PhotoImage(icon_img)
if icon_x + icon_size + 8 > ui_x + ui_width - 8:
break # Don't overflow toolbar
# Center icon vertically in toolbar
icon_y = ui_y + (toolbar_height - icon_size) // 2
self.preview_canvas.create_image(
icon_x, icon_y, image=photo, anchor="nw"
)
# Keep reference
self._preview_refs.append(photo)
icon_x += icon_size + 8
toolbar_images += 1
# Main content area
content_y = ui_y + toolbar_height + 10
content_height = max(60, canvas_height - content_y - 40)
if content_height > 20:
self.preview_canvas.create_rectangle(
ui_x,
content_y,
ui_x + ui_width,
content_y + content_height,
fill="#ffffff",
outline="#dddddd",
width=1,
)
# Add remaining images in content area if any
remaining_images = list(self.current_images.items())[toolbar_images:]
if remaining_images and content_height > 40:
# Show some images in content area
content_icon_x = ui_x + 10
content_icon_y = content_y + 10
content_images_shown = 0
max_content_images = min(6, len(remaining_images))
for name, image in remaining_images[:max_content_images]:
# Create content icon (32x32)
content_icon_size = 32
content_img = image.resize(
(content_icon_size, content_icon_size),
Image.Resampling.LANCZOS,
)
photo = ImageTk.PhotoImage(content_img)
if (
content_icon_x + content_icon_size + 10
> ui_x + ui_width - 10
):
content_icon_x = ui_x + 10
content_icon_y += content_icon_size + 10
if (
content_icon_y + content_icon_size
> content_y + content_height - 10
):
break # Don't overflow content area
self.preview_canvas.create_image(
content_icon_x, content_icon_y, image=photo, anchor="nw"
)
# Keep reference
self._preview_refs.append(photo)
content_icon_x += content_icon_size + 10
content_images_shown += 1
# Add text if there's space
if content_icon_y + 30 < content_y + content_height - 10:
self.preview_canvas.create_text(
ui_x + ui_width // 2,
content_y + content_height - 20,
text=f"Content Area ({content_images_shown} images)",
fill="#666666",
font=("Arial", 9),
anchor="center",
)
except Exception as e:
print(f"Error creating UI elements preview: {e}")
[docs]
def preview_general(self):
"""Preview images in general format."""
if not hasattr(self, "preview_canvas"):
return
if not self.current_images:
return
try:
# Use selected image or first available
if self.selected_image and self.selected_image in self.current_images:
name = self.selected_image
image = self.current_images[name]
else:
name, image = next(iter(self.current_images.items()))
# Create general preview
preview_width, preview_height = 80, 60
prev_img = image.resize(
(preview_width, preview_height), Image.Resampling.LANCZOS
)
photo = ImageTk.PhotoImage(prev_img)
canvas_width = self.preview_canvas.winfo_width() or 200
center_x = canvas_width // 2
# Create image frame
frame_x = center_x - preview_width // 2
frame_y = 15
self.preview_canvas.create_rectangle(
frame_x - 2,
frame_y - 2,
frame_x + preview_width + 2,
frame_y + preview_height + 2,
fill="white",
outline="#333333",
width=2,
)
self.preview_canvas.create_image(frame_x, frame_y, image=photo, anchor="nw")
# Add image info
self.preview_canvas.create_text(
center_x,
frame_y + preview_height + 10,
text=f"{name[:15]}{'...' if len(name) > 15 else ''}",
anchor="n",
font=("Arial", 9, "bold"),
fill="#333333",
)
# Add size info
self.preview_canvas.create_text(
center_x,
frame_y + preview_height + 25,
text=f"{image.width}×{image.height}px",
anchor="n",
font=("Arial", 7),
fill="#666666",
)
# Add mode info
mode_text = f"Mode: {image.mode}"
if image.mode == "RGBA":
mode_text += " (with transparency)"
self.preview_canvas.create_text(
center_x,
frame_y + preview_height + 38,
text=mode_text,
anchor="n",
font=("Arial", 7),
fill="#666666",
)
# Keep reference
self._preview_refs.append(photo)
except Exception as e:
print(f"Error creating general preview: {e}")
# Image operations
[docs]
def new_image(self):
"""Create a new blank image."""
dialog = ImageSizeDialog(self.root)
if dialog.result:
width, height, name = dialog.result
# Generate unique name if needed
if name in self.current_images:
counter = 1
base_name = name
while f"{base_name}_{counter}" in self.current_images:
counter += 1
name = f"{base_name}_{counter}"
# Create new image
image = Image.new("RGBA", (width, height), (255, 255, 255, 255))
self.current_images[name] = image
self.original_images[name] = image.copy()
self.base_images[name] = image.copy()
self.current_rotations[name] = 0
self.update_image_list()
self.select_image(name)
self.update_preview()
[docs]
def load_image(self):
"""Load an image from file."""
file_path = filedialog.askopenfilename(
title="Load Image",
filetypes=[
("Image files", "*.png *.jpg *.jpeg *.gif *.bmp *.tiff *.webp"),
("All files", "*.*"),
],
)
if file_path:
try:
image = Image.open(file_path)
# Check image size before processing
max_load_size = 4096 # Maximum dimension for loading
if image.width > max_load_size or image.height > max_load_size:
messagebox.showerror(
"Image Too Large",
f"Image is too large to load safely:\n"
f"Size: {image.width}x{image.height}\n"
f"Maximum: {max_load_size}x{max_load_size}\n\n"
f"Please resize the image before loading.",
)
return
# Convert to RGBA for consistency
if image.mode != "RGBA":
image = image.convert("RGBA")
# Get name from filename
name = os.path.splitext(os.path.basename(file_path))[0]
# Ensure unique name
if name in self.current_images:
counter = 1
base_name = name
while f"{base_name}_{counter}" in self.current_images:
counter += 1
name = f"{base_name}_{counter}"
self.current_images[name] = image
self.original_images[name] = image.copy() # Store original for rotation
self.base_images[name] = (
image.copy()
) # Store base image (before rotation)
self.current_rotations[name] = 0 # Initialize rotation angle
self.cleanup_memory() # Clean up before updating UI
self.update_image_list()
self.select_image(name)
self.update_preview()
except Exception as e:
messagebox.showerror("Error", f"Failed to load image: {str(e)}")
[docs]
def export_images(self):
"""Export images to a folder."""
if not self.current_images:
messagebox.showwarning("Warning", "No images to export")
return
# Ask for output directory
output_dir = filedialog.askdirectory(title="Select Export Directory")
if not output_dir:
return
try:
for name, image in self.current_images.items():
image_path = os.path.join(output_dir, f"{name}.png")
image.save(image_path, "PNG")
messagebox.showinfo("Success", f"Images exported to: {output_dir}")
except Exception as e:
messagebox.showerror("Error", f"Failed to export images: {str(e)}")
def resize_image(self):
"""Resize the current image."""
if not self.selected_image:
messagebox.showwarning("Warning", "No image selected")
return
width = self.width_var.get()
height = self.height_var.get()
if width <= 0 or height <= 0:
messagebox.showerror("Error", "Invalid size")
return
image = self.current_images[self.selected_image]
resized = image.resize((width, height), Image.LANCZOS)
self.current_images[self.selected_image] = resized
# Update base image to the resized version and reset rotation
self.update_base_image()
self.update_canvas()
self.update_preview()
[docs]
def update_base_image(self):
"""Update base image after resize."""
if self.selected_image:
self.base_images[self.selected_image] = self.current_images[
self.selected_image
].copy()
self.original_images[self.selected_image] = self.current_images[
self.selected_image
].copy()
self.current_rotations[self.selected_image] = 0
[docs]
def clear_canvas(self):
"""Clear the canvas."""
self.canvas_manager.clear_canvas()
# View operations
[docs]
def zoom_in(self):
"""Zoom in on the canvas."""
current_zoom = self.drawing_tools.get_zoom_level()
self.drawing_tools.set_zoom_level(min(current_zoom * 1.5, 10.0))
self.update_canvas()
[docs]
def zoom_out(self):
"""Zoom out on the canvas."""
current_zoom = self.drawing_tools.get_zoom_level()
self.drawing_tools.set_zoom_level(max(current_zoom / 1.5, 0.1))
self.update_canvas()
[docs]
def reset_zoom(self):
"""Reset zoom to 100%."""
self.drawing_tools.set_zoom_level(1.0)
self.update_canvas()
[docs]
def fit_to_window(self):
"""Fit canvas to window."""
# Placeholder for fit to window functionality
pass
[docs]
def toggle_grid(self):
"""Toggle grid display."""
if hasattr(self, "grid_var"):
self.drawing_tools.show_grid = self.grid_var.get()
else:
self.drawing_tools.show_grid = not self.drawing_tools.show_grid
self.update_canvas()
# Settings
[docs]
def open_cursor_settings(self):
"""Open cursor settings dialog."""
# Placeholder for cursor settings dialog
pass
# Help
[docs]
def show_about(self):
"""Show about dialog."""
messagebox.showinfo(
"About",
"GUI Image Studio - Enhanced\n\n"
"A visual tool for developers to design images/icons\n"
"and generate embedded code.\n\n"
"Refactored for better maintainability.",
)
[docs]
def show_help(self):
"""Show help window."""
HelpWindow(self.root, self)
[docs]
def start_tutorial(self):
"""Start the interactive tutorial."""
from .ui.help_system import InteractiveTutorial
tutorial = InteractiveTutorial(self.root, self)
tutorial.start_basic_tutorial()
[docs]
def show_shortcuts_help(self):
"""Show keyboard shortcuts help."""
help_window = HelpWindow(self.root, self)
# Switch to shortcuts tab
help_window.notebook.select(3) # Shortcuts is the 4th tab (index 3)
[docs]
def show_troubleshooting_help(self):
"""Show troubleshooting help."""
help_window = HelpWindow(self.root, self)
# Switch to troubleshooting tab
help_window.notebook.select(5) # Troubleshooting is the 6th tab (index 5)
[docs]
def open_online_help(self):
"""Open online documentation."""
messagebox.showinfo(
"Online Help",
"Online documentation would be available at:\n"
"https://your-domain.com/image-studio/docs\n\n"
"For now, use the comprehensive help system (F1) for detailed information.",
)
# Additional methods referenced by panels (choose_color implemented above)
[docs]
def duplicate_image(self):
"""Duplicate the selected image."""
if self.selected_image and self.selected_image in self.current_images:
original = self.current_images[self.selected_image]
base_name = f"{self.selected_image}_copy"
new_name = base_name
# Ensure unique name
counter = 1
while new_name in self.current_images:
new_name = f"{base_name}_{counter}"
counter += 1
# Duplicate the image and all related data
self.current_images[new_name] = original.copy()
self.original_images[new_name] = original.copy()
self.base_images[new_name] = original.copy()
self.current_rotations[new_name] = 0
self.update_image_list()
self.select_image(new_name)
self.update_preview()
[docs]
def delete_image(self):
"""Delete the selected image."""
if self.selected_image and self.selected_image in self.current_images:
result = messagebox.askyesno(
"Confirm Delete", f"Delete image '{self.selected_image}'?"
)
if result:
# Remove from all image dictionaries
name = self.selected_image
if name in self.current_images:
del self.current_images[name]
if name in self.original_images:
del self.original_images[name]
if name in self.base_images:
del self.base_images[name]
if name in self.current_rotations:
del self.current_rotations[name]
if name in self.image_previews:
del self.image_previews[name]
# Clear selection if this was the selected image
self.selected_image = None
# Select another image if available
if self.current_images:
first_image = next(iter(self.current_images.keys()))
self.select_image(first_image)
else:
# No images left, clear canvas
self.update_canvas()
self.update_image_list()
self.update_preview()
[docs]
def zoom_fit(self):
"""Fit image to canvas."""
if not self.selected_image:
return
image = self.current_images[self.selected_image]
if hasattr(self, "canvas"):
canvas_width = self.canvas.winfo_width() - 20
canvas_height = self.canvas.winfo_height() - 20
if canvas_width > 0 and canvas_height > 0:
zoom_x = canvas_width / image.width
zoom_y = canvas_height / image.height
self.drawing_tools.set_zoom_level(min(zoom_x, zoom_y, 10.0))
self.update_canvas()
[docs]
def on_name_change(self, event):
"""Handle name change in properties."""
if self.selected_image and hasattr(self, "name_var"):
new_name = self.name_var.get().strip()
if new_name and new_name != self.selected_image:
self.rename_image(self.selected_image, new_name)
[docs]
def rename_image(self, old_name, new_name):
"""Rename an image."""
if old_name not in self.current_images:
return
if new_name in self.current_images:
messagebox.showwarning("Warning", "Name already exists")
if hasattr(self, "name_var"):
self.name_var.set(old_name)
return
# Rename image in all dictionaries
image = self.current_images[old_name]
del self.current_images[old_name]
self.current_images[new_name] = image
if old_name in self.original_images:
self.original_images[new_name] = self.original_images[old_name]
del self.original_images[old_name]
if old_name in self.base_images:
self.base_images[new_name] = self.base_images[old_name]
del self.base_images[old_name]
if old_name in self.current_rotations:
self.current_rotations[new_name] = self.current_rotations[old_name]
del self.current_rotations[old_name]
if old_name in self.image_previews:
self.image_previews[new_name] = self.image_previews[old_name]
del self.image_previews[old_name]
# Update selected image
if self.selected_image == old_name:
self.selected_image = new_name
self.update_image_list()
self.select_image(new_name)
[docs]
def resize_image(self):
"""Resize the current image."""
if not self.selected_image:
messagebox.showwarning("Warning", "No image selected")
return
if hasattr(self, "width_var") and hasattr(self, "height_var"):
try:
width = self.width_var.get()
height = self.height_var.get()
if width <= 0 or height <= 0:
messagebox.showerror("Error", "Invalid size values")
return
# Resize the current image
image = self.current_images[self.selected_image]
resized = image.resize((width, height), Image.LANCZOS)
self.current_images[self.selected_image] = resized
# Update base image to the resized version and reset rotation
self.update_base_image()
self.update_canvas()
self.update_preview()
except (tk.TclError, ValueError):
messagebox.showerror("Error", "Invalid size values")
[docs]
def generate_python_code(self):
"""Generate Python code for images."""
if not self.image_manager.list_images():
messagebox.showwarning("Warning", "No images to generate code for.")
return
# Placeholder for code generation
code = "# Generated Python code would go here\n"
CodePreviewWindow(self.root, "Python Code", code)
[docs]
def generate_tkinter_code(self):
"""Generate Tkinter code for images."""
if not self.image_manager.list_images():
messagebox.showwarning("Warning", "No images to generate code for.")
return
# Placeholder for Tkinter code generation
code = "# Generated Tkinter code would go here\n"
CodePreviewWindow(self.root, "Tkinter Code", code)
[docs]
def update_image_list(self):
"""Update the image list display."""
if hasattr(self, "image_listbox"):
self.image_listbox.delete(0, tk.END)
for name in self.current_images.keys():
self.image_listbox.insert(tk.END, name)
# Also update the image manager to keep it in sync
if hasattr(self, "image_manager"):
try:
# Clear and repopulate image manager
current_manager_images = self.image_manager.list_images()
for name in current_manager_images:
self.image_manager.remove_image(name)
for name, image in self.current_images.items():
self.image_manager.add_image(name, image)
except Exception as e:
print(f"Error syncing image manager: {e}")
# Update button styles based on whether images are present
self.update_button_styles()
# Drawing methods - copied from original
[docs]
def add_text(self, x, y):
"""Add text to the current image."""
if not self.selected_image:
return
# Simple text input dialog
text = simpledialog.askstring("Add Text", "Enter text:")
if text:
image = self.current_images[self.selected_image]
draw = ImageDraw.Draw(image)
try:
# Try to use a default font with size based on brush size
font_size = max(12, self.size_var.get() * 2)
font = ImageFont.load_default()
except (OSError, IOError, ImportError) as e:
# Font loading failed, use None (PIL will use built-in font)
print(f"Warning: Could not load default font: {e}")
font = None
draw.text(
(x, y), text, fill=self.drawing_tools.get_brush_color(), font=font
)
self.update_canvas()
[docs]
def on_closing(self):
"""Handle application closing."""
# Cleanup temporary files and icons
try:
import shutil
if hasattr(self, "temp_dir"):
shutil.rmtree(self.temp_dir, ignore_errors=True)
if hasattr(self.image_manager, "temp_dir"):
shutil.rmtree(self.image_manager.temp_dir, ignore_errors=True)
# Cleanup embedded icons
for icon_path in self.icon_paths:
cleanup_icon(icon_path)
except (OSError, IOError, PermissionError) as e:
# Log cleanup errors but don't prevent application from closing
print(f"Warning: Cleanup error during application shutdown: {e}")
except Exception as e:
# Catch any other unexpected errors during cleanup
print(f"Warning: Unexpected error during cleanup: {e}")
# Force garbage collection
gc.collect()
self.root.destroy()
[docs]
def run(self):
"""Run the application."""
self.root.mainloop()
def main():
"""Main entry point."""
app = EnhancedImageDesignerGUI()
app.run()
if __name__ == "__main__":
main()