adding requirements for image menu; integrating area selection with crop and resize functionalities
This commit is contained in:
62
src/GUI.py
62
src/GUI.py
@@ -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."""
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user