adding brush functionality and a new canvas for the brush settings
This commit is contained in:
262
src/GUI.py
262
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("<ButtonRelease-1>", self._onMouseRelease)
|
||||
self._imageCanvas.bind("<Button-3>", 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('<Control-y>', lambda event: self._redo())
|
||||
root.bind_all('<Control-s>', lambda event: self._toggleSelectionMode())
|
||||
root.bind_all('<Control-Shift-C>', lambda event: self._toggleSelectionShape())
|
||||
root.bind_all('<Control-b>', 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)
|
||||
Reference in New Issue
Block a user