diff --git a/src/GUI.py b/src/GUI.py index 76e862f..fb16cb2 100644 --- a/src/GUI.py +++ b/src/GUI.py @@ -371,15 +371,24 @@ class GUI: selection_coords["left"], selection_coords["top"], selection_coords["right"], selection_coords["bottom"] ) + # Redraw the selection highlight to ensure it's visible + self._renderCurrentImage() def _onRightClick(self, event) -> None: """Handle right-click to show context menu for manipulation options.""" - if not self._selectionArea.is_active or not self._selectionArea.is_valid(): + if not self._selectionArea or not self._selectionArea.is_active: + return + + if not self._selectionArea.is_valid(): return # Create context menu with all available manipulations context_menu = tk.Menu(self._root, tearoff=0) + # Add crop to selection option first (most common use case) + context_menu.add_command(label="Crop to Selection", command=self._cropToSelection) + context_menu.add_separator() + # Add manipulation options manipulations = GetImageManipulationList() for manipulation in manipulations: @@ -407,25 +416,37 @@ class GUI: if pil_image is None: return None + # Convert widget coordinates to canvas coordinates (accounts for borders/scroll) + canvas_x1 = self._imageCanvas.canvasx(start_x) + canvas_y1 = self._imageCanvas.canvasy(start_y) + canvas_x2 = self._imageCanvas.canvasx(end_x) + canvas_y2 = self._imageCanvas.canvasy(end_y) + # Get image dimensions img_width, img_height = pil_image.size - # Get canvas dimensions - canvas_width = self._imageCanvas.winfo_width() - canvas_height = self._imageCanvas.winfo_height() + # Get canvas dimensions for scaling + # Use stored canvas image dimensions if available (from when image was rendered) + if self._canvasImageWidth and self._canvasImageHeight: + canvas_width = self._canvasImageWidth + canvas_height = self._canvasImageHeight + else: + # Fallback: use actual canvas widget dimensions + canvas_width = self._imageCanvas.winfo_width() + canvas_height = self._imageCanvas.winfo_height() + + if canvas_width <= 0 or canvas_height <= 0: + return None - if canvas_width <= 0 or canvas_height <= 0: - return None - - # Calculate scaling factors + # Calculate scaling factors to convert canvas coordinates to image coordinates scale_x = img_width / canvas_width scale_y = img_height / canvas_height - # Convert coordinates - left = int(min(start_x, end_x) * scale_x) - top = int(min(start_y, end_y) * scale_y) - right = int(max(start_x, end_x) * scale_x) - bottom = int(max(start_y, end_y) * scale_y) + # Convert canvas coordinates to image coordinates + left = int(min(canvas_x1, canvas_x2) * scale_x) + top = int(min(canvas_y1, canvas_y2) * scale_y) + right = int(max(canvas_x1, canvas_x2) * scale_x) + bottom = int(max(canvas_y1, canvas_y2) * scale_y) # Ensure coordinates are within image bounds left = max(0, min(left, img_width)) @@ -439,6 +460,58 @@ class GUI: return None + def _cropToSelection(self) -> None: + """Crop the entire image to the selected area.""" + if self._currentImage 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 + + # Validate coordinates + if right <= left or bottom <= top: + return + + # Get the original image + original_image = self._currentImage.getImage() + if original_image is None: + return + + img_width, img_height = original_image.size + + # Ensure coordinates are within image bounds + left = max(0, min(left, img_width)) + top = max(0, min(top, img_height)) + right = max(left, min(right, img_width)) + bottom = max(top, min(bottom, img_height)) + + # Validate final coordinates + if right <= left or bottom <= top: + return + + try: + # Take undo snapshot + self._currentImage.snapshot() + + # Crop the image to the selection area + cropped_image = original_image.crop((left, top, right, bottom)) + + # Update the current image + self._currentImage._imageData = cropped_image + + # Clear selection before re-rendering + self._clearSelection() + + # Re-render the image (without selection) + self._renderCurrentImage() + except Exception as e: + # Silently handle errors - crop operation failed + pass + def _applyManipulationToSelection(self, manipulation) -> None: """Apply the specified manipulation to the selected area only.""" if self._currentImage is None or not self._selectionArea.is_active: @@ -531,11 +604,19 @@ class GUI: # Convert image coordinates to canvas coordinates pil_image = self._currentImage.getImage() img_width, img_height = pil_image.size - canvas_width = self._imageCanvas.winfo_width() - canvas_height = self._imageCanvas.winfo_height() - if canvas_width <= 0 or canvas_height <= 0: - return + # Get canvas dimensions for scaling + # Use stored canvas image dimensions if available (from when image was rendered) + if self._canvasImageWidth and self._canvasImageHeight: + canvas_width = self._canvasImageWidth + canvas_height = self._canvasImageHeight + else: + # Fallback: use actual canvas widget dimensions + canvas_width = self._imageCanvas.winfo_width() + canvas_height = self._imageCanvas.winfo_height() + + if canvas_width <= 0 or canvas_height <= 0: + return # Calculate scaling factors scale_x = canvas_width / img_width