Skip to content

Improve playback feedback, window sizing, and settings handling#134

Open
siggismalz wants to merge 6 commits intoLOUDO56:mainfrom
siggismalz:main
Open

Improve playback feedback, window sizing, and settings handling#134
siggismalz wants to merge 6 commits intoLOUDO56:mainfrom
siggismalz:main

Conversation

@siggismalz
Copy link
Copy Markdown
Contributor

Summary

This PR bundles the last 6 commits and improves both UX and behavior around playback, window sizing, settings, and file handling.

Changes

  • add an activity/progress indicator for macro recording and playback
  • use a determinate playback progress bar when the total duration can be estimated
  • dynamically increase the main window width when needed so the menu bar does not wrap
  • re-apply the menu width logic after changing the application language
  • fix the Linux standby command to use systemctl suspend
  • preserve current_file when opening a macro via file association / CLI argument
  • reload settings in memory immediately after resetting them
  • simplify macro settings import during macro load and persist imported settings

Notes

  • includes the menu sizing fix related to #1

Testing

Manual verification is recommended for:

  • recording and playback progress feedback
  • menu bar sizing after startup and after language changes
  • loading a macro from file association and saving it again
  • resetting settings and importing macro settings
  • Linux standby behavior after playback

Copy link
Copy Markdown
Owner

@LOUDO56 LOUDO56 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey! Thanks for the pr, however it needs some changes

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()
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The progress bar don't need to show up on recording, it feels weird imo

Comment on lines +312 to +337
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
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the calculation is wrong: for example, when I record something and set the number of repetitions to 10, the progress bar stops before the last three repetitions.


def _hide_activity_progress(self):
self._cancel_activity_progress_job()
self.activity_progress_mode = "hidden"
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do not hard string, use enum

Comment on lines +93 to +277
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
else:
self.playBtn = Button(self.center_frame, image=self.playImg, state=DISABLED)
self.playBtn.pack(side=LEFT, padx=50)

# Record Button
self.recordImg = PhotoImage(file=resource_path(path.join("assets", "button", "record.png")))
self.recordBtn = Button(self.center_frame, image=self.recordImg, command=self.macro.start_record)
self.recordBtn.pack(side=RIGHT, padx=50)

# Stop Button
self.stopImg = PhotoImage(file=resource_path(path.join("assets", "button", "stop.png")))

record_management = RecordFileManagement(self, self.menu)

self.bind('<Control-Shift-S>', record_management.save_macro_as)
self.bind('<Control-s>', record_management.save_macro)
self.bind('<Control-l>', record_management.load_macro)
self.bind('<Control-n>', record_management.new_macro)

self.protocol("WM_DELETE_WINDOW", self.quit_software)
if platform == "win32":
Thread(target=self.systemTray).start()

self.attributes("-topmost", 0)

if platform != "win32" and self.settings.first_time:
NotWindows(self)

if self.settings.settings_dict["Others"]["Check_update"]:
if self.version.new_version != "" and self.version.version != self.version.new_version:
if time() > self.settings.settings_dict["Others"]["Remind_new_ver_at"]:
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:
self.text_content = json.load(f)
self.text_content = self.text_content["content"]

if self.lang != "en":
with open(resource_path(path.join('langs', 'en.json')), encoding='utf-8') as f:
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

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not a big fan of writing the progress bar logic inside main_app.py. It should have his own class in utils to keep it organized.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants