adding requirements for file menu; improving brush color palette.

This commit is contained in:
vb
2025-11-06 15:43:24 +01:00
parent bedcc40596
commit db4fc94385
8 changed files with 327 additions and 26 deletions

View File

@@ -1,5 +1,5 @@
import tkinter as tk
from tkinter import filedialog, messagebox, colorchooser, simpledialog
from tkinter import filedialog, colorchooser
from PIL import ImageTk, ImageDraw, Image
from ImageContainer import ImageContainer
from ImageManipulation.ManipulationList import *
@@ -7,6 +7,8 @@ from SelectionArea import SelectionArea
from utils.area_selection import AreaSelectionHandler
from utils.brush import BrushHandler
from utils.text_entry import TextEntryHandler
from utils.file_operations import FileOperationsHandler
from utils.image_properties import ImagePropertiesHandler
from functools import partial
@@ -31,6 +33,12 @@ class GUI:
# Text entry handler (will be initialized in initialise())
_textEntryHandler = None
# File operations handler (will be initialized in initialise())
_fileOperationsHandler = None
# Image properties handler (will be initialized in initialise())
_imagePropertiesHandler = None
_canvasImageWidth = None # Store canvas image dimensions
_canvasImageHeight = None
@@ -61,9 +69,15 @@ class GUI:
root.config(menu=menu)
file_menu = tk.Menu(menu, tearoff=0)
menu.add_cascade(label="File", menu=file_menu)
file_menu.add_command(label="Open Image", accelerator="Ctrl+O", command=lambda: self._openImage())
file_menu.add_command(label="New", accelerator="Ctrl+N", command=lambda: self._newImage())
file_menu.add_command(label="Open", accelerator="Ctrl+O", command=lambda: self._openImage())
file_menu.add_command(label="Open Camera", accelerator="Ctrl+K", command=lambda: self._openCamera())
file_menu.add_command(label="Save Image", accelerator="Ctrl+S", command=lambda: self._saveImage())
file_menu.add_separator()
file_menu.add_command(label="Save", accelerator="Ctrl+S", command=lambda: self._saveImage())
file_menu.add_command(label="Save As", command=lambda: self._saveImageAs())
file_menu.add_separator()
file_menu.add_command(label="Properties", command=lambda: self._showProperties())
file_menu.add_separator()
file_menu.add_command(label="Exit", accelerator="Ctrl+Q", command=root.quit)
test_menu = tk.Menu(menu, tearoff=0)
@@ -167,10 +181,26 @@ class GUI:
# Text entry handler from utils/text_entry.py
self._textEntryHandler = TextEntryHandler(root)
# File operations handler from utils/file_operations.py
def set_current_image(image):
self._currentImage = image
self._fileOperationsHandler = FileOperationsHandler(
get_current_image=get_current_image,
set_current_image=set_current_image,
render_image=self._renderCurrentImage
)
# Image properties handler from utils/image_properties.py
self._imagePropertiesHandler = ImagePropertiesHandler(
get_current_image=get_current_image
)
# Key bindings
root.bind_all('<Control-z>', lambda event: self._undo())
root.bind_all('<Control-y>', lambda event: self._redo())
root.bind_all('<Control-n>', lambda event: self._newImage())
root.bind_all('<Control-o>', lambda event: self._openImage())
root.bind_all('<Control-s>', lambda event: self._saveImage())
root.bind_all('<Control-k>', lambda event: self._openCamera())
@@ -181,6 +211,11 @@ class GUI:
root.mainloop()
def _newImage(self) -> None:
"""Create a new blank image with user-specified dimensions."""
if self._fileOperationsHandler:
self._fileOperationsHandler.new_image()
def _openImage(self) -> None:
file_path = filedialog.askopenfilename(
filetypes=[("Image files", "*.jpg *.jpeg *.png *.gif *.bmp")]
@@ -240,14 +275,14 @@ class GUI:
self._renderCurrentImage()
def _saveImage(self) -> None:
if self._currentImage is None or self._currentImage.getImage() is None:
return
path = filedialog.asksaveasfilename(defaultextension=".png", filetypes=[
("PNG", "*.png"), ("JPEG", "*.jpg;*.jpeg"), ("Bitmap", "*.bmp"), ("All Files", "*.*")
])
if not path:
return
self._currentImage.saveImage(path)
"""Save the current image without asking for a name (uses current path)."""
if self._fileOperationsHandler:
self._fileOperationsHandler.save_image()
def _saveImageAs(self) -> None:
"""Save the current image with a new name (prompts for filename)."""
if self._fileOperationsHandler:
self._fileOperationsHandler.save_image_as()
def _undo(self) -> None:
if self._currentImage is None:
@@ -336,13 +371,15 @@ class GUI:
else:
return {}
def _toggleBrushMode(self) -> None:
"""Toggle brush mode on/off."""
if self._brushHandler:
self._brushHandler.toggle_brush_mode()
def _showProperties(self) -> None:
"""Display image properties in a dialog."""
if self._imagePropertiesHandler:
self._imagePropertiesHandler.show_properties()
def _openCamera(self):
camera = CameraWindow()

View File

@@ -33,9 +33,31 @@ class ImageContainer:
def getImage(self) -> Image or None:
return self._imageData
def getPath(self) -> str or None:
"""Get the current file path of the image."""
return self._path
def setPath(self, path: str) -> None:
"""Set the file path of the image."""
self._path = path
def loadBlankImage(self, width: int = 800, height: int = 600) -> None:
"""Load a blank white image with specified dimensions.
:param width: Width of the blank image (default: 800)
:param height: Height of the blank image (default: 600)
"""
self._imageData = Image.new('RGB', (width, height), color='white')
self._path = None # Blank image has no file path initially
self._history = []
self._redo_history = []
print("Created blank image:", width, "x", height)
def saveImage(self, path: str) -> None:
if self._imageData is not None:
self._imageData.save(path)
# Update the path after saving
self._path = path
def snapshot(self) -> None:
"""Push a copy of current image to history for undo."""

View File

@@ -54,6 +54,7 @@ class BrushHandler:
# UI element references (will be set by GUI)
self._colorPreview = None
self._brushStatusLabel = None
self._colorSwatches = [] # List of color swatch buttons
def create_ui_panel(self, parent_frame: tk.Frame) -> None:
"""Create the brush settings UI panel.
@@ -70,20 +71,68 @@ class BrushHandler:
tk.Label(color_section, text="Brush Color:", font=("Arial", 10), bg="lightgray").pack(anchor="w")
color_frame = tk.Frame(color_section, bg="lightgray")
color_frame.pack(pady=5)
# Color preview
self._colorPreview = tk.Label(color_frame, bg=self._brushColor, width=15, height=3, relief="sunken", bd=2)
self._colorPreview.pack(side="left", padx=5)
preview_frame = tk.Frame(color_section, bg="lightgray")
preview_frame.pack(pady=5, fill="x")
def choose_color():
color = colorchooser.askcolor(title="Choose Brush Color", color=self._brushColor)
self._colorPreview = tk.Label(preview_frame, bg=self._brushColor, width=20, height=2, relief="sunken", bd=2)
self._colorPreview.pack(pady=5)
# Color palette
palette_frame = tk.Frame(color_section, bg="lightgray")
palette_frame.pack(pady=5, fill="x")
# Define color palette - common colors arranged in a grid
color_palette = [
# Row 1: Basic colors
["#000000", "#FFFFFF", "#FF0000", "#00FF00", "#0000FF", "#FFFF00"],
# Row 2: Secondary colors
["#FF00FF", "#00FFFF", "#FFA500", "#800080", "#FFC0CB", "#A52A2A"],
# Row 3: Grays and browns
["#808080", "#C0C0C0", "#D3D3D3", "#8B4513", "#654321", "#DEB887"],
# Row 4: Blues and greens
["#000080", "#008080", "#008000", "#00CED1", "#4682B4", "#32CD32"],
# Row 5: Reds and oranges
["#DC143C", "#FF4500", "#FF6347", "#FF1493", "#8B0000", "#CD5C5C"],
# Row 6: Yellows and purples
["#FFD700", "#FFA500", "#DA70D6", "#9370DB", "#4B0082", "#9932CC"]
]
# Create color swatches
self._colorSwatches = []
for row_idx, row_colors in enumerate(color_palette):
row_frame = tk.Frame(palette_frame, bg="lightgray")
row_frame.pack(pady=2)
row_swatches = []
for col_idx, color in enumerate(row_colors):
swatch = tk.Button(
row_frame,
bg=color,
activebackground=color,
width=3,
height=1,
relief="raised",
bd=2,
cursor="hand2",
command=lambda c=color: self._select_color(c)
)
swatch.pack(side="left", padx=1, pady=1)
row_swatches.append(swatch)
self._colorSwatches.append(row_swatches)
# Custom color button (still allow custom color selection)
custom_frame = tk.Frame(color_section, bg="lightgray")
custom_frame.pack(pady=5, fill="x")
def choose_custom_color():
color = colorchooser.askcolor(title="Choose Custom Color", color=self._brushColor)
if color[1]: # color[1] is the hex string
self._brushColor = color[1]
self._colorPreview.config(bg=self._brushColor)
self._select_color(color[1])
tk.Button(color_frame, text="Choose Color", command=choose_color, width=12).pack(side="left", padx=5)
tk.Button(custom_frame, text="Custom Color...", command=choose_custom_color, width=15).pack(pady=2)
# Initialize color selection highlighting
self._select_color(self._brushColor)
# Size selection section
size_section = tk.Frame(parent_frame, bg="lightgray")
@@ -116,6 +165,29 @@ class BrushHandler:
self._brushStatusLabel = tk.Label(info_frame, text="Inactive", fg="red", bg="lightgray", font=("Arial", 9))
self._brushStatusLabel.pack(anchor="w", pady=5)
def _select_color(self, color: str) -> None:
"""Select a color from the palette or custom color.
Args:
color: Hex color string (e.g., "#FF0000")
"""
self._brushColor = color
if self._colorPreview:
self._colorPreview.config(bg=self._brushColor)
# Update visual feedback on swatches (highlight selected if in palette)
color_found = False
for row_swatches in self._colorSwatches:
for swatch in row_swatches:
swatch_color = swatch.cget("bg")
if swatch_color.upper() == color.upper():
swatch.config(relief="sunken", bd=3)
color_found = True
else:
swatch.config(relief="raised", bd=2)
# If custom color not in palette, all swatches remain raised (normal state)
def toggle_brush_mode(self) -> None:
"""Toggle brush mode on/off."""
self._brushMode = not self._brushMode

View File

@@ -0,0 +1,106 @@
import tkinter as tk
from tkinter import filedialog, messagebox, simpledialog
from typing import Callable, Optional
import sys
import os
# Add parent directory to path for imports (since we're in utils/ subdirectory)
_parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if _parent_dir not in sys.path:
sys.path.insert(0, _parent_dir)
from ImageContainer import ImageContainer
class FileOperationsHandler:
"""Handler for file operations: New, Save, and Save As.
This class encapsulates file operations including creating new images,
saving images, and managing file paths.
"""
def __init__(self,
get_current_image: Callable,
set_current_image: Callable,
render_image: Callable):
"""Initialize the file operations handler.
Args:
get_current_image: Callable that returns the current ImageContainer
set_current_image: Callable that sets the current ImageContainer
render_image: Callable that renders the current image to the canvas
"""
self._get_current_image = get_current_image
self._set_current_image = set_current_image
self._render_image = render_image
def new_image(self) -> None:
"""Create a new blank image with user-specified dimensions."""
# Prompt for width
width = simpledialog.askinteger(
"New Image",
"Enter image width (pixels):",
initialvalue=800,
minvalue=1,
maxvalue=10000
)
if width is None: # User cancelled
return
# Prompt for height
height = simpledialog.askinteger(
"New Image",
"Enter image height (pixels):",
initialvalue=600,
minvalue=1,
maxvalue=10000
)
if height is None: # User cancelled
return
# Create the blank image with specified dimensions
new_image = ImageContainer()
new_image.loadBlankImage(width, height)
self._set_current_image(new_image)
self._render_image()
def save_image(self) -> None:
"""Save the current image without asking for a name (uses current path)."""
current_image = self._get_current_image()
if current_image is None or current_image.getImage() is None:
messagebox.showwarning("No Image", "No image to save.")
return
# Get the current path
current_path = current_image.getPath()
# If no path exists (e.g., new blank image), prompt for save as
if current_path is None:
self.save_image_as()
else:
# Save to the current path
try:
current_image.saveImage(current_path)
messagebox.showinfo("Save", f"Image saved to {current_path}")
except Exception as e:
messagebox.showerror("Save Error", f"Failed to save image: {str(e)}")
def save_image_as(self) -> None:
"""Save the current image with a new name (prompts for filename)."""
current_image = self._get_current_image()
if current_image is None or current_image.getImage() is None:
messagebox.showwarning("No Image", "No image to save.")
return
path = filedialog.asksaveasfilename(defaultextension=".png", filetypes=[
("PNG", "*.png"), ("JPEG", "*.jpg;*.jpeg"), ("Bitmap", "*.bmp"), ("All Files", "*.*")
])
if not path:
return
try:
current_image.saveImage(path)
messagebox.showinfo("Save As", f"Image saved to {path}")
except Exception as e:
messagebox.showerror("Save Error", f"Failed to save image: {str(e)}")

View File

@@ -0,0 +1,64 @@
import tkinter as tk
from tkinter import messagebox
from typing import Callable
import sys
import os
# Add parent directory to path for imports (since we're in utils/ subdirectory)
_parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if _parent_dir not in sys.path:
sys.path.insert(0, _parent_dir)
class ImagePropertiesHandler:
"""Handler for displaying image properties.
This class encapsulates the functionality to display image information
such as dimensions, mode, format, file size, and path.
"""
def __init__(self, get_current_image: Callable):
"""Initialize the image properties handler.
Args:
get_current_image: Callable that returns the current ImageContainer
"""
self._get_current_image = get_current_image
def show_properties(self) -> None:
"""Display image properties in a dialog."""
current_image = self._get_current_image()
if current_image is None or current_image.getImage() is None:
messagebox.showwarning("No Image", "No image loaded.")
return
image = current_image.getImage()
width, height = current_image.getDimensions()
path = current_image.getPath()
mode = image.mode if image else "N/A"
format_name = image.format if image else "N/A"
# Get file size if path exists
file_size = "N/A"
if path:
try:
size_bytes = os.path.getsize(path)
if size_bytes < 1024:
file_size = f"{size_bytes} bytes"
elif size_bytes < 1024 * 1024:
file_size = f"{size_bytes / 1024:.2f} KB"
else:
file_size = f"{size_bytes / (1024 * 1024):.2f} MB"
except:
file_size = "Unknown"
# Create properties message
properties_text = f"Image Properties\n\n"
properties_text += f"Dimensions: {width} x {height} pixels\n"
properties_text += f"Mode: {mode}\n"
properties_text += f"Format: {format_name}\n"
properties_text += f"File Size: {file_size}\n"
properties_text += f"Path: {path if path else 'Unsaved'}"
messagebox.showinfo("Image Properties", properties_text)