From 51f65b577109746fbac5ab2698cc3ceaf7aa6d18 Mon Sep 17 00:00:00 2001 From: bit-dome Date: Thu, 3 Aug 2023 18:02:19 +0700 Subject: [PATCH] feat: add profile dropdown --- assets/images/add_drop.png | Bin 0 -> 336 bytes assets/images/add_prof.png | Bin 0 -> 996 bytes assets/images/edit.png | Bin 397 -> 465 bytes assets/images/prof_drop_head.png | Bin 0 -> 1127 bytes assets/images/proj_icon.png | Bin 0 -> 491 bytes assets/images/proj_icon_blank.png | Bin 0 -> 167 bytes assets/images/rename.png | Bin 0 -> 283 bytes assets/themes/google_theme.json | 2 +- configs/default.json | 2 +- configs/profile_1/cursor.json | 10 +- configs/profile_2/cursor.json | 10 +- requirements.txt | 15 +- run_app.py | 3 +- src/camera_manager.py | 8 + src/config_manager.py | 19 +- src/controllers/keybinder.py | 7 +- src/controllers/mouse_controller.py | 6 +- src/gui/frames/__init__.py | 3 +- src/gui/frames/frame_menu.py | 45 +- src/gui/frames/frame_profile_editor.py | 517 +++++++++++++++++++++++ src/gui/frames/frame_profile_switcher.py | 463 ++++++++++++++++++++ src/gui/main_gui.py | 66 ++- src/gui/pages/page_home.py | 18 +- src/gui/pages/page_keyboard.py | 17 +- src/task_killer.py | 2 +- 25 files changed, 1118 insertions(+), 95 deletions(-) create mode 100644 assets/images/add_drop.png create mode 100644 assets/images/add_prof.png create mode 100644 assets/images/prof_drop_head.png create mode 100644 assets/images/proj_icon.png create mode 100644 assets/images/proj_icon_blank.png create mode 100644 assets/images/rename.png create mode 100644 src/gui/frames/frame_profile_editor.py create mode 100644 src/gui/frames/frame_profile_switcher.py diff --git a/assets/images/add_drop.png b/assets/images/add_drop.png new file mode 100644 index 0000000000000000000000000000000000000000..00341ed5e2990315ccf27abad6a01bcbea987204 GIT binary patch literal 336 zcmeAS@N?(olHy`uVBq!ia0vp^DnKm3!3-pqrme35QjEnx?oNz1PwLbIIh+L^k;M!Q z+`=Ht$S`Y;1W?c~z$e5NNISc`y12W!xw`>L5Zlew#lyoD2;AL)2q@^{29$Df29j=W zAQ3k=HxD;gFQ5X56i}BNP`Qh{Yv{km3ZRKnB|(0{3=`h>|DXTf!G3;ze}6&1`upeO z4er;kzi+UAQ~By}psG?&7srr_IZyk2g_{g`T&8bWD8E8uo7lhq@>|ax;F&(fdS5V~ z{!QKw_9aSxS2~Bzi(J&eS6c98n@B@|s<`A_)n1-P^9K7f!HYdgAH44=yXUv~a&pzA z^_h7&`-NSL|A{O+Iwk0)QjMffXZF2_x92MsTzpkk!&vFVyvFm6S^&@i44$rjF6*2U FngC^`ba?;( literal 0 HcmV?d00001 diff --git a/assets/images/add_prof.png b/assets/images/add_prof.png new file mode 100644 index 0000000000000000000000000000000000000000..619ac65050e42640fedf01fd279f6eb61b9edce9 GIT binary patch literal 996 zcmVciD%}$`FQU?gOHNqZeK$i_5i8Ek0rO13?N}r4%^n( z)l*mi2nkup#qK>p6jZ`_1h!A)C& z3{ac_KZFz;iAZkh1)%DH8K@My9FQZDuYlF=?h77^8c9~Mdsb*s3 zB{b$<->($6dz*;4bsn11_-w$Pm^@Mj5gb^}wJt1x#14$Hn$;G6wcwFSm5Ki)p1cFf zo4D|?S9pGtukadQ_T@}L9uUH9e{7{h~VCfW$&jDB&Qf6qVb z(FrOMItkGNy6w*MpdabLy&RAS5H0Z;Sgq66tt}3(nyWJlJf8#3T@S(j97bCeov&WD zW|@({4lFm$4_GnoDOKXE$Y%nRwUJb*L-cBYH!qZenRF_gHwNFzpCAQ}198T?X`F8! zY;41`Tzl~4?GI2uN{kPwWzaTp`hw>0|t<=yG5QN)R2zoCvo z>`#&*X>Kkss5zX5`^2D00002w4hMFUXv&mui**9cNq;54WFuHP?jTQPF|-Cv)%bfJt|>V$Z;v+WjdI)K_IdAc};RLpsLe!I{i z0|D2I>8vV=8&1S6ay790|NnSmV0Kz|+w~O}G`}&`Z}#z68qIU*n3b}`5}5?m<|V9i zk|r3OlRCS=M_bcq!CYg3xp@z3FTP%0&wlvg4gY|koFzt?%Zz8~Pty)jG;7o9yf*u- z-oCGPW^GoTFA8$X4$Vwbj=l84x_Iwh`Lhqti;K_LZ~0OAH^XKnFce*tq+ zM?wIu&K&6g000DMK}|sb0I`n?{9y$E00AONL_t(|0qv5(je~EKx}5Blf4p_`1%mbQAUsvV;K}sEz7Kmy(E?9Crp37LykpPR zeNPEwEknkvDSxvUVkeT4gvzZn7G!YQ`5#;+Rwc3`&MaTu-{>2Z;6z`D?2~}Hy6zjl zfeJq%^v3_=Sl+JlDG^=Aaa`TGhXmK~Ni>>;KjBnP|ABysh0KehDAAp*R;}nZ9Q-LpcLp6~oMj0@?Qf0000JZH`v zpy97yziw@9O;1nf<>hT_Ys<*UC@(Ju>e{}2`_!pZfsCU^j{=ne89=V)29>iwXBn0R z`2{m5FS zZ*QK>yX7F#_R#RgaZO&<So!=uBRhDe7Fttmb zV?BYltupbzY^_t>j?Z@Om0miJ@A>MQ`xBp}&a2Xyv;9x%w8#7pU!UBvYn44y{;GMQ z*Kh0C_x4XaX{2&fKt*V#d3QmC+?r{T6(#p4s0hu{?_VX;uD1G$^TRu}-z2BK6}EXZ zdG!@x#?>3b-^H^Q`Z+mUXy9c`TZD*-?9Vre-ov+p@B=~pr zrtrxwoio0Le#-_*U)^?Up0AMVBd*YIIY92s{(C@PQuKF_QFnKxPj=z7b>Xz615!M7 zXpNsO`-N3!xu(yKT`I?tarR8zu7#0Ps{|H=&WzpBc+&(F8`EzdNKF%-=G8IdceUTM zJ15q7+H!5U+%omjPWC>p6*KhD$kk4H`0uavR|AI)Ca0Yzzq_=f!MSOF)q6g#=kxv? z4}DeHFqQvf=Jh4g)8|a}(4RB;XNCOI(|@8ePglNLSa9yu>8a!7f1GmPyf(kiI(A!e&&FS?X0@rj&3w~m`-Dwp%KyUkwo_IfKlD?|woGMbtnPgM nQ%YY;EX-Z)YW7*b`JlgJy-wq#yWOh5jK|>V>gTe~DWM4frzHv@ literal 0 HcmV?d00001 diff --git a/assets/images/proj_icon.png b/assets/images/proj_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..ec1f973e558f33989bd12c7522fe71c0dadf3ba9 GIT binary patch literal 491 zcmeAS@N?(olHy`uVBq!ia0vp^4nS87*xm==?GmEuM>pJD*x#xL# zD#U%hbH5JgC>c)|$B>FSZ^JI~H5mvnTc&iZ==kw}{;EwZ+wL7&Do|_ir7*Jp*}A(E zQ?38;{A@X_Ri`L?>M>W{(`QVR)@uZ?*Jx_THMm&h3V(2O{&fDp^l3T|HZ+Kc><_3Z zyrQAMi$Pt{U*mz=&aLc`i&=V(U!B9hT2)tM$0CE%cOM+7vgv6{{`y4iRoN8%%Taf2 zcf9#}C`YIK`>N&(Vwdk0AOE}EApX+^ts@c>&fA6DioWB#bj{a^8$um@MK1`gsd*@J z;cnPIru)B+v(!GA_Oo*@gP#eD%l5x<9d~j|4Q3v*voW%oagWP7~&_J8M@IAcD!t9)Z>KN6=P*fS~SZ7^%a#IiqJ`V$@r U2+s=>-4BWoPgg&ebxsLQ0M^*Ax&QzG literal 0 HcmV?d00001 diff --git a/assets/images/proj_icon_blank.png b/assets/images/proj_icon_blank.png new file mode 100644 index 0000000000000000000000000000000000000000..6201c40f8934aa27e09550ab6ba3b57f61541e2f GIT binary patch literal 167 zcmeAS@N?(olHy`uVBq!ia0vp^4nSG7TVo1B!TYeg3n5V0s%Q~loCIIW9B$faG literal 0 HcmV?d00001 diff --git a/assets/images/rename.png b/assets/images/rename.png new file mode 100644 index 0000000000000000000000000000000000000000..16eb0705357e72c68fa28caf3e9dff5dc5a8aba2 GIT binary patch literal 283 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM3?#3wJbMaAF%}28J2BoosZ$T+a29w(7Bet# z3xhBt!>lhR%Ty6KfQbI;BA^w)=}#(c8H`I;4&C%*;KR`NGtcN#}s( OGkCiCxvX None: frame.flags.writeable = False h, w, _ = frame.shape + + # Trim image if h != ConfigManager().config["fix_height"] or w != ConfigManager( ).config["fix_width"]: + target_width = int(h * 4 / 3) + if w > target_width: + trim_width = w - target_width + trim_left = trim_width // 2 + trim_right = trim_width - trim_left + frame = frame[:, trim_left:-trim_right, :] frame = cv2.resize(frame, (ConfigManager().config["fix_width"], ConfigManager().config["fix_height"])) diff --git a/src/config_manager.py b/src/config_manager.py index ad7a8bbc..cbfd5a84 100644 --- a/src/config_manager.py +++ b/src/config_manager.py @@ -15,15 +15,15 @@ import copy import json import logging -import tkinter as tk -import time import shutil +import time +import tkinter as tk from pathlib import Path from src.singleton_meta import Singleton from src.task_killer import TaskKiller -VERSION = "0.3.30" +VERSION = "0.3.33" DEFAULT_JSON = Path("configs/default.json") BACKUP_PROFILE = Path("configs/default") @@ -46,6 +46,8 @@ def __init__(self): self.curr_profile_name = tk.StringVar() self.is_started = False + self.profiles = self.list_profile() + def start(self): if not self.is_started: logger.info("Start ConfigManager singleton") @@ -75,6 +77,8 @@ def list_profile(self) -> list: def remove_profile(self, profile_name): logger.info(f"Remove profile {profile_name}") shutil.rmtree(Path(DEFAULT_JSON.parent, profile_name)) + self.profiles.remove(profile_name) + logger.info(f"Current profiles: {self.profiles}") def add_profile(self): # Random name base on local timestamp @@ -82,11 +86,20 @@ def add_profile(self): logger.info(f"Add profile {new_profile_name}") shutil.copytree(BACKUP_PROFILE, Path(DEFAULT_JSON.parent, new_profile_name)) + self.profiles.append(new_profile_name) + logger.info(f"Current profiles: {self.profiles}") def rename_profile(self, old_profile_name, new_profile_name): logger.info(f"Rename profile {old_profile_name} to {new_profile_name}") shutil.move(Path(DEFAULT_JSON.parent, old_profile_name), Path(DEFAULT_JSON.parent, new_profile_name)) + self.profiles.remove(old_profile_name) + self.profiles.append(new_profile_name) + + if self.curr_profile_name.get() == old_profile_name: + self.curr_profile_name.set(new_profile_name) + + def load_profile(self, profile_name: str) -> list[bool, Path]: profile_path = Path(DEFAULT_JSON.parent, profile_name) diff --git a/src/controllers/keybinder.py b/src/controllers/keybinder.py index 2561a91c..e3a6c38d 100644 --- a/src/controllers/keybinder.py +++ b/src/controllers/keybinder.py @@ -13,9 +13,9 @@ # limitations under the License. import copy +import logging import math import time -import logging import pydirectinput import win32api @@ -195,8 +195,9 @@ def act(self, blendshape_values) -> dict: if mon_id is None: return - pydirectinput.moveTo(self.monitors[mon_id]["center_x"], - self.monitors[mon_id]["center_y"]) + pydirectinput.moveTo( + self.monitors[mon_id]["center_x"], + self.monitors[mon_id]["center_y"]) self.key_states[state_name] = True elif (val < thres) and (self.key_states[state_name] is True): diff --git a/src/controllers/mouse_controller.py b/src/controllers/mouse_controller.py index 66ad850f..25bafd15 100644 --- a/src/controllers/mouse_controller.py +++ b/src/controllers/mouse_controller.py @@ -15,11 +15,11 @@ import logging import threading import time -import pyautogui import tkinter as tk import numpy as np import numpy.typing as npt +import pyautogui import src.utils as utils from src.accel_graph import SigmoidAccel @@ -100,7 +100,7 @@ def main_loop(self) -> None: if self.is_destroyed: return - + while not self.stop_flag.is_set(): if not self.is_active.get(): time.sleep(0.001) @@ -132,7 +132,7 @@ def main_loop(self) -> None: vel_y *= self.accel(vel_y) # pydirectinput is not working here - pyautogui.move(xOffset=vel_x, yOffset=vel_y) + pyautogui.move(xOffset=vel_x, yOffset=vel_y) time.sleep(ConfigManager().config["tick_interval_ms"] / 1000) diff --git a/src/gui/frames/__init__.py b/src/gui/frames/__init__.py index 6c88e7b7..3fc7740c 100644 --- a/src/gui/frames/__init__.py +++ b/src/gui/frames/__init__.py @@ -14,5 +14,6 @@ from .frame_cam_preview import * from .frame_menu import * -from .frame_profile import * +from .frame_profile_editor import * +from .frame_profile_switcher import * from .safe_disposable_frame import * diff --git a/src/gui/frames/frame_menu.py b/src/gui/frames/frame_menu.py index ded819ab..404d3e49 100644 --- a/src/gui/frames/frame_menu.py +++ b/src/gui/frames/frame_menu.py @@ -17,10 +17,12 @@ import customtkinter from PIL import Image +from src.config_manager import ConfigManager from src.gui.frames.safe_disposable_frame import SafeDisposableFrame LIGHT_BLUE = "#F9FBFE" BTN_SIZE = 225, 48 +PROF_DROP_SIZE = 220, 40 class FrameMenu(SafeDisposableFrame): @@ -28,7 +30,7 @@ class FrameMenu(SafeDisposableFrame): def __init__(self, master, master_callback: callable, **kwargs): super().__init__(master, **kwargs) - self.grid_rowconfigure(11, weight=1) + self.grid_rowconfigure(6, weight=1) self.grid_columnconfigure(0, weight=1) self.grid_propagate(False) self.configure(fg_color=LIGHT_BLUE) @@ -38,8 +40,7 @@ def __init__(self, master, master_callback: callable, **kwargs): self.menu_btn_images = { "page_home": [ customtkinter.CTkImage( - Image.open("assets/images/menu_btn_home.png").resize( - BTN_SIZE, resample=Image.ANTIALIAS), + Image.open("assets/images/menu_btn_home.png"), size=BTN_SIZE), customtkinter.CTkImage( Image.open("assets/images/menu_btn_home_selected.png"), @@ -79,10 +80,35 @@ def __init__(self, master, master_callback: callable, **kwargs): ] } + # Profile button + prof_drop = customtkinter.CTkImage( + Image.open("assets/images/prof_drop_head.png"), size=PROF_DROP_SIZE) + profile_btn = customtkinter.CTkLabel( + master=self, + textvariable=ConfigManager().curr_profile_name, + image=prof_drop, + height=42, + compound="center", + anchor="w", + cursor="hand2", + ) + profile_btn.bind("", + partial(self.master_callback, "show_profile_switcher")) + + profile_btn.grid(row=0, + column=0, + padx=35, + pady=10, + ipadx=0, + ipady=0, + sticky="nw", + columnspan=1, + rowspan=1) + self.btns = {} - self.btns = self.create_tab_btn(self.menu_btn_images) + self.btns = self.create_tab_btn(self.menu_btn_images, offset=1) - def create_tab_btn(self, btns: dict): + def create_tab_btn(self, btns: dict, offset): out_dict = {} for idx, (k, im_paths) in enumerate(btns.items()): @@ -94,11 +120,12 @@ def create_tab_btn(self, btns: dict): hover=False, corner_radius=0, text="", - command=partial(self.master_callback, - command="change_page", - args={"target": k})) + command=partial( + self.master_callback, + function_name="change_page", + args={"target": k})) - btn.grid(row=idx, + btn.grid(row=idx + offset, column=0, padx=(0, 0), pady=0, diff --git a/src/gui/frames/frame_profile_editor.py b/src/gui/frames/frame_profile_editor.py new file mode 100644 index 00000000..2ab26b67 --- /dev/null +++ b/src/gui/frames/frame_profile_editor.py @@ -0,0 +1,517 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging +import re +import time +import tkinter as tk +from functools import partial + +import customtkinter +from PIL import Image + +from src.config_manager import ConfigManager +from src.gui.frames.safe_disposable_frame import SafeDisposableScrollableFrame +from src.task_killer import TaskKiller + +logger = logging.getLogger("FrameProfileEditor") + +CLOSE_ICON_SIZE = 24, 24 +POPUP_SIZE = 350, 600 +POPUP_OFFSET = 337, 30 +MAX_PROF_ROWS = 11 +EDIT_ICON_SIZE = (24, 24) +BIN_ICON_SIZE = (24, 24) + +LIGHT_GREEN = "#a6eacf" +LIGHT_BLUE = "#e8f0fe" +MEDIUM_BLUE = "#D0E1F9" +DARK_BLUE = "#1A73E8" +BACKUP_PROFILE_NAME = "default" + +DIV_COLORS = {"default": "white"} + + +def random_name(row): + return str(row) + str(hex(int(time.time() * 1000)))[2:] + + +class ItemProfileEditor(SafeDisposableScrollableFrame): + + def __init__( + self, + owner_frame, + top_level, + main_gui_callback, + **kwargs, + ): + + super().__init__(top_level, **kwargs) + self.main_gui_callback = main_gui_callback + self.is_active = False + + self.grid_rowconfigure(MAX_PROF_ROWS, weight=1) + self.grid_columnconfigure(0, weight=1) + + self.edit_image = customtkinter.CTkImage( + Image.open("assets/images/rename.png").resize(EDIT_ICON_SIZE), + size=EDIT_ICON_SIZE) + + self.bin_image = customtkinter.CTkImage( + Image.open("assets/images/bin.png").resize(BIN_ICON_SIZE), + size=BIN_ICON_SIZE) + + self.divs = self.load_initial_profiles() + + div_id = self.get_div_id(ConfigManager().curr_profile_name.get()) + + def load_initial_profiles(self): + """Create div according to profiles in config + """ + profile_names = ConfigManager().list_profile() + + divs = {} + for row, profile_name in enumerate(profile_names): + div_id = random_name(row) + div = self.create_div(row, div_id, profile_name) + div["wrap_label"].grid() + + # Random unique div id + divs[div_id] = div + + return divs + + def get_div_id(self, profile_name: str): + """Get div unique id from profile name + """ + for div_id, div in self.divs.items(): + if div["profile_name"] == profile_name: + return div_id + logger.critical(f"{profile_name} not found") + TaskKiller().exit() + + def remove_div(self, div_name): + logger.info(f"Remove {div_name}") + div = self.divs[div_name] + + for widget in div.values(): + if isinstance( + widget, customtkinter.windows.widgets.core_widget_classes. + CTkBaseClass): + widget.grid_forget() + widget.destroy() + self.refresh_scrollbar() + + def clear_divs(self): + for div_id, div in self.divs.items(): + self.remove_div(div_id) + self.divs = {} + + def refresh_frame(self): + """Refresh the divs if profile directory has changed + """ + + logger.info("Refresh frame_profile") + + # Check if folders same as divs + name_list = [div["profile_name"] for _, div in self.divs.items()] + + + + if set(ConfigManager().list_profile()) == set(name_list): + return + logger.info("Profile directory changed, reload...") + + # Delete all divs and re-create + self.clear_divs() + self.divs = self.load_initial_profiles() + current_profile = ConfigManager().curr_profile_name.get() + + # Check if selected profile exist + new_name_list = [div["profile_name"] for _, div in self.divs.items()] + if current_profile not in new_name_list: + logger.critical( + f"Profile {current_profile} not found in {new_name_list}") + TaskKiller().exit() + + + self.refresh_scrollbar() + logger.info(f"Current selected profile {current_profile}") + + def rename_button_callback(self, div: dict): + # Hide all rename buttons + for _, hdiv in self.divs.items(): + if hdiv["edit_button"] is None: + continue + hdiv["edit_button"].grid_remove() + + div["is_editing"] = True + div["entry"].configure(state="normal", + border_width=2, + border_color=LIGHT_GREEN, + fg_color="white") + div["entry"].focus_set() + div["entry"].icursor("end") + + + def check_profile_name_valid(self, div, var, index, mode): + pattern = re.compile(r'^[a-zA-Z0-9_-]+$') + is_valid_input = bool(pattern.match(div["entry_var"].get())) + + # Change border color + if is_valid_input: + div["entry"].configure(border_width=2, border_color=LIGHT_GREEN) + else: + div["entry"].configure(border_width=2, border_color="#ee9e9d") + + return is_valid_input + + def finish_rename(self, div, event): + + if not self.check_profile_name_valid(div, None, None, None): + logger.warning("Invalid profile name") + return + + div["is_editing"] = False + + div["entry"].configure(state="disabled", border_width=0) + + ConfigManager().rename_profile(div["profile_name"], + div["entry_var"].get()) + + div["profile_name"] = div["entry_var"].get() + + # Show all rename buttons + for div_id, div in self.divs.items(): + if div["edit_button"] is None: + continue + div["edit_button"].grid() + + def remove_button_callback(self, div): + ConfigManager().remove_profile(div["profile_name"]) + + # If user remove an active profile, roll back to default + if div["profile_name"] == ConfigManager().curr_profile_name.get(): + logger.warning(f"Removing active profile, rollback to default") + + ConfigManager().switch_profile(BACKUP_PROFILE_NAME) + # Refresh values in each page + self.main_gui_callback("refresh_profiles") + + self.refresh_frame() + + def create_div(self, row: int, div_id: str, profile_name) -> dict: + # Box wrapper + wrap_label = customtkinter.CTkLabel(self, + text="", + height=54, + fg_color="white", + corner_radius=10) + wrap_label.grid(row=row, column=0, padx=10, pady=4, sticky="new") + + # Edit button + if profile_name != BACKUP_PROFILE_NAME: + edit_button = customtkinter.CTkButton(self, + text="", + width=20, + border_width=0, + corner_radius=0, + image=self.edit_image, + hover=False, + compound="right", + fg_color="transparent", + anchor="e", + command=None) + + edit_button.grid(row=row, + column=0, + padx=(0, 55), + pady=(20, 0), + sticky="ne", + columnspan=10, + rowspan=10) + else: + edit_button = None + + # Bin button + if profile_name != BACKUP_PROFILE_NAME: + bin_button = customtkinter.CTkButton(self, + text="", + width=20, + border_width=0, + corner_radius=0, + image=self.bin_image, + hover=False, + compound="right", + fg_color="transparent", + anchor="e", + command=None) + + bin_button.grid(row=row, + column=0, + padx=(0, 20), + pady=(20, 0), + sticky="ne", + columnspan=10, + rowspan=10) + else: + bin_button = None + + # Entry + entry_var = tk.StringVar() + entry_var.set(profile_name) + entry = customtkinter.CTkEntry(self, + textvariable=entry_var, + placeholder_text="", + width=170, + height=30, + corner_radius=0, + state="disabled", + border_width=0, + insertborderwidth=0, + fg_color="white") + entry.cget("font").configure(size=16) + entry.grid(row=row, + column=0, + padx=20, + pady=20, + ipadx=10, + ipady=0, + sticky="nw") + + sep = tk.ttk.Separator(wrap_label, orient='horizontal') + sep.grid(row=row, column=0, padx=0, pady=0, sticky="sew") + + div = { + "div_id": div_id, + "profile_name": profile_name, + "wrap_label": wrap_label, + "entry": entry, + "entry_var": entry_var, + "edit_button": edit_button, + "bin_button": bin_button, + "is_editing": False + } + + # Bin button : remove div function + if bin_button is not None: + bin_button.configure( + command=partial(self.remove_button_callback, div)) + + # Edit button : rename profile function + if edit_button is not None: + edit_button.configure( + command=partial(self.rename_button_callback, div)) + entry_var_trace_id = entry_var.trace( + "w", partial(self.check_profile_name_valid, div)) + entry.bind('', command=partial(self.finish_rename, div)) + + return div + + def enter(self): + super().enter() + self.refresh_frame() + self.refresh_scrollbar() + + def leave(self): + super().leave() + + +class FrameProfileEditor(): + + def __init__(self, root_window, main_gui_callback: callable, **kwargs): + + self.root_window = root_window + self.main_gui_callback = main_gui_callback + self.float_window = customtkinter.CTkToplevel(root_window) + self.float_window.wm_overrideredirect(True) + self.float_window.lift() + self.float_window.wm_attributes("-disabled", True) + self.float_window.wm_attributes('-toolwindow', 'True') + self.float_window.grid_rowconfigure(3, weight=1) + self.float_window.grid_columnconfigure(0, weight=1) + self.float_window.configure(fg_color="white") + #self.float_window.attributes('-topmost', True) + self.float_window.geometry( + f"{POPUP_SIZE[0]}x{POPUP_SIZE[1]}+{POPUP_OFFSET[0]}+{POPUP_OFFSET[1]}" + ) + + # Gray overlay + self.shadow_window = customtkinter.CTkToplevel(root_window) + self.shadow_window.configure(fg_color="black") + self.shadow_window.wm_attributes("-alpha", 0.7) + self.shadow_window.wm_overrideredirect(True) + self.shadow_window.lift() + self.shadow_window.wm_attributes('-toolwindow', 'True') + #self.shadow_window.attributes('-topmost', True) + self.shadow_window.geometry( + f"{self.root_window.winfo_width()}x{self.root_window.winfo_height()}" + ) + + # Label + top_label = customtkinter.CTkLabel(master=self.float_window, + text="User profiles") + top_label.cget("font").configure(size=24) + top_label.grid(row=0, + column=0, + padx=20, + pady=20, + sticky="nw", + columnspan=1) + + # Description label + des_label = customtkinter.CTkLabel( + master=self.float_window, + text= + "With profile manager you can create and manage multiple profiles for each usage, so that you can easily switch between them.", + wraplength=300, + justify=tk.LEFT) + des_label.cget("font").configure(size=14) + des_label.grid(row=1, column=0, padx=20, pady=10, sticky="nw") + + # Close button + self.close_icon = customtkinter.CTkImage( + Image.open("assets/images/close.png").resize(CLOSE_ICON_SIZE), + size=CLOSE_ICON_SIZE) + + close_btn = customtkinter.CTkButton(master=self.float_window, + text="", + image=self.close_icon, + fg_color="white", + hover_color="white", + border_width=0, + corner_radius=4, + width=24, + command=self.hide_window) + close_btn.grid(row=0, + column=0, + padx=10, + pady=10, + sticky="ne", + columnspan=1, + rowspan=1) + + # Add butotn + add_prof_image = customtkinter.CTkImage( + Image.open("assets/images/add_prof.png"), size=(16, 12)) + add_button = customtkinter.CTkButton(master=self.float_window, + text="Add profile", + image=add_prof_image, + fg_color="white", + width=100, + text_color=DARK_BLUE, + command=self.add_button_callback) + add_button.grid(row=2, column=0, padx=15, pady=5, sticky="nw") + + # Inner scrollable frame + self.inner_frame = ItemProfileEditor( + owner_frame=self, + top_level=self.float_window, + main_gui_callback=main_gui_callback) + self.inner_frame.grid(row=3, column=0, padx=5, pady=5, sticky="nswe") + + self._displayed = True + self.hide_window() + + self.prev_event = None + + def add_button_callback(self): + ConfigManager().add_profile() + self.inner_frame.refresh_frame() + + def lift_window(self, event): + """Lift windows when root window get focus + """ + self.shadow_window.lift() + self.float_window.lift() + + def follow_window(self, event): + """Move profile window when root window is moved + """ + if self.prev_event is None: + self.prev_event = event + + # Clicked but not moved + if (self.prev_event.x == event.x) and (self.prev_event.y == event.y): + self.lift_window(None) + self.prev_event = event + return + + shift_x = self.root_window.winfo_rootx() + shift_y = self.root_window.winfo_rooty() + self.float_window.geometry( + f"+{POPUP_OFFSET[0]+shift_x}+{POPUP_OFFSET[1]+shift_y}") + self.shadow_window.geometry(f"+{shift_x}+{shift_y}") + self.prev_event = event + + def change_profile(self, target): + ConfigManager().switch_profile(target) + self.pages["page_camera"].refresh_profile() + self.pages["page_cursor"].refresh_profile() + self.pages["page_gestures"].refresh_profile() + self.pages["page_keyboard"].refresh_profile() + + def show_window(self): + # Close the opening dropdown first + if self._displayed: + self.hide_window() + + if not self._displayed: + logger.info("show") + # Make new windows stick with root window + self.root_window.bind("", self.follow_window) + self.root_window.bind("", self.lift_window) + + self.inner_frame.enter() + + shift_x = self.root_window.winfo_rootx() + shift_y = self.root_window.winfo_rooty() + + # Gray overlay + self.shadow_window.geometry(f"+{shift_x}+{shift_y}") + self.shadow_window.deiconify() + self.shadow_window.lift() + self.shadow_window.wm_attributes('-disabled', True) + + # Popup + self.float_window.geometry( + f"+{POPUP_OFFSET[0]+shift_x}+{POPUP_OFFSET[1]+shift_y}") + self.float_window.deiconify() + self.float_window.lift() + self.float_window.wm_attributes('-disabled', False) + self._displayed = True + + def hide_window(self, event=None): + + if self._displayed: + + self.root_window.unbind_all("") + self.root_window.unbind_all("") + + logger.info("hide") + self.float_window.wm_attributes('-disabled', True) + self._displayed = False + + self.float_window.withdraw() + self.shadow_window.withdraw() + + def enter(self): + self.inner_frame.enter() + self.show_window() + logger.info("enter") + + def leave(self): + super().leave() + self.inner_frame.leave() + logger.info("leave") diff --git a/src/gui/frames/frame_profile_switcher.py b/src/gui/frames/frame_profile_switcher.py new file mode 100644 index 00000000..7fa7fbe8 --- /dev/null +++ b/src/gui/frames/frame_profile_switcher.py @@ -0,0 +1,463 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging +import re +import time +import tkinter as tk +from functools import partial + +import customtkinter +from PIL import Image + +from src.config_manager import ConfigManager +from src.gui.frames.safe_disposable_frame import SafeDisposableFrame +from src.task_killer import TaskKiller + +logger = logging.getLogger("FrameProfileSwitcher") + +PROFILE_ITEM_SIZE = 231, 43 +POPUP_OFFSET = 30, 5 +MAX_PROF_ROWS = 11 +PREFIX_ICON_SIZE = 36, 24 +TOP_PAD = 6 + +EXTEND_PAD = 10 + +LIGHT_GREEN = "#a6eacf" +LIGHT_BLUE = "#e8f0fe" +MEDIUM_BLUE = "#D0E1F9" +DARK_BLUE = "#1A73E8" +BACKUP_PROFILE_NAME = "default" + +DIV_COLORS = { + "default": "white", + "hovering": LIGHT_BLUE, + "selected": MEDIUM_BLUE +} + + +def random_name(row): + return str(row) + str(hex(int(time.time() * 1000)))[2:] + + +class ItemProfileSwitcher(SafeDisposableFrame): + + def __init__( + self, + owner_frame, + top_level, + main_gui_callback, + **kwargs, + ): + + super().__init__(top_level, **kwargs) + self.owner_frame = owner_frame + self.main_gui_callback = main_gui_callback + self.is_active = False + + self.grid_rowconfigure(MAX_PROF_ROWS, weight=1) + self.grid_columnconfigure(0, weight=1) + + self.divs = self.load_initial_profiles() + + div_id = self.get_div_id(ConfigManager().curr_profile_name.get()) + # self.set_div_selected(self.divs[div_id]) + + # Custom border + self.configure(border_color="gray60") + self.configure(fg_color="transparent") + self.configure(bg_color="transparent") + self.configure(background_corner_colors=[ + "#000000", "#000000", "#000000", "#000000" + ]) + + def load_initial_profiles(self): + """Create div according to profiles in config + """ + profile_names = ConfigManager().list_profile() + + divs = {} + row = 0 + for profile_name in profile_names: + div_id = random_name(row) + div = self.create_div(row, div_id, profile_name) + div["wrap_label"].grid() + + # Random unique div id + divs[div_id] = div + row += 1 + + # Create add profile button + drop_add_div_id = random_name(row ) + addp_div = self.create_add_profiles_div(row, drop_add_div_id) + addp_div["wrap_label"].grid() + divs[drop_add_div_id] = addp_div + row += 1 + + # Create edit profile button + edit_div_id = random_name(row) + edit_div = self.create_edit_profiles_div(row, edit_div_id) + edit_div["wrap_label"].grid() + divs[edit_div_id] = edit_div + + + return divs + + + + def get_div_id(self, profile_name: str): + """Get div unique id from profile name + """ + for div_id, div in self.divs.items(): + if div["profile_name"] == profile_name: + return div_id + logger.critical(f"{profile_name} not found") + TaskKiller().exit() + + def hover_enter(self, div, event): + for widget_name, widget in div.items(): + target_widgets = ["wrap_label"] + if widget is None: + continue + if widget_name in target_widgets: + widget.configure(fg_color=DIV_COLORS["hovering"]) + # widget.configure(image=div["item_bg_hover"]) + + div["is_hovering"] = True + + def hover_leave(self, div, event): + for widget_name, widget in div.items(): + target_widgets = ["wrap_label"] + if widget is None: + continue + if widget_name in target_widgets: + widget.configure(fg_color="white") + # widget.configure(image=div["item_bg"]) + + div["is_hovering"] = False + + + + + def remove_div(self, div_name): + logger.info(f"Remove {div_name}") + div = self.divs[div_name] + + for widget in div.values(): + if isinstance( + widget, customtkinter.windows.widgets.core_widget_classes. + CTkBaseClass): + widget.grid_forget() + widget.destroy() + + def refresh_frame(self): + """Refresh the divs if profile directory has changed + """ + + logger.info("Refresh frame_profile") + + # Check if folders same as divs + name_list = [div["profile_name"] for _, div in self.divs.items()] + name_list.remove("Manage Profiles") + name_list.remove("Add Profile") + + if set(ConfigManager().list_profile()) == set(name_list): + return + logger.info( + f"Profile directory changed {ConfigManager().profiles} != {name_list}, reload..." + ) + + # Delete all divs and re-create + self.clear_divs() + self.divs = self.load_initial_profiles() + current_profile = ConfigManager().curr_profile_name.get() + + # Check if selected profile exist + new_name_list = [div["profile_name"] for _, div in self.divs.items()] + if current_profile not in new_name_list: + logger.critical(f"Profile {current_profile} not found.") + TaskKiller().exit() + + self.owner_frame.show_window() + logger.info(f"Current selected profile {current_profile}") + + def clear_divs(self): + for div_id, div in self.divs.items(): + self.remove_div(div_id) + self.divs = {} + + def switch_div_profile(self, div, event): + # profile item click callback + ConfigManager().switch_profile(div["profile_name"]) + # Refresh values in each page + self.main_gui_callback("refresh_profiles") + self.owner_frame.hide_window() + + def create_div(self, row: int, div_id: str, profile_name) -> dict: + prefix_icon = customtkinter.CTkImage( + Image.open("assets/images/proj_icon_blank.png"), + size=PREFIX_ICON_SIZE) + + # Box + entry_var = tk.StringVar() + entry_var.set(profile_name) + wrap_label = customtkinter.CTkLabel(self, + text="", + textvariable=entry_var, + height=40, + image=prefix_icon, + compound="left", + anchor="w", + cursor="hand2", + fg_color="white", + corner_radius=0) + + top_pad = TOP_PAD if row == 0 else 0 + wrap_label.grid(row=row, + column=0, + padx=(1, 2), + pady=(top_pad, 0), + ipadx=0, + ipady=0, + sticky="new") + + sep = tk.ttk.Separator(wrap_label, orient='horizontal') + sep.grid(row=row, + column=0, + padx=0, + pady=0, + ipadx=0, + ipady=0, + sticky="sew") + + div = { + "div_id": div_id, + "profile_name": profile_name, + "wrap_label": wrap_label, + "entry_var": entry_var, + "is_hovering": False + } + + # Hover effect + for widget in [wrap_label]: + if widget is None: + continue + widget.bind('', partial(self.hover_enter, div)) + widget.bind('', partial(self.hover_leave, div)) + + # Click label : swap profile function + for widget in [wrap_label]: + widget.bind("", partial(self.switch_div_profile, div)) + + return div + + def create_edit_profiles_div(self, row: int, div_id: str) -> dict: + prefix_icon = customtkinter.CTkImage( + Image.open("assets/images/edit.png"), size=PREFIX_ICON_SIZE) + + # Box + wrap_label = customtkinter.CTkLabel(self, + text="Manage Profiles", + height=40, + image=prefix_icon, + compound="left", + justify="left", + anchor="w", + cursor="hand2", + fg_color="white", + corner_radius=0) + + top_pad = TOP_PAD if row == 0 else 0 + wrap_label.grid(row=row, + column=0, + padx=(1, 2), + pady=(top_pad, 0), + ipadx=0, + ipady=0, + sticky="new") + + div = { + "div_id": div_id, + "profile_name": "Manage Profiles", + "wrap_label": wrap_label, + "is_hovering": False + } + + # Hover effect + for widget in [wrap_label]: + if widget is None: + continue + widget.bind('', partial(self.hover_enter, div)) + widget.bind('', partial(self.hover_leave, div)) + + # Click label : swap profile function + for widget in [wrap_label]: + widget.bind("", self.owner_frame.show_profile_editor) + + return div + + def create_add_profiles_div(self, row: int, div_id: str) -> dict: + prefix_icon = customtkinter.CTkImage( + Image.open("assets/images/add_drop.png"), size=PREFIX_ICON_SIZE) + + # Box + wrap_label = customtkinter.CTkLabel(self, + text="Add Profile", + height=40, + image=prefix_icon, + compound="left", + justify="left", + anchor="w", + cursor="hand2", + fg_color="white", + corner_radius=0) + + top_pad = TOP_PAD if row == 0 else 0 + wrap_label.grid(row=row, + column=0, + padx=(1, 2), + pady=(top_pad, 0), + ipadx=0, + ipady=0, + sticky="new") + + div = { + "div_id": div_id, + "profile_name": "Add Profile", + "wrap_label": wrap_label, + "is_hovering": False + } + + # Hover effect + for widget in [wrap_label]: + if widget is None: + continue + widget.bind('', partial(self.hover_enter, div)) + widget.bind('', partial(self.hover_leave, div)) + + # Click label : swap profile function + for widget in [wrap_label]: + widget.bind("", self.owner_frame.dropdown_add_profile) + + return div + + + def enter(self): + super().enter() + self.refresh_frame() + + + def leave(self): + super().leave() + + +class FrameProfileSwitcher(): + + def __init__(self, root_window, main_gui_callback: callable, **kwargs): + + self.root_window = root_window + self.main_gui_callback = main_gui_callback + self.float_window = customtkinter.CTkToplevel(root_window) + self.float_window.wm_overrideredirect(True) + self.float_window.lift() + self.float_window.wm_attributes("-disabled", True) + self.float_window.wm_attributes('-toolwindow', 'True') + self.float_window.grid_rowconfigure(3, weight=1) + self.float_window.grid_columnconfigure(0, weight=1) + self.float_window.configure(fg_color="white") + self._displayed = True + # Rounded corder + self.float_window.config(background='#000000') + self.float_window.attributes("-transparentcolor", "#000000") + + # Custom border + self.float_window.configure(highlightthickness=0, bd=0) + + n_rows = len(ConfigManager().list_profile()) + 2 + self.float_window.geometry( + f"{PROFILE_ITEM_SIZE[0]}x{PROFILE_ITEM_SIZE[1]*n_rows+EXTEND_PAD}+{POPUP_OFFSET[0]}+{POPUP_OFFSET[1]}" + ) + + # Inner frame + self.inner_frame = ItemProfileSwitcher( + owner_frame=self, + top_level=self.float_window, + main_gui_callback=main_gui_callback) + self.inner_frame.grid(row=3, column=0, padx=5, pady=5, sticky="nswe") + + self.hide_window() + self.root_window.bind("", self.hide_window) + + self.prev_event = None + + def change_profile(self, target): + ConfigManager().switch_profile(target) + self.pages["page_camera"].refresh_profile() + self.pages["page_cursor"].refresh_profile() + self.pages["page_gestures"].refresh_profile() + self.pages["page_keyboard"].refresh_profile() + + def show_window(self): + + self.root_window.bind("", self.hide_window) + shift_x = self.root_window.winfo_rootx() + shift_y = self.root_window.winfo_rooty() + + # Popup + n_rows = len(ConfigManager().profiles) + 2 + + + + self.float_window.geometry( + f"{PROFILE_ITEM_SIZE[0]}x{PROFILE_ITEM_SIZE[1]*n_rows+EXTEND_PAD}+{POPUP_OFFSET[0]+shift_x}+{POPUP_OFFSET[1]+shift_y}" + ) + + self.float_window.deiconify() + self.float_window.lift() + self.float_window.wm_attributes('-disabled', False) + self._displayed = True + + def hide_window(self, event=None): + if self._displayed: + logger.info("hide") + self.root_window.unbind_all("") + + self.float_window.wm_attributes('-disabled', True) + self._displayed = False + + self.float_window.withdraw() + + def show_profile_editor(self, event, **kwargs): + self.hide_window() + self.main_gui_callback("show_profile_editor") + + + def dropdown_add_profile(self, event, **kwargs): + ConfigManager().add_profile() + self.inner_frame.refresh_frame() + self.show_window() + + + def enter(self): + + # refresh UI + self.inner_frame.enter() + self.show_window() + logger.info("enter") + + def leave(self): + super().leave() + self.inner_frame.leave() + logger.info("leave") diff --git a/src/gui/main_gui.py b/src/gui/main_gui.py index f96903bf..ff052b7c 100644 --- a/src/gui/main_gui.py +++ b/src/gui/main_gui.py @@ -46,7 +46,7 @@ def __init__(self, tk_root): # Create menu frame and assign callbacks self.frame_menu = frames.FrameMenu(self.tk_root, - self.change_frame_callback, + self.root_function_callback, height=360, width=260, logger_name="frame_menu") @@ -75,7 +75,7 @@ def __init__(self, tk_root): "page_home": pages.PageHome(master=self.tk_root, logger_name="page_home", - master_callback=self.change_frame_callback), + root_callback=self.root_function_callback), "page_camera": pages.PageSelectCamera( master=self.tk_root, @@ -121,45 +121,37 @@ def __init__(self, tk_root): self.change_page("page_home") - self.frame_profile = frames.FrameProfile( - self.tk_root, refresh_master_fn=self.refresh_profile) - - # Profile button - profile_btn = customtkinter.CTkButton( - master=self.tk_root, - textvariable=ConfigManager().curr_profile_name, - border_width=1, - corner_radius=4, - compound="right", - border_color="gray70", - anchor="e", - command=self.frame_profile.show_window) - # profile_btn.grid(row=0, - # column=0, - # padx=10, - # pady=10, - # sticky="ne", - # columnspan=10, - # rowspan=10) - - def refresh_profile(self): - logger.info("refresh_profile") - self.pages["page_gestures"].refresh_profile() - self.pages["page_camera"].refresh_profile() - self.pages["page_cursor"].refresh_profile() - self.pages["page_keyboard"].refresh_profile() - - def change_frame_callback(self, command, args: dict): - logger.info(f"change_frame_callback {command} with {args}") - if command == "change_page": + # Profile UI + self.frame_profile_switcher = frames.FrameProfileSwitcher( + self.tk_root, main_gui_callback=self.root_function_callback) + self.frame_profile_editor = frames.FrameProfileEditor( + self.tk_root, main_gui_callback=self.root_function_callback) + + def root_function_callback(self, function_name, args: dict = {}, **kwargs): + logger.info(f"root_function_callback {function_name} with {args}") + + # Basic page navigate + if function_name == "change_page": self.change_page(args["target"]) + self.frame_menu.set_tab_active(tab_name=args["target"]) + + # Profiles + elif function_name == "show_profile_switcher": + self.frame_profile_switcher.enter() + elif function_name == "show_profile_editor": + self.frame_profile_editor.enter() - self.frame_menu.set_tab_active(tab_name=args["target"]) + elif function_name == "refresh_profiles": + logger.info("refresh_profile") + self.pages["page_gestures"].refresh_profile() + self.pages["page_camera"].refresh_profile() + self.pages["page_cursor"].refresh_profile() + self.pages["page_keyboard"].refresh_profile() - def cam_preview_callback(self, command, args: dict): - logger.info(f"cam_preview_callback {command} with {args}") + def cam_preview_callback(self, function_name, args: dict, **kwargs): + logger.info(f"cam_preview_callback {function_name} with {args}") - if command == "toggle_switch": + if function_name == "toggle_switch": self.set_mediapipe_mouse_enable(new_state=args["switch_status"]) def set_mediapipe_mouse_enable(self, new_state: bool): diff --git a/src/gui/pages/page_home.py b/src/gui/pages/page_home.py index 02183396..11b071f8 100644 --- a/src/gui/pages/page_home.py +++ b/src/gui/pages/page_home.py @@ -27,7 +27,7 @@ class PageHome(SafeDisposableFrame): - def __init__(self, master, master_callback: callable, **kwargs): + def __init__(self, master, root_callback: callable, **kwargs): super().__init__(master, **kwargs) logging.info("Create PageHome") @@ -86,8 +86,8 @@ def __init__(self, master, master_callback: callable, **kwargs): border_width=0, corner_radius=12, image=page_camera_btn_im, - command=partial(master_callback, - command="change_page", + command=partial(root_callback, + function_name="change_page", args={"target": "page_camera"})) page_camera_btn.grid(row=3, column=0, padx=80, pady=10, sticky="nw") @@ -100,8 +100,8 @@ def __init__(self, master, master_callback: callable, **kwargs): border_width=0, corner_radius=12, image=page_cursor_btn_im, - command=partial(master_callback, - command="change_page", + command=partial(root_callback, + function_name="change_page", args={"target": "page_cursor"})) page_cursor_btn.grid(row=4, column=0, padx=80, pady=10, sticky="nw") @@ -114,8 +114,8 @@ def __init__(self, master, master_callback: callable, **kwargs): border_width=0, corner_radius=12, image=page_gestures_btn_im, - command=partial(master_callback, - command="change_page", + command=partial(root_callback, + function_name="change_page", args={"target": "page_gestures"})) page_gestures_btn.grid(row=5, column=0, padx=80, pady=10, sticky="nw") @@ -128,8 +128,8 @@ def __init__(self, master, master_callback: callable, **kwargs): border_width=0, corner_radius=12, image=page_keyboard_btn_im, - command=partial(master_callback, - command="change_page", + command=partial(root_callback, + function_name="change_page", args={"target": "page_keyboard"})) page_keyboard_btn.grid(row=6, column=0, padx=80, pady=10, sticky="nw") diff --git a/src/gui/pages/page_keyboard.py b/src/gui/pages/page_keyboard.py index 0869a713..464d2c48 100644 --- a/src/gui/pages/page_keyboard.py +++ b/src/gui/pages/page_keyboard.py @@ -25,8 +25,7 @@ from src.detectors import FaceMesh from src.gui.balloon import Balloon from src.gui.dropdown import Dropdown -from src.gui.frames.safe_disposable_frame import (SafeDisposableFrame, - SafeDisposableScrollableFrame) +from src.gui.frames.safe_disposable_frame import SafeDisposableFrame, SafeDisposableScrollableFrame logger = logging.getLogger("PageKeyboard") @@ -178,6 +177,7 @@ def create_div(self, row: int, div_name: str, gesture_name: str, image=self.bin_image, fg_color="white", anchor="e", + cursor="hand2", width=25) remove_button.cget("font").configure(size=18) @@ -192,13 +192,12 @@ def create_div(self, row: int, div_name: str, gesture_name: str, # Key entry field_txt = "" if key_action == "None" else key_action - entry_field = customtkinter.CTkLabel( - master=self, - text=field_txt, - image=self.a_button_image, - width=A_BUTTON_SIZE[0], - height=A_BUTTON_SIZE[1], - ) + entry_field = customtkinter.CTkLabel(master=self, + text=field_txt, + image=self.a_button_image, + width=A_BUTTON_SIZE[0], + height=A_BUTTON_SIZE[1], + cursor="hand2") entry_field.cget("font").configure(size=17) entry_field.bind( diff --git a/src/task_killer.py b/src/task_killer.py index 90d091bb..57407c30 100644 --- a/src/task_killer.py +++ b/src/task_killer.py @@ -56,9 +56,9 @@ def start(self): def exit(self): logger.info("Exit program") + from src.camera_manager import CameraManager from src.controllers import Keybinder, MouseController from src.detectors import FaceMesh - from src.camera_manager import CameraManager CameraManager().destroy() MouseController().destroy()