diff --git a/src/macro/macro.py b/src/macro/macro.py index 03fe0cb..8668a9b 100644 --- a/src/macro/macro.py +++ b/src/macro/macro.py @@ -37,6 +37,7 @@ def __init__(self, main_app): self.mouse_listener = None self.time = time() self.event_delta_time=0 + self.playback_progress_total = None self.keyboard_listener = keyboard.Listener( on_press=self.__on_press, on_release=self.__on_release @@ -93,6 +94,7 @@ def start_record(self, by_hotkey=False): self.main_menu.file_menu.entryconfig(self.main_app.text_content["file_menu"]["save_as_text"], state=DISABLED) self.main_menu.file_menu.entryconfig(self.main_app.text_content["file_menu"]["new_text"], state=DISABLED) self.main_menu.file_menu.entryconfig(self.main_app.text_content["file_menu"]["load_text"], state=DISABLED) + self.main_app.show_activity_progress() if userSettings["Minimization"]["When_Recording"]: self.main_app.withdraw() Thread(target=lambda: show_notification_minim(self.main_app)).start() @@ -125,6 +127,7 @@ def stop_record(self): self.main_app.macro_recorded = True self.main_app.macro_saved = False + self.main_app.hide_activity_progress() if userSettings["Minimization"]["When_Recording"]: self.main_app.deiconify() @@ -134,6 +137,7 @@ def stop_record(self): def start_playback(self): userSettings = self.user_settings.settings_dict self.playback = True + self.playback_progress_total = self.__get_total_playback_duration(userSettings) self.main_app.playBtn.configure( image=self.main_app.stopImg, command=lambda: self.stop_playback(True) ) @@ -142,6 +146,10 @@ def start_playback(self): self.main_menu.file_menu.entryconfig(self.main_app.text_content["file_menu"]["new_text"], state=DISABLED) self.main_menu.file_menu.entryconfig(self.main_app.text_content["file_menu"]["load_text"], state=DISABLED) self.main_app.recordBtn.configure(state=DISABLED) + if self.playback_progress_total is None: + self.main_app.show_activity_progress() + else: + self.main_app.show_precise_progress(self.playback_progress_total) if userSettings["Minimization"]["When_Playing"]: self.main_app.withdraw() Thread(target=lambda: show_notification_minim(self.main_app)).start() @@ -205,7 +213,9 @@ def __play_events(self): secondsToWait = userSettings["Playback"]["Repeat"]["Scheduled"] - seconds_since_midnight if secondsToWait < 0: secondsToWait = 86400 + secondsToWait # 86400 + -secondsToWait. Meaning it will happen tomorrow - sleep(secondsToWait) + if not self.__sleep_with_progress(secondsToWait): + self.unPressEverything(keyToUnpress) + return repeat_count = 0 now = time() @@ -228,7 +238,9 @@ def __play_events(self): ) if timeSleep < 0: timeSleep = abs(timeSleep) - sleep(timeSleep) + if not self.__sleep_with_progress(timeSleep): + self.unPressEverything(keyToUnpress) + return event_type = self.macro_events["events"][events]["type"] if event_type == "cursorMove": # Cursor Move @@ -287,7 +299,9 @@ def __play_events(self): if userSettings["Playback"]["Repeat"]["Delay"] > 0: if is_infinite or repeat_count < repeat_times: - sleep(userSettings["Playback"]["Repeat"]["Delay"]) + if not self.__sleep_with_progress(userSettings["Playback"]["Repeat"]["Delay"]): + self.unPressEverything(keyToUnpress) + return self.unPressEverything(keyToUnpress) if userSettings["Playback"]["Repeat"]["Interval"] == 0 and userSettings["Playback"]["Repeat"]["For"] == 0 and repeat_count: @@ -295,6 +309,46 @@ def __play_events(self): if userSettings["Minimization"]["When_Playing"]: self.main_app.deiconify() + def __get_total_playback_duration(self, userSettings): + repeat_settings = userSettings["Playback"]["Repeat"] + if repeat_settings.get("Infinite", False) or repeat_settings["Interval"] > 0: + return None + scheduled_duration = repeat_settings.get("Scheduled", 0) + if repeat_settings["For"] > 0: + return scheduled_duration + repeat_settings["For"] + if "events" not in self.macro_events: + return scheduled_duration + + fixed_timestamp = userSettings["Others"]["Fixed_timestamp"] + if fixed_timestamp > 0: + single_run_duration = fixed_timestamp * len(self.macro_events["events"]) + else: + speed = userSettings["Playback"]["Speed"] + if speed <= 0: + return None + single_run_duration = sum( + abs(event.get("timestamp", 0)) * (1 / speed) + for event in self.macro_events["events"] + ) + + total_duration = scheduled_duration + (single_run_duration * repeat_settings["Times"]) + if repeat_settings["Times"] > 1: + total_duration += repeat_settings["Delay"] * (repeat_settings["Times"] - 1) + return total_duration + + def __sleep_with_progress(self, duration): + if duration <= 0: + return self.playback + + remaining = duration + started_at = time() + while self.playback and remaining > 0: + chunk = min(0.05, remaining) + sleep(chunk) + elapsed = time() - started_at + remaining = duration - elapsed + return self.playback + def unPressEverything(self, keyToUnpress): for key in keyToUnpress: self.keyboardControl.release(key) @@ -316,6 +370,7 @@ def stop_playback(self, playback_stopped_manually=False): self.main_menu.file_menu.entryconfig(self.main_app.text_content["file_menu"]["save_as_text"], state=NORMAL) self.main_menu.file_menu.entryconfig(self.main_app.text_content["file_menu"]["new_text"], state=NORMAL) self.main_menu.file_menu.entryconfig(self.main_app.text_content["file_menu"]["load_text"], state=NORMAL) + self.main_app.hide_activity_progress() if userSettings["Minimization"]["When_Playing"]: self.main_app.deiconify() if userSettings["After_Playback"]["Mode"] != "Idle" and not playback_stopped_manually: @@ -323,7 +378,7 @@ def stop_playback(self, playback_stopped_manually=False): if platform == "win32": system("rundll32.exe powrprof.dll, SetSuspendState 0,1,0") elif "linux" in platform.lower(): - system("subprocess.callctl suspend") + system("systemctl suspend") elif "darwin" in platform.lower(): system("pmset sleepnow") elif userSettings["After_Playback"]["Mode"].lower() == "log_off_computer": diff --git a/src/utils/record_file_management.py b/src/utils/record_file_management.py index 053603f..887137e 100644 --- a/src/utils/record_file_management.py +++ b/src/utils/record_file_management.py @@ -86,12 +86,15 @@ def load_macro(self, event=None): self.main_app.macro_saved = True self.main_app.current_file = macroFile.name if "settings" in self.main_app.macro.macro_events: - if not self.main_app.settings.settings_dict["Loading"]["Always_import_macro_settings"]: - if messagebox.askyesno("PyMacroRecord", self.config_text["global"]["load_macro_settings"]): - macro_settings = self.main_app.macro.macro_events["settings"] - self.main_app.settings.settings_dict["Playback"] = macro_settings["Playback"] - self.main_app.settings.settings_dict["Minimization"] = macro_settings["Minimization"] - self.main_app.settings.settings_dict["After_Playback"] = macro_settings["After_Playback"] + should_import_settings = self.main_app.settings.settings_dict["Loading"]["Always_import_macro_settings"] + if not should_import_settings: + should_import_settings = messagebox.askyesno("PyMacroRecord", self.config_text["global"]["load_macro_settings"]) + if should_import_settings: + macro_settings = self.main_app.macro.macro_events["settings"] + self.main_app.settings.settings_dict["Playback"] = macro_settings["Playback"] + self.main_app.settings.settings_dict["Minimization"] = macro_settings["Minimization"] + self.main_app.settings.settings_dict["After_Playback"] = macro_settings["After_Playback"] + self.main_app.settings.update_settings() self.main_app.prevent_record = False diff --git a/src/utils/user_settings.py b/src/utils/user_settings.py index e20fe7f..98878f6 100644 --- a/src/utils/user_settings.py +++ b/src/utils/user_settings.py @@ -111,6 +111,8 @@ def update_settings(self): def reset_settings(self): if messagebox.askyesno(self.main_app.text_content["global"]["confirm"], self.main_app.text_content["options_menu"]["others_menu"]["reset_settings_confirmation"]): self.init_settings() + self.settings_dict = self.__get_config() + self.check_new_options() def get_path(self): return self.path_setting diff --git a/src/windows/main/main_app.py b/src/windows/main/main_app.py index dc9cf79..102a1c6 100644 --- a/src/windows/main/main_app.py +++ b/src/windows/main/main_app.py @@ -9,6 +9,7 @@ from tkinter import ( BOTH, BOTTOM, + Canvas, DISABLED, LEFT, RIGHT, @@ -17,6 +18,7 @@ W, X, ) +from tkinter import font as tkfont from tkinter.ttk import Button, Frame, Label from PIL import Image @@ -49,7 +51,9 @@ class MainApp(Window): """Main windows of the application""" def __init__(self): - super().__init__("PyMacroRecord", 350, 200) + self.default_width = 350 + self.default_height = 200 + super().__init__("PyMacroRecord", self.default_width, self.default_height) self.attributes("-topmost", 1) if platform == "win32": self.iconbitmap(resource_path(path.join("assets", "logo.ico"))) @@ -67,6 +71,7 @@ def __init__(self): self.version = Version(self.settings.settings_dict, self) self.menu = MenuBar(self) # Menu Bar + self.adjust_width_for_menu() self.macro = Macro(self) self.validate_cmd = self.register(self.validate_input) @@ -85,11 +90,28 @@ def __init__(self): self.center_frame = Frame(self) self.center_frame.pack(expand=True, fill=BOTH) + self.activity_progress = Canvas( + self, + height=10, + bg="#d8d8d8", + highlightthickness=0, + bd=0, + ) + self.activity_progress_fill = self.activity_progress.create_rectangle(0, 0, 0, 10, fill="#2e8b57", width=0) + self.activity_progress_mode = "hidden" + self.activity_progress_value = 0 + self.activity_progress_maximum = 1 + self.activity_progress_job = None + self.activity_progress_offset = 0 + self.activity_progress_started_at = None + self.activity_progress.bind("", lambda _event: self._render_activity_progress()) + # Import record if opened with .pmr extension if len(argv) > 1: with open(sys.argv[1], 'r') as record: loaded_content = load(record) self.macro.import_record(loaded_content) + self.current_file = sys.argv[1] self.playBtn = Button(self.center_frame, image=self.playImg, command=self.macro.start_playback) self.macro_recorded = True self.macro_saved = True @@ -127,6 +149,30 @@ def __init__(self): NewVerAvailable(self, self.version.new_version) self.mainloop() + def adjust_width_for_menu(self): + labels = ( + self.text_content["file_menu"]["file_text"], + self.text_content["options_menu"]["options_text"], + self.text_content["help_menu"]["help_text"], + self.text_content["others_menu"]["others_text"], + ) + + try: + menu_font = tkfont.nametofont("TkMenuFont") + except Exception: + menu_font = tkfont.nametofont("TkDefaultFont") + + required_width = 20 + sum(menu_font.measure(label) + 32 for label in labels) + target_width = max(self.default_width, required_width) + + self.update_idletasks() + current_height = max(self.winfo_height(), self.default_height) + current_width = self.winfo_width() + if current_width >= target_width: + return + + self.geometry(f"{target_width}x{current_height}+{self.winfo_x()}+{self.winfo_y()}") + def load_language(self): self.lang = self.settings.settings_dict["Language"] with open(resource_path(path.join('langs', self.lang + '.json')), encoding='utf-8') as f: @@ -138,6 +184,97 @@ def load_language(self): en = json.load(f) deepcopy_dict_missing_entries(self.text_content, en["content"]) + def show_activity_progress(self): + self.after(0, self._show_activity_progress) + + def _show_activity_progress(self): + self._cancel_activity_progress_job() + self.activity_progress_mode = "indeterminate" + self.activity_progress_value = 0 + self.activity_progress_offset = 0 + self.activity_progress_started_at = None + if not self.activity_progress.winfo_ismapped(): + self.activity_progress.pack(side=BOTTOM, fill=X, padx=12, pady=(0, 8)) + self._render_activity_progress() + self._animate_activity_progress() + + def show_precise_progress(self, maximum): + self.after(0, self._show_precise_progress, maximum) + + def _show_precise_progress(self, maximum): + self._cancel_activity_progress_job() + self.activity_progress_mode = "determinate" + self.activity_progress_maximum = max(maximum, 1) + self.activity_progress_value = 0 + self.activity_progress_started_at = time() + if not self.activity_progress.winfo_ismapped(): + self.activity_progress.pack(side=BOTTOM, fill=X, padx=12, pady=(0, 8)) + self._render_activity_progress() + self._animate_precise_progress() + + def set_precise_progress(self, value): + self.after(0, self._set_precise_progress, value) + + def _set_precise_progress(self, value): + if self.activity_progress_mode != "determinate": + return + self.activity_progress_value = min(max(value, 0), self.activity_progress_maximum) + self._render_activity_progress() + + def hide_activity_progress(self): + self.after(0, self._hide_activity_progress) + + def _hide_activity_progress(self): + self._cancel_activity_progress_job() + self.activity_progress_mode = "hidden" + self.activity_progress_value = 0 + self.activity_progress_started_at = None + if self.activity_progress.winfo_ismapped(): + self.activity_progress.pack_forget() + + def _render_activity_progress(self): + width = max(self.activity_progress.winfo_width(), 1) + height = max(self.activity_progress.winfo_height(), 10) + + if self.activity_progress_mode == "determinate": + ratio = self.activity_progress_value / max(self.activity_progress_maximum, 1) + fill_width = int(width * ratio) + self.activity_progress.coords(self.activity_progress_fill, 0, 0, fill_width, height) + elif self.activity_progress_mode == "indeterminate": + block_width = max(width // 4, 40) + start_x = self.activity_progress_offset - block_width + end_x = self.activity_progress_offset + self.activity_progress.coords(self.activity_progress_fill, start_x, 0, end_x, height) + else: + self.activity_progress.coords(self.activity_progress_fill, 0, 0, 0, height) + + def _animate_activity_progress(self): + if self.activity_progress_mode != "indeterminate": + return + width = max(self.activity_progress.winfo_width(), 240) + block_width = max(width // 4, 40) + self.activity_progress_offset = (self.activity_progress_offset + 10) % (width + block_width) + self._render_activity_progress() + self.activity_progress_job = self.after(30, self._animate_activity_progress) + + def _animate_precise_progress(self): + if self.activity_progress_mode != "determinate": + return + if self.activity_progress_started_at is None: + return + + elapsed = time() - self.activity_progress_started_at + self.activity_progress_value = min(elapsed, self.activity_progress_maximum) + self._render_activity_progress() + + if getattr(self, "macro", None) is not None and self.macro.playback and self.activity_progress_value < self.activity_progress_maximum: + self.activity_progress_job = self.after(50, self._animate_precise_progress) + + def _cancel_activity_progress_job(self): + if self.activity_progress_job is not None: + self.after_cancel(self.activity_progress_job) + self.activity_progress_job = None + def systemTray(self): """Just to show little icon on system tray""" image = Image.open(resource_path(path.join("assets", "logo.ico"))) diff --git a/src/windows/options/settings/select_language.py b/src/windows/options/settings/select_language.py index 2ade260..61559c3 100644 --- a/src/windows/options/settings/select_language.py +++ b/src/windows/options/settings/select_language.py @@ -58,6 +58,7 @@ def setNewLanguage(self, newLang, main_app): main_app.nametowidget(old_menu_name).destroy() from windows.main.menu_bar import MenuBar main_app.menu = MenuBar(main_app) + main_app.adjust_width_for_menu() main_app.macro.main_menu = main_app.menu main_app.macro.macro_file_management.menu_bar = main_app.menu self.title(main_app.text_content["options_menu"]["settings_menu"]["lang_settings"]["title"])