Source code for tools
import sys
import customtkinter as ctk
import tkinter as tk
from fontTools.ttLib import TTFont
import os
from functools import wraps
[docs]
class TextInsertWindow(ctk.CTkFrame):
"""
Custom Widget that handles the insertion of text items into the canvas.
"""
selected_font = ""
new_font_pixel_size = -50
selected_font_index = 0
selected_font_size = -50
selected_font_file = ""
stored_text_size: int = None
stored_text_position = None
def __init__(self, app, *args, **kwargs):
"""
Args:
app: tkinter Main app
*args:
**kwargs:
"""
super().__init__(*args, **kwargs)
self.app = app
width = int(self.app.image_frame_width_maxed * .32)
height = int(self.app.image_frame_height_maxed * .30)
self.configure(height=height, width=width, fg_color="#43356B", corner_radius=0)
self.pack_propagate(False)
self.top_frame = ctk.CTkFrame(self, fg_color="#43356B", height=25, corner_radius=0)
self.top_frame.pack(side="top", fill="both")
self.reset_font_size_btn = ctk.CTkButton(master=self.top_frame, text="Reset Font Size", fg_color="#851936",
hover_color="#75143b", command=self.reset_font_size)
self.reset_font_size_btn.pack(side="left")
self.highlight_warning = ctk.CTkLabel(master=self.top_frame, text_color="#FFF500",
text="",
font=("Arial Bold", 13))
self.highlight_warning.pack(side="right", padx=20)
self.main_frame = ctk.CTkFrame(self, fg_color="#43356B", corner_radius=0)
self.main_frame.grid_columnconfigure(0, weight=1)
self.main_frame.grid_columnconfigure(1, weight=3)
self.main_frame.grid_rowconfigure(0, weight=1)
self.main_frame.pack(side="bottom", fill="both", padx=(0, 5), pady=(0, 5))
self.text_insert_layout()
# Binding the Return key to insert the text from the widget onto the canvas.
self.insert_textbox.bind('<Return>', self.handle_insert_text_btn)
[docs]
def reset_font_size(self):
"""
Resets the current font size to the default size.
Returns:
None
"""
TextInsertWindow.selected_font_size = -50
[docs]
def text_insert_layout(self):
"""
Creates the text insert widget.
Returns:
None
"""
# Colors to visualize the frame layout. (debug purpose)
if 1 != 1:
self.lavender_col = "lavender"
self.violet_col = "violet"
self.indigo_col = "indigo"
self.maroon_col = "maroon"
self.olive_col = "olive"
self.navy_col = "navy"
else:
TEXT_WINDOW_COL = "#6C59A1"
self.lavender_col = TEXT_WINDOW_COL
self.violet_col = TEXT_WINDOW_COL
self.indigo_col = TEXT_WINDOW_COL
self.maroon_col = TEXT_WINDOW_COL
self.olive_col = TEXT_WINDOW_COL
self.navy_col = TEXT_WINDOW_COL
self.TEXT_BOX_FONT_SIZE = 20
# self.configure(bg="#332A50")
self.left_frame = ctk.CTkFrame(self.main_frame, fg_color="#553C9E", corner_radius=0)
self.left_frame.columnconfigure(0, weight=1)
self.left_frame.columnconfigure(1, weight=3)
self.left_frame.rowconfigure(0, weight=1)
self.left_frame.rowconfigure(1, weight=2)
self.left_frame.grid(row=0, column=0, sticky="news", padx=(0, 5))
self.right_frame = ctk.CTkFrame(self.main_frame, fg_color=self.indigo_col)
self.right_frame.columnconfigure(0, weight=1)
self.right_frame.rowconfigure(0, weight=3)
self.right_frame.rowconfigure(1, weight=1)
self.right_frame.grid(row=0, column=1, sticky="news", )
self.font_refresh_btn = ctk.CTkButton(self.left_frame, fg_color="#553C9E",
border_color="#4A338D",
text_color="white", text="Refresh",
hover=False, command=self.populate_listbox,
corner_radius=0)
self.font_refresh_btn.grid(row=0, column=0, columnspan=2, sticky="news")
self.font_list_box = tk.Listbox(self.left_frame, background=self.lavender_col,
fg="white", borderwidth=0, selectbackground="#352D4D",
highlightthickness=0,
font=("Arial", 10), exportselection=0)
self.font_list_box.grid(row=1, column=1, sticky="news")
self.font_list_box_scrollbar_y = ctk.CTkScrollbar(self.left_frame, command=self.font_list_box.yview,
bg_color="#43356B", button_color="#E4DEF5")
self.font_list_box.configure(yscrollcommand=self.font_list_box_scrollbar_y.set)
self.font_list_box_scrollbar_y.grid(row=1, column=0, sticky="nws")
self.font_list_box.bind("<<ListboxSelect>>", self.on_listbox_select)
self.text_top_frame = ctk.CTkFrame(self.right_frame, fg_color=self.maroon_col)
self.text_top_frame.grid(row=0, column=0, sticky="news")
self.text_bottom_frame = ctk.CTkFrame(self.right_frame, fg_color=self.olive_col)
self.text_bottom_frame.columnconfigure((0, 1, 2), weight=1)
self.text_bottom_frame.rowconfigure((0, 1), weight=1)
self.text_bottom_frame.grid(row=1, column=0, sticky="news")
self.insert_textbox = ctk.CTkTextbox(master=self.text_top_frame, corner_radius=5,
fg_color="#E4DEF5", border_color="#352D4D",
border_width=2, text_color="black", wrap="word",
activate_scrollbars=True, exportselection=False,
font=(TextInsertWindow.selected_font, self.TEXT_BOX_FONT_SIZE))
self.insert_textbox.pack(side="top", fill="both", expand=True)
self.font_warning_label = ctk.CTkLabel(master=self.text_bottom_frame, text_color="white",
text="Use the Dropdown only if the font update fails.",
font=("Arial", 15))
self.font_warning_label.grid(row=0, column=0, columnspan=3, pady=5)
self.cancel_btn = ctk.CTkButton(master=self.text_bottom_frame, text="Cancel", font=("Arial", 20),
fg_color="#A53A4D", hover_color="#C71616", height=50, width=0,
command=self.hide_text_insert_window)
self.cancel_btn.grid(row=1, column=0, pady=(0, 5))
self.font_variation_combobox = ctk.CTkComboBox(self.text_bottom_frame, state="readonly", font=("Arial", 15),
border_width=0, button_color="#5A4499",
command=self.on_combobox_select,
fg_color="#BDB1DF", text_color="#2B2528",
dropdown_fg_color="#504080", dropdown_text_color="#E4E4E4",
border_color="#504080", dropdown_hover_color="#352D4D")
self.font_variation_combobox.grid(row=1, column=1, pady=(0, 5))
self.font_variation_combobox.set(TextInsertWindow.selected_font)
self.insert_text_btn = ctk.CTkButton(master=self.text_bottom_frame, text="Insert", font=("Arial", 20),
fg_color="#1C54A7", height=50, width=0, hover_color="#213552",
command=self.handle_insert_text_btn)
self.insert_text_btn.grid(row=1, column=2, pady=(0, 5))
self.populate_listbox() # Loads and lists the fonts on the sidebar.
[docs]
def populate_listbox(self):
"""
Loads the fonts from the 'fonts' folder in the program directory and lists the fonts in the widget list box.
Returns:
None
"""
FONT_FOLDER = "fonts"
self.font_list_box.delete(0, tk.END)
if os.path.exists(FONT_FOLDER) and os.path.isdir(FONT_FOLDER):
files = os.listdir(FONT_FOLDER)
font_files = [file for file in files if file.lower().endswith(('.ttf', '.otf'))]
for font_file in font_files:
self.font_list_box.insert(tk.END, font_file)
self.font_list_box.selection_set(0)
font_name = font_files[0]
self.on_listbox_select(file_name=font_name)
else:
self.app.error_prompt.display_error_prompt(error_msg="fonts folder not Found", priority=1)
self.font_list_box.insert(tk.END, "fonts folder not Found!")
[docs]
def check_if_overlay(func):
"""
Decorator that checks if the current active canvas is the base canvas or the overlay canvas.
"""
@wraps(func)
def wrapper(self, *args, **kwargs):
# If the overlay canvas is visible, reassigning the variables accordingly.
if self.app.overlay_canvas_visible:
self.gm = self.app.overlay_gm
self.active_canvas = self.app.overlay_canvas
else:
self.gm = self.app.canvas_gm
self.active_canvas = self.app.image_canvas
result = func(self, *args, **kwargs)
return result
return wrapper
[docs]
@check_if_overlay
def on_listbox_select(self, event=0, file_name=None):
"""
Called on selecting a font name from the listbox. Assigns the Text font as the selected font.
Args:
event(tkinter.Event,optional):Mouse click event.
file_name (str,optional): Font filename to use.
Returns:
None
"""
if not file_name:
# Get the selected item in the listbox
font_file_name = self.font_list_box.get(self.font_list_box.curselection())
else:
font_file_name = file_name
self.font_variation_list = []
# TextInsertWindow.selected_font = self.font_list_box.get(self.font_list_box.curselection())
try:
font = TTFont(os.path.join("fonts", font_file_name))
for record in font['name'].names:
# 95% of time The Full font name comes just before the Version.
if str(record).split()[0] == "Version":
break
else:
try: # Sometimes it won't have 'Version' and will only be the version numbers.
float_value = float(str(record))
except ValueError:
pass
else:
break
self.font_variation_list.append(str(record))
font.close()
self.font_variation_combobox.configure(state="normal")
self.font_variation_list = self.font_variation_list[1:]
self.font_variation_list.reverse()
self.font_variation_combobox.configure(values=self.font_variation_list)
# Usually the full font name comes just before version.
self.font_variation_combobox.set(self.font_variation_list[0])
self.font_variation_combobox.configure(state="readonly")
selected_font = self.font_variation_list[0]
self.insert_textbox.configure(font=(selected_font, self.TEXT_BOX_FONT_SIZE))
except Exception as e:
self.app.error_prompt.display_error_prompt(error_msg=e, priority=1)
[docs]
@check_if_overlay
def on_combobox_select(self, choice):
"""
Called on selecting an option from the dropdown combobox.
Args:
choice(str): Name of the font.
Returns:
None
"""
self.font_variation_combobox.configure(state="normal")
self.font_variation_combobox.set(choice)
self.font_variation_combobox.configure(state="readonly")
selected_font = choice
self.insert_textbox.configure(font=(selected_font, self.TEXT_BOX_FONT_SIZE))
[docs]
@check_if_overlay
def handle_insert_text_btn(self, event=None):
"""
Inserts the text from the widget onto the canvas.
Args:
event (tkinter.Event):
Returns:
None
"""
text = self.insert_textbox.get("0.0", "end").replace("\n", " ") # remove all linebreaks before text insertion.
if text and not text.isspace():
TextInsertWindow.selected_font = self.font_variation_combobox.get()
TextInsertWindow.selected_font_index = self.font_list_box.curselection()[0]
TextInsertWindow.selected_font_file = self.font_list_box.get(TextInsertWindow.selected_font_index)
self.gm.draw_release(text=text)
self.hide_text_insert_window()
self.active_canvas.focus_set()
elif text.isspace(): # If there is no text hide the widget.
self.hide_text_insert_window()
[docs]
@check_if_overlay
def hide_text_insert_window(self):
"""
Hides the text_insert_window widget.
Returns:
None
"""
# Clear the initial click Coordinates on closing the main window.
self.insert_textbox.delete("0.0", "end")
self.grab_release()
self.place_forget()
self.active_canvas.focus_set()
[docs]
@check_if_overlay
def reveal_text_insert_window(self, event=None):
"""
Displays the text_insert_window widget.
Args:
event(tkinter.Event,optional):
Returns:
None
"""
if self.app.highlight_checkbox.get() == 1: # If highlight box is checked.
self.highlight_warning.configure(text="Highlight Mode ENABLED")
else:
self.highlight_warning.configure(text="")
self.place(in_=self.app.image_frame, relx=0.5, rely=0.5, anchor="center")
self.wait_visibility()
self.grab_set()
self.insert_textbox.delete("0.0", "end")
self.after(150, self.insert_textbox.focus_set)
[docs]
class Tools:
"""
Class that handles the different tools in the main app.
"""
current_tool = 1
previous_tool = 0
stroke_width = 10
fill_color = "#FF0000"
decimate_factor = 1.5
shape_fill = False
stipple = ""
shape_constraint = False
endcap = "round"
def __init__(self, app, canvas, graphics_manager):
"""
Initializer for the tool class.
Args:
app: Main app
canvas: Canvas the tool is being used in.
graphics_manager: GraphicManager or OverlayGraphicsManager.
"""
self.app: ctk = app
self.active_canvas = canvas
self.gm = graphics_manager
self.default_tool_button_col = "#4A4A4A"
self.active_tool_button_col = "black"
self.enabled_checkbox_col = "#1F6AA5"
self.disabled_checkbox_col = "grey"
# -------Decorator------------------
[docs]
def select_tool_button(func):
"""
Decorator that handles the switch to a new tool.
"""
@wraps(func)
def wrapper_function(self, *args, **kwargs):
# "Before executing wrapped function"
if self.app.overlay_canvas_visible:
self.active_canvas = self.app.overlay_canvas
else:
self.active_canvas = self.app.image_canvas
self.tool_buttons[Tools.current_tool].configure(fg_color=self.default_tool_button_col, border_width=0)
Tools.previous_tool = Tools.current_tool
if self.app.error_prompt.error_prompt_in_display:
self.app.error_prompt.hide_error_prompt(animate=False)
main_func = func(self, *args, **kwargs)
self.active_canvas.focus_set()
self.tool_buttons[Tools.current_tool].configure(fg_color=self.active_tool_button_col, border_width=2,
border_color="#B2B2B2")
# Removes all item selections if any.
self.app.canvas_gm.remove_text_item_selection()
self.app.overlay_gm.remove_text_item_selection()
self.app.overlay_gm.remove_overlay_image_selection()
if Tools.current_tool not in [1] and not self.gm.annotation_visibility:
if Tools.current_tool == 7:
if self.app.display_mode == "default":
self.app.handle_hide_annotations_btn(enable=True)
else:
self.app.handle_hide_annotations_btn(enable=True)
return main_func
return wrapper_function
# ------------------------------------
[docs]
@select_tool_button
def color_picker_tool(self):
"""
Activates the Color Picker tool.
Returns:
None
"""
Tools.current_tool = 0
self.active_canvas.configure(cursor="dotbox")
self.disable_all_modifiers()
[docs]
@select_tool_button
def cursor_tool(self): # 1
"""
Activates the Cursor tool.
Returns:
None
"""
Tools.current_tool = 1 # 1 for cursor
self.active_canvas.configure(cursor="arrow")
self.app.canvas_text_insert.hide_text_insert_window()
self.disable_all_modifiers()
[docs]
@select_tool_button
def brush_tool(self): # 2
"""
Activates the brush tool.
Returns:
None
"""
Tools.current_tool = 2 # 2 for brush
self.active_canvas.configure(cursor="dot")
self.disable_all_modifiers()
self.app.highlight_checkbox.configure(state="normal", fg_color=self.enabled_checkbox_col)
[docs]
@select_tool_button
def eraser_tool(self):
"""
Activates the Eraser tool.
Returns:
None
"""
Tools.current_tool = 3 # 3 for eraser
try:
self.active_canvas.configure(cursor="x_cursor")
except: # For Linux.
self.active_canvas.configure(cursor="star")
if Tools.stroke_width == 50:
self.app.error_prompt.display_error_prompt(error_msg="Eraser set to 'Wipe Current'(50).", priority=2)
self.disable_all_modifiers()
[docs]
@select_tool_button
def line_tool(self):
"""
Activates the Line tool.
Returns:
None
"""
Tools.current_tool = 4 # 4 for line
self.active_canvas.configure(cursor="target")
self.disable_all_modifiers()
self.app.flat_cap_checkbox.configure(state="normal", fg_color=self.enabled_checkbox_col)
self.app.highlight_checkbox.configure(state="normal", fg_color=self.enabled_checkbox_col)
[docs]
@select_tool_button
def rectangle_tool(self): # 5
"""
Activates the Rectangle tool.
Returns:
None
"""
Tools.current_tool = 5 # 5 for rectangle
if self.app.highlight_checkbox.get() == 1:
Tools.shape_fill = True
self.app.fill_color_checkbox.select()
self.app.fill_color_checkbox.configure(state="disabled", fg_color=self.enabled_checkbox_col)
self.disable_all_modifiers()
if self.app.highlight_checkbox.get() != 1:
self.app.fill_color_checkbox.configure(state="normal", fg_color=self.enabled_checkbox_col)
# self.app.fill_color_checkbox.configure(state="normal",fg_color=self.enabled_checkbox_col)
self.app.uniform_checkbox.configure(state="normal", fg_color=self.enabled_checkbox_col)
self.app.highlight_checkbox.configure(state="normal", fg_color=self.enabled_checkbox_col)
self.active_canvas.configure(cursor="sizing")
[docs]
@select_tool_button
def oval_tool(self): # 5
"""
Activates the Oval tool.
Returns:
None
"""
Tools.current_tool = 6 # 5 for circle
self.disable_all_modifiers()
self.app.uniform_checkbox.configure(state="normal", fg_color=self.enabled_checkbox_col)
self.app.fill_color_checkbox.configure(state="normal", fg_color=self.enabled_checkbox_col)
if sys.platform.startswith("win"):
self.active_canvas.configure(cursor="circle")
else:
self.active_canvas.configure(cursor="target")
[docs]
@select_tool_button
def pan_tool(self):
"""
Activates the Pan tool.
Returns:
None
"""
Tools.current_tool = 7
self.active_canvas.configure(cursor="fleur")
self.disable_all_modifiers()
[docs]
@select_tool_button
def text_tool(self):
"""
Activates the Text tool.
Returns:
None
"""
Tools.current_tool = 8
self.active_canvas.configure(cursor="cross")
self.disable_all_modifiers()
Tools.stipple = ""
self.app.highlight_checkbox.configure(state="normal", fg_color=self.enabled_checkbox_col)
self.app.highlight_checkbox.deselect()
[docs]
@select_tool_button
def text_color_tool(self):
"""
Activates the Text color tool.
Returns:
None
"""
Tools.current_tool = 9
self.active_canvas.configure(cursor="plus")
[docs]
def create_tool_button_dict(self):
"""
Creates a dictionary consisting of the tool index as keys and the buttons as values. For ease of configuring the buttons.
Returns:
None
"""
self.tool_buttons = {0: self.app.color_picker_btn,
1: self.app.cursor_btn,
2: self.app.brush_btn,
3: self.app.eraser_btn,
4: self.app.line_btn,
5: self.app.rectangle_btn,
6: self.app.oval_btn,
7: self.app.pan_btn,
8: self.app.text_btn,
9: self.app.text_color_btn}
[docs]
def enable_all_tool_buttons(self):
"""
Sets the state of all tool buttons to normal.
Returns:
None
"""
for key, button in self.tool_buttons:
button.configure(state="normal")
[docs]
def disable_all_tool_buttons(self):
"""
Sets the state of all tool buttons to 'disabled'.
Returns:
None
"""
for key, button in self.tool_buttons:
button.configure(state="disabled")
# -----Modifier checkboxes--------------------------
[docs]
def uniform_shape_checkbox_handler(self):
"""
Gets the state of the uniform checkbox and sets the shape_constraint variable accordingly.
Returns:
None
"""
if self.app.uniform_checkbox.get() == 1: # If checked.
Tools.shape_constraint = True
else:
Tools.shape_constraint = False
[docs]
def fill_color_checkbox_handler(self):
"""
Gets the state of the fill checkbox and sets the shape_fill variable accordingly.
Returns:
None
"""
if self.app.fill_color_checkbox.get() == 1: # if checked.
Tools.shape_fill = True
else:
Tools.shape_fill = False
[docs]
def flat_cap_checkbox_handler(self):
"""
Gets the state of the flatcap checkbox and sets the endcap variable accordingly.
Returns:
None
"""
if self.app.flat_cap_checkbox.get() == 1: # If enabled the ends of the line segments are flat.
Tools.endcap = "butt"
else:
Tools.endcap = "round"
[docs]
def highlight_checkbox_handler(self):
"""
Gets the state of the highlight checkbox and sets the stipple variable accordingly.
Returns:
None
"""
# For better Ux sets the fill color to a highlight color when highight box is enabled.
if self.app.highlight_checkbox.get() == 1:
Tools.stipple = "gray25"
# Set fill color to yellow.
self.app.pick_color(color="#FFFF00")
if Tools.current_tool == 4 and self.app.flat_cap_checkbox.get() == 0: # If line set endcap to flat
self.app.flat_cap_checkbox.toggle()
elif Tools.current_tool == 5:
Tools.shape_fill = True
self.app.fill_color_checkbox.select()
self.app.fill_color_checkbox.configure(state="disabled", fg_color=self.disabled_checkbox_col)
else:
Tools.stipple = ""
if Tools.current_tool == 5 or Tools.current_tool == 6:
self.app.fill_color_checkbox.configure(state="normal", fg_color=self.enabled_checkbox_col)
[docs]
def disable_all_modifiers(self):
"""
Sets the state of all modifier checkbox to 'disabled'.
Returns:
None
"""
self.app.uniform_checkbox.configure(state="disabled", fg_color=self.disabled_checkbox_col)
self.app.fill_color_checkbox.configure(state="disabled", fg_color=self.disabled_checkbox_col)
self.app.flat_cap_checkbox.configure(state="disabled", fg_color=self.disabled_checkbox_col)
self.app.highlight_checkbox.configure(state="disabled", fg_color=self.disabled_checkbox_col)
# ----------------------------------------------
[docs]
def reset_tool_variables(self):
"""
Resets all tool variables like width, fill, constraint to the default state.
Returns:
"""
Tools.current_tool = 1
Tools.previous_tool = 0
Tools.stroke_width = 10
Tools.fill_color = "#FF0000"
Tools.decimate_factor = 1.5
Tools.shape_fill = False
Tools.stipple = ""
Tools.shape_constraint = False
Tools.endcap = "round"