added files
0
gui/custom_widgets/__init__.py
Normal file
12
gui/custom_widgets/ctk_notification.py
Normal file
@@ -0,0 +1,12 @@
|
||||
import customtkinter
|
||||
class CTkNotification(customtkinter.CTkFrame):
|
||||
def __init__(self, text, master, **kwargs):
|
||||
super().__init__(master=master, **kwargs)
|
||||
self.label = customtkinter.CTkLabel(self, text=text, width=200, wraplength=200, font=("Inter", 16))
|
||||
self.label.grid(row=0, column=0, sticky="nsew")
|
||||
self.close_button = customtkinter.CTkButton(self, width=40, text="X", command=self.destroy, fg_color="transparent")
|
||||
self.close_button.grid(row=0, column=1)
|
||||
self.progress_bar = customtkinter.CTkProgressBar(self, progress_color="white", determinate_speed=0.4)
|
||||
self.progress_bar.grid(row=1, column=0, columnspan=2, sticky="nsew")
|
||||
self.progress_bar.set(0)
|
||||
self.progress_bar.start()
|
||||
212
gui/custom_widgets/ctk_tooltip.py
Normal file
@@ -0,0 +1,212 @@
|
||||
"""
|
||||
CTkToolTip Widget
|
||||
version: 0.8
|
||||
"""
|
||||
|
||||
import time
|
||||
import sys
|
||||
import customtkinter
|
||||
from tkinter import Toplevel, Frame
|
||||
|
||||
|
||||
class CTkToolTip(Toplevel):
|
||||
"""
|
||||
Creates a ToolTip (pop-up) widget for customtkinter.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
widget: any = None,
|
||||
message: str = None,
|
||||
delay: float = 0.2,
|
||||
follow: bool = True,
|
||||
x_offset: int = +20,
|
||||
y_offset: int = +10,
|
||||
bg_color: str = None,
|
||||
corner_radius: int = 10,
|
||||
border_width: int = 0,
|
||||
border_color: str = None,
|
||||
alpha: float = 0.95,
|
||||
padding: tuple = (10, 2),
|
||||
**message_kwargs):
|
||||
|
||||
super().__init__()
|
||||
|
||||
self.widget = widget
|
||||
|
||||
self.withdraw()
|
||||
|
||||
# Disable ToolTip's title bar
|
||||
self.overrideredirect(True)
|
||||
|
||||
if sys.platform.startswith("win"):
|
||||
self.transparent_color = self.widget._apply_appearance_mode(
|
||||
customtkinter.ThemeManager.theme["CTkToplevel"]["fg_color"])
|
||||
self.attributes("-transparentcolor", self.transparent_color)
|
||||
self.transient()
|
||||
elif sys.platform.startswith("darwin"):
|
||||
self.transparent_color = 'systemTransparent'
|
||||
self.attributes("-transparent", True)
|
||||
self.transient(self.master)
|
||||
else:
|
||||
self.transparent_color = '#000001'
|
||||
corner_radius = 0
|
||||
self.transient()
|
||||
|
||||
self.resizable(width=True, height=True)
|
||||
|
||||
# Make the background transparent
|
||||
self.config(background=self.transparent_color)
|
||||
|
||||
# StringVar instance for msg string
|
||||
self.messageVar = customtkinter.StringVar()
|
||||
self.message = message
|
||||
self.messageVar.set(self.message)
|
||||
|
||||
self.delay = delay
|
||||
self.follow = follow
|
||||
self.x_offset = x_offset
|
||||
self.y_offset = y_offset
|
||||
self.corner_radius = corner_radius
|
||||
self.alpha = alpha
|
||||
self.border_width = border_width
|
||||
self.padding = padding
|
||||
self.bg_color = customtkinter.ThemeManager.theme["CTkFrame"]["fg_color"] if bg_color is None else bg_color
|
||||
self.border_color = border_color
|
||||
self.disable = False
|
||||
|
||||
# visibility status of the ToolTip inside|outside|visible
|
||||
self.status = "outside"
|
||||
self.last_moved = 0
|
||||
self.attributes('-alpha', self.alpha)
|
||||
|
||||
if sys.platform.startswith("win"):
|
||||
if self.widget._apply_appearance_mode(self.bg_color) == self.transparent_color:
|
||||
self.transparent_color = "#000001"
|
||||
self.config(background=self.transparent_color)
|
||||
self.attributes("-transparentcolor", self.transparent_color)
|
||||
|
||||
# Add the message widget inside the tooltip
|
||||
self.transparent_frame = Frame(self, bg=self.transparent_color)
|
||||
self.transparent_frame.pack(padx=0, pady=0, fill="both", expand=True)
|
||||
|
||||
self.frame = customtkinter.CTkFrame(self.transparent_frame, bg_color=self.transparent_color,
|
||||
corner_radius=self.corner_radius,
|
||||
border_width=self.border_width, fg_color=self.bg_color,
|
||||
border_color=self.border_color)
|
||||
self.frame.pack(padx=0, pady=0, fill="both", expand=True)
|
||||
|
||||
self.message_label = customtkinter.CTkLabel(self.frame, textvariable=self.messageVar, **message_kwargs)
|
||||
self.message_label.pack(fill="both", padx=self.padding[0] + self.border_width,
|
||||
pady=self.padding[1] + self.border_width, expand=True)
|
||||
|
||||
if self.widget.winfo_name() != "tk":
|
||||
if self.frame.cget("fg_color") == self.widget.cget("bg_color"):
|
||||
if not bg_color:
|
||||
self._top_fg_color = self.frame._apply_appearance_mode(
|
||||
customtkinter.ThemeManager.theme["CTkFrame"]["top_fg_color"])
|
||||
if self._top_fg_color != self.transparent_color:
|
||||
self.frame.configure(fg_color=self._top_fg_color)
|
||||
|
||||
# Add bindings to the widget without overriding the existing ones
|
||||
self.widget.bind("<Enter>", self.on_enter, add="+")
|
||||
self.widget.bind("<Leave>", self.on_leave, add="+")
|
||||
self.widget.bind("<Motion>", self.on_enter, add="+")
|
||||
self.widget.bind("<B1-Motion>", self.on_enter, add="+")
|
||||
self.widget.bind("<Destroy>", lambda _: self.hide(), add="+")
|
||||
|
||||
def show(self) -> None:
|
||||
"""
|
||||
Enable the widget.
|
||||
"""
|
||||
self.disable = False
|
||||
|
||||
def on_enter(self, event) -> None:
|
||||
"""
|
||||
Processes motion within the widget including entering and moving.
|
||||
"""
|
||||
|
||||
if self.disable:
|
||||
return
|
||||
self.last_moved = time.time()
|
||||
|
||||
# Set the status as inside for the very first time
|
||||
if self.status == "outside":
|
||||
self.status = "inside"
|
||||
|
||||
# If the follow flag is not set, motion within the widget will make the ToolTip dissapear
|
||||
if not self.follow:
|
||||
self.status = "inside"
|
||||
self.withdraw()
|
||||
|
||||
# Calculate available space on the right side of the widget relative to the screen
|
||||
root_width = self.winfo_screenwidth()
|
||||
widget_x = event.x_root
|
||||
space_on_right = root_width - widget_x
|
||||
|
||||
# Calculate the width of the tooltip's text based on the length of the message string
|
||||
text_width = self.message_label.winfo_reqwidth()
|
||||
|
||||
# Calculate the offset based on available space and text width to avoid going off-screen on the right side
|
||||
offset_x = self.x_offset
|
||||
if space_on_right < text_width + 20: # Adjust the threshold as needed
|
||||
offset_x = -text_width - 20 # Negative offset when space is limited on the right side
|
||||
|
||||
# Offsets the ToolTip using the coordinates od an event as an origin
|
||||
self.geometry(f"+{event.x_root + offset_x}+{event.y_root + self.y_offset}")
|
||||
|
||||
# Time is in integer: milliseconds
|
||||
self.after(int(self.delay * 1000), self._show)
|
||||
|
||||
def on_leave(self, event=None) -> None:
|
||||
"""
|
||||
Hides the ToolTip temporarily.
|
||||
"""
|
||||
|
||||
if self.disable: return
|
||||
self.status = "outside"
|
||||
self.withdraw()
|
||||
|
||||
def _show(self) -> None:
|
||||
"""
|
||||
Displays the ToolTip.
|
||||
"""
|
||||
|
||||
if not self.widget.winfo_exists():
|
||||
self.hide()
|
||||
self.destroy()
|
||||
|
||||
if self.status == "inside" and time.time() - self.last_moved >= self.delay:
|
||||
self.status = "visible"
|
||||
self.deiconify()
|
||||
|
||||
def hide(self) -> None:
|
||||
"""
|
||||
Disable the widget from appearing.
|
||||
"""
|
||||
if not self.winfo_exists():
|
||||
return
|
||||
self.withdraw()
|
||||
self.disable = True
|
||||
|
||||
def is_disabled(self) -> None:
|
||||
"""
|
||||
Return the window state
|
||||
"""
|
||||
return self.disable
|
||||
|
||||
def get(self) -> None:
|
||||
"""
|
||||
Returns the text on the tooltip.
|
||||
"""
|
||||
return self.messageVar.get()
|
||||
|
||||
def configure(self, message: str = None, delay: float = None, bg_color: str = None, **kwargs):
|
||||
"""
|
||||
Set new message or configure the label parameters.
|
||||
"""
|
||||
if delay: self.delay = delay
|
||||
if bg_color: self.frame.configure(fg_color=bg_color)
|
||||
|
||||
self.messageVar.set(message)
|
||||
self.message_label.configure(**kwargs)
|
||||
448
gui/custom_widgets/ctkmessagebox.py
Normal file
@@ -0,0 +1,448 @@
|
||||
"""
|
||||
CustomTkinter Messagebox
|
||||
Author: Akash Bora
|
||||
Version: 2.5
|
||||
"""
|
||||
|
||||
import customtkinter
|
||||
from PIL import Image, ImageTk
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from typing import Literal
|
||||
|
||||
class CTkMessagebox(customtkinter.CTkToplevel):
|
||||
ICONS = {
|
||||
"check": None,
|
||||
"cancel": None,
|
||||
"info": None,
|
||||
"question": None,
|
||||
"warning": None
|
||||
}
|
||||
ICON_BITMAP = {}
|
||||
def __init__(self,
|
||||
master: any = None,
|
||||
width: int = 400,
|
||||
height: int = 200,
|
||||
title: str = "CTkMessagebox",
|
||||
message: str = "This is a CTkMessagebox!",
|
||||
option_1: str = "OK",
|
||||
option_2: str = None,
|
||||
option_3: str = None,
|
||||
options: list = [],
|
||||
border_width: int = 1,
|
||||
border_color: str = "default",
|
||||
button_color: str = "default",
|
||||
bg_color: str = "default",
|
||||
fg_color: str = "default",
|
||||
text_color: str = "default",
|
||||
title_color: str = "default",
|
||||
button_text_color: str = "default",
|
||||
button_width: int = None,
|
||||
button_height: int = None,
|
||||
cancel_button_color: str = None,
|
||||
cancel_button: str = None, # types: circle, cross or none
|
||||
button_hover_color: str = "default",
|
||||
icon: str = "info",
|
||||
icon_size: tuple = None,
|
||||
corner_radius: int = 15,
|
||||
justify: str = "right",
|
||||
font: tuple = None,
|
||||
header: bool = False,
|
||||
topmost: bool = True,
|
||||
fade_in_duration: int = 0,
|
||||
sound: bool = False,
|
||||
option_focus: Literal[1, 2, 3] = None):
|
||||
|
||||
super().__init__()
|
||||
|
||||
|
||||
self.master_window = master
|
||||
|
||||
self.width = 250 if width<250 else width
|
||||
self.height = 150 if height<150 else height
|
||||
|
||||
if self.master_window is None:
|
||||
self.spawn_x = int((self.winfo_screenwidth()-self.width)/2)
|
||||
self.spawn_y = int((self.winfo_screenheight()-self.height)/2)
|
||||
else:
|
||||
self.spawn_x = int(self.master_window.winfo_width() * .5 + self.master_window.winfo_x() - .5 * self.width + 7)
|
||||
self.spawn_y = int(self.master_window.winfo_height() * .5 + self.master_window.winfo_y() - .5 * self.height + 20)
|
||||
|
||||
self.after(10)
|
||||
self.geometry(f"{self.width}x{self.height}+{self.spawn_x}+{self.spawn_y}")
|
||||
self.title(title)
|
||||
self.resizable(width=False, height=False)
|
||||
self.fade = fade_in_duration
|
||||
|
||||
if self.fade:
|
||||
self.fade = 20 if self.fade<20 else self.fade
|
||||
self.attributes("-alpha", 0)
|
||||
|
||||
if not header:
|
||||
self.overrideredirect(1)
|
||||
|
||||
if topmost:
|
||||
self.attributes("-topmost", True)
|
||||
else:
|
||||
self.transient(self.master_window)
|
||||
|
||||
if sys.platform.startswith("win"):
|
||||
self.transparent_color = self._apply_appearance_mode(self.cget("fg_color"))
|
||||
self.attributes("-transparentcolor", self.transparent_color)
|
||||
default_cancel_button = "cross"
|
||||
elif sys.platform.startswith("darwin"):
|
||||
self.transparent_color = 'systemTransparent'
|
||||
self.attributes("-transparent", True)
|
||||
default_cancel_button = "circle"
|
||||
else:
|
||||
self.transparent_color = '#000001'
|
||||
corner_radius = 0
|
||||
default_cancel_button = "cross"
|
||||
|
||||
self.lift()
|
||||
|
||||
self.config(background=self.transparent_color)
|
||||
self.protocol("WM_DELETE_WINDOW", self.button_event)
|
||||
self.grid_columnconfigure(0, weight=1)
|
||||
self.grid_rowconfigure(0, weight=1)
|
||||
self.x = self.winfo_x()
|
||||
self.y = self.winfo_y()
|
||||
self._title = title
|
||||
self.message = message
|
||||
self.font = font
|
||||
self.justify = justify
|
||||
self.sound = sound
|
||||
self.cancel_button = cancel_button if cancel_button else default_cancel_button
|
||||
self.round_corners = corner_radius if corner_radius<=30 else 30
|
||||
self.button_width = button_width if button_width else self.width/4
|
||||
self.button_height = button_height if button_height else 28
|
||||
|
||||
if self.fade: self.attributes("-alpha", 0)
|
||||
|
||||
if self.button_height>self.height/4: self.button_height = self.height/4 -20
|
||||
self.dot_color = cancel_button_color
|
||||
self.border_width = border_width if border_width<6 else 5
|
||||
|
||||
if type(options) is list and len(options)>0:
|
||||
try:
|
||||
option_1 = options[-1]
|
||||
option_2 = options[-2]
|
||||
option_3 = options[-3]
|
||||
except IndexError: None
|
||||
|
||||
if bg_color=="default":
|
||||
self.bg_color = self._apply_appearance_mode(customtkinter.ThemeManager.theme["CTkFrame"]["fg_color"])
|
||||
else:
|
||||
self.bg_color = bg_color
|
||||
|
||||
if fg_color=="default":
|
||||
self.fg_color = self._apply_appearance_mode(customtkinter.ThemeManager.theme["CTkFrame"]["top_fg_color"])
|
||||
else:
|
||||
self.fg_color = fg_color
|
||||
|
||||
default_button_color = self._apply_appearance_mode(customtkinter.ThemeManager.theme["CTkButton"]["fg_color"])
|
||||
|
||||
if sys.platform.startswith("win"):
|
||||
if self.bg_color==self.transparent_color or self.fg_color==self.transparent_color:
|
||||
self.configure(fg_color="#000001")
|
||||
self.transparent_color = "#000001"
|
||||
self.attributes("-transparentcolor", self.transparent_color)
|
||||
|
||||
if button_color=="default":
|
||||
self.button_color = (default_button_color, default_button_color, default_button_color)
|
||||
else:
|
||||
if type(button_color) is tuple:
|
||||
if len(button_color)==2:
|
||||
self.button_color = (button_color[0], button_color[1], default_button_color)
|
||||
elif len(button_color)==1:
|
||||
self.button_color = (button_color[0], default_button_color, default_button_color)
|
||||
else:
|
||||
self.button_color = button_color
|
||||
else:
|
||||
self.button_color = (button_color, button_color, button_color)
|
||||
|
||||
if text_color=="default":
|
||||
self.text_color = self._apply_appearance_mode(customtkinter.ThemeManager.theme["CTkLabel"]["text_color"])
|
||||
else:
|
||||
self.text_color = text_color
|
||||
|
||||
if title_color=="default":
|
||||
self.title_color = self._apply_appearance_mode(customtkinter.ThemeManager.theme["CTkLabel"]["text_color"])
|
||||
else:
|
||||
self.title_color = title_color
|
||||
|
||||
if button_text_color=="default":
|
||||
self.bt_text_color = self._apply_appearance_mode(customtkinter.ThemeManager.theme["CTkButton"]["text_color"])
|
||||
else:
|
||||
self.bt_text_color = button_text_color
|
||||
|
||||
if button_hover_color=="default":
|
||||
self.bt_hv_color = self._apply_appearance_mode(customtkinter.ThemeManager.theme["CTkButton"]["hover_color"])
|
||||
else:
|
||||
self.bt_hv_color = button_hover_color
|
||||
|
||||
if border_color=="default":
|
||||
self.border_color = self._apply_appearance_mode(customtkinter.ThemeManager.theme["CTkFrame"]["border_color"])
|
||||
else:
|
||||
self.border_color = border_color
|
||||
|
||||
if icon_size:
|
||||
self.size_height = icon_size[1] if icon_size[1]<=self.height-100 else self.height-100
|
||||
self.size = (icon_size[0], self.size_height)
|
||||
else:
|
||||
self.size = (self.height/4, self.height/4)
|
||||
|
||||
self.icon = self.load_icon(icon, icon_size) if icon else None
|
||||
|
||||
self.frame_top = customtkinter.CTkFrame(self, corner_radius=self.round_corners, width=self.width, border_width=self.border_width,
|
||||
bg_color=self.transparent_color, fg_color=self.bg_color, border_color=self.border_color)
|
||||
self.frame_top.grid(sticky="nswe")
|
||||
|
||||
if button_width:
|
||||
self.frame_top.grid_columnconfigure(0, weight=1)
|
||||
else:
|
||||
self.frame_top.grid_columnconfigure((1,2,3), weight=1)
|
||||
|
||||
if button_height:
|
||||
self.frame_top.grid_rowconfigure((0,1,3), weight=1)
|
||||
else:
|
||||
self.frame_top.grid_rowconfigure((0,1,2), weight=1)
|
||||
|
||||
self.frame_top.bind("<B1-Motion>", self.move_window)
|
||||
self.frame_top.bind("<ButtonPress-1>", self.oldxyset)
|
||||
|
||||
if self.cancel_button=="cross":
|
||||
self.button_close = customtkinter.CTkButton(self.frame_top, corner_radius=10, width=0, height=0, hover=False, border_width=0,
|
||||
text_color=self.dot_color if self.dot_color else self.title_color,
|
||||
text="✕", fg_color="transparent", command=self.button_event)
|
||||
self.button_close.grid(row=0, column=5, sticky="ne", padx=5+self.border_width, pady=5+self.border_width)
|
||||
elif self.cancel_button=="circle":
|
||||
self.button_close = customtkinter.CTkButton(self.frame_top, corner_radius=10, width=10, height=10, hover=False, border_width=0,
|
||||
text="", fg_color=self.dot_color if self.dot_color else "#c42b1c", command=self.button_event)
|
||||
self.button_close.grid(row=0, column=5, sticky="ne", padx=10, pady=10)
|
||||
|
||||
self.title_label = customtkinter.CTkLabel(self.frame_top, width=1, text=self._title, text_color=self.title_color, font=self.font)
|
||||
self.title_label.grid(row=0, column=0, columnspan=6, sticky="nw", padx=(15,30), pady=5)
|
||||
self.title_label.bind("<B1-Motion>", self.move_window)
|
||||
self.title_label.bind("<ButtonPress-1>", self.oldxyset)
|
||||
|
||||
self.info = customtkinter.CTkButton(self.frame_top, width=1, height=self.height/2, corner_radius=0, text=self.message, font=self.font,
|
||||
fg_color=self.fg_color, hover=False, text_color=self.text_color, image=self.icon)
|
||||
self.info._text_label.configure(wraplength=self.width/2, justify="left")
|
||||
self.info.grid(row=1, column=0, columnspan=6, sticky="nwes", padx=self.border_width)
|
||||
|
||||
if self.info._text_label.winfo_reqheight()>self.height/2:
|
||||
height_offset = int((self.info._text_label.winfo_reqheight())-(self.height/2) + self.height)
|
||||
self.geometry(f"{self.width}x{height_offset}")
|
||||
|
||||
|
||||
self.option_text_1 = option_1
|
||||
|
||||
self.button_1 = customtkinter.CTkButton(self.frame_top, text=self.option_text_1, fg_color=self.button_color[0],
|
||||
width=self.button_width, font=self.font, text_color=self.bt_text_color,
|
||||
hover_color=self.bt_hv_color, height=self.button_height,
|
||||
command=lambda: self.button_event(self.option_text_1))
|
||||
|
||||
|
||||
self.option_text_2 = option_2
|
||||
if option_2:
|
||||
self.button_2 = customtkinter.CTkButton(self.frame_top, text=self.option_text_2, fg_color=self.button_color[1],
|
||||
width=self.button_width, font=self.font, text_color=self.bt_text_color,
|
||||
hover_color=self.bt_hv_color, height=self.button_height,
|
||||
command=lambda: self.button_event(self.option_text_2))
|
||||
|
||||
self.option_text_3 = option_3
|
||||
if option_3:
|
||||
self.button_3 = customtkinter.CTkButton(self.frame_top, text=self.option_text_3, fg_color=self.button_color[2],
|
||||
width=self.button_width, font=self.font, text_color=self.bt_text_color,
|
||||
hover_color=self.bt_hv_color, height=self.button_height,
|
||||
command=lambda: self.button_event(self.option_text_3))
|
||||
|
||||
if self.justify=="center":
|
||||
if button_width:
|
||||
columns = [4,3,2]
|
||||
span = 1
|
||||
else:
|
||||
columns = [4,2,0]
|
||||
span = 2
|
||||
if option_3:
|
||||
self.frame_top.columnconfigure((0,1,2,3,4,5), weight=1)
|
||||
self.button_1.grid(row=2, column=columns[0], columnspan=span, sticky="news", padx=(0,10), pady=10)
|
||||
self.button_2.grid(row=2, column=columns[1], columnspan=span, sticky="news", padx=10, pady=10)
|
||||
self.button_3.grid(row=2, column=columns[2], columnspan=span, sticky="news", padx=(10,0), pady=10)
|
||||
elif option_2:
|
||||
self.frame_top.columnconfigure((0,5), weight=1)
|
||||
columns = [2,3]
|
||||
self.button_1.grid(row=2, column=columns[0], sticky="news", padx=(0,5), pady=10)
|
||||
self.button_2.grid(row=2, column=columns[1], sticky="news", padx=(5,0), pady=10)
|
||||
else:
|
||||
if button_width:
|
||||
self.frame_top.columnconfigure((0,1,2,3,4,5), weight=1)
|
||||
else:
|
||||
self.frame_top.columnconfigure((0,2,4), weight=2)
|
||||
self.button_1.grid(row=2, column=columns[1], columnspan=span, sticky="news", padx=(0,10), pady=10)
|
||||
elif self.justify=="left":
|
||||
self.frame_top.columnconfigure((0,1,2,3,4,5), weight=1)
|
||||
if button_width:
|
||||
columns = [0,1,2]
|
||||
span = 1
|
||||
else:
|
||||
columns = [0,2,4]
|
||||
span = 2
|
||||
if option_3:
|
||||
self.button_1.grid(row=2, column=columns[2], columnspan=span, sticky="news", padx=(0,10), pady=10)
|
||||
self.button_2.grid(row=2, column=columns[1], columnspan=span, sticky="news", padx=10, pady=10)
|
||||
self.button_3.grid(row=2, column=columns[0], columnspan=span, sticky="news", padx=(10,0), pady=10)
|
||||
elif option_2:
|
||||
self.button_1.grid(row=2, column=columns[1], columnspan=span, sticky="news", padx=10, pady=10)
|
||||
self.button_2.grid(row=2, column=columns[0], columnspan=span, sticky="news", padx=(10,0), pady=10)
|
||||
else:
|
||||
self.button_1.grid(row=2, column=columns[0], columnspan=span, sticky="news", padx=(10,0), pady=10)
|
||||
else:
|
||||
self.frame_top.columnconfigure((0,1,2,3,4,5), weight=1)
|
||||
if button_width:
|
||||
columns = [5,4,3]
|
||||
span = 1
|
||||
else:
|
||||
columns = [4,2,0]
|
||||
span = 2
|
||||
self.button_1.grid(row=2, column=columns[0], columnspan=span, sticky="news", padx=(0,10), pady=10)
|
||||
if option_2:
|
||||
self.button_2.grid(row=2, column=columns[1], columnspan=span, sticky="news", padx=10, pady=10)
|
||||
if option_3:
|
||||
self.button_3.grid(row=2, column=columns[2], columnspan=span, sticky="news", padx=(10,0), pady=10)
|
||||
|
||||
if header:
|
||||
self.title_label.configure(text="")
|
||||
self.title_label.grid_configure(pady=0)
|
||||
self.button_close.configure(text_color=self.bg_color)
|
||||
self.frame_top.configure(corner_radius=0)
|
||||
|
||||
if self.winfo_exists():
|
||||
self.grab_set()
|
||||
|
||||
if self.sound:
|
||||
self.bell()
|
||||
|
||||
if self.fade:
|
||||
self.fade_in()
|
||||
|
||||
if option_focus:
|
||||
self.option_focus = option_focus
|
||||
self.focus_button(self.option_focus)
|
||||
else:
|
||||
if not self.option_text_2 and not self.option_text_3:
|
||||
self.button_1.focus()
|
||||
self.button_1.bind("<Return>", lambda event: self.button_event(self.option_text_1))
|
||||
|
||||
self.bind("<Escape>", lambda e: self.button_event())
|
||||
|
||||
def focus_button(self, option_focus):
|
||||
try:
|
||||
self.selected_button = getattr(self, "button_"+str(option_focus))
|
||||
self.selected_button.focus()
|
||||
self.selected_button.configure(border_color=self.bt_hv_color, border_width=3)
|
||||
self.selected_option = getattr(self, "option_text_"+str(option_focus))
|
||||
self.selected_button.bind("<Return>", lambda event: self.button_event(self.selected_option))
|
||||
except AttributeError:
|
||||
return
|
||||
|
||||
self.bind("<Left>", lambda e: self.change_left())
|
||||
self.bind("<Right>", lambda e: self.change_right())
|
||||
|
||||
def change_left(self):
|
||||
if self.option_focus==3:
|
||||
return
|
||||
|
||||
self.selected_button.unbind("<Return>")
|
||||
self.selected_button.configure(border_width=0)
|
||||
|
||||
if self.option_focus==1:
|
||||
if self.option_text_2:
|
||||
self.option_focus = 2
|
||||
|
||||
elif self.option_focus==2:
|
||||
if self.option_text_3:
|
||||
self.option_focus = 3
|
||||
|
||||
self.focus_button(self.option_focus)
|
||||
|
||||
def change_right(self):
|
||||
if self.option_focus==1:
|
||||
return
|
||||
|
||||
self.selected_button.unbind("<Return>")
|
||||
self.selected_button.configure(border_width=0)
|
||||
|
||||
if self.option_focus==2:
|
||||
self.option_focus = 1
|
||||
|
||||
elif self.option_focus==3:
|
||||
self.option_focus = 2
|
||||
|
||||
self.focus_button(self.option_focus)
|
||||
|
||||
def load_icon(self, icon, icon_size):
|
||||
if icon not in self.ICONS or self.ICONS[icon] is None:
|
||||
if icon in ["check", "cancel", "info", "question", "warning"]:
|
||||
image_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'icons', icon + '.png')
|
||||
else:
|
||||
image_path = icon
|
||||
if icon_size:
|
||||
size_height = icon_size[1] if icon_size[1] <= self.height - 100 else self.height - 100
|
||||
size = (icon_size[0], size_height)
|
||||
else:
|
||||
size = (self.height / 4, self.height / 4)
|
||||
self.ICONS[icon] = customtkinter.CTkImage(Image.open(image_path), size=size)
|
||||
self.ICON_BITMAP[icon] = ImageTk.PhotoImage(file=image_path)
|
||||
self.after(200, lambda: self.iconphoto(False, self.ICON_BITMAP[icon]))
|
||||
return self.ICONS[icon]
|
||||
|
||||
def fade_in(self):
|
||||
for i in range(0,110,10):
|
||||
if not self.winfo_exists():
|
||||
break
|
||||
self.attributes("-alpha", i/100)
|
||||
self.update()
|
||||
time.sleep(1/self.fade)
|
||||
|
||||
def fade_out(self):
|
||||
for i in range(100,0,-10):
|
||||
if not self.winfo_exists():
|
||||
break
|
||||
self.attributes("-alpha", i/100)
|
||||
self.update()
|
||||
time.sleep(1/self.fade)
|
||||
|
||||
def get(self):
|
||||
if self.winfo_exists():
|
||||
self.master.wait_window(self)
|
||||
return self.event
|
||||
|
||||
def oldxyset(self, event):
|
||||
self.oldx = event.x
|
||||
self.oldy = event.y
|
||||
|
||||
def move_window(self, event):
|
||||
self.y = event.y_root - self.oldy
|
||||
self.x = event.x_root - self.oldx
|
||||
self.geometry(f'+{self.x}+{self.y}')
|
||||
|
||||
def button_event(self, event=None):
|
||||
try:
|
||||
self.button_1.configure(state="disabled")
|
||||
self.button_2.configure(state="disabled")
|
||||
self.button_3.configure(state="disabled")
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
if self.fade:
|
||||
self.fade_out()
|
||||
self.grab_release()
|
||||
self.destroy()
|
||||
self.event = event
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = CTkMessagebox()
|
||||
app.mainloop()
|
||||
BIN
gui/custom_widgets/icons/cancel.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
gui/custom_widgets/icons/check.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
gui/custom_widgets/icons/info.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
gui/custom_widgets/icons/question.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
gui/custom_widgets/icons/warning.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
0
gui/frames/__init__.py
Normal file
63
gui/frames/create_backup.py
Normal file
@@ -0,0 +1,63 @@
|
||||
import customtkinter
|
||||
from gui.custom_widgets.ctk_tooltip import CTkToolTip
|
||||
from tkinter import filedialog, END
|
||||
|
||||
class CreateBackupFrame(customtkinter.CTkFrame):
|
||||
def __init__(self, master, linker, config, **kwargs):
|
||||
super().__init__(master, **kwargs)
|
||||
self.linker = linker
|
||||
self.config = config
|
||||
self.create_widgets()
|
||||
self.bind_to_config()
|
||||
|
||||
def create_widgets(self):
|
||||
self.login_settings_label = customtkinter.CTkLabel(self, text="Create Backup Settings", font=customtkinter.CTkFont(family="Inter", size=30, weight="bold"))
|
||||
self.login_settings_label.grid(row=0, column=0, columnspan =2, sticky="nw", padx=20, pady=20)
|
||||
|
||||
self.create_downloadpath_widgets()
|
||||
self.create_filename_widgets()
|
||||
self.create_folders_widgets()
|
||||
self.linker.login = self
|
||||
|
||||
def create_downloadpath_widgets(self):
|
||||
self.downloadpath = customtkinter.CTkLabel(self, text="Output Directory:", font=customtkinter.CTkFont(size=20, underline=True))
|
||||
self.downloadpath.grid(row=3, column=0, padx=40, pady=(20, 10), sticky="nw")
|
||||
|
||||
self.downloadpath_entry = customtkinter.CTkEntry(self, font=customtkinter.CTkFont(family="Inter", size=16))
|
||||
self.downloadpath_entry.grid(row=4, column=0, columnspan=2, padx=(60,0), pady=(20, 10), sticky="nsew")
|
||||
|
||||
self.downloadpath_button = customtkinter.CTkButton(self, width=50, text="Select", command = self.open_folder)
|
||||
self.downloadpath_button.grid(row=4, column=2, padx=20, pady=(20, 10), sticky="nsew")
|
||||
|
||||
def create_filename_widgets(self):
|
||||
self.filename_label = customtkinter.CTkLabel(self, text="Filename")
|
||||
self.filename_label.grid(row=5, column=0)
|
||||
self.filename_entry = customtkinter.CTkEntry(self, font=customtkinter.CTkFont(family="Inter", size=16))
|
||||
self.filename_entry.grid(row=6, column=0, columnspan=2, padx=(60,0), pady=(20, 10), sticky="nsew")
|
||||
|
||||
def create_folders_widgets(self):
|
||||
self.folders_label = customtkinter.CTkLabel(self, text="Game Folders:")
|
||||
self.folders_label.grid(row=7, column=0)
|
||||
|
||||
self.userdata_checkbox = customtkinter.CTkCheckBox(self, text="Userdata")
|
||||
self.userdata_checkbox.grid(row=8, column=0)
|
||||
|
||||
self.mods_checkbox = customtkinter.CTkCheckBox(self, text="mods")
|
||||
self.mods_checkbox.grid(row=8, column=1)
|
||||
|
||||
self.bepinex_checkbox = customtkinter.CTkCheckBox(self, text="BepInEx")
|
||||
self.bepinex_checkbox.grid(row=8, column=2)
|
||||
|
||||
def bind_to_config(self):
|
||||
self.config.bind(self.downloadpath_entry, ["CreateBackup", "OutputPath"])
|
||||
self.config.bind(self.filename_entry, ["CreateBackup", "Filename"])
|
||||
self.config.bind(self.userdata_checkbox, ["CreateBackup", "GameFolders", "UserData"])
|
||||
self.config.bind(self.mods_checkbox, ["CreateBackup", "GameFolders", "mods"])
|
||||
self.config.bind(self.bepinex_checkbox, ["CreateBackup", "GameFolders", "BepInEx"])
|
||||
|
||||
def open_folder(self):
|
||||
folderpath = filedialog.askdirectory()
|
||||
if folderpath != "":
|
||||
self.downloadpath_entry.delete(0, END)
|
||||
self.downloadpath_entry.insert(0, folderpath)
|
||||
self.config.save_to_json(["CreateBackup", "OutputPath"])
|
||||
44
gui/frames/fc_kks.py
Normal file
@@ -0,0 +1,44 @@
|
||||
import customtkinter
|
||||
from gui.custom_widgets.ctk_tooltip import CTkToolTip
|
||||
from tkinter import filedialog, END
|
||||
|
||||
class FilterConvertKKSFrame(customtkinter.CTkFrame):
|
||||
def __init__(self, master, linker, config, **kwargs):
|
||||
super().__init__(master, **kwargs)
|
||||
self.linker = linker
|
||||
self.config = config
|
||||
self.create_widgets()
|
||||
self.bind_to_config()
|
||||
|
||||
def create_widgets(self):
|
||||
self.login_settings_label = customtkinter.CTkLabel(self, text="Filter & Convert KKS Chara Settings", font=customtkinter.CTkFont(family="Inter", size=30, weight="bold"))
|
||||
self.login_settings_label.grid(row=0, column=0, columnspan=2, sticky="nw", padx=20, pady=20)
|
||||
|
||||
self.create_downloadpath_widgets()
|
||||
self.create_convert_widgets()
|
||||
self.linker.login = self
|
||||
|
||||
def create_downloadpath_widgets(self):
|
||||
self.downloadpath = customtkinter.CTkLabel(self, text="Input Directory:", font=customtkinter.CTkFont(size=20, underline=True))
|
||||
self.downloadpath.grid(row=3, column=0, padx=40, pady=(20, 10), sticky="nw")
|
||||
|
||||
self.downloadpath_entry = customtkinter.CTkEntry(self, font=customtkinter.CTkFont(family="Inter", size=16))
|
||||
self.downloadpath_entry.grid(row=4, column=0, columnspan=2, padx=(60,0), pady=(20, 10), sticky="nsew")
|
||||
|
||||
self.downloadpath_button = customtkinter.CTkButton(self, width=50, text="Select", command = self.open_folder)
|
||||
self.downloadpath_button.grid(row=4, column=2, padx=20, pady=(20, 10), sticky="nsew")
|
||||
|
||||
def create_convert_widgets(self):
|
||||
self.convert_checkbox = customtkinter.CTkCheckBox(self, text="Convert KKS to KK Chara")
|
||||
self.convert_checkbox.grid(row=5, column=0)
|
||||
|
||||
def bind_to_config(self):
|
||||
self.config.bind(self.downloadpath_entry, ["FCKKS", "InputPath"])
|
||||
self.config.bind(self.convert_checkbox, ["FCKKS", "Convert"])
|
||||
|
||||
def open_folder(self):
|
||||
folderpath = filedialog.askdirectory()
|
||||
if folderpath != "":
|
||||
self.downloadpath_entry.delete(0, END)
|
||||
self.downloadpath_entry.insert(0, folderpath)
|
||||
self.config.save_to_json(["FCKKS", "InputPath"])
|
||||
56
gui/frames/install_chara.py
Normal file
@@ -0,0 +1,56 @@
|
||||
import customtkinter
|
||||
from gui.custom_widgets.ctk_tooltip import CTkToolTip
|
||||
from tkinter import filedialog, END
|
||||
|
||||
class InstallCharaFrame(customtkinter.CTkFrame):
|
||||
def __init__(self, master, linker, config, **kwargs):
|
||||
super().__init__(master, **kwargs)
|
||||
self.linker = linker
|
||||
self.config = config
|
||||
self.create_widgets()
|
||||
self.bind_to_config()
|
||||
|
||||
def create_widgets(self):
|
||||
self.login_settings_label = customtkinter.CTkLabel(self, text="Install Chara Settings", font=customtkinter.CTkFont(family="Inter", size=30, weight="bold"))
|
||||
self.login_settings_label.grid(row=0, column=0, columnspan =2, sticky="nw", padx=20, pady=20)
|
||||
|
||||
self.create_downloadpath_widgets()
|
||||
self.create_conflict_widgets()
|
||||
self.create_password_widgets()
|
||||
self.linker.login = self
|
||||
|
||||
def create_downloadpath_widgets(self):
|
||||
self.downloadpath = customtkinter.CTkLabel(self, text="Input Directory:", font=customtkinter.CTkFont(size=20, underline=True))
|
||||
self.downloadpath.grid(row=3, column=0, padx=40, pady=(20, 10), sticky="nw")
|
||||
|
||||
self.downloadpath_entry = customtkinter.CTkEntry(self, font=customtkinter.CTkFont(family="Inter", size=16))
|
||||
self.downloadpath_entry.grid(row=4, column=0, columnspan=2, padx=(60,0), pady=(20, 10), sticky="nsew")
|
||||
|
||||
self.downloadpath_button = customtkinter.CTkButton(self, width=50, text="Select", command = self.open_folder)
|
||||
self.downloadpath_button.grid(row=4, column=2, padx=20, pady=(20, 10), sticky="nsew")
|
||||
|
||||
def create_conflict_widgets(self):
|
||||
self.conflict_label = customtkinter.CTkLabel(self, text="If file conflicts:", font=customtkinter.CTkFont(size=20))
|
||||
self.conflict_label.grid(row=6, column=0, padx=20, pady=(20, 10))
|
||||
|
||||
self.conflict_dropdown = customtkinter.CTkOptionMenu(self, values=["Skip", "Replace", "Rename"])
|
||||
self.conflict_dropdown.grid(row=6, column=1, padx=20, pady=(20, 10))
|
||||
|
||||
def create_password_widgets(self):
|
||||
self.password_label = customtkinter.CTkLabel(self, text="If password required for archives:", font=customtkinter.CTkFont(size=20), wraplength=200)
|
||||
self.password_label.grid(row=7, column=0, padx=20, pady=(20, 10))
|
||||
|
||||
self.password_dropdown = customtkinter.CTkOptionMenu(self, values=["Skip", "Request Password"])
|
||||
self.password_dropdown.grid(row=7, column=1, padx=20, pady=(20, 10))
|
||||
|
||||
def bind_to_config(self):
|
||||
self.config.bind(self.downloadpath_entry, ["InstallChara", "InputPath"])
|
||||
self.config.bind(self.conflict_dropdown, ["InstallChara", "FileConflicts"])
|
||||
self.config.bind(self.password_dropdown, ["InstallChara", "ArchivePassword"])
|
||||
|
||||
def open_folder(self):
|
||||
folderpath = filedialog.askdirectory()
|
||||
if folderpath != "":
|
||||
self.downloadpath_entry.delete(0, END)
|
||||
self.downloadpath_entry.insert(0, folderpath)
|
||||
self.config.save_to_json(["InstallChara", "InputPath"])
|
||||
39
gui/frames/logger.py
Normal file
@@ -0,0 +1,39 @@
|
||||
import customtkinter
|
||||
|
||||
class LoggerTextBox(customtkinter.CTkFrame):
|
||||
def __init__(self, master, linker, config, **kwargs):
|
||||
super().__init__(master=master, **kwargs)
|
||||
self.linker = linker
|
||||
self.config = config
|
||||
self.grid_rowconfigure(0, weight=0)
|
||||
self.grid_rowconfigure(1, weight=1)
|
||||
self.grid_columnconfigure(0, weight=1)
|
||||
|
||||
self.log_label = customtkinter.CTkLabel(self, text="Log",font=customtkinter.CTkFont(family="Inter", size=30, weight="bold"))
|
||||
self.log_label.grid(row=0, column=0, sticky="nw", padx=20, pady=(20,0))
|
||||
# Button to toggle autoscroll
|
||||
self.toggle_autoscroll_button = customtkinter.CTkButton(self, height=35, text="Autoscroll On", command=self.toggle_autoscroll, font=("Inter", 16))
|
||||
self.toggle_autoscroll_button.grid(row=0, column=1, padx=20, pady=20, sticky="nsew")
|
||||
self.autoscroll_enabled = True # Initially, autoscroll is enabled
|
||||
self.log_textbox = customtkinter.CTkTextbox(self, state="disabled", font=("Inter", 16), wrap="word")
|
||||
self.log_textbox.grid(row=1, column=0,columnspan=4, padx=20, pady=20, sticky="nsew")
|
||||
self.log_level_colors = {
|
||||
"[MSG]": "white",
|
||||
"[INFO]": "light blue",
|
||||
"[SUCCESS]": "light green",
|
||||
"[ERROR]": "red",
|
||||
"[SKIPPED]": "orange",
|
||||
"[REPLACED]": "orange",
|
||||
"[RENAMED]": "orange",
|
||||
"[REMOVED]": "orange",
|
||||
}
|
||||
for level, color in self.log_level_colors.items():
|
||||
self.log_textbox.tag_config(level, foreground=color)
|
||||
self.linker.logger = self
|
||||
|
||||
def toggle_autoscroll(self):
|
||||
self.autoscroll_enabled = not self.autoscroll_enabled
|
||||
if self.autoscroll_enabled:
|
||||
self.toggle_autoscroll_button.configure(text="Autoscroll On")
|
||||
else:
|
||||
self.toggle_autoscroll_button.configure(text="Autoscroll Off")
|
||||
38
gui/frames/remove_chara.py
Normal file
@@ -0,0 +1,38 @@
|
||||
import customtkinter
|
||||
from gui.custom_widgets.ctk_tooltip import CTkToolTip
|
||||
from tkinter import filedialog, END
|
||||
|
||||
class RemoveCharaFrame(customtkinter.CTkFrame):
|
||||
def __init__(self, master, linker, config, **kwargs):
|
||||
super().__init__(master, **kwargs)
|
||||
self.linker = linker
|
||||
self.config = config
|
||||
self.create_widgets()
|
||||
self.bind_to_config()
|
||||
|
||||
def create_widgets(self):
|
||||
self.login_settings_label = customtkinter.CTkLabel(self, text="Remove Chara Settings", font=customtkinter.CTkFont(family="Inter", size=30, weight="bold"))
|
||||
self.login_settings_label.grid(row=0, column=0, columnspan =2, sticky="nw", padx=20, pady=20)
|
||||
|
||||
self.create_downloadpath_widgets()
|
||||
self.linker.login = self
|
||||
|
||||
def create_downloadpath_widgets(self):
|
||||
self.downloadpath = customtkinter.CTkLabel(self, text="Input Directory:", font=customtkinter.CTkFont(size=20, underline=True))
|
||||
self.downloadpath.grid(row=3, column=0, padx=40, pady=(20, 10), sticky="nw")
|
||||
|
||||
self.downloadpath_entry = customtkinter.CTkEntry(self, font=customtkinter.CTkFont(family="Inter", size=16))
|
||||
self.downloadpath_entry.grid(row=4, column=0, columnspan=2, padx=(60,0), pady=(20, 10), sticky="nsew")
|
||||
|
||||
self.downloadpath_button = customtkinter.CTkButton(self, width=50, text="Select", command = self.open_folder)
|
||||
self.downloadpath_button.grid(row=4, column=2, padx=20, pady=(20, 10), sticky="nsew")
|
||||
|
||||
def bind_to_config(self):
|
||||
self.config.bind(self.downloadpath_entry, ["RemoveChara", "InputPath"])
|
||||
|
||||
def open_folder(self):
|
||||
folderpath = filedialog.askdirectory()
|
||||
if folderpath != "":
|
||||
self.downloadpath_entry.delete(0, END)
|
||||
self.downloadpath_entry.insert(0, folderpath)
|
||||
self.config.save_to_json(["RemoveChara", "InputPath"])
|
||||
128
gui/frames/sidebar.py
Normal file
@@ -0,0 +1,128 @@
|
||||
import customtkinter
|
||||
from PIL import Image
|
||||
from gui.frames.install_chara import InstallCharaFrame
|
||||
from gui.frames.remove_chara import RemoveCharaFrame
|
||||
from gui.frames.fc_kks import FilterConvertKKSFrame
|
||||
from gui.frames.create_backup import CreateBackupFrame
|
||||
from gui.custom_widgets.ctk_tooltip import CTkToolTip
|
||||
from tkinter import filedialog, END
|
||||
|
||||
|
||||
class Sidebar(customtkinter.CTkFrame):
|
||||
def __init__(self, master, linker, config, **kwargs):
|
||||
self.master = master
|
||||
self.linker = linker
|
||||
self.config = config
|
||||
super().__init__(master=self.master, **kwargs)
|
||||
self.grid_rowconfigure((0, 1, 2), weight=1)
|
||||
self.grid_columnconfigure(0, weight=1)
|
||||
karin_logo = customtkinter.CTkImage(light_image=Image.open("gui/icons/karin.png"), size=(152,152))
|
||||
karin_logo_label = customtkinter.CTkLabel(self, image=karin_logo, text="")
|
||||
karin_logo_label.grid(row=0, column=0, sticky="nsew")
|
||||
self.gear_on = customtkinter.CTkImage(Image.open("gui/icons/gear_on.png"), size=(50,38))
|
||||
self.gear_off = customtkinter.CTkImage(Image.open("gui/icons/gear_off.png"), size=(50,38))
|
||||
self.create_module_frames()
|
||||
self.create_all_button_frame()
|
||||
self.create_gamepath_frame()
|
||||
self.create_start_button()
|
||||
self.create_notification_frames()
|
||||
self.linker.sidebar = self
|
||||
|
||||
def create_module_frames(self):
|
||||
|
||||
self.checkbox_frame = customtkinter.CTkFrame(self, fg_color="transparent", border_color="white", border_width=2)
|
||||
self.checkbox_frame.grid(row=1, column=0, columnspan=4, padx=10, pady=10, sticky="w")
|
||||
self.prettify = {
|
||||
"InstallChara": "Install Chara",
|
||||
"RemoveChara": "Remove Chara",
|
||||
"CreateBackup": "Create Backup",
|
||||
"FCKKS": "F&C KKS"
|
||||
}
|
||||
|
||||
self.module_list = [["CreateBackup", CreateBackupFrame], ["FCKKS", FilterConvertKKSFrame], ["InstallChara", InstallCharaFrame], ["RemoveChara", RemoveCharaFrame]]
|
||||
for index, sublist in enumerate(self.module_list):
|
||||
module = sublist[0]
|
||||
self.linker.modules_dictionary[module] = {}
|
||||
self.create_module_checkbox(module, index)
|
||||
self.create_module_button(module, index)
|
||||
frame = sublist[1](self.master, self.linker, self.config, fg_color="#262250")
|
||||
self.linker.modules_dictionary[module]['frame'] = frame
|
||||
self.linker.modules_dictionary["CreateBackup"]["button"].configure(image=self.gear_on)
|
||||
self.linker.modules_dictionary["CreateBackup"]["checkbox"].configure(text_color="#53B9E9")
|
||||
self.current_frame = self.linker.modules_dictionary["CreateBackup"]["frame"] # Update the current frame
|
||||
self.current_frame.grid(row=0, column=1, padx=20, pady=20, sticky="nsew")
|
||||
|
||||
def create_module_checkbox(self, module, i):
|
||||
self.linker.modules_dictionary[module]['checkbox'] = customtkinter.CTkCheckBox(
|
||||
self.checkbox_frame, text=self.prettify[module], text_color="#FFFFFF", font=("Inter", 16), command=lambda x=[module, "Enable"]: self.config.save_to_json(x))
|
||||
self.linker.modules_dictionary[module]['checkbox'].grid(row=i, column=0, columnspan=2,padx=20, pady=(10, 5), sticky="nw")
|
||||
self.linker.widgets[module]['Enable'] = self.linker.modules_dictionary[module]['checkbox']
|
||||
|
||||
def create_module_button(self, module, i):
|
||||
self.linker.modules_dictionary[module]['button'] = customtkinter.CTkButton(
|
||||
self.checkbox_frame, width=50, image=self.gear_off, text="", fg_color="transparent", command=lambda x=module: self.display_settings(module))
|
||||
self.linker.modules_dictionary[module]['button'].grid(row=i, column=1, padx=(40,0), pady=(2,0), sticky="nw")
|
||||
|
||||
def create_all_button_frame(self):
|
||||
self.select_all_button = customtkinter.CTkButton(self.checkbox_frame, width=100, text="Select All", fg_color="#DC621D", font=("Inter",20), command=self.select_all)
|
||||
self.select_all_button.grid(row=4, column=0, padx=10, pady=(15,20), sticky="w")
|
||||
self.clear_all_button = customtkinter.CTkButton(self.checkbox_frame, width=100, text="Clear All", fg_color="#DC621D", font=("Inter",20), command=self.clear_all)
|
||||
self.clear_all_button.grid(row=4, column=1, padx=10, pady=(15,20), sticky="w")
|
||||
|
||||
def create_gamepath_frame(self):
|
||||
self.gamepath_frame = customtkinter.CTkFrame(self, fg_color="transparent")
|
||||
self.gamepath_frame.grid(row=2, column=0)
|
||||
|
||||
self.gamepath_label = customtkinter.CTkLabel(self.gamepath_frame, text="Game Directory", font=customtkinter.CTkFont(size=16, family="Inter", underline=True))
|
||||
self.gamepath_label.grid(row=0, column=0, padx=(0, 10), sticky="nw")
|
||||
|
||||
self.gamepath_entry = customtkinter.CTkEntry(self.gamepath_frame, font=customtkinter.CTkFont(family="Inter", size=16))
|
||||
self.gamepath_entry.grid(row=1, column=0, columnspan=2)
|
||||
self.config.bind(self.gamepath_entry, ["Core", "GamePath"])
|
||||
self.gamepath_button = customtkinter.CTkButton(self.gamepath_frame, width=50, text="Select", command = self.open_folder)
|
||||
self.gamepath_button.grid(row=1, column=1)
|
||||
|
||||
def create_start_button(self):
|
||||
self.start_button = customtkinter.CTkButton(self, text="Start", width=200, height=40, command=self.linker.start_stop, font=customtkinter.CTkFont(family="Inter", size=16))
|
||||
self.start_button.grid(row=3, column=0, pady=20, sticky="n")
|
||||
|
||||
def create_notification_frames(self):
|
||||
for index, element in enumerate(["Template", "Queue", "Configuration"]):
|
||||
frame = customtkinter.CTkFrame(self, fg_color="transparent", height=50)
|
||||
if index == 0:
|
||||
top_pady=170
|
||||
else:
|
||||
top_pady=0
|
||||
frame.grid(row=3+index, column=0, sticky="s", pady=(top_pady,0))
|
||||
self.linker.name_to_sidebar_frame[element] = frame
|
||||
|
||||
def select_all(self):
|
||||
for module in self.linker.modules_dictionary:
|
||||
self.linker.modules_dictionary[module]["checkbox"].select()
|
||||
self.config.config_data[module]["Enable"] = True
|
||||
self.config.save_file("Configuration")
|
||||
|
||||
def clear_all(self):
|
||||
for module in self.linker.modules_dictionary:
|
||||
self.linker.modules_dictionary[module]["checkbox"].deselect()
|
||||
self.config.config_data[module]["Enable"] = False
|
||||
self.config.save_file("Configuration")
|
||||
|
||||
def display_settings(self, module):
|
||||
for key in self.linker.modules_dictionary:
|
||||
if key == module:
|
||||
self.linker.modules_dictionary[key]["button"].configure(image=self.gear_on)
|
||||
self.linker.modules_dictionary[key]["checkbox"].configure(text_color="#53B9E9")
|
||||
self.current_frame.grid_remove() # Hide the current frame
|
||||
self.current_frame = self.linker.modules_dictionary[key]["frame"] # Update the current frame
|
||||
self.current_frame.grid(row=0, column=1, padx=20, pady=20, sticky="nsew")
|
||||
else:
|
||||
self.linker.modules_dictionary[key]["button"].configure(image=self.gear_off)
|
||||
self.linker.modules_dictionary[key]["checkbox"].configure(text_color="#FFFFFF")
|
||||
|
||||
def open_folder(self):
|
||||
folderpath = filedialog.askdirectory()
|
||||
if folderpath != "":
|
||||
self.gamepath_entry.delete(0, END)
|
||||
self.gamepath_entry.insert(0, folderpath)
|
||||
self.config.save_to_json(["Core", "GamePath"])
|
||||
BIN
gui/icons/gear_off.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
gui/icons/gear_on.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
gui/icons/karin.ico
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
gui/icons/karin.png
Normal file
|
After Width: | Height: | Size: 48 KiB |
85
gui/util/config.py
Normal file
@@ -0,0 +1,85 @@
|
||||
import sys
|
||||
import json
|
||||
import customtkinter
|
||||
|
||||
class Config:
|
||||
def __init__(self, linker, config_file):
|
||||
self.linker = linker
|
||||
self.config_file = config_file
|
||||
self.config_data = self.read()
|
||||
self.linker.widgets = self.set_values_to_none(self.config_data)
|
||||
linker.config = self
|
||||
|
||||
def read(self):
|
||||
# Read the JSON file
|
||||
try:
|
||||
with open(self.config_file, 'r') as json_file:
|
||||
config_data = json.load(json_file)
|
||||
return config_data
|
||||
except FileNotFoundError:
|
||||
print(f"Config file '{self.config_file}' not found.")
|
||||
sys.exit(1)
|
||||
except json.JSONDecodeError:
|
||||
print(f"Invalid JSON format in '{self.config_file}'.")
|
||||
sys.exit(1)
|
||||
|
||||
def set_values_to_none(self, input_dict):
|
||||
result = {}
|
||||
for key, value in input_dict.items():
|
||||
if isinstance(value, dict):
|
||||
result[key] = self.set_values_to_none(value)
|
||||
else:
|
||||
result[key] = None
|
||||
return result
|
||||
|
||||
def load_config(self, widgets=None, config_data=None):
|
||||
if widgets == None:
|
||||
widgets = self.linker.widgets
|
||||
config_data = self.config_data
|
||||
for key in widgets:
|
||||
if isinstance(widgets[key], dict) and isinstance(config_data[key], dict):
|
||||
self.load_config(widgets[key], config_data[key])
|
||||
else:
|
||||
if widgets[key] is not None:
|
||||
if isinstance(widgets[key], customtkinter.CTkCheckBox):
|
||||
if config_data[key] == True:
|
||||
widgets[key].select()
|
||||
else:
|
||||
widgets[key].deselect()
|
||||
elif isinstance(widgets[key], customtkinter.CTkEntry):
|
||||
widgets[key].insert(0, config_data[key])
|
||||
else:
|
||||
widgets[key].set(config_data[key])
|
||||
|
||||
def bind(self, widget, list_keys):
|
||||
|
||||
if isinstance(widget, customtkinter.CTkEntry):
|
||||
widget.bind("<KeyRelease>", lambda event, x=list_keys: self.save_to_json(x))
|
||||
elif isinstance(widget, (customtkinter.CTkCheckBox)):
|
||||
widget.configure(command=lambda x=list_keys: self.save_to_json(x))
|
||||
else:
|
||||
widget.configure(command=lambda x, y=list_keys: self.save_to_json(y))
|
||||
|
||||
widgets_dictionary = self.linker.widgets
|
||||
for key in list_keys[:-1]:
|
||||
widgets_dictionary = widgets_dictionary[key]
|
||||
widgets_dictionary[list_keys[-1]] = widget
|
||||
|
||||
def save_to_json(self, list_keys):
|
||||
widget = self.linker.widgets
|
||||
data = self.config_data
|
||||
for i in list_keys[:-1]:
|
||||
widget = widget[i]
|
||||
data = data[i]
|
||||
widget = widget[list_keys[-1]]
|
||||
value = widget.get()
|
||||
if isinstance(widget, customtkinter.CTkCheckBox):
|
||||
value = True if value==1 else False
|
||||
data[list_keys[-1]] = value
|
||||
self.save_file("Configuration")
|
||||
|
||||
def save_file(self, name=None):
|
||||
with open("config.json", "w") as config_file:
|
||||
json.dump(self.config_data, config_file, indent=2)
|
||||
if name:
|
||||
self.linker.show_notification(name)
|
||||
67
gui/util/linker.py
Normal file
@@ -0,0 +1,67 @@
|
||||
import subprocess
|
||||
import threading
|
||||
from gui.custom_widgets.ctk_notification import CTkNotification
|
||||
|
||||
class Linker:
|
||||
def __init__(self, master):
|
||||
self.config = None
|
||||
self.widgets = {}
|
||||
self.logger = None
|
||||
self.login = None
|
||||
# script.py process
|
||||
self.script = None
|
||||
self.master = master
|
||||
self.modules_dictionary = {}
|
||||
self.name_to_sidebar_frame = {}
|
||||
|
||||
def terminate_script(self):
|
||||
# If process is running, terminate it
|
||||
self.script.terminate()
|
||||
self.script = None
|
||||
self.sidebar.start_button.configure(text="Start", fg_color = ['#3B8ED0', '#1F6AA5'])
|
||||
|
||||
def start_stop(self):
|
||||
if hasattr(self, 'script') and self.script is not None:
|
||||
self.terminate_script()
|
||||
else:
|
||||
# If process is not running, start it
|
||||
self.script = subprocess.Popen(['python', 'script.py'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
threading.Thread(target=self.read_output).start()
|
||||
self.sidebar.start_button.configure(text="Stop", fg_color = "crimson")
|
||||
|
||||
def read_output(self):
|
||||
while self.script is not None:
|
||||
line = self.script.stdout.readline().decode('utf-8')
|
||||
if line == "":
|
||||
if hasattr(self, 'script') and self.script is not None:
|
||||
self.master.after(10, self.terminate_script)
|
||||
return
|
||||
|
||||
# Check if line contains any log level
|
||||
for level, color in self.logger.log_level_colors.items():
|
||||
if level in line:
|
||||
# Display output in text box with color
|
||||
self.logger.log_textbox.configure(state="normal")
|
||||
if level == "[MSG]":
|
||||
self.logger.log_textbox.insert("end", "-" * 87 + "\n", level)
|
||||
line = line.replace("[MSG]", "")
|
||||
self.logger.log_textbox.insert("end", line, level)
|
||||
elif level == "[INFO]" and "Start Task:" in line:
|
||||
self.logger.log_textbox.insert("end", "*" * 87 + "\n", level)
|
||||
self.logger.log_textbox.insert("end", line, level)
|
||||
else:
|
||||
self.logger.log_textbox.insert("end", line, level)
|
||||
self.logger.log_textbox.configure(state="disabled")
|
||||
break
|
||||
|
||||
if self.logger.autoscroll_enabled:
|
||||
self.logger.log_textbox.yview_moveto(1.0)
|
||||
|
||||
def show_notification(self, name):
|
||||
sidebar_frame = self.name_to_sidebar_frame[name]
|
||||
if self.script:
|
||||
new_notification = CTkNotification(text= f"{name} was saved but will be read by the script in the next run.", master=sidebar_frame, fg_color="orange")
|
||||
else:
|
||||
new_notification = CTkNotification(text= f"{name} was saved successfully.", master=sidebar_frame, fg_color="green")
|
||||
new_notification.grid(row=0, column=0, sticky="nsew")
|
||||
self.sidebar.master.after(2500, new_notification.destroy)
|
||||