adding requirements for image menu; integrating area selection with crop and resize functionalities

This commit is contained in:
vb
2025-11-06 16:10:48 +01:00
parent db4fc94385
commit 5aba41a4f9
3 changed files with 176 additions and 13 deletions

View File

@@ -1,5 +1,5 @@
import tkinter as tk
from tkinter import filedialog, colorchooser
from tkinter import filedialog, colorchooser, messagebox
from PIL import ImageTk, ImageDraw, Image
from ImageContainer import ImageContainer
from ImageManipulation.ManipulationList import *
@@ -100,6 +100,17 @@ class GUI:
image_menu = tk.Menu(menu, tearoff=0)
menu.add_cascade(label="Image", menu=image_menu)
# Select submenu
select_menu = tk.Menu(image_menu, tearoff=0)
image_menu.add_cascade(label="Select", menu=select_menu)
select_menu.add_command(label="Rectangular Selection", command=lambda: self._setRectangularSelection())
select_menu.add_command(label="Free-form Selection (Lasso)", command=lambda: self._setLassoSelection())
select_menu.add_command(label="Circular Selection", command=lambda: self._setCircularSelection())
image_menu.add_separator()
image_menu.add_command(label="Crop", command=lambda: self._cropImage())
image_menu.add_command(label="Resize", command=lambda: self._resizeImage())
filter_menu = tk.Menu(menu, tearoff=0)
menu.add_cascade(label="Filter", menu=filter_menu)
clipboard_menu = tk.Menu(menu, tearoff=0)
@@ -305,6 +316,55 @@ class GUI:
"""Toggle selection shape between rectangle, circle, and lasso."""
if self._areaSelectionHandler:
self._areaSelectionHandler.toggle_selection_shape()
def _setRectangularSelection(self) -> None:
"""Set selection to rectangular mode."""
if self._areaSelectionHandler:
self._areaSelectionHandler.set_rectangular_selection()
def _setCircularSelection(self) -> None:
"""Set selection to circular mode."""
if self._areaSelectionHandler:
self._areaSelectionHandler.set_circular_selection()
def _setLassoSelection(self) -> None:
"""Set selection to lasso (free-form) mode."""
if self._areaSelectionHandler:
self._areaSelectionHandler.set_lasso_selection()
def _cropImage(self) -> None:
"""Crop the image to the selected rectangular area. Starts selection mode if no selection exists."""
if not self._areaSelectionHandler:
return
# If there's already a valid selection, crop immediately
if (self._areaSelectionHandler.selection_area and
self._areaSelectionHandler.selection_area.is_active and
self._areaSelectionHandler.selection_area.is_valid()):
self._areaSelectionHandler.crop_to_selection()
else:
# Set up rectangular selection mode and callback
self._areaSelectionHandler.set_rectangular_selection()
self._areaSelectionHandler.set_pending_operation_callback(
lambda: self._areaSelectionHandler.crop_to_selection()
)
def _resizeImage(self) -> None:
"""Resize the image to match the selected rectangular area dimensions. Starts selection mode if no selection exists."""
if not self._areaSelectionHandler:
return
# If there's already a valid selection, resize immediately
if (self._areaSelectionHandler.selection_area and
self._areaSelectionHandler.selection_area.is_active and
self._areaSelectionHandler.selection_area.is_valid()):
self._areaSelectionHandler.resize_to_selection_dimensions()
else:
# Set up rectangular selection mode and callback
self._areaSelectionHandler.set_rectangular_selection()
self._areaSelectionHandler.set_pending_operation_callback(
lambda: self._areaSelectionHandler.resize_to_selection_dimensions()
)
def _onMouseClick(self, event) -> None:
"""Handle mouse click for selection or brush."""

View File

@@ -57,6 +57,9 @@ class AreaSelectionHandler:
self._selectionRectangle = None
self._lassoPath = [] # List of canvas coordinates for lasso drawing
self._lassoLine = None # Canvas line item for lasso path
# Pending operation callback (called when selection is completed)
self._pendingOperationCallback = None
def toggle_selection_mode(self) -> None:
"""Toggle interactive selection mode on/off."""
@@ -65,6 +68,7 @@ class AreaSelectionHandler:
self._root.config(cursor="crosshair")
else:
self._root.config(cursor="")
self._pendingOperationCallback = None # Clear pending callback when disabling selection mode
self.clear_selection()
def toggle_selection_shape(self) -> None:
@@ -86,6 +90,44 @@ class AreaSelectionHandler:
if self._selectionArea.is_active and self._selectionArea.is_valid():
self._render_image()
def set_selection_shape(self, shape: str) -> None:
"""Set the selection shape directly.
Args:
shape: One of "rectangle", "circle", or "lasso"
"""
if self._selectionArea is None:
return
if shape in ("rectangle", "circle", "lasso"):
self._selectionArea.set_shape(shape)
self._selectionType = shape
# Enable selection mode if not already enabled
if not self._selectionMode:
self.toggle_selection_mode()
# Refresh selection drawing if active
if self._selectionArea.is_active and self._selectionArea.is_valid():
self._render_image()
def set_rectangular_selection(self) -> None:
"""Set selection to rectangular mode and enable selection."""
self.set_selection_shape("rectangle")
def set_circular_selection(self) -> None:
"""Set selection to circular mode and enable selection."""
self.set_selection_shape("circle")
def set_lasso_selection(self) -> None:
"""Set selection to lasso (free-form) mode and enable selection."""
self.set_selection_shape("lasso")
def set_pending_operation_callback(self, callback: Optional[Callable]) -> None:
"""Set a callback to be executed when a selection is completed.
Args:
callback: Function to call when selection is completed, or None to clear
"""
self._pendingOperationCallback = callback
def on_mouse_click(self, event) -> bool:
"""Handle mouse click for selection.
@@ -212,6 +254,12 @@ class AreaSelectionHandler:
# Redraw the selection highlight to ensure it's visible
self._render_image()
# Check if there's a pending operation callback and execute it
if self._pendingOperationCallback and self._selectionArea.is_valid():
callback = self._pendingOperationCallback
self._pendingOperationCallback = None # Clear callback to prevent multiple calls
callback()
return True
# Handle rectangle/circle mode
@@ -236,6 +284,12 @@ class AreaSelectionHandler:
)
# Redraw the selection highlight to ensure it's visible
self._render_image()
# Check if there's a pending operation callback and execute it
if self._pendingOperationCallback and self._selectionArea.is_valid():
callback = self._pendingOperationCallback
self._pendingOperationCallback = None # Clear callback to prevent multiple calls
callback()
return True
def on_right_click(self, event, get_default_params: Callable) -> bool:
@@ -391,6 +445,56 @@ class AreaSelectionHandler:
# Silently handle errors - crop operation failed
pass
def resize_to_selection_dimensions(self) -> None:
"""Resize the entire image to match the selection area dimensions."""
current_image = self._get_current_image()
if current_image is None or not self._selectionArea or not self._selectionArea.is_active:
return
# Get the selection coordinates
coords = self._selectionArea.get_coordinates()
if not coords:
return
left, top, right, bottom = coords
# Calculate dimensions from selection
width = right - left
height = bottom - top
# Validate dimensions
if width <= 0 or height <= 0:
return
# Get the original image
original_image = current_image.getImage()
if original_image is None:
return
try:
# Take undo snapshot
current_image.snapshot()
# Resize the entire image to the selection dimensions
from utils.image_utils import pil_to_cv2, cv2_to_pil
import cv2
cv_img = pil_to_cv2(original_image)
resized = cv2.resize(cv_img, (int(width), int(height)), interpolation=cv2.INTER_AREA)
resized_image = cv2_to_pil(resized)
# Update the current image
current_image._imageData = resized_image
# Clear selection before re-rendering
self.clear_selection()
# Re-render the image (without selection)
self._render_image()
except Exception as e:
# Silently handle errors - resize operation failed
pass
def apply_manipulation_to_selection(self, manipulation, get_default_params: Callable) -> None:
"""Apply the specified manipulation to the selected area only."""
current_image = self._get_current_image()