Files
IKT213-photo-app/src/GUI.py

712 lines
31 KiB
Python

import tkinter as tk
from tkinter import filedialog, colorchooser, messagebox
from PIL import ImageTk, ImageDraw, Image
from ImageContainer import ImageContainer
from ImageManipulation.ManipulationList import *
from ImageManipulation.Filters import GaussianBlur, SobelEdge, BinaryThreshold, HistogramThreshold
from ImageManipulation.ZoomImage import ZoomImage
from ImageManipulation.FlipImage import FlipImage
from ImageManipulation.RotateImage import RotateImage
from SelectionArea import SelectionArea
from utils.area_selection import AreaSelectionHandler
from utils.brush import BrushHandler
from utils.text_entry import TextEntryHandler
from utils.file_operations import FileOperationsHandler
from utils.image_properties import ImagePropertiesHandler
from utils.filter_params import FilterParamsHandler
from ImageManipulation.CopyToClipboard import CopyToClipboard
from ImageManipulation.PasteFromClipboard import PasteFromClipboard
from ImageManipulation.CutToClipboard import CutToClipboard
from functools import partial
class GUI:
"""The GUI class responsible for the main application GUI.
@warning This class is a singleton.
"""
_instance = None
# If the GUI has been initialised.
_isInitialised = False
_currentImage = None
# Area selection handler (will be initialized in initialise())
_areaSelectionHandler = None
# Brush handler (will be initialized in initialise())
_brushHandler = None
# Text entry handler (will be initialized in initialise())
_textEntryHandler = None
# File operations handler (will be initialized in initialise())
_fileOperationsHandler = None
# Image properties handler (will be initialized in initialise())
_imagePropertiesHandler = None
# Filter parameters handler (will be initialized in initialise())
_filterParamsHandler = None
_canvasImageWidth = None # Store canvas image dimensions
_canvasImageHeight = None
def __new__(cls):
if cls._instance is None:
cls._instance = super(GUI, cls).__new__(cls)
return cls._instance
def initialise(self):
"""Initialise the GUI."""
if self._isInitialised:
return
else:
self._isInitialised = True
# Main window
root = tk.Tk()
root.title("Image Viewer")
root.geometry("1100x900")
root.config(bg="white")
icon = tk.PhotoImage(file='icon.png')
root.tk.call('wm', 'iconphoto', root._w, icon)
root.minsize(1100, 600)
# Menus
menu = tk.Menu(root)
root.config(menu=menu)
file_menu = tk.Menu(menu, tearoff=0)
menu.add_cascade(label="File", menu=file_menu)
file_menu.add_command(label="New", accelerator="Ctrl+N", command=lambda: self._newImage())
file_menu.add_command(label="Open", accelerator="Ctrl+O", command=lambda: self._openImage())
file_menu.add_command(label="Open Camera", accelerator="Ctrl+K", command=lambda: self._openCamera())
file_menu.add_separator()
file_menu.add_command(label="Save", accelerator="Ctrl+S", command=lambda: self._saveImage())
file_menu.add_command(label="Save As", command=lambda: self._saveImageAs())
file_menu.add_separator()
file_menu.add_command(label="Properties", command=lambda: self._showProperties())
file_menu.add_separator()
file_menu.add_command(label="Exit", accelerator="Ctrl+Q", command=root.quit)
test_menu = tk.Menu(menu, tearoff=0)
menu.add_cascade(label="Test", menu=test_menu)
for manipulation in GetImageManipulationList():
test_menu.add_command(
label=manipulation.getManipulationName(),
command=partial(self._applyManipulation, manipulation)
)
edit_menu = tk.Menu(menu, tearoff=0)
menu.add_cascade(label="Edit", menu=edit_menu)
edit_menu.add_command(label="Undo", accelerator="Ctrl+Z", command=lambda: self._undo())
edit_menu.add_command(label="Redo", accelerator="Ctrl+Y", command=lambda: self._redo())
edit_menu.add_separator()
edit_menu.add_command(label="Toggle Selection Mode", accelerator="Ctrl+Shift+S", command=lambda: self._toggleSelectionMode())
edit_menu.add_command(label="Toggle Selection Shape (Rect/Circle/Lasso)", 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())
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="Circular Selection", command=lambda: self._setCircularSelection())
select_menu.add_command(label="Free-form Selection (Lasso)", command=lambda: self._setLassoSelection())
select_menu.add_command(label="Polygon Selection", command=lambda: self._setPolygonSelection())
image_menu.add_separator()
image_menu.add_command(label="Crop", command=lambda: self._cropImage())
image_menu.add_command(label="Resize", command=lambda: self._resizeImage())
image_menu.add_separator()
image_menu.add_command(label="Flip Horizontal", command=lambda: self._flipHorizontal())
image_menu.add_command(label="Flip Vertical", command=lambda: self._flipVertical())
image_menu.add_separator()
image_menu.add_command(label="Rotate Right 90°", command=lambda: self._rotateRight())
image_menu.add_command(label="Rotate Left 90°", command=lambda: self._rotateLeft())
filter_menu = tk.Menu(menu, tearoff=0)
menu.add_cascade(label="Filter", menu=filter_menu)
filter_menu.add_command(label="Gaussian Filter", command=lambda: self._applyGaussianFilter())
filter_menu.add_command(label="Sobel Filter", command=lambda: self._applySobelFilter())
filter_menu.add_command(label="Binary Filter", command=lambda: self._applyBinaryFilter())
filter_menu.add_command(label="Histogram Thresholding", command=lambda: self._applyHistogramThreshold())
clipboard_menu = tk.Menu(menu, tearoff=0)
menu.add_cascade(label="Clipboard", menu=clipboard_menu)
clipboard_menu.add_command(label="Copy Image", accelerator="Ctrl+C", command=lambda: self._copyImage())
clipboard_menu.add_command(label="Cut Image", accelerator="Ctrl+X", command=lambda: self._cutImage())
clipboard_menu.add_command(label="Paste Image", accelerator="Ctrl+V", command=lambda: self._pasteImage())
tools_menu = tk.Menu(menu, tearoff=0)
menu.add_cascade(label="Tools", menu=tools_menu)
tools_menu.add_command(label="Zoom In", accelerator="Ctrl+Plus", command=lambda: self._zoomIn())
tools_menu.add_command(label="Zoom Out", accelerator="Ctrl+Minus", command=lambda: self._zoomOut())
shapes_menu = tk.Menu(menu, tearoff=0)
menu.add_cascade(label="Shapes", menu=shapes_menu)
colors_menu = tk.Menu(menu, tearoff=0)
menu.add_cascade(label="Colors", menu=colors_menu)
# 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(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, fill="both")
# Label to hold the image (will be placed on canvas)
self._imageLabel = tk.Label(self._imageCanvas, bg="white")
# Bind mouse events for interactive selection and brush
self._imageCanvas.bind("<Button-1>", self._onMouseClick)
self._imageCanvas.bind("<B1-Motion>", self._onMouseDrag)
self._imageCanvas.bind("<ButtonRelease-1>", self._onMouseRelease)
self._imageCanvas.bind("<Double-Button-1>", self._onDoubleClick) # Double-click for polygon completion
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
self._root = root
# Initialize selection area
selection_area = SelectionArea()
# Initialize area selection handler
def get_current_image():
return self._currentImage
def get_canvas_dimensions():
if self._canvasImageWidth and self._canvasImageHeight:
return (self._canvasImageWidth, self._canvasImageHeight)
else:
canvas_width = self._imageCanvas.winfo_width()
canvas_height = self._imageCanvas.winfo_height()
return (canvas_width, canvas_height)
# Area selection handler from utils/area_selection.py
self._areaSelectionHandler = AreaSelectionHandler(
canvas=self._imageCanvas,
get_current_image=get_current_image,
get_canvas_dimensions=get_canvas_dimensions,
render_image=self._renderCurrentImage,
root_window=root,
selection_area=selection_area
)
# Brush handler from utils/brush.py
self._brushHandler = BrushHandler(
canvas=self._imageCanvas,
get_current_image=get_current_image,
get_canvas_dimensions=get_canvas_dimensions,
render_image=self._renderCurrentImage,
root_window=root,
area_selection_handler=self._areaSelectionHandler
)
# Create brush UI panel
self._brushHandler.create_ui_panel(settings_panel)
# Text entry handler from utils/text_entry.py
self._textEntryHandler = TextEntryHandler(root)
# File operations handler from utils/file_operations.py
def set_current_image(image):
self._currentImage = image
self._fileOperationsHandler = FileOperationsHandler(
get_current_image=get_current_image,
set_current_image=set_current_image,
render_image=self._renderCurrentImage
)
# Image properties handler from utils/image_properties.py
self._imagePropertiesHandler = ImagePropertiesHandler(
get_current_image=get_current_image
)
# Filter parameters handler from utils/filter_params.py
self._filterParamsHandler = FilterParamsHandler(root)
# Key bindings
root.bind_all('<Control-z>', lambda event: self._undo())
root.bind_all('<Control-y>', lambda event: self._redo())
root.bind_all('<Control-n>', lambda event: self._newImage())
root.bind_all('<Control-o>', lambda event: self._openImage())
root.bind_all('<Control-s>', lambda event: self._saveImage())
root.bind_all('<Control-k>', lambda event: self._openCamera())
root.bind_all('<Control-q>', lambda event: root.quit())
root.bind_all('<Control-Shift-S>', lambda event: self._toggleSelectionMode())
root.bind_all('<Control-Shift-C>', lambda event: self._toggleSelectionShape())
root.bind_all('<Control-b>', lambda event: self._toggleBrushMode() if self._brushHandler else None)
root.bind_all('<Control-plus>', lambda event: self._zoomIn())
root.bind_all('<Control-equal>', lambda event: self._zoomIn()) # Plus key without shift
root.bind_all('<Control-minus>', lambda event: self._zoomOut())
root.bind_all('<Control-c>', lambda event: self._copyImage())
root.bind_all('<Control-x>', lambda event: self._cutImage())
root.bind_all('<Control-v>', lambda event: self._pasteImage())
root.mainloop()
def _newImage(self) -> None:
"""Create a new blank image with user-specified dimensions."""
if self._fileOperationsHandler:
self._fileOperationsHandler.new_image()
def _openImage(self) -> None:
file_path = filedialog.askopenfilename(
filetypes=[("Image files", "*.jpg *.jpeg *.png *.gif *.bmp")]
)
if not file_path:
return
self._currentImage = ImageContainer()
self._currentImage.loadImage(file_path)
self._renderCurrentImage()
def _renderCurrentImage(self) -> None:
# Clear canvas
self._imageCanvas.delete("all")
if self._currentImage is None or self._currentImage.getImage() is None:
return
pil_img = self._currentImage.getImage()
width, height = pil_img.size
tk_img = ImageTk.PhotoImage(pil_img)
# 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)
# Update canvas size
self._imageCanvas.config(width=width, height=height)
# If there's an active selection, draw it
if self._areaSelectionHandler and self._areaSelectionHandler.selection_area.is_active and self._areaSelectionHandler.selection_area.is_valid():
self._areaSelectionHandler.draw_selection_highlight()
def _applyManipulation(self, manipulation, params: dict | None = None) -> None:
if self._currentImage is None:
return
# Special handling for AddText - show dialog to get text input
if manipulation.getManipulationName() == "Add Text" and params is None:
if self._textEntryHandler:
params = self._textEntryHandler.show_text_input_dialog()
if params is None: # User cancelled
return
# Take undo snapshot
self._currentImage.snapshot()
if params is None:
# default demo params for crop
params = {"width": 200, "height": 200}
manipulation.manipulateImage(self._currentImage, params)
self._renderCurrentImage()
def _saveImage(self) -> None:
"""Save the current image without asking for a name (uses current path)."""
if self._fileOperationsHandler:
self._fileOperationsHandler.save_image()
def _saveImageAs(self) -> None:
"""Save the current image with a new name (prompts for filename)."""
if self._fileOperationsHandler:
self._fileOperationsHandler.save_image_as()
def _undo(self) -> None:
if self._currentImage is None:
return
self._currentImage.undo()
self._renderCurrentImage()
def _redo(self) -> None:
if self._currentImage is None:
return
self._currentImage.redo()
self._renderCurrentImage()
def _toggleSelectionMode(self) -> None:
"""Toggle interactive selection mode on/off."""
if self._areaSelectionHandler:
self._areaSelectionHandler.toggle_selection_mode()
def _toggleSelectionShape(self) -> None:
"""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 _setPolygonSelection(self) -> None:
"""Set selection to polygon mode."""
if self._areaSelectionHandler:
self._areaSelectionHandler.set_polygon_selection()
def _onDoubleClick(self, event) -> None:
"""Handle double-click for polygon completion."""
if self._areaSelectionHandler:
self._areaSelectionHandler.on_double_click(event)
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 _flipHorizontal(self) -> None:
"""Flip the image horizontally."""
if self._currentImage is None:
messagebox.showwarning("No Image", "Please load an image first.")
return
manipulation = FlipImage()
params = {"mode": "horizontal"}
self._applyManipulation(manipulation, params)
def _flipVertical(self) -> None:
"""Flip the image vertically."""
if self._currentImage is None:
messagebox.showwarning("No Image", "Please load an image first.")
return
manipulation = FlipImage()
params = {"mode": "vertical"}
self._applyManipulation(manipulation, params)
def _rotateRight(self) -> None:
"""Rotate the image 90 degrees to the right (clockwise)."""
if self._currentImage is None:
messagebox.showwarning("No Image", "Please load an image first.")
return
manipulation = RotateImage()
params = {"angle": -90} # Negative angle for clockwise rotation
self._applyManipulation(manipulation, params)
def _rotateLeft(self) -> None:
"""Rotate the image 90 degrees to the left (counter-clockwise)."""
if self._currentImage is None:
messagebox.showwarning("No Image", "Please load an image first.")
return
manipulation = RotateImage()
params = {"angle": 90} # Positive angle for counter-clockwise rotation
self._applyManipulation(manipulation, params)
def _onMouseClick(self, event) -> None:
"""Handle mouse click for selection or brush."""
# Handle brush mode - delegate to handler
if self._brushHandler and self._brushHandler.on_mouse_click(event):
return
# Handle selection mode - delegate to handler
if self._areaSelectionHandler:
self._areaSelectionHandler.on_mouse_click(event)
def _onMouseDrag(self, event) -> None:
"""Handle mouse drag for selection or brush."""
# Handle brush mode - delegate to handler
if self._brushHandler and self._brushHandler.on_mouse_drag(event):
return
# Handle selection mode - delegate to handler
if self._areaSelectionHandler:
self._areaSelectionHandler.on_mouse_drag(event)
def _onMouseRelease(self, event) -> None:
"""Handle mouse release to finalize selection or stop brush."""
# Handle brush mode - delegate to handler
if self._brushHandler and self._brushHandler.on_mouse_release(event):
return
# Handle selection mode - delegate to handler
if self._areaSelectionHandler:
self._areaSelectionHandler.on_mouse_release(event)
def _onRightClick(self, event) -> None:
"""Handle right-click to show context menu for manipulation options."""
if self._areaSelectionHandler:
self._areaSelectionHandler.on_right_click(event, self._getDefaultParams)
def _getDefaultParams(self, manipulation) -> dict:
"""Get default parameters for a manipulation."""
manipulation_name = manipulation.getManipulationName().lower()
if "crop" in manipulation_name:
return {"width": 200, "height": 200}
elif "resize" in manipulation_name:
return {"width": 200, "height": 200}
elif "rotate" in manipulation_name:
return {"angle": 90}
elif "flip" in manipulation_name:
return {"mode": "horizontal"}
elif "color" in manipulation_name or "adjust" in manipulation_name:
return {"brightness": 10, "contrast": 1.2, "saturation": 1.1}
elif "blur" in manipulation_name:
return {"ksize": 5}
elif "hue" in manipulation_name:
return {"hue": 50}
elif "padding" in manipulation_name:
return {"border_width": 50}
elif "sobel" in manipulation_name:
return {"dx": 1, "dy": 0, "ksize": 3}
elif "threshold" in manipulation_name:
return {"thresh": 127}
elif "text" in manipulation_name or "add text" in manipulation_name:
return {"text": "Hello World", "x": 10, "y": 10, "font_size": 40, "color": (0, 0, 0)}
else:
return {}
def _toggleBrushMode(self) -> None:
"""Toggle brush mode on/off."""
if self._brushHandler:
self._brushHandler.toggle_brush_mode()
def _showProperties(self) -> None:
"""Display image properties in a dialog."""
if self._imagePropertiesHandler:
self._imagePropertiesHandler.show_properties()
def _applyGaussianFilter(self) -> None:
"""Apply Gaussian blur filter to the image."""
if self._currentImage is None:
messagebox.showwarning("No Image", "Please load an image first.")
return
# Get default parameters
default_params = self._getDefaultParams(GaussianBlur())
default_ksize = default_params.get("ksize", 5)
# Show parameter dialog
if self._filterParamsHandler:
params = self._filterParamsHandler.show_gaussian_filter_dialog(default_ksize)
if params is None: # User cancelled
return
else:
params = default_params
manipulation = GaussianBlur()
self._applyManipulation(manipulation, params)
def _applySobelFilter(self) -> None:
"""Apply Sobel edge detection filter to the image."""
if self._currentImage is None:
messagebox.showwarning("No Image", "Please load an image first.")
return
# Get default parameters
default_params = self._getDefaultParams(SobelEdge())
default_dx = default_params.get("dx", 1)
default_dy = default_params.get("dy", 0)
default_ksize = default_params.get("ksize", 3)
# Show parameter dialog
if self._filterParamsHandler:
params = self._filterParamsHandler.show_sobel_filter_dialog(default_dx, default_dy, default_ksize)
if params is None: # User cancelled
return
else:
params = default_params
manipulation = SobelEdge()
self._applyManipulation(manipulation, params)
def _applyBinaryFilter(self) -> None:
"""Apply binary threshold filter to the image."""
if self._currentImage is None:
messagebox.showwarning("No Image", "Please load an image first.")
return
# Get default parameters
default_params = self._getDefaultParams(BinaryThreshold())
default_thresh = default_params.get("thresh", 127)
# Show parameter dialog
if self._filterParamsHandler:
params = self._filterParamsHandler.show_binary_filter_dialog(default_thresh)
if params is None: # User cancelled
return
else:
params = default_params
manipulation = BinaryThreshold()
self._applyManipulation(manipulation, params)
def _applyHistogramThreshold(self) -> None:
"""Apply histogram-based thresholding (Otsu's method) to the image."""
if self._currentImage is None:
messagebox.showwarning("No Image", "Please load an image first.")
return
# Histogram thresholding uses Otsu's method which doesn't need parameters
manipulation = HistogramThreshold()
params = {} # No parameters needed
self._applyManipulation(manipulation, params)
def _zoomIn(self) -> None:
"""Zoom in the image by a factor of 1.2."""
if self._currentImage is None:
messagebox.showwarning("No Image", "Please load an image first.")
return
manipulation = ZoomImage()
params = {"scale": 1.2}
self._applyManipulation(manipulation, params)
def _zoomOut(self) -> None:
"""Zoom out the image by a factor of 0.8."""
if self._currentImage is None:
messagebox.showwarning("No Image", "Please load an image first.")
return
manipulation = ZoomImage()
params = {"scale": 0.8}
self._applyManipulation(manipulation, params)
def _copyImage(self) -> None:
"""Copy the current image to the system clipboard."""
if self._currentImage is None or self._currentImage.getImage() is None:
messagebox.showwarning("No Image", "Please load an image first.")
return
manipulation = CopyToClipboard()
manipulation.manipulateImage(self._currentImage, {})
# Check if copy was successful
if self._currentImage._clipboard_copy_success:
messagebox.showinfo("Copy", "Image copied to clipboard.")
else:
messagebox.showerror(
"Copy Error",
"Failed to copy image to clipboard.\n\n"
"Windows: Please install pywin32 (pip install pywin32)\n"
"Linux: Please install xclip (sudo apt-get install xclip)"
)
# Clean up the attribute
delattr(self._currentImage, '_clipboard_copy_success')
def _cutImage(self) -> None:
"""Cut the current image to the system clipboard (copy and remove)."""
if self._currentImage is None or self._currentImage.getImage() is None:
messagebox.showwarning("No Image", "Please load an image first.")
return
manipulation = CutToClipboard()
manipulation.manipulateImage(self._currentImage, {})
# Check if cut was successful
if hasattr(self._currentImage, '_clipboard_cut_success'):
if self._currentImage._clipboard_cut_success:
# Re-render the canvas (will be empty now)
self._renderCurrentImage()
messagebox.showinfo("Cut", "Image cut to clipboard.")
else:
error_msg = getattr(self._currentImage, '_clipboard_cut_error',
"Failed to cut image to clipboard.")
messagebox.showerror(
"Cut Error",
f"{error_msg}\n\n"
"Windows: Please install pywin32 (pip install pywin32)\n"
"Linux: Please install xclip (sudo apt-get install xclip)"
)
# Clean up the attributes
if hasattr(self._currentImage, '_clipboard_cut_success'):
delattr(self._currentImage, '_clipboard_cut_success')
if hasattr(self._currentImage, '_clipboard_cut_error'):
delattr(self._currentImage, '_clipboard_cut_error')
def _pasteImage(self) -> None:
"""Paste an image from the system clipboard."""
manipulation = PasteFromClipboard()
# If no current image exists, create a new ImageContainer
if self._currentImage is None or self._currentImage.getImage() is None:
self._currentImage = ImageContainer()
manipulation.manipulateImage(self._currentImage, {})
# Check if paste was successful
if hasattr(self._currentImage, '_clipboard_paste_success'):
if self._currentImage._clipboard_paste_success:
# Re-render the image
self._renderCurrentImage()
messagebox.showinfo("Paste", "Image pasted from clipboard.")
else:
error_msg = getattr(self._currentImage, '_clipboard_paste_error',
"Failed to paste image from clipboard.")
messagebox.showerror(
"Paste Error",
f"{error_msg}\n\n"
"Windows: Please install pywin32 (pip install pywin32)\n"
"Linux: Please install xclip (sudo apt-get install xclip)"
)
# Clean up the attributes
if hasattr(self._currentImage, '_clipboard_paste_success'):
delattr(self._currentImage, '_clipboard_paste_success')
if hasattr(self._currentImage, '_clipboard_paste_error'):
delattr(self._currentImage, '_clipboard_paste_error')
def _openCamera(self):
camera = CameraWindow()
camera.run(self)