From b83405d91863839a0f80014ce6f707d3cce4b2de Mon Sep 17 00:00:00 2001 From: Falko Habel Date: Wed, 3 Apr 2024 18:03:47 +0200 Subject: [PATCH] improved code and folder structure --- Widgets/color_widget/ctk_color_picker.py | 544 +++++++++--------- .../color_widget => icons}/color_wheel.png | Bin {Widgets/color_widget => icons}/target.png | Bin 3 files changed, 274 insertions(+), 270 deletions(-) rename {Widgets/color_widget => icons}/color_wheel.png (100%) rename {Widgets/color_widget => icons}/target.png (100%) diff --git a/Widgets/color_widget/ctk_color_picker.py b/Widgets/color_widget/ctk_color_picker.py index d9c6b6d..58ef94d 100644 --- a/Widgets/color_widget/ctk_color_picker.py +++ b/Widgets/color_widget/ctk_color_picker.py @@ -1,271 +1,275 @@ -# this is a modified version of the CTkColorPicker from GitHub - -import customtkinter as Ctk -from PIL import Image, ImageTk -import os -import math - -PATH = os.path.dirname(os.path.realpath(__file__)) - -class AskColor(Ctk.CTkToplevel): - - def __init__(self, - master, - font = None, - width: int = 250, - title: str = "Choose Color", - initial_color: str = None, - text: str = "apply", - corner_radius: int = 16, - slider_border: int = 1, - **button_kwargs): - super().__init__(master, **button_kwargs) - self.selected_color = None - self.title(title) - WIDTH = width if width >= 250 else 250 - HEIGHT = WIDTH + 110 - self.font = font if font is not None else Ctk.CTkFont(family="kDefaultFont", size=16) - self.image_dimension = self._apply_window_scaling(WIDTH - 100) - self.target_dimension = self._apply_window_scaling(20) - self.initial_color = initial_color - self.maxsize(WIDTH, HEIGHT) - self.minsize(WIDTH, HEIGHT) - self.resizable(width=False, height=False) - self.transient(self.master) - self.lift() - self.after(10) - self.protocol("WM_DELETE_WINDOW", self._on_closing) - - self.default_hex_color = "#ffffff" - self.default_rgb = [255, 255, 255] - self.rgb_color = self.default_rgb[:] - self.button_text = text - self.corner_radius = corner_radius - self.slider_border = 10 if slider_border >= 10 else slider_border - - self.frame = Ctk.CTkFrame(master=self) - self.frame.grid(sticky="nswe", padx=5, pady=5) - self.fg_color = self.fg_color = self._apply_appearance_mode(Ctk.ThemeManager.theme["CTkFrame"]["fg_color"]) - self.canvas = Ctk.CTkCanvas(self.frame, height=self.image_dimension, width=self.image_dimension, highlightthickness=0, bg=self.fg_color) - self.canvas.grid(row=0, column=0, columnspan=2, pady=20) - - - self.canvas.bind("", self.on_mouse_drag) - - self.img1 = Image.open(os.path.join(PATH, 'color_wheel.png')).resize((self.image_dimension, self.image_dimension), Image.Resampling.LANCZOS) - self.img2 = Image.open(os.path.join(PATH, 'target.png')).resize((self.target_dimension, self.target_dimension), Image.Resampling.LANCZOS) - - self.wheel = ImageTk.PhotoImage(self.img1) - self.target = ImageTk.PhotoImage(self.img2) - - self.canvas.create_image(self.image_dimension/2, self.image_dimension/2, image=self.wheel) - self.set_initial_color(initial_color) - - self.brightness_slider_value = Ctk.IntVar() - self.brightness_slider_value.set(255) - - self.slider = Ctk.CTkSlider(master=self.frame, height=20, border_width=self.slider_border, - button_length=15, progress_color=self.default_hex_color, from_=0, to=255, - variable=self.brightness_slider_value, number_of_steps=256, - button_corner_radius=self.corner_radius, corner_radius=self.corner_radius, - command=lambda x:self.update_colors()) - - self.slider.grid(row=1, column=0, columnspan=2, pady=(0, 15), padx=20-self.slider_border) - - self.label = Ctk.CTkLabel(master=self.frame, text_color="#000000", height=50, width=75, fg_color=self.default_hex_color, - corner_radius=self.corner_radius, text="") - - self.color_entry = Ctk.CTkEntry(master=self.frame, height=50, corner_radius=self.corner_radius, font=self.font, width=100) - self.color_entry.configure(placeholder_text=self.default_hex_color) # Insert the new text - - - - self.button = Ctk.CTkButton(master=self.frame, text=self.button_text, height=50, corner_radius=self.corner_radius,width = 200, - command=self._ok_event, **button_kwargs) - - - self.label.grid(row=2, column=0, padx=(5, 20), pady=10, sticky="e") - self.color_entry.grid(row=2, column=1, padx=(5, 5), pady=10, sticky="w") - self.button.grid(row=3, column=0, columnspan=3, padx=5, pady=10) - self.after(150, lambda: self.label.focus()) - - self.grab_set() - - - def get(self): - # Use the stored selected_color instead of accessing the widget - return self.selected_color - - def _ok_event(self, event=None): - input_string = self.color_entry.get() - self.selected_color = self.check_rgb_hex_color(input_string) # Store the selected color - self.grab_release() - self.destroy() - del self.img1 - del self.img2 - del self.wheel - del self.target - - def check_rgb_hex_color(self, input_string): - if not input_string: - return self.default_hex_color - if not input_string.startswith("#"): - input_string = "#" + input_string - hex_color = input_string.lstrip("#") - if len(hex_color) != 6: - return None - try: - int(hex_color, 16) - return input_string - except ValueError: - return None - - - def _on_closing(self): - self._color = None - self.grab_release() - self.destroy() - del self.img1 - del self.img2 - del self.wheel - del self.target - - def on_mouse_drag(self, event): - x = event.x - y = event.y - self.canvas.delete("all") - self.canvas.create_image(self.image_dimension/2, self.image_dimension/2, image=self.wheel) - - d_from_center = math.sqrt(((self.image_dimension/2)-x)**2 + ((self.image_dimension/2)-y)**2) - - if d_from_center < self.image_dimension/2: - self.target_x, self.target_y = x, y - else: - self.target_x, self.target_y = self.projection_on_circle(x, y, self.image_dimension/2, self.image_dimension/2, self.image_dimension/2 -1) - - self.canvas.create_image(self.target_x, self.target_y, image=self.target) - - self.get_target_color() - self.update_colors() - - def get_target_color(self): - try: - self.rgb_color = self.img1.getpixel((self.target_x, self.target_y)) - - r = self.rgb_color[0] - g = self.rgb_color[1] - b = self.rgb_color[2] - self.rgb_color = [r, g, b] - - except AttributeError: - self.rgb_color = self.default_rgb - - def update_colors(self): - brightness = self.brightness_slider_value.get() - - self.get_target_color() - - r = int(self.rgb_color[0] * (brightness/255)) - g = int(self.rgb_color[1] * (brightness/255)) - b = int(self.rgb_color[2] * (brightness/255)) - - self.rgb_color = [r, g, b] - - self.default_hex_color = "#{:02x}{:02x}{:02x}".format(*self.rgb_color) - - self.slider.configure(progress_color=self.default_hex_color) - self.label.configure(fg_color=self.default_hex_color) - - self.color_entry.delete(0, Ctk.END) # Delete any existing text in the color_entry widget - self.color_entry.configure(placeholder_text=self.default_hex_color) # Insert the new text - - - def projection_on_circle(self, point_x, point_y, circle_x, circle_y, radius): - angle = math.atan2(point_y - circle_y, point_x - circle_x) - projection_x = circle_x + radius * math.cos(angle) - projection_y = circle_y + radius * math.sin(angle) - - return projection_x, projection_y - - def set_initial_color(self, initial_color): - """ - Sets the initial color of the target if it matches the specified `initial_color`. - Falls back to the center of the image if no matching color is found or `initial_color` is invalid. - - Parameters: - - initial_color (str): The hexadecimal color value (e.g., '#RRGGBB') to match in the image. - - Note: This method is in beta stage and may not accurately handle all colors. - """ - if not initial_color or not initial_color.startswith("#"): - self._place_image_at_center() - return - - r, g, b = self._hex_to_rgb(initial_color) - - for i in range(self.image_dimension): - for j in range(self.image_dimension): - if self._pixel_matches_color(i, j, (r, g, b)): - self._place_image_at(i, j) - return - - self._place_image_at_center() - - def _hex_to_rgb(self, hex_color): - """ - Converts a hexadecimal color to an RGB tuple. - - Parameters: - - hex_color (str): The hexadecimal color string (e.g., '#RRGGBB'). - - Returns: - (tuple): A tuple containing the RGB values (r, g, b). - """ - try: - return tuple(int(hex_color.lstrip('#')[i:i+2], 16) for i in (0, 2, 4)) - except ValueError: - return None - - def _pixel_matches_color(self, x, y, color): - """ - Checks if the pixel at (x, y) matches the specified `color`. - - Parameters: - - x (int): The x-coordinate of the pixel. - - y (int): The y-coordinate of the pixel. - - color (tuple): The RGB tuple to match. - - Returns: - (bool): True if the pixel matches the `color`; False otherwise. - """ - try: - return self.img1.getpixel((x, y))[:3] == color - except IndexError: - # Outside the image bounds - return False - - def _place_image_at(self, x, y): - """ - Places the image at the specified (x, y) coordinates. - - Parameters: - - x (int): The x-coordinate where the image should be placed. - - y (int): The y-coordinate where the image should be placed. - """ - self.default_hex_color = self.initial_color - self.canvas.create_image(x, y, image=self.target) - self.target_x = x - self.target_y = y - - def _place_image_at_center(self): - """ - Places the image at the center of the canvas. - """ - center = self.image_dimension // 2 - self.canvas.create_image(center, center, image=self.target) - -if __name__ == "__main__": - app = AskColor(master=None, font=None) - app.wait_window(app) # This waits until the window is closed - color = app.get() # Access the color stored before the window was destroyed +"""Fabelous CTK Color Picker for customtkinter +---------------------------------------- +Based on the original work by Akash Bora (Akascape) +Contributions from Victor Vimbert-Guerlais (helloHackYnow) +Original: https://github.com/Akascape/CTkColorPicker/tree/main +""" + + +import customtkinter as Ctk +from PIL import Image, ImageTk +import os +import math + + +class AskColor(Ctk.CTkToplevel): + + def __init__(self, + master, + font = None, + width: int = 250, + title: str = "Choose Color", + initial_color: str = None, + text: str = "apply", + corner_radius: int = 16, + slider_border: int = 1, + **button_kwargs): + super().__init__(master, **button_kwargs) + self.selected_color = None + self.title(title) + WIDTH = width if width >= 250 else 250 + HEIGHT = WIDTH + 110 + self.font = font if font is not None else Ctk.CTkFont(family="kDefaultFont", size=16) + self.image_dimension = self._apply_window_scaling(WIDTH - 100) + self.target_dimension = self._apply_window_scaling(20) + self.initial_color = initial_color + self.maxsize(WIDTH, HEIGHT) + self.minsize(WIDTH, HEIGHT) + self.resizable(width=False, height=False) + self.transient(self.master) + self.lift() + self.after(10) + self.protocol("WM_DELETE_WINDOW", self._on_closing) + + self.default_hex_color = "#ffffff" + self.default_rgb = [255, 255, 255] + self.rgb_color = self.default_rgb[:] + self.button_text = text + self.corner_radius = corner_radius + self.slider_border = 10 if slider_border >= 10 else slider_border + + self.frame = Ctk.CTkFrame(master=self) + self.frame.grid(sticky="nswe", padx=5, pady=5) + self.fg_color = self.fg_color = self._apply_appearance_mode(Ctk.ThemeManager.theme["CTkFrame"]["fg_color"]) + self.canvas = Ctk.CTkCanvas(self.frame, height=self.image_dimension, width=self.image_dimension, highlightthickness=0, bg=self.fg_color) + self.canvas.grid(row=0, column=0, columnspan=2, pady=20) + + + self.canvas.bind("", self.on_mouse_drag) + + self.img1 = Image.open('icons/color_wheel.png').resize((self.image_dimension, self.image_dimension), Image.Resampling.LANCZOS) + self.img2 = Image.open('icons/target.png').resize((self.target_dimension, self.target_dimension), Image.Resampling.LANCZOS) + self.wheel = ImageTk.PhotoImage(self.img1) + self.target = ImageTk.PhotoImage(self.img2) + + self.canvas.create_image(self.image_dimension/2, self.image_dimension/2, image=self.wheel) + self.set_initial_color(initial_color) + + self.brightness_slider_value = Ctk.IntVar() + self.brightness_slider_value.set(255) + + self.slider = Ctk.CTkSlider(master=self.frame, height=20, border_width=self.slider_border, + button_length=15, progress_color=self.default_hex_color, from_=0, to=255, + variable=self.brightness_slider_value, number_of_steps=256, + button_corner_radius=self.corner_radius, corner_radius=self.corner_radius, + command=lambda x:self.update_colors()) + + self.slider.grid(row=1, column=0, columnspan=2, pady=(0, 15), padx=20-self.slider_border) + + self.label = Ctk.CTkLabel(master=self.frame, text_color="#000000", height=50, width=75, fg_color=self.default_hex_color, + corner_radius=self.corner_radius, text="") + + self.color_entry = Ctk.CTkEntry(master=self.frame, height=50, corner_radius=self.corner_radius, font=self.font, width=100) + self.color_entry.configure(placeholder_text=self.default_hex_color) # Insert the new text + + + + self.button = Ctk.CTkButton(master=self.frame, text=self.button_text, height=50, corner_radius=self.corner_radius,width = 200, + command=self._ok_event, **button_kwargs) + + + self.label.grid(row=2, column=0, padx=(5, 20), pady=10, sticky="e") + self.color_entry.grid(row=2, column=1, padx=(5, 5), pady=10, sticky="w") + self.button.grid(row=3, column=0, columnspan=3, padx=5, pady=10) + self.after(150, lambda: self.label.focus()) + + self.grab_set() + + + def get(self): + # Use the stored selected_color instead of accessing the widget + return self.selected_color + + def _ok_event(self, event=None): + input_string = self.color_entry.get() + self.selected_color = self.check_rgb_hex_color(input_string) # Store the selected color + self.grab_release() + self.destroy() + del self.img1 + del self.img2 + del self.wheel + del self.target + + def check_rgb_hex_color(self, input_string): + if not input_string: + return self.default_hex_color + if not input_string.startswith("#"): + input_string = "#" + input_string + hex_color = input_string.lstrip("#") + if len(hex_color) != 6: + return None + try: + int(hex_color, 16) + return input_string + except ValueError: + return None + + + def _on_closing(self): + self._color = None + self.grab_release() + self.destroy() + del self.img1 + del self.img2 + del self.wheel + del self.target + + def on_mouse_drag(self, event): + x = event.x + y = event.y + self.canvas.delete("all") + self.canvas.create_image(self.image_dimension/2, self.image_dimension/2, image=self.wheel) + + d_from_center = math.sqrt(((self.image_dimension/2)-x)**2 + ((self.image_dimension/2)-y)**2) + + if d_from_center < self.image_dimension/2: + self.target_x, self.target_y = x, y + else: + self.target_x, self.target_y = self.projection_on_circle(x, y, self.image_dimension/2, self.image_dimension/2, self.image_dimension/2 -1) + + self.canvas.create_image(self.target_x, self.target_y, image=self.target) + + self.get_target_color() + self.update_colors() + + def get_target_color(self): + try: + self.rgb_color = self.img1.getpixel((self.target_x, self.target_y)) + + r = self.rgb_color[0] + g = self.rgb_color[1] + b = self.rgb_color[2] + self.rgb_color = [r, g, b] + + except AttributeError: + self.rgb_color = self.default_rgb + + def update_colors(self): + brightness = self.brightness_slider_value.get() + + self.get_target_color() + + r = int(self.rgb_color[0] * (brightness/255)) + g = int(self.rgb_color[1] * (brightness/255)) + b = int(self.rgb_color[2] * (brightness/255)) + + self.rgb_color = [r, g, b] + + self.default_hex_color = "#{:02x}{:02x}{:02x}".format(*self.rgb_color) + + self.slider.configure(progress_color=self.default_hex_color) + self.label.configure(fg_color=self.default_hex_color) + + self.color_entry.delete(0, Ctk.END) # Delete any existing text in the color_entry widget + self.color_entry.configure(placeholder_text=self.default_hex_color) # Insert the new text + + + def projection_on_circle(self, point_x, point_y, circle_x, circle_y, radius): + angle = math.atan2(point_y - circle_y, point_x - circle_x) + projection_x = circle_x + radius * math.cos(angle) + projection_y = circle_y + radius * math.sin(angle) + + return projection_x, projection_y + + def set_initial_color(self, initial_color): + """ + Sets the initial color of the target if it matches the specified `initial_color`. + Falls back to the center of the image if no matching color is found or `initial_color` is invalid. + + Parameters: + - initial_color (str): The hexadecimal color value (e.g., '#RRGGBB') to match in the image. + + Note: This method is in beta stage and may not accurately handle all colors. + """ + if not initial_color or not initial_color.startswith("#"): + self._place_image_at_center() + return + + r, g, b = self._hex_to_rgb(initial_color) + + for i in range(self.image_dimension): + for j in range(self.image_dimension): + if self._pixel_matches_color(i, j, (r, g, b)): + self._place_image_at(i, j) + return + + self._place_image_at_center() + + def _hex_to_rgb(self, hex_color): + """ + Converts a hexadecimal color to an RGB tuple. + + Parameters: + - hex_color (str): The hexadecimal color string (e.g., '#RRGGBB'). + + Returns: + (tuple): A tuple containing the RGB values (r, g, b). + """ + try: + return tuple(int(hex_color.lstrip('#')[i:i+2], 16) for i in (0, 2, 4)) + except ValueError: + return None + + def _pixel_matches_color(self, x, y, color): + """ + Checks if the pixel at (x, y) matches the specified `color`. + + Parameters: + - x (int): The x-coordinate of the pixel. + - y (int): The y-coordinate of the pixel. + - color (tuple): The RGB tuple to match. + + Returns: + (bool): True if the pixel matches the `color`; False otherwise. + """ + try: + return self.img1.getpixel((x, y))[:3] == color + except IndexError: + # Outside the image bounds + return False + + def _place_image_at(self, x, y): + """ + Places the image at the specified (x, y) coordinates. + + Parameters: + - x (int): The x-coordinate where the image should be placed. + - y (int): The y-coordinate where the image should be placed. + """ + self.default_hex_color = self.initial_color + self.canvas.create_image(x, y, image=self.target) + self.target_x = x + self.target_y = y + + def _place_image_at_center(self): + """ + Places the image at the center of the canvas. + """ + center = self.image_dimension // 2 + self.canvas.create_image(center, center, image=self.target) + +if __name__ == "__main__": + app = AskColor(master=None, font=None) + app.wait_window(app) # This waits until the window is closed + color = app.get() # Access the color stored before the window was destroyed print("Selected color:", color) \ No newline at end of file diff --git a/Widgets/color_widget/color_wheel.png b/icons/color_wheel.png similarity index 100% rename from Widgets/color_widget/color_wheel.png rename to icons/color_wheel.png diff --git a/Widgets/color_widget/target.png b/icons/target.png similarity index 100% rename from Widgets/color_widget/target.png rename to icons/target.png