diff --git a/f512418272.png b/f512418272.png deleted file mode 100644 index 113325f..0000000 --- a/f512418272.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e038797906ba26309506d0ad281e866a3cfca6ca4d44d1dfcc1324357840f601 -size 87227 diff --git a/src/GUI.py b/src/GUI.py index 78cb35a..76e862f 100644 --- a/src/GUI.py +++ b/src/GUI.py @@ -1,6 +1,6 @@ import tkinter as tk -from tkinter import filedialog, messagebox -from PIL import ImageTk +from tkinter import filedialog, messagebox, colorchooser, simpledialog +from PIL import ImageTk, ImageDraw from ImageContainer import ImageContainer from ImageManipulation.ManipulationList import * from SelectionArea import SelectionArea @@ -27,6 +27,17 @@ class GUI: _selectionEndY = None _selectionRectangle = None _selectionArea = None + + # Brush variables + _brushMode = False + _brushColor = "#000000" # Default black + _brushSize = 5 # Default brush size + _lastBrushX = None + _lastBrushY = None + _isDrawing = False + _snapshotTaken = False # Track if snapshot was taken for current stroke + _canvasImageWidth = None # Store canvas image dimensions + _canvasImageHeight = None def __new__(cls): if cls._instance is None: @@ -44,11 +55,11 @@ class GUI: # Main window root = tk.Tk() root.title("Image Viewer") - root.geometry("800x600") + root.geometry("1100x700") root.config(bg="white") icon = tk.PhotoImage(file='icon.png') root.tk.call('wm', 'iconphoto', root._w, icon) - root.minsize(800, 600) + root.minsize(1100, 600) # Menus menu = tk.Menu(root) @@ -83,14 +94,20 @@ class GUI: edit_menu.add_separator() edit_menu.add_command(label="Toggle Selection Mode", accelerator="Ctrl+S", command=lambda: self._toggleSelectionMode()) edit_menu.add_command(label="Toggle Selection Shape (Rect/Circle)", accelerator="Ctrl+Shift+C", command=lambda: self._toggleSelectionShape()) + edit_menu.add_separator() + edit_menu.add_command(label="Brush Tool", accelerator="Ctrl+B", command=lambda: self._toggleBrushMode()) + + # Main container frame for image and settings + main_container = tk.Frame(root, bg="white") + main_container.pack(side="top", fill="both", expand=True, padx=10, pady=10) # Frame to hold image - imgframe = tk.Frame(root, width=500, height=500, bg="lightgray", relief="sunken", bd=2) - imgframe.pack(side="top", pady=10) + imgframe = tk.Frame(main_container, width=500, height=500, bg="lightgray", relief="sunken", bd=2) + imgframe.pack(side="left", fill="both", expand=True, padx=(0, 10)) # Canvas to display image and draw selection self._imageCanvas = tk.Canvas(imgframe, width=500, height=500, bg="white") - self._imageCanvas.pack(expand=True) + self._imageCanvas.pack(expand=True, fill="both") # Label to hold the image (will be placed on canvas) self._imageLabel = tk.Label(self._imageCanvas, bg="white") @@ -101,6 +118,66 @@ class GUI: self._imageCanvas.bind("", self._onMouseRelease) self._imageCanvas.bind("", self._onRightClick) # Right-click for context menu + # Settings panel for brush controls + settings_panel = tk.Frame(main_container, width=250, bg="lightgray", relief="sunken", bd=2) + settings_panel.pack(side="right", fill="y", padx=(10, 0)) + settings_panel.pack_propagate(False) # Prevent frame from shrinking + + # Brush settings title + tk.Label(settings_panel, text="Brush Settings", font=("Arial", 12, "bold"), bg="lightgray").pack(pady=15) + + # Color selection section + color_section = tk.Frame(settings_panel, bg="lightgray") + color_section.pack(pady=10, padx=15, fill="x") + + 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) + + def choose_color(): + color = colorchooser.askcolor(title="Choose Brush Color", color=self._brushColor) + if color[1]: # color[1] is the hex string + self._brushColor = color[1] + self._colorPreview.config(bg=self._brushColor) + + tk.Button(color_frame, text="Choose Color", command=choose_color, width=12).pack(side="left", padx=5) + + # Size selection section + size_section = tk.Frame(settings_panel, bg="lightgray") + size_section.pack(pady=10, padx=15, fill="x") + + tk.Label(size_section, text="Brush Size:", font=("Arial", 10), bg="lightgray").pack(anchor="w") + + size_frame = tk.Frame(size_section, bg="lightgray") + size_frame.pack(pady=5, fill="x") + + self._sizeVar = tk.IntVar(value=self._brushSize) + self._sizeScale = tk.Scale(size_frame, from_=1, to=50, orient="horizontal", + variable=self._sizeVar, length=200, bg="lightgray") + self._sizeScale.pack(side="left", padx=5) + + self._sizeLabel = tk.Label(size_frame, textvariable=self._sizeVar, width=3, bg="lightgray") + self._sizeLabel.pack(side="left", padx=5) + + def update_size(value): + self._brushSize = self._sizeVar.get() + self._sizeLabel.config(text=str(self._brushSize)) + + self._sizeScale.config(command=update_size) + + # Current brush info + info_frame = tk.Frame(settings_panel, bg="lightgray") + info_frame.pack(pady=20, padx=15, fill="x") + + tk.Label(info_frame, text="Brush Status:", font=("Arial", 10, "bold"), bg="lightgray").pack(anchor="w") + self._brushStatusLabel = tk.Label(info_frame, text="Inactive", fg="red", bg="lightgray", font=("Arial", 9)) + self._brushStatusLabel.pack(anchor="w", pady=5) + self._root = root # Initialize selection area @@ -111,6 +188,7 @@ class GUI: root.bind_all('', lambda event: self._redo()) root.bind_all('', lambda event: self._toggleSelectionMode()) root.bind_all('', lambda event: self._toggleSelectionShape()) + root.bind_all('', lambda event: self._toggleBrushMode()) root.mainloop() @@ -139,6 +217,10 @@ class GUI: # Keep reference to avoid garbage collection self._imageLabel.image = tk_img + # Store canvas image dimensions for coordinate conversion + self._canvasImageWidth = width + self._canvasImageHeight = height + # Place image on canvas self._imageCanvas.create_image(0, 0, anchor="nw", image=tk_img) @@ -202,7 +284,24 @@ class GUI: self._renderCurrentImage() def _onMouseClick(self, event) -> None: - """Handle mouse click for selection.""" + """Handle mouse click for selection or brush.""" + if self._brushMode and self._currentImage is None: + return + + # Handle brush mode + if self._brushMode: + self._isDrawing = True + self._lastBrushX = event.x + self._lastBrushY = event.y + # Take snapshot for undo (only once per stroke) + if not self._snapshotTaken: + self._currentImage.snapshot() + self._snapshotTaken = True + # Draw first point + self._drawBrushPoint(event.x, event.y) + return + + # Handle selection mode if not self._selectionMode or self._currentImage is None: return @@ -214,7 +313,15 @@ class GUI: self._selectionStartY = event.y def _onMouseDrag(self, event) -> None: - """Handle mouse drag for selection.""" + """Handle mouse drag for selection or brush.""" + # Handle brush mode + if self._brushMode and self._isDrawing: + self._drawBrushLine(self._lastBrushX, self._lastBrushY, event.x, event.y) + self._lastBrushX = event.x + self._lastBrushY = event.y + return + + # Handle selection mode if not self._selectionMode or self._currentImage is None or self._selectionStartX is None: return @@ -235,7 +342,16 @@ class GUI: ) def _onMouseRelease(self, event) -> None: - """Handle mouse release to finalize selection.""" + """Handle mouse release to finalize selection or stop brush.""" + # Handle brush mode + if self._brushMode and self._isDrawing: + self._isDrawing = False + self._lastBrushX = None + self._lastBrushY = None + self._snapshotTaken = False # Reset for next stroke + return + + # Handle selection mode if not self._selectionMode or self._currentImage is None or self._selectionStartX is None: return @@ -443,6 +559,132 @@ class GUI: outline="red", width=3, fill="" ) + def _toggleBrushMode(self) -> None: + """Toggle brush mode on/off.""" + self._brushMode = not self._brushMode + if self._brushMode: + # Disable selection mode when brush mode is enabled + self._selectionMode = False + self._root.config(cursor="pencil") + # Update status label + if hasattr(self, '_brushStatusLabel'): + self._brushStatusLabel.config(text="Active", fg="green") + else: + self._root.config(cursor="") + self._isDrawing = False + self._lastBrushX = None + self._lastBrushY = None + # Update status label + if hasattr(self, '_brushStatusLabel'): + self._brushStatusLabel.config(text="Inactive", fg="red") + + def _drawBrushPoint(self, x: int, y: int) -> None: + """Draw a single brush point at the given canvas coordinates.""" + if self._currentImage is None: + return + + # Convert widget coordinates to canvas coordinates (accounts for borders/scroll) + canvas_x = self._imageCanvas.canvasx(x) + canvas_y = self._imageCanvas.canvasy(y) + + # Get image dimensions + pil_image = self._currentImage.getImage() + img_width, img_height = pil_image.size + + # Use stored canvas dimensions if available, otherwise use image dimensions + if self._canvasImageWidth and self._canvasImageHeight: + canvas_width = self._canvasImageWidth + canvas_height = self._canvasImageHeight + else: + canvas_width = img_width + canvas_height = img_height + + # Convert canvas coordinates to image coordinates + # Since image is placed at (0,0) with anchor="nw", coordinates align directly + img_x = int(canvas_x) + img_y = int(canvas_y) + + # Ensure coordinates are within bounds + img_x = max(0, min(img_x, img_width - 1)) + img_y = max(0, min(img_y, img_height - 1)) + + # Convert hex color to RGB tuple + hex_color = self._brushColor.lstrip('#') + rgb_color = tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4)) + + # Draw on the image + draw = ImageDraw.Draw(pil_image) + # Draw an ellipse (circle) for the brush point + radius = self._brushSize // 2 + draw.ellipse([img_x - radius, img_y - radius, img_x + radius, img_y + radius], + fill=rgb_color, outline=rgb_color) + + # Update the image + self._currentImage._imageData = pil_image + + # Re-render + self._renderCurrentImage() + + def _drawBrushLine(self, x1: int, y1: int, x2: int, y2: int) -> None: + """Draw a brush line between two canvas coordinates.""" + if self._currentImage is None: + return + + # Convert widget coordinates to canvas coordinates (accounts for borders/scroll) + canvas_x1 = self._imageCanvas.canvasx(x1) + canvas_y1 = self._imageCanvas.canvasy(y1) + canvas_x2 = self._imageCanvas.canvasx(x2) + canvas_y2 = self._imageCanvas.canvasy(y2) + + # Get image dimensions + pil_image = self._currentImage.getImage() + img_width, img_height = pil_image.size + + # Use stored canvas dimensions if available, otherwise use image dimensions + if self._canvasImageWidth and self._canvasImageHeight: + canvas_width = self._canvasImageWidth + canvas_height = self._canvasImageHeight + else: + canvas_width = img_width + canvas_height = img_height + + # Convert canvas coordinates to image coordinates + # Since image is placed at (0,0) with anchor="nw", coordinates align directly + img_x1 = int(canvas_x1) + img_y1 = int(canvas_y1) + img_x2 = int(canvas_x2) + img_y2 = int(canvas_y2) + + # Ensure coordinates are within bounds + img_x1 = max(0, min(img_x1, img_width - 1)) + img_y1 = max(0, min(img_y1, img_height - 1)) + img_x2 = max(0, min(img_x2, img_width - 1)) + img_y2 = max(0, min(img_y2, img_height - 1)) + + # Convert hex color to RGB tuple + hex_color = self._brushColor.lstrip('#') + rgb_color = tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4)) + + # Draw on the image + draw = ImageDraw.Draw(pil_image) + # Draw a line with rounded ends + radius = self._brushSize // 2 + + # Draw the line itself + draw.line([(img_x1, img_y1), (img_x2, img_y2)], fill=rgb_color, width=self._brushSize) + + # Draw rounded ends (circles at start and end) + draw.ellipse([img_x1 - radius, img_y1 - radius, img_x1 + radius, img_y1 + radius], + fill=rgb_color) + draw.ellipse([img_x2 - radius, img_y2 - radius, img_x2 + radius, img_y2 + radius], + fill=rgb_color) + + # Update the image + self._currentImage._imageData = pil_image + + # Re-render + self._renderCurrentImage() + def _openCamera(self): camera = CameraWindow() camera.run(self) \ No newline at end of file