Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 59 additions & 4 deletions src/macro/macro.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand All @@ -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)
)
Expand All @@ -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()
Expand Down Expand Up @@ -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()
Expand All @@ -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
Expand Down Expand Up @@ -287,14 +299,56 @@ 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:
self.stop_playback()
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)
Expand All @@ -316,14 +370,15 @@ 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:
if userSettings["After_Playback"]["Mode"].lower() == "standby":
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":
Expand Down
15 changes: 9 additions & 6 deletions src/utils/record_file_management.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
2 changes: 2 additions & 0 deletions src/utils/user_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
139 changes: 138 additions & 1 deletion src/windows/main/main_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from tkinter import (
BOTH,
BOTTOM,
Canvas,
DISABLED,
LEFT,
RIGHT,
Expand All @@ -17,6 +18,7 @@
W,
X,
)
from tkinter import font as tkfont
from tkinter.ttk import Button, Frame, Label

from PIL import Image
Expand Down Expand Up @@ -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")))
Expand All @@ -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)
Expand All @@ -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("<Configure>", 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
Expand Down Expand Up @@ -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:
Expand All @@ -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")))
Expand Down
1 change: 1 addition & 0 deletions src/windows/options/settings/select_language.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"])
Expand Down