GameSentenceMiner 2.9.29__py3-none-any.whl → 2.10.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,9 @@
1
1
  import asyncio
2
+ import subprocess
2
3
  import tkinter as tk
3
- from tkinter import filedialog, messagebox, simpledialog, scrolledtext
4
+ from tkinter import filedialog, messagebox, simpledialog, scrolledtext, font
4
5
 
6
+ import pyperclip
5
7
  import ttkbootstrap as ttk
6
8
 
7
9
  from GameSentenceMiner import obs
@@ -40,8 +42,8 @@ class HoverInfoWidget:
40
42
  self.tooltip = tk.Toplevel(self.info_icon)
41
43
  self.tooltip.wm_overrideredirect(True)
42
44
  self.tooltip.wm_geometry(f"+{x}+{y}")
43
- label = tk.Label(self.tooltip, text=text, background="yellow", relief="solid", borderwidth=1,
44
- font=("tahoma", "12", "normal"))
45
+ label = ttk.Label(self.tooltip, text=text, relief="solid", borderwidth=1,
46
+ font=("tahoma", "12", "normal"))
45
47
  label.pack(ipadx=1)
46
48
 
47
49
  def hide_info_box(self):
@@ -49,6 +51,29 @@ class HoverInfoWidget:
49
51
  self.tooltip.destroy()
50
52
  self.tooltip = None
51
53
 
54
+ class HoverInfoLabelWidget:
55
+ def __init__(self, parent, text, tooltip, row, column, padx=5, pady=2, foreground="white", sticky='W', bootstyle=None, font=("Arial", 10, "normal")):
56
+ self.label = ttk.Label(parent, text=text, foreground=foreground, cursor="hand2", bootstyle=bootstyle, font=font)
57
+ self.label.grid(row=row, column=column, padx=(0, padx), pady=0, sticky=sticky)
58
+ self.label.bind("<Enter>", lambda e: self.show_info_box(tooltip))
59
+ self.label.bind("<Leave>", lambda e: self.hide_info_box())
60
+ self.tooltip = None
61
+
62
+ def show_info_box(self, text):
63
+ x, y, _, _ = self.label.bbox("insert")
64
+ x += self.label.winfo_rootx() + 25
65
+ y += self.label.winfo_rooty() + 20
66
+ self.tooltip = tk.Toplevel(self.label)
67
+ self.tooltip.wm_overrideredirect(True)
68
+ self.tooltip.wm_geometry(f"+{x}+{y}")
69
+ label = ttk.Label(self.tooltip, text=text, relief="solid", borderwidth=1,
70
+ font=("tahoma", "12", "normal"))
71
+ label.pack(ipadx=1)
72
+
73
+ def hide_info_box(self):
74
+ if self.tooltip:
75
+ self.tooltip.destroy()
76
+ self.tooltip = None
52
77
 
53
78
 
54
79
  class ConfigApp:
@@ -60,6 +85,7 @@ class ConfigApp:
60
85
  self.window.protocol("WM_DELETE_WINDOW", self.hide)
61
86
  self.obs_scene_listbox_changed = False
62
87
 
88
+ self.window.geometry("800x600")
63
89
  self.current_row = 0
64
90
 
65
91
  self.master_config: Config = configuration.load_config()
@@ -67,7 +93,7 @@ class ConfigApp:
67
93
  self.settings = self.master_config.get_config()
68
94
 
69
95
  self.notebook = ttk.Notebook(self.window)
70
- self.notebook.pack(pady=10, expand=True)
96
+ self.notebook.pack(pady=10, expand=True, fill='both')
71
97
 
72
98
  self.general_frame = self.create_general_tab()
73
99
  self.create_paths_tab()
@@ -77,18 +103,26 @@ class ConfigApp:
77
103
  self.create_screenshot_tab()
78
104
  self.create_audio_tab()
79
105
  self.create_obs_tab()
80
- self.create_hotkeys_tab()
81
106
  self.create_profiles_tab()
82
- self.create_advanced_tab()
83
107
  self.create_ai_tab()
108
+ self.create_advanced_tab()
109
+ # self.create_help_tab()
84
110
 
85
111
  self.notebook.bind("<<NotebookTabChanged>>", self.on_profiles_tab_selected)
86
112
 
87
113
  button_frame = ttk.Frame(self.window)
88
- button_frame.pack(side="top", pady=20, anchor="center")
89
-
90
- ttk.Button(button_frame, text="Save Settings", command=self.save_settings).grid(row=0, column=0, padx=10)
91
- ttk.Button(button_frame, text="Save and Sync Changes", command=lambda: self.save_settings(profile_change=False, sync_changes=True)).grid(row=0, column=1, padx=10)
114
+ button_frame.pack(side="bottom", pady=20, anchor="center")
115
+
116
+ ttk.Button(button_frame, text="Save Settings", command=self.save_settings, bootstyle="success").grid(row=0,
117
+ column=0,
118
+ padx=10)
119
+ # if len(self.master_config.configs) > 1:
120
+ # ttk.Button(button_frame, text="Save and Sync Changes",
121
+ # command=lambda: self.save_settings(profile_change=False, sync_changes=True),
122
+ # bootstyle="info").grid(row=0, column=1, padx=10)
123
+ # HoverInfoWidget(button_frame,
124
+ # "Saves Settings and Syncs changed settings to all profiles (if you have them)", row=0,
125
+ # column=2)
92
126
 
93
127
  self.window.withdraw()
94
128
 
@@ -97,16 +131,23 @@ class ConfigApp:
97
131
  if matched_configs:
98
132
  selection_window = tk.Toplevel(self.window)
99
133
  selection_window.title("Select Profile")
100
- ttk.Label(selection_window, text="Multiple profiles match the current scene. Please select the profile:").pack(pady=10)
134
+ selection_window.transient(self.window) # Make it modal relative to the main window
135
+ selection_window.grab_set() # Grab all events for this window
136
+
137
+ ttk.Label(selection_window,
138
+ text="Multiple profiles match the current scene. Please select the profile:").pack(pady=10)
101
139
  profile_var = tk.StringVar(value=matched_configs[0])
102
- profile_dropdown = ttk.Combobox(selection_window, textvariable=profile_var, values=matched_configs, state="readonly")
140
+ profile_dropdown = ttk.Combobox(selection_window, textvariable=profile_var, values=matched_configs,
141
+ state="readonly")
103
142
  profile_dropdown.pack(pady=5)
104
- ttk.Button(selection_window, text="OK", command=lambda: [selection_window.destroy(), setattr(self, 'selected_scene', profile_var.get())]).pack(pady=10)
105
- self.window.wait_window(selection_window)
143
+ ttk.Button(selection_window, text="OK",
144
+ command=lambda: [selection_window.destroy(), setattr(self, 'selected_scene', profile_var.get())],
145
+ bootstyle="primary").pack(pady=10)
146
+
147
+ self.window.wait_window(selection_window) # Wait for selection_window to close
106
148
  selected_scene = self.selected_scene
107
149
  return selected_scene
108
150
 
109
-
110
151
  def add_save_hook(self, func):
111
152
  on_save.append(func)
112
153
 
@@ -128,7 +169,8 @@ class ConfigApp:
128
169
 
129
170
  # Create a new Config instance
130
171
  config = ProfileConfig(
131
- scenes=[self.obs_scene_listbox.get(i) for i in self.obs_scene_listbox.curselection()] if self.obs_scene_listbox_changed else self.settings.scenes,
172
+ scenes=[self.obs_scene_listbox.get(i) for i in
173
+ self.obs_scene_listbox.curselection()] if self.obs_scene_listbox_changed else self.settings.scenes,
132
174
  general=General(
133
175
  use_websocket=self.websocket_enabled.get(),
134
176
  use_clipboard=self.clipboard_enabled.get(),
@@ -137,7 +179,7 @@ class ConfigApp:
137
179
  open_multimine_on_startup=self.open_multimine_on_startup.get(),
138
180
  texthook_replacement_regex=self.texthook_replacement_regex.get(),
139
181
  use_both_clipboard_and_websocket=self.use_both_clipboard_and_websocket.get(),
140
- use_old_texthooker=self.use_old_texthooker.get()
182
+ texthooker_port=int(self.texthooker_port.get())
141
183
  ),
142
184
  paths=Paths(
143
185
  folder_to_watch=self.folder_to_watch.get(),
@@ -163,10 +205,6 @@ class ConfigApp:
163
205
  overwrite_audio=self.overwrite_audio.get(),
164
206
  overwrite_picture=self.overwrite_picture.get(),
165
207
  multi_overwrites_sentence=self.multi_overwrites_sentence.get(),
166
- anki_custom_fields={
167
- key_entry.get(): value_entry.get() for key_entry, value_entry, delete_button in
168
- self.custom_field_entries if key_entry.get()
169
- }
170
208
  ),
171
209
  features=Features(
172
210
  full_auto=self.full_auto.get(),
@@ -184,9 +222,7 @@ class ConfigApp:
184
222
  extension=self.screenshot_extension.get(),
185
223
  custom_ffmpeg_settings=self.screenshot_custom_ffmpeg_settings.get(),
186
224
  screenshot_hotkey_updates_anki=self.screenshot_hotkey_update_anki.get(),
187
- seconds_after_line = float(self.seconds_after_line.get()) if self.seconds_after_line.get() else 0.0,
188
- # use_beginning_of_line_as_screenshot=self.use_beginning_of_line_as_screenshot.get(),
189
- # use_new_screenshot_logic=self.use_new_screenshot_logic.get(),
225
+ seconds_after_line=float(self.seconds_after_line.get()) if self.seconds_after_line.get() else 0.0,
190
226
  screenshot_timing_setting=self.screenshot_timing.get(),
191
227
  use_screenshot_selector=self.use_screenshot_selector.get(),
192
228
  ),
@@ -196,7 +232,7 @@ class ConfigApp:
196
232
  beginning_offset=float(self.beginning_offset.get()),
197
233
  end_offset=float(self.end_offset.get()),
198
234
  ffmpeg_reencode_options=self.audio_ffmpeg_reencode_options.get(),
199
- external_tool = self.external_tool.get(),
235
+ external_tool=self.external_tool.get(),
200
236
  anki_media_collection=self.anki_media_collection.get(),
201
237
  external_tool_enabled=self.external_tool_enabled.get(),
202
238
  pre_vad_end_offset=float(self.pre_vad_audio_offset.get()),
@@ -214,7 +250,7 @@ class ConfigApp:
214
250
  hotkeys=Hotkeys(
215
251
  reset_line=self.reset_line_hotkey.get(),
216
252
  take_screenshot=self.take_screenshot_hotkey.get(),
217
- #open_utility=self.open_utility_hotkey.get(),
253
+ # open_utility=self.open_utility_hotkey.get(),
218
254
  play_latest_audio=self.play_latest_audio_hotkey.get()
219
255
  ),
220
256
  vad=VAD(
@@ -235,7 +271,6 @@ class ConfigApp:
235
271
  video_player_path=self.video_player_path.get(),
236
272
  multi_line_line_break=self.multi_line_line_break.get(),
237
273
  multi_line_sentence_storage_field=self.multi_line_sentence_storage_field.get(),
238
- ocr_sends_to_clipboard=self.ocr_sends_to_clipboard.get(),
239
274
  use_anki_note_creation_time=self.use_anki_note_creation_time.get(),
240
275
  ocr_websocket_port=int(self.ocr_websocket_port.get()),
241
276
  texthooker_communication_websocket_port=int(self.texthooker_communication_websocket_port.get()),
@@ -259,7 +294,8 @@ class ConfigApp:
259
294
  config.audio.custom_encode_settings = self.audio_ffmpeg_reencode_options.get()
260
295
 
261
296
  if config.features.backfill_audio and config.features.full_auto:
262
- messagebox.showerror("Configuration Error", "Cannot have Full Auto and Backfill mode on at the same time! Note: Backfill is a very niche workflow.")
297
+ messagebox.showerror("Configuration Error",
298
+ "Cannot have Full Auto and Backfill mode on at the same time! Note: Backfill is a very niche workflow.")
263
299
  return
264
300
 
265
301
  if not config.general.use_websocket and not config.general.use_clipboard:
@@ -268,7 +304,7 @@ class ConfigApp:
268
304
 
269
305
  current_profile = self.profile_combobox.get()
270
306
  prev_config = self.master_config.get_config()
271
- self.master_config.switch_to_default_if_not_found=self.switch_to_default_if_not_found.get()
307
+ self.master_config.switch_to_default_if_not_found = self.switch_to_default_if_not_found.get()
272
308
  if profile_change:
273
309
  self.master_config.current_profile = current_profile
274
310
  else:
@@ -296,14 +332,12 @@ class ConfigApp:
296
332
  for func in on_save:
297
333
  func()
298
334
 
299
-
300
335
  def reload_settings(self):
301
336
  new_config = configuration.load_config()
302
337
  current_config = new_config.get_config()
303
338
 
304
339
  self.window.title("GameSentenceMiner Configuration - " + current_config.name)
305
340
 
306
-
307
341
  if current_config.name != self.settings.name or self.settings.config_changed(current_config):
308
342
  logger.info("Config changed, reloading settings.")
309
343
  self.master_config = new_config
@@ -319,11 +353,10 @@ class ConfigApp:
319
353
  self.create_screenshot_tab()
320
354
  self.create_audio_tab()
321
355
  self.create_obs_tab()
322
- self.create_hotkeys_tab()
323
356
  self.create_profiles_tab()
324
357
  self.create_advanced_tab()
325
358
  self.create_ai_tab()
326
-
359
+ self.create_help_tab() # Re-add help tab for modern styling consistent application
327
360
 
328
361
  def increment_row(self):
329
362
  """Increment the current row index and return the new value."""
@@ -339,229 +372,281 @@ class ConfigApp:
339
372
 
340
373
  @new_tab
341
374
  def create_general_tab(self):
342
- general_frame = ttk.Frame(self.notebook)
375
+ general_frame = ttk.Frame(self.notebook, padding=5)
343
376
  self.notebook.add(general_frame, text='General')
344
377
 
345
- ttk.Label(general_frame, text="Websocket Enabled:").grid(row=self.current_row, column=0, sticky='W')
378
+ HoverInfoLabelWidget(general_frame, text="Websocket Enabled:",
379
+ foreground="dark orange", font=("Helvetica", 10, "bold"),
380
+ tooltip="Enable or disable WebSocket communication. Enabling this will disable the clipboard monitor.",
381
+ row=self.current_row, column=0)
346
382
  self.websocket_enabled = tk.BooleanVar(value=self.settings.general.use_websocket)
347
- ttk.Checkbutton(general_frame, variable=self.websocket_enabled).grid(row=self.current_row, column=1,
348
- sticky='W')
349
- self.add_label_and_increment_row(general_frame, "Enable or disable WebSocket communication. Enabling this will disable the clipboard monitor. RESTART REQUIRED.",
350
- row=self.current_row, column=2)
383
+ ttk.Checkbutton(general_frame, variable=self.websocket_enabled, bootstyle="round-toggle").grid(
384
+ row=self.current_row, column=1,
385
+ sticky='W', pady=2)
386
+ self.current_row += 1
351
387
 
352
- ttk.Label(general_frame, text="Clipboard Enabled:").grid(row=self.current_row, column=0, sticky='W')
388
+ HoverInfoLabelWidget(general_frame, text="Clipboard Enabled:",
389
+ foreground="dark orange", font=("Helvetica", 10, "bold"),
390
+ tooltip="Enable or disable Clipboard monitoring.", row=self.current_row, column=0)
353
391
  self.clipboard_enabled = tk.BooleanVar(value=self.settings.general.use_clipboard)
354
- ttk.Checkbutton(general_frame, variable=self.clipboard_enabled).grid(row=self.current_row, column=1,
355
- sticky='W')
356
- self.add_label_and_increment_row(general_frame, "Enable to allow GSM to see clipboard for text and line timing.",
357
- row=self.current_row, column=2)
358
-
359
- ttk.Label(general_frame, text="Allow Both Simultaneously:").grid(row=self.current_row, column=0, sticky='W')
360
- self.use_both_clipboard_and_websocket = tk.BooleanVar(value=self.settings.general.use_both_clipboard_and_websocket)
361
- ttk.Checkbutton(general_frame, variable=self.use_both_clipboard_and_websocket).grid(row=self.current_row, column=1,
362
- sticky='W')
363
- self.add_label_and_increment_row(general_frame, "Enable to allow GSM to accept both clipboard and websocket input at the same time.",
364
- row=self.current_row, column=2)
365
-
366
- ttk.Label(general_frame, text="Websocket URI(s):").grid(row=self.current_row, column=0, sticky='W')
392
+ ttk.Checkbutton(general_frame, variable=self.clipboard_enabled, bootstyle="round-toggle").grid(
393
+ row=self.current_row, column=1,
394
+ sticky='W', pady=2)
395
+ self.current_row += 1
396
+
397
+ HoverInfoLabelWidget(general_frame, text="Allow Both Simultaneously:",
398
+ foreground="red", font=("Helvetica", 10, "bold"),
399
+ tooltip="Enable to allow GSM to accept both clipboard and websocket input at the same time.",
400
+ row=self.current_row, column=0)
401
+ self.use_both_clipboard_and_websocket = tk.BooleanVar(
402
+ value=self.settings.general.use_both_clipboard_and_websocket)
403
+ ttk.Checkbutton(general_frame, variable=self.use_both_clipboard_and_websocket, bootstyle="round-toggle").grid(
404
+ row=self.current_row, column=1,
405
+ sticky='W', pady=2)
406
+ self.current_row += 1
407
+
408
+ HoverInfoLabelWidget(general_frame, text="Websocket URI(s):",
409
+ tooltip="WebSocket URI for connecting. Allows Comma Separated Values for Connecting Multiple.",
410
+ row=self.current_row, column=0)
367
411
  self.websocket_uri = ttk.Entry(general_frame, width=50)
368
412
  self.websocket_uri.insert(0, self.settings.general.websocket_uri)
369
- self.websocket_uri.grid(row=self.current_row, column=1)
370
- self.add_label_and_increment_row(general_frame, "WebSocket URI for connecting. Allows Comma Seperated Values for Connecting Multiple.", row=self.current_row,
371
- column=2)
413
+ self.websocket_uri.grid(row=self.current_row, column=1, sticky='EW', pady=2)
414
+ self.current_row += 1
372
415
 
373
- ttk.Label(general_frame, text="TextHook Replacement Regex:").grid(row=self.current_row, column=0, sticky='W')
416
+ HoverInfoLabelWidget(general_frame, text="TextHook Replacement Regex:",
417
+ tooltip="Regex to run replacement on texthook input, set this to the same as what you may have in your texthook page.",
418
+ row=self.current_row, column=0)
374
419
  self.texthook_replacement_regex = ttk.Entry(general_frame)
375
420
  self.texthook_replacement_regex.insert(0, self.settings.general.texthook_replacement_regex)
376
- self.texthook_replacement_regex.grid(row=self.current_row, column=1)
377
- self.add_label_and_increment_row(general_frame, "Regex to run replacement on texthook input, set this to the same as what you may have in your texthook page.", row=self.current_row,
378
- column=2)
421
+ self.texthook_replacement_regex.grid(row=self.current_row, column=1, sticky='EW', pady=2)
422
+ self.current_row += 1
379
423
 
380
- ttk.Label(general_frame, text="Open Config on Startup:").grid(row=self.current_row, column=0, sticky='W')
424
+ HoverInfoLabelWidget(general_frame, text="Open Config on Startup:",
425
+ tooltip="Whether to open config when the script starts.", row=self.current_row, column=0)
381
426
  self.open_config_on_startup = tk.BooleanVar(value=self.settings.general.open_config_on_startup)
382
- ttk.Checkbutton(general_frame, variable=self.open_config_on_startup).grid(row=self.current_row, column=1,
383
- sticky='W')
384
- self.add_label_and_increment_row(general_frame, "Whether to open config when the script starts.",
385
- row=self.current_row, column=2)
427
+ ttk.Checkbutton(general_frame, variable=self.open_config_on_startup, bootstyle="round-toggle").grid(
428
+ row=self.current_row, column=1,
429
+ sticky='W', pady=2)
430
+ self.current_row += 1
386
431
 
387
- ttk.Label(general_frame, text="Open GSM Texthooker on Startup:").grid(row=self.current_row, column=0, sticky='W')
432
+ HoverInfoLabelWidget(general_frame, text="Open GSM Texthooker on Startup:",
433
+ tooltip="Whether to open Texthooking page when the script starts.", row=self.current_row,
434
+ column=0)
388
435
  self.open_multimine_on_startup = tk.BooleanVar(value=self.settings.general.open_multimine_on_startup)
389
- ttk.Checkbutton(general_frame, variable=self.open_multimine_on_startup).grid(row=self.current_row, column=1,
390
- sticky='W')
391
- self.add_label_and_increment_row(general_frame, "Whether to open Texthooking page when the script starts.",
392
- row=self.current_row, column=2)
436
+ ttk.Checkbutton(general_frame, variable=self.open_multimine_on_startup, bootstyle="round-toggle").grid(
437
+ row=self.current_row, column=1,
438
+ sticky='W', pady=2)
439
+ self.current_row += 1
393
440
 
394
- ttk.Label(general_frame, text="GSM Texthooker Port:").grid(row=self.current_row, column=0, sticky='W')
441
+ HoverInfoLabelWidget(general_frame, text="GSM Texthooker Port:",
442
+ tooltip="Port for the Texthooker to run on. Only change if you know what you are doing.",
443
+ row=self.current_row, column=0)
395
444
  self.texthooker_port = ttk.Entry(general_frame)
396
445
  self.texthooker_port.insert(0, str(self.settings.general.texthooker_port))
397
- self.texthooker_port.grid(row=self.current_row, column=1)
398
- self.add_label_and_increment_row(general_frame, "Port for the Texthooker to run on. Only change if you know what you are doing", row=self.current_row,
399
- column=2)
400
-
401
- ttk.Label(general_frame, text="Use Old Texthooker").grid(row=self.current_row, column=0, sticky='W')
402
- self.use_old_texthooker = tk.BooleanVar(value=self.settings.general.use_old_texthooker)
403
- ttk.Checkbutton(general_frame, variable=self.use_old_texthooker).grid(row=self.current_row, column=1,
404
- sticky='W')
405
- self.add_label_and_increment_row(general_frame, "Use the old Texthooker page, this option will be removed at a later date.", row=self.current_row,
406
- column=2)
407
-
408
-
409
- ttk.Label(general_frame, text="Current Version:").grid(row=self.current_row, column=0, sticky='W')
410
- self.current_version = ttk.Label(general_frame, text=get_current_version())
411
- self.current_version.grid(row=self.current_row, column=1)
412
- self.add_label_and_increment_row(general_frame, "The current version of the application.", row=self.current_row,
413
- column=2)
414
-
415
- ttk.Label(general_frame, text="Latest Version:").grid(row=self.current_row, column=0, sticky='W')
416
- self.latest_version = ttk.Label(general_frame, text=get_latest_version())
417
- self.latest_version.grid(row=self.current_row, column=1)
418
- self.add_label_and_increment_row(general_frame, "The latest available version of the application.",
419
- row=self.current_row, column=2)
420
-
421
- # ttk.Label(general_frame, text="Per Scene Config:").grid(row=self.current_row, column=0, sticky='W')
422
- # self.per_scene_config = tk.BooleanVar(value=self.master_config.per_scene_config)
423
- # ttk.Checkbutton(general_frame, variable=self.per_scene_config).grid(row=self.current_row, column=1,
424
- # sticky='W')
425
- # self.add_label_and_increment_row(general_frame, "Enable Per-Scene Config, REQUIRES RESTART. Disable to edit the DEFAULT Config.",
426
- # row=self.current_row, column=2)
446
+ self.texthooker_port.grid(row=self.current_row, column=1, sticky='EW', pady=2)
447
+ self.current_row += 1
448
+
449
+ HoverInfoLabelWidget(general_frame, text="Current Version:", bootstyle="secondary",
450
+ tooltip="The current version of the application.", row=self.current_row, column=0)
451
+ self.current_version = ttk.Label(general_frame, text=get_current_version(), bootstyle="secondary")
452
+ self.current_version.grid(row=self.current_row, column=1, sticky='W', pady=2)
453
+ self.current_row += 1
454
+
455
+ HoverInfoLabelWidget(general_frame, text="Latest Version:", bootstyle="secondary",
456
+ tooltip="The latest available version of the application.", row=self.current_row, column=0)
457
+ self.latest_version = ttk.Label(general_frame, text=get_latest_version(), bootstyle="secondary")
458
+ self.latest_version.grid(row=self.current_row, column=1, sticky='W', pady=2)
459
+ self.current_row += 1
460
+
461
+ ttk.Label(general_frame, text="Indicates important/required settings.", foreground="dark orange",
462
+ font=("Helvetica", 10, "bold")).grid(row=self.current_row, column=0, columnspan=2, sticky='W', pady=2)
463
+ self.current_row += 1
464
+ ttk.Label(general_frame, text="Highlights Advanced Features that may break things.", foreground="red",
465
+ font=("Helvetica", 10, "bold")).grid(row=self.current_row, column=0, columnspan=2, sticky='W', pady=2)
466
+ self.current_row += 1
467
+ ttk.Label(general_frame, text="Indicates Recommended, but completely optional settings.", foreground="green",
468
+ font=("Helvetica", 10, "bold")).grid(row=self.current_row, column=0, columnspan=2, sticky='W', pady=2)
469
+ self.current_row += 1
470
+ ttk.Label(general_frame, text="Every Label in settings has a tooltip with more information if you hover over them.",
471
+ font=("Helvetica", 10, "bold")).grid(row=self.current_row, column=0, columnspan=2, sticky='W', pady=2)
472
+ self.current_row += 1
473
+
474
+ general_frame.grid_columnconfigure(0, weight=0) # No expansion for the label column
475
+ general_frame.grid_columnconfigure(1, weight=0) # Entry column gets more space
476
+ for row in range(self.current_row):
477
+ general_frame.grid_rowconfigure(row, minsize=30)
427
478
 
428
479
  return general_frame
429
480
 
481
+ @new_tab
482
+ def create_required_settings_tab(self):
483
+ required_settings_frame = ttk.Frame(self.notebook)
484
+ self.notebook.add(required_settings_frame, text='Required Settings')
485
+ return required_settings_frame
486
+
430
487
  @new_tab
431
488
  def create_vad_tab(self):
432
- vad_frame = ttk.Frame(self.notebook)
489
+ vad_frame = ttk.Frame(self.notebook, padding=15)
433
490
  self.notebook.add(vad_frame, text='VAD')
434
491
 
435
- ttk.Label(vad_frame, text="Voice Detection Postprocessing:").grid(row=self.current_row, column=0, sticky='W')
492
+ HoverInfoLabelWidget(vad_frame, text="Voice Detection Postprocessing:",
493
+ tooltip="Enable post-processing of audio to trim just the voiceline.",
494
+ row=self.current_row, column=0)
436
495
  self.do_vad_postprocessing = tk.BooleanVar(
437
496
  value=self.settings.vad.do_vad_postprocessing)
438
- ttk.Checkbutton(vad_frame, variable=self.do_vad_postprocessing).grid(row=self.current_row, column=1, sticky='W')
439
- self.add_label_and_increment_row(vad_frame, "Enable post-processing of audio to trim just the voiceline.",
440
- row=self.current_row, column=2)
497
+ ttk.Checkbutton(vad_frame, variable=self.do_vad_postprocessing, bootstyle="round-toggle").grid(
498
+ row=self.current_row, column=1, sticky='W', pady=2)
499
+ self.current_row += 1
441
500
 
442
- ttk.Label(vad_frame, text="Language:").grid(row=self.current_row, column=0, sticky='W')
443
- self.language = ttk.Combobox(vad_frame, values=AVAILABLE_LANGUAGES)
501
+ HoverInfoLabelWidget(vad_frame, text="Language:",
502
+ tooltip="Select the language for VAD. This is used for Whisper and Groq (if i implemented it)",
503
+ row=self.current_row, column=0)
504
+ self.language = ttk.Combobox(vad_frame, values=AVAILABLE_LANGUAGES, state="readonly")
444
505
  self.language.set(self.settings.vad.language)
445
- self.language.grid(row=self.current_row, column=1)
446
- self.add_label_and_increment_row(vad_frame, "Select the language for VAD. This is used for Whisper and Groq (if i implemented it)", row=self.current_row,
447
- column=2)
506
+ self.language.grid(row=self.current_row, column=1, sticky='EW', pady=2)
507
+ self.current_row += 1
448
508
 
449
- ttk.Label(vad_frame, text="Whisper Model:").grid(row=self.current_row, column=0, sticky='W')
509
+ HoverInfoLabelWidget(vad_frame, text="Whisper Model:", tooltip="Select the Whisper model size for VAD.",
510
+ row=self.current_row, column=0)
450
511
  self.whisper_model = ttk.Combobox(vad_frame, values=[WHISPER_TINY, WHISPER_BASE, WHISPER_SMALL, WHISPER_MEDIUM,
451
- WHSIPER_LARGE, WHISPER_TURBO])
512
+ WHSIPER_LARGE, WHISPER_TURBO], state="readonly")
452
513
  self.whisper_model.set(self.settings.vad.whisper_model)
453
- self.whisper_model.grid(row=self.current_row, column=1)
454
- self.add_label_and_increment_row(vad_frame, "Select the Whisper model size for VAD.", row=self.current_row,
455
- column=2)
456
-
457
- ttk.Label(vad_frame, text="Vosk URL:").grid(row=self.current_row, column=0, sticky='W')
458
- self.vosk_url = ttk.Combobox(vad_frame, values=[VOSK_BASE, VOSK_SMALL])
459
- self.vosk_url.insert(0,
460
- VOSK_BASE if self.settings.vad.vosk_url == 'https://alphacephei.com/vosk/models/vosk-model-ja-0.22.zip' else VOSK_SMALL)
461
- self.vosk_url.grid(row=self.current_row, column=1)
462
- self.add_label_and_increment_row(vad_frame, "URL for connecting to the Vosk server.", row=self.current_row,
463
- column=2)
464
-
465
- ttk.Label(vad_frame, text="Select VAD Model:").grid(row=self.current_row, column=0, sticky='W')
466
- self.selected_vad_model = ttk.Combobox(vad_frame, values=[VOSK, SILERO, WHISPER, GROQ])
514
+ self.whisper_model.grid(row=self.current_row, column=1, sticky='EW', pady=2)
515
+ self.current_row += 1
516
+
517
+ HoverInfoLabelWidget(vad_frame, text="Select VAD Model:", tooltip="Select which VAD model to use.",
518
+ foreground="dark orange", font=("Helvetica", 10, "bold"), row=self.current_row, column=0)
519
+ self.selected_vad_model = ttk.Combobox(vad_frame, values=[VOSK, SILERO, WHISPER, GROQ], state="readonly")
467
520
  self.selected_vad_model.set(self.settings.vad.selected_vad_model)
468
- self.selected_vad_model.grid(row=self.current_row, column=1)
469
- self.add_label_and_increment_row(vad_frame, "Select which VAD model to use.", row=self.current_row, column=2)
521
+ self.selected_vad_model.grid(row=self.current_row, column=1, sticky='EW', pady=2)
522
+ self.current_row += 1
470
523
 
471
- ttk.Label(vad_frame, text="Backup VAD Model:").grid(row=self.current_row, column=0, sticky='W')
472
- self.backup_vad_model = ttk.Combobox(vad_frame, values=[OFF, VOSK, SILERO, WHISPER, GROQ])
524
+ HoverInfoLabelWidget(vad_frame, text="Backup VAD Model:",
525
+ tooltip="Select which model to use as a backup if no audio is found.",
526
+ row=self.current_row, column=0)
527
+ self.backup_vad_model = ttk.Combobox(vad_frame, values=[OFF, VOSK, SILERO, WHISPER, GROQ], state="readonly")
473
528
  self.backup_vad_model.set(self.settings.vad.backup_vad_model)
474
- self.backup_vad_model.grid(row=self.current_row, column=1)
475
- self.add_label_and_increment_row(vad_frame, "Select which model to use as a backup if no audio is found.",
476
- row=self.current_row, column=2)
529
+ self.backup_vad_model.grid(row=self.current_row, column=1, sticky='EW', pady=2)
530
+ self.current_row += 1
531
+
532
+ HoverInfoLabelWidget(vad_frame, text="Add Audio on No Results:",
533
+ tooltip="Add audio even if no results are found by VAD.", row=self.current_row, column=0)
534
+ self.add_audio_on_no_results = tk.BooleanVar(value=self.settings.vad.add_audio_on_no_results)
535
+ ttk.Checkbutton(vad_frame, variable=self.add_audio_on_no_results, bootstyle="round-toggle").grid(
536
+ row=self.current_row, column=1, sticky='W', pady=2)
537
+ self.current_row += 1
477
538
 
478
- ttk.Label(vad_frame, text="Trim Beginning:").grid(row=self.current_row, column=0, sticky='W')
539
+ HoverInfoLabelWidget(vad_frame, text="Audio End Offset:",
540
+ tooltip="Offset in seconds from end of the video to extract.", foreground="dark orange",
541
+ font=("Helvetica", 10, "bold"), row=self.current_row, column=0)
542
+ self.end_offset = ttk.Entry(vad_frame)
543
+ self.end_offset.insert(0, str(self.settings.audio.end_offset))
544
+ self.end_offset.grid(row=self.current_row, column=1, sticky='EW', pady=2)
545
+ self.current_row += 1
546
+
547
+ HoverInfoLabelWidget(vad_frame, text="Trim Beginning:",
548
+ tooltip='Beginning offset after VAD Trim, Only active if "Trim Beginning" is ON. Negative values = more time at the beginning',
549
+ row=self.current_row, column=0)
479
550
  self.vad_trim_beginning = tk.BooleanVar(
480
551
  value=self.settings.vad.trim_beginning)
481
- ttk.Checkbutton(vad_frame, variable=self.vad_trim_beginning).grid(row=self.current_row, column=1, sticky='W')
482
- self.add_label_and_increment_row(vad_frame, "Trim the beginning of the audio based on Voice Detection Results",
483
- row=self.current_row, column=2)
552
+ ttk.Checkbutton(vad_frame, variable=self.vad_trim_beginning, bootstyle="round-toggle").grid(
553
+ row=self.current_row, column=1, sticky='W', pady=2)
484
554
 
485
- ttk.Label(vad_frame, text="Beginning Offset After Beginning Trim:").grid(row=self.current_row, column=0, sticky='W')
555
+ HoverInfoLabelWidget(vad_frame, text="Beginning Offset:",
556
+ tooltip='Beginning offset after VAD Trim, Only active if "Trim Beginning" is ON. Negative values = more time at the beginning',
557
+ row=self.current_row, column=2)
486
558
  self.vad_beginning_offset = ttk.Entry(vad_frame)
487
559
  self.vad_beginning_offset.insert(0, str(self.settings.vad.beginning_offset))
488
- self.vad_beginning_offset.grid(row=self.current_row, column=1)
489
- self.add_label_and_increment_row(vad_frame, 'Beginning offset after VAD Trim, Only active if "Trim Beginning" is ON. Negative values = more time at the beginning', row=self.current_row, column=2)
490
-
491
- ttk.Label(vad_frame, text="Audio End Offset:").grid(row=self.current_row, column=0, sticky='W')
492
- self.end_offset = ttk.Entry(vad_frame)
493
- self.end_offset.insert(0, str(self.settings.audio.end_offset))
494
- self.end_offset.grid(row=self.current_row, column=1)
495
- self.add_label_and_increment_row(vad_frame, "Offset in seconds from end of the video to extract.",
496
- row=self.current_row, column=2)
497
-
498
- ttk.Label(vad_frame, text="Add Audio on No Results:").grid(row=self.current_row, column=0, sticky='W')
499
- self.add_audio_on_no_results = tk.BooleanVar(value=self.settings.vad.add_audio_on_no_results)
500
- ttk.Checkbutton(vad_frame, variable=self.add_audio_on_no_results).grid(row=self.current_row, column=1, sticky='W')
501
- self.add_label_and_increment_row(vad_frame, "Add audio even if no results are found by VAD.", row=self.current_row, column=2)
560
+ self.vad_beginning_offset.grid(row=self.current_row, column=3, sticky='EW', pady=2)
561
+ self.current_row += 1
502
562
 
503
- ttk.Label(vad_frame, text="Cut and Splice Segments:").grid(row=self.current_row, column=0, sticky='W')
563
+ HoverInfoLabelWidget(vad_frame, text="Cut and Splice Segments:",
564
+ tooltip="Cut Detected Voice Segments and Paste them back together. More Padding = More Space between voicelines.",
565
+ row=self.current_row, column=0)
504
566
  self.cut_and_splice_segments = tk.BooleanVar(value=self.settings.vad.cut_and_splice_segments)
505
- ttk.Checkbutton(vad_frame, variable=self.cut_and_splice_segments).grid(row=self.current_row, column=1, sticky='W')
506
- self.add_label_and_increment_row(vad_frame, "Enable to cut and splice audio segments together based on VAD results.", row=self.current_row, column=2)
507
-
508
- ttk.Label(vad_frame, text="Splice Padding (seconds):").grid(row=self.current_row, column=0, sticky='W')
567
+ ttk.Checkbutton(vad_frame, variable=self.cut_and_splice_segments, bootstyle="round-toggle").grid(
568
+ row=self.current_row, column=1, sticky='W', pady=2)
569
+ HoverInfoLabelWidget(vad_frame, text="Padding:",
570
+ tooltip="Cut Detected Voice Segments and Paste them back together. More Padding = More Space between voicelines.",
571
+ row=self.current_row, column=2)
509
572
  self.splice_padding = ttk.Entry(vad_frame)
510
573
  self.splice_padding.insert(0, str(self.settings.vad.splice_padding))
511
- self.splice_padding.grid(row=self.current_row, column=1)
512
- self.add_label_and_increment_row(vad_frame, "Padding in seconds added to spliced audio segments. WARNING: This may result in duplicated voicelines if too high!", row=self.current_row, column=2)
574
+ self.splice_padding.grid(row=self.current_row, column=3, sticky='EW', pady=2)
575
+ self.current_row += 1
513
576
 
577
+ for col in range(5):
578
+ vad_frame.grid_columnconfigure(col, weight=0)
514
579
 
580
+ for row in range(self.current_row):
581
+ vad_frame.grid_rowconfigure(row, minsize=30)
515
582
 
516
583
  @new_tab
517
584
  def create_paths_tab(self):
518
- paths_frame = ttk.Frame(self.notebook)
585
+ paths_frame = ttk.Frame(self.notebook, padding=15)
519
586
  self.notebook.add(paths_frame, text='Paths')
520
587
 
521
- ttk.Label(paths_frame, text="Folder to Watch:").grid(row=self.current_row, column=0, sticky='W')
588
+ HoverInfoLabelWidget(paths_frame, text="Folder to Watch:", tooltip="Path where the OBS Replays will be saved.",
589
+ foreground="dark orange", font=("Helvetica", 10, "bold"), row=self.current_row, column=0)
522
590
  self.folder_to_watch = ttk.Entry(paths_frame, width=50)
523
591
  self.folder_to_watch.insert(0, self.settings.paths.folder_to_watch)
524
- self.folder_to_watch.grid(row=self.current_row, column=1)
525
- ttk.Button(paths_frame, text="Browse", command=lambda: self.browse_folder(self.folder_to_watch)).grid(
592
+ self.folder_to_watch.grid(row=self.current_row, column=1, sticky='W', pady=2)
593
+ ttk.Button(paths_frame, text="Browse", command=lambda: self.browse_folder(self.folder_to_watch),
594
+ bootstyle="outline").grid(
526
595
  row=self.current_row,
527
- column=2)
528
- self.add_label_and_increment_row(paths_frame, "Path where the OBS Replays will be saved.", row=self.current_row,
529
- column=3)
596
+ column=2, padx=5, pady=2)
597
+ self.current_row += 1
530
598
 
531
- ttk.Label(paths_frame, text="Audio Destination:").grid(row=self.current_row, column=0, sticky='W')
599
+ HoverInfoLabelWidget(paths_frame, text="Audio Destination:", tooltip="Path where the cut Audio will be saved.",
600
+ foreground="dark orange", font=("Helvetica", 10, "bold"), row=self.current_row, column=0)
532
601
  self.audio_destination = ttk.Entry(paths_frame, width=50)
533
602
  self.audio_destination.insert(0, self.settings.paths.audio_destination)
534
- self.audio_destination.grid(row=self.current_row, column=1)
535
- ttk.Button(paths_frame, text="Browse", command=lambda: self.browse_folder(self.audio_destination)).grid(
603
+ self.audio_destination.grid(row=self.current_row, column=1, sticky='W', pady=2)
604
+ ttk.Button(paths_frame, text="Browse", command=lambda: self.browse_folder(self.audio_destination),
605
+ bootstyle="outline").grid(
536
606
  row=self.current_row,
537
- column=2)
538
- self.add_label_and_increment_row(paths_frame, "Path where the cut Audio will be saved.", row=self.current_row,
539
- column=3)
607
+ column=2, padx=5, pady=2)
608
+ self.current_row += 1
540
609
 
541
- ttk.Label(paths_frame, text="Screenshot Destination:").grid(row=self.current_row, column=0, sticky='W')
610
+ HoverInfoLabelWidget(paths_frame, text="Screenshot Destination:",
611
+ tooltip="Path where the Screenshot will be saved.", foreground="dark orange",
612
+ font=("Helvetica", 10, "bold"), row=self.current_row, column=0)
542
613
  self.screenshot_destination = ttk.Entry(paths_frame, width=50)
543
614
  self.screenshot_destination.insert(0, self.settings.paths.screenshot_destination)
544
- self.screenshot_destination.grid(row=self.current_row, column=1)
545
- ttk.Button(paths_frame, text="Browse", command=lambda: self.browse_folder(self.screenshot_destination)).grid(
546
- row=self.current_row, column=2)
547
- self.add_label_and_increment_row(paths_frame, "Path where the Screenshot will be saved.", row=self.current_row,
548
- column=3)
615
+ self.screenshot_destination.grid(row=self.current_row, column=1, sticky='W', pady=2)
616
+ ttk.Button(paths_frame, text="Browse", command=lambda: self.browse_folder(self.screenshot_destination),
617
+ bootstyle="outline").grid(
618
+ row=self.current_row, column=2, padx=5, pady=2)
619
+ self.current_row += 1
549
620
 
550
- ttk.Label(paths_frame, text="Remove Video:").grid(row=self.current_row, column=0, sticky='W')
621
+ HoverInfoLabelWidget(paths_frame, text="Remove Video:", tooltip="Remove video from the output.",
622
+ row=self.current_row, column=0)
551
623
  self.remove_video = tk.BooleanVar(value=self.settings.paths.remove_video)
552
- ttk.Checkbutton(paths_frame, variable=self.remove_video).grid(row=self.current_row, column=1, sticky='W')
553
- self.add_label_and_increment_row(paths_frame, "Remove video from the output.", row=self.current_row, column=2)
624
+ ttk.Checkbutton(paths_frame, variable=self.remove_video, bootstyle="round-toggle").grid(row=self.current_row,
625
+ column=1, sticky='W',
626
+ pady=2)
627
+ self.current_row += 1
554
628
 
555
- ttk.Label(paths_frame, text="Remove Audio:").grid(row=self.current_row, column=0, sticky='W')
629
+ HoverInfoLabelWidget(paths_frame, text="Remove Audio:", tooltip="Remove audio from the output.",
630
+ row=self.current_row, column=0)
556
631
  self.remove_audio = tk.BooleanVar(value=self.settings.paths.remove_audio)
557
- ttk.Checkbutton(paths_frame, variable=self.remove_audio).grid(row=self.current_row, column=1, sticky='W')
558
- self.add_label_and_increment_row(paths_frame, "Remove audio from the output.", row=self.current_row, column=2)
632
+ ttk.Checkbutton(paths_frame, variable=self.remove_audio, bootstyle="round-toggle").grid(row=self.current_row,
633
+ column=1, sticky='W',
634
+ pady=2)
635
+ self.current_row += 1
559
636
 
560
- ttk.Label(paths_frame, text="Remove Screenshot:").grid(row=self.current_row, column=0, sticky='W')
637
+ HoverInfoLabelWidget(paths_frame, text="Remove Screenshot:", tooltip="Remove screenshots after processing.",
638
+ row=self.current_row, column=0)
561
639
  self.remove_screenshot = tk.BooleanVar(value=self.settings.paths.remove_screenshot)
562
- ttk.Checkbutton(paths_frame, variable=self.remove_screenshot).grid(row=self.current_row, column=1, sticky='W')
563
- self.add_label_and_increment_row(paths_frame, "Remove screenshots after processing.", row=self.current_row,
564
- column=2)
640
+ ttk.Checkbutton(paths_frame, variable=self.remove_screenshot, bootstyle="round-toggle").grid(
641
+ row=self.current_row, column=1, sticky='W', pady=2)
642
+ self.current_row += 1
643
+
644
+ paths_frame.grid_columnconfigure(0, weight=0)
645
+ paths_frame.grid_columnconfigure(1, weight=0)
646
+ paths_frame.grid_columnconfigure(2, weight=0)
647
+
648
+ for row in range(self.current_row):
649
+ paths_frame.grid_rowconfigure(row, minsize=30)
565
650
 
566
651
  return paths_frame
567
652
 
@@ -579,182 +664,127 @@ class ConfigApp:
579
664
 
580
665
  @new_tab
581
666
  def create_anki_tab(self):
582
- anki_frame = ttk.Frame(self.notebook)
667
+ anki_frame = ttk.Frame(self.notebook, padding=15)
583
668
  self.notebook.add(anki_frame, text='Anki')
584
669
 
585
- ttk.Label(anki_frame, text="Update Anki:").grid(row=self.current_row, column=0, sticky='W')
670
+ HoverInfoLabelWidget(anki_frame, text="Update Anki:", tooltip="Automatically update Anki with new data.",
671
+ row=self.current_row, column=0)
586
672
  self.update_anki = tk.BooleanVar(value=self.settings.anki.update_anki)
587
- ttk.Checkbutton(anki_frame, variable=self.update_anki).grid(row=self.current_row, column=1, sticky='W')
588
- self.add_label_and_increment_row(anki_frame, "Automatically update Anki with new data.", row=self.current_row,
589
- column=2)
673
+ ttk.Checkbutton(anki_frame, variable=self.update_anki, bootstyle="round-toggle").grid(row=self.current_row,
674
+ column=1, sticky='W',
675
+ pady=2)
676
+ self.current_row += 1
590
677
 
591
- ttk.Label(anki_frame, text="Anki URL:").grid(row=self.current_row, column=0, sticky='W')
678
+ HoverInfoLabelWidget(anki_frame, text="Anki URL:", tooltip="The URL to connect to your Anki instance.",
679
+ foreground="dark orange", font=("Helvetica", 10, "bold"), row=self.current_row, column=0)
592
680
  self.anki_url = ttk.Entry(anki_frame, width=50)
593
681
  self.anki_url.insert(0, self.settings.anki.url)
594
- self.anki_url.grid(row=self.current_row, column=1)
595
- self.add_label_and_increment_row(anki_frame, "The URL to connect to your Anki instance.", row=self.current_row,
596
- column=2)
682
+ self.anki_url.grid(row=self.current_row, column=1, sticky='EW', pady=2)
683
+ self.current_row += 1
597
684
 
598
- ttk.Label(anki_frame, text="Sentence Field:").grid(row=self.current_row, column=0, sticky='W')
685
+ HoverInfoLabelWidget(anki_frame, text="Sentence Field:", tooltip="Field in Anki for the main sentence.",
686
+ foreground="dark orange", font=("Helvetica", 10, "bold"), row=self.current_row, column=0)
599
687
  self.sentence_field = ttk.Entry(anki_frame)
600
688
  self.sentence_field.insert(0, self.settings.anki.sentence_field)
601
- self.sentence_field.grid(row=self.current_row, column=1)
602
- self.add_label_and_increment_row(anki_frame, "Field in Anki for the main sentence.", row=self.current_row,
603
- column=2)
689
+ self.sentence_field.grid(row=self.current_row, column=1, sticky='EW', pady=2)
690
+ self.current_row += 1
604
691
 
605
- ttk.Label(anki_frame, text="Sentence Audio Field:").grid(row=self.current_row, column=0, sticky='W')
692
+ HoverInfoLabelWidget(anki_frame, text="Sentence Audio Field:",
693
+ tooltip="Field in Anki for audio associated with the sentence. Leave Blank to Disable Audio Processing.",
694
+ foreground="dark orange", font=("Helvetica", 10, "bold"), row=self.current_row, column=0)
606
695
  self.sentence_audio_field = ttk.Entry(anki_frame)
607
696
  self.sentence_audio_field.insert(0, self.settings.anki.sentence_audio_field)
608
- self.sentence_audio_field.grid(row=self.current_row, column=1)
609
- self.add_label_and_increment_row(anki_frame,
610
- "Field in Anki for audio associated with the sentence. Leave Blank to Disable Audio Processing.",
611
- row=self.current_row, column=2)
697
+ self.sentence_audio_field.grid(row=self.current_row, column=1, sticky='EW', pady=2)
698
+ self.current_row += 1
612
699
 
613
- ttk.Label(anki_frame, text="Picture Field:").grid(row=self.current_row, column=0, sticky='W')
700
+ HoverInfoLabelWidget(anki_frame, text="Picture Field:", tooltip="Field in Anki for associated pictures.",
701
+ foreground="dark orange", font=("Helvetica", 10, "bold"), row=self.current_row, column=0)
614
702
  self.picture_field = ttk.Entry(anki_frame)
615
703
  self.picture_field.insert(0, self.settings.anki.picture_field)
616
- self.picture_field.grid(row=self.current_row, column=1)
617
- self.add_label_and_increment_row(anki_frame, "Field in Anki for associated pictures.", row=self.current_row,
618
- column=2)
704
+ self.picture_field.grid(row=self.current_row, column=1, sticky='EW', pady=2)
705
+ self.current_row += 1
619
706
 
620
- ttk.Label(anki_frame, text="Word Field:").grid(row=self.current_row, column=0, sticky='W')
707
+ HoverInfoLabelWidget(anki_frame, text="Word Field:", tooltip="Field in Anki for individual words.",
708
+ foreground="dark orange", font=("Helvetica", 10, "bold"), row=self.current_row, column=0)
621
709
  self.word_field = ttk.Entry(anki_frame)
622
710
  self.word_field.insert(0, self.settings.anki.word_field)
623
- self.word_field.grid(row=self.current_row, column=1)
624
- self.add_label_and_increment_row(anki_frame, "Field in Anki for individual words.", row=self.current_row,
625
- column=2)
711
+ self.word_field.grid(row=self.current_row, column=1, sticky='EW', pady=2)
712
+ self.current_row += 1
626
713
 
627
- ttk.Label(anki_frame, text="Previous Sentence Field:").grid(row=self.current_row, column=0, sticky='W')
714
+ HoverInfoLabelWidget(anki_frame, text="Previous Sentence Field:",
715
+ tooltip="Field in Anki for the previous line of dialogue. If Empty, will not populate",
716
+ row=self.current_row, column=0)
628
717
  self.previous_sentence_field = ttk.Entry(anki_frame)
629
718
  self.previous_sentence_field.insert(0, self.settings.anki.previous_sentence_field)
630
- self.previous_sentence_field.grid(row=self.current_row, column=1)
631
- self.add_label_and_increment_row(anki_frame,
632
- "Field in Anki for the previous line of dialogue. If Empty, will not populate",
633
- row=self.current_row,
634
- column=2)
719
+ self.previous_sentence_field.grid(row=self.current_row, column=1, sticky='EW', pady=2)
720
+ self.current_row += 1
635
721
 
636
- ttk.Label(anki_frame, text="Previous VoiceLine SS Field:").grid(row=self.current_row, column=0, sticky='W')
722
+ HoverInfoLabelWidget(anki_frame, text="Previous VoiceLine SS Field:",
723
+ tooltip="Field in Anki for the screenshot of previous line. If Empty, will not populate",
724
+ row=self.current_row, column=0)
637
725
  self.previous_image_field = ttk.Entry(anki_frame)
638
726
  self.previous_image_field.insert(0, self.settings.anki.previous_image_field)
639
- self.previous_image_field.grid(row=self.current_row, column=1)
640
- self.add_label_and_increment_row(anki_frame,
641
- "Field in Anki for the screenshot of previous line. If Empty, will not populate",
642
- row=self.current_row,
643
- column=2)
727
+ self.previous_image_field.grid(row=self.current_row, column=1, sticky='EW', pady=2)
728
+ self.current_row += 1
644
729
 
645
- ttk.Label(anki_frame, text="Custom Tags:").grid(row=self.current_row, column=0, sticky='W')
730
+ HoverInfoLabelWidget(anki_frame, text="Add Tags:", tooltip="Comma-separated custom tags for the Anki cards.",
731
+ row=self.current_row, column=0)
646
732
  self.custom_tags = ttk.Entry(anki_frame, width=50)
647
733
  self.custom_tags.insert(0, ', '.join(self.settings.anki.custom_tags))
648
- self.custom_tags.grid(row=self.current_row, column=1)
649
- self.add_label_and_increment_row(anki_frame, "Comma-separated custom tags for the Anki cards.",
650
- row=self.current_row, column=2)
734
+ self.custom_tags.grid(row=self.current_row, column=1, sticky='EW', pady=2)
735
+ self.current_row += 1
651
736
 
652
- ttk.Label(anki_frame, text="Tags to work on:").grid(row=self.current_row, column=0, sticky='W')
737
+ HoverInfoLabelWidget(anki_frame, text="Tags to work on:",
738
+ tooltip="Comma-separated Tags, script will only do 1-click on cards with these tags (Recommend keep empty, or use Yomitan Profile to add custom tag from texthooker page)",
739
+ foreground="green", font=("Helvetica", 10, "bold"), row=self.current_row, column=0)
653
740
  self.tags_to_check = ttk.Entry(anki_frame, width=50)
654
741
  self.tags_to_check.insert(0, ', '.join(self.settings.anki.tags_to_check))
655
- self.tags_to_check.grid(row=self.current_row, column=1)
656
- self.add_label_and_increment_row(anki_frame,
657
- "Comma-separated Tags, script will only do 1-click on cards with these tags (Recommend keep empty, or use Yomitan Profile to add custom tag from texthooker page)",
658
- row=self.current_row, column=2)
742
+ self.tags_to_check.grid(row=self.current_row, column=1, sticky='EW', pady=2)
743
+ self.current_row += 1
659
744
 
660
- ttk.Label(anki_frame, text="Add Game Tag:").grid(row=self.current_row, column=0, sticky='W')
745
+ HoverInfoLabelWidget(anki_frame, text="Add Game as Tag:",
746
+ tooltip="Include a tag for the game on the Anki card.", foreground="green",
747
+ font=("Helvetica", 10, "bold"), row=self.current_row, column=0)
661
748
  self.add_game_tag = tk.BooleanVar(value=self.settings.anki.add_game_tag)
662
- ttk.Checkbutton(anki_frame, variable=self.add_game_tag).grid(row=self.current_row, column=1, sticky='W')
663
- self.add_label_and_increment_row(anki_frame, "Include a tag for the game on the Anki card.",
664
- row=self.current_row, column=2)
665
-
666
- ttk.Label(anki_frame, text="Polling Rate:").grid(row=self.current_row, column=0, sticky='W')
667
- self.polling_rate = ttk.Entry(anki_frame)
668
- self.polling_rate.insert(0, str(self.settings.anki.polling_rate))
669
- self.polling_rate.grid(row=self.current_row, column=1)
670
- self.add_label_and_increment_row(anki_frame, "Rate at which Anki will check for updates (in milliseconds).",
671
- row=self.current_row, column=2)
749
+ ttk.Checkbutton(anki_frame, variable=self.add_game_tag, bootstyle="round-toggle").grid(row=self.current_row,
750
+ column=1, sticky='W',
751
+ pady=2)
752
+ self.current_row += 1
672
753
 
673
- ttk.Label(anki_frame, text="Overwrite Audio:").grid(row=self.current_row, column=0, sticky='W')
754
+ HoverInfoLabelWidget(anki_frame, text="Overwrite Audio:", tooltip="Overwrite existing audio in Anki cards.",
755
+ row=self.current_row, column=0)
674
756
  self.overwrite_audio = tk.BooleanVar(
675
757
  value=self.settings.anki.overwrite_audio)
676
- ttk.Checkbutton(anki_frame, variable=self.overwrite_audio).grid(row=self.current_row, column=1, sticky='W')
677
- self.add_label_and_increment_row(anki_frame, "Overwrite existing audio in Anki cards.", row=self.current_row,
678
- column=2)
758
+ ttk.Checkbutton(anki_frame, variable=self.overwrite_audio, bootstyle="round-toggle").grid(row=self.current_row,
759
+ column=1, sticky='W',
760
+ pady=2)
761
+ self.current_row += 1
679
762
 
680
- ttk.Label(anki_frame, text="Overwrite Picture:").grid(row=self.current_row, column=0, sticky='W')
763
+ HoverInfoLabelWidget(anki_frame, text="Overwrite Picture:",
764
+ tooltip="Overwrite existing pictures in Anki cards.", row=self.current_row, column=0)
681
765
  self.overwrite_picture = tk.BooleanVar(
682
766
  value=self.settings.anki.overwrite_picture)
683
- ttk.Checkbutton(anki_frame, variable=self.overwrite_picture).grid(row=self.current_row, column=1, sticky='W')
684
- self.add_label_and_increment_row(anki_frame, "Overwrite existing pictures in Anki cards.", row=self.current_row,
685
- column=2)
767
+ ttk.Checkbutton(anki_frame, variable=self.overwrite_picture, bootstyle="round-toggle").grid(
768
+ row=self.current_row, column=1, sticky='W', pady=2)
769
+ self.current_row += 1
686
770
 
687
- ttk.Label(anki_frame, text="Multi-line Mining Overwrite Sentence:").grid(row=self.current_row, column=0, sticky='W')
771
+ HoverInfoLabelWidget(anki_frame, text="Multi-line Mining Overwrite Sentence:",
772
+ tooltip="When using Multi-line Mining, overwrite the sentence with a concatenation of the lines selected.",
773
+ row=self.current_row, column=0)
688
774
  self.multi_overwrites_sentence = tk.BooleanVar(
689
775
  value=self.settings.anki.multi_overwrites_sentence)
690
- ttk.Checkbutton(anki_frame, variable=self.multi_overwrites_sentence).grid(row=self.current_row, column=1, sticky='W')
691
- self.add_label_and_increment_row(anki_frame, "When using Multi-line Mining, overrwrite the sentence with a concatenation of the lines selected.", row=self.current_row,
692
- column=2)
693
-
694
- self.anki_custom_fields = self.settings.anki.anki_custom_fields
695
- self.custom_field_entries = []
776
+ ttk.Checkbutton(anki_frame, variable=self.multi_overwrites_sentence, bootstyle="round-toggle").grid(
777
+ row=self.current_row, column=1, sticky='W', pady=2)
778
+ self.current_row += 1
696
779
 
697
- row_at_the_time = self.current_row + 1
780
+ anki_frame.grid_columnconfigure(0, weight=0)
781
+ anki_frame.grid_columnconfigure(1, weight=0)
698
782
 
699
- ttk.Button(anki_frame, text="Add Field",
700
- command=lambda: self.add_custom_field(anki_frame, row_at_the_time)).grid(row=self.current_row,
701
- column=0, pady=5)
702
- self.add_label_and_increment_row(anki_frame, "Add a new custom field for Anki cards.", row=self.current_row,
703
- column=2)
704
- self.display_custom_fields(anki_frame, self.current_row)
783
+ for row in range(self.current_row):
784
+ anki_frame.grid_rowconfigure(row, minsize=30)
705
785
 
706
786
  return anki_frame
707
787
 
708
- def add_custom_field(self, frame, start_row):
709
- row = len(self.custom_field_entries) + 1 + start_row
710
-
711
- key_entry = ttk.Entry(frame)
712
- key_entry.grid(row=row, column=0, padx=5, pady=2, sticky='W')
713
- value_entry = ttk.Entry(frame)
714
- value_entry.grid(row=row, column=1, padx=5, pady=2, sticky='W')
715
-
716
- # Create a delete button for this custom field
717
- delete_button = ttk.Button(frame, text="X",
718
- command=lambda: self.delete_custom_field(row, key_entry, value_entry, delete_button))
719
- delete_button.grid(row=row, column=2, padx=5, pady=2)
720
-
721
- self.custom_field_entries.append((key_entry, value_entry, delete_button))
722
-
723
- def display_custom_fields(self, frame, start_row):
724
- for row, (key, value) in enumerate(self.anki_custom_fields.items()):
725
- key_entry = ttk.Entry(frame)
726
- key_entry.insert(0, key)
727
- key_entry.grid(row=row + start_row, column=0, padx=5, pady=2, sticky='W')
728
-
729
- value_entry = ttk.Entry(frame)
730
- value_entry.insert(0, value)
731
- value_entry.grid(row=row + start_row, column=1, padx=5, pady=2, sticky='W')
732
-
733
- # Create a delete button for each existing custom field
734
- delete_button = ttk.Button(frame, text="X",
735
- command=lambda: self.delete_custom_field(row + start_row, key_entry, value_entry,
736
- delete_button))
737
- delete_button.grid(row=row + start_row, column=2, padx=5, pady=2)
738
-
739
- self.custom_field_entries.append((key_entry, value_entry, delete_button))
740
-
741
- def delete_custom_field(self, row, key_entry, value_entry, delete_button):
742
- # Remove the entry from the GUI
743
- key_entry.destroy()
744
- value_entry.destroy()
745
- delete_button.destroy()
746
-
747
- # Remove the entry from the custom field entries list
748
- self.custom_field_entries.remove((key_entry, value_entry, delete_button))
749
-
750
- # Update the GUI rows below to fill the gap if necessary
751
- # for (ke, ve, db) in self.custom_field_entries:
752
- # if self.custom_field_entries.index((ke, ve, db)) > self.custom_field_entries.index(
753
- # (key_entry, value_entry, delete_button)):
754
- # ke.grid_configure(row=ke.grid_info()['row'] - 1)
755
- # ve.grid_configure(row=ve.grid_info()['row'] - 1)
756
- # db.grid_configure(row=db.grid_info()['row'] - 1)
757
-
758
788
  def on_profiles_tab_selected(self, event):
759
789
  try:
760
790
  if self.window.state() != "withdrawn" and self.notebook.tab(self.notebook.select(), "text") == "Profiles":
@@ -764,129 +794,155 @@ class ConfigApp:
764
794
 
765
795
  @new_tab
766
796
  def create_features_tab(self):
767
- features_frame = ttk.Frame(self.notebook)
797
+ features_frame = ttk.Frame(self.notebook, padding=15)
768
798
  self.notebook.add(features_frame, text='Features')
769
799
 
770
- ttk.Label(features_frame, text="Notify on Update:").grid(row=self.current_row, column=0, sticky='W')
800
+ HoverInfoLabelWidget(features_frame, text="Notify on Update:", tooltip="Notify the user when an update occurs.",
801
+ row=self.current_row, column=0)
771
802
  self.notify_on_update = tk.BooleanVar(value=self.settings.features.notify_on_update)
772
- ttk.Checkbutton(features_frame, variable=self.notify_on_update).grid(row=self.current_row, column=1, sticky='W')
773
- self.add_label_and_increment_row(features_frame, "Notify the user when an update occurs.", row=self.current_row,
774
- column=2)
803
+ ttk.Checkbutton(features_frame, variable=self.notify_on_update, bootstyle="round-toggle").grid(
804
+ row=self.current_row, column=1, sticky='W', pady=2)
805
+ self.current_row += 1
775
806
 
776
- ttk.Label(features_frame, text="Open Anki Edit:").grid(row=self.current_row, column=0, sticky='W')
807
+ HoverInfoLabelWidget(features_frame, text="Open Anki Edit:",
808
+ tooltip="Automatically open Anki for editing after updating.", row=self.current_row,
809
+ column=0)
777
810
  self.open_anki_edit = tk.BooleanVar(value=self.settings.features.open_anki_edit)
778
- ttk.Checkbutton(features_frame, variable=self.open_anki_edit).grid(row=self.current_row, column=1, sticky='W')
779
- self.add_label_and_increment_row(features_frame, "Automatically open Anki for editing after updating.",
780
- row=self.current_row, column=2)
811
+ ttk.Checkbutton(features_frame, variable=self.open_anki_edit, bootstyle="round-toggle").grid(
812
+ row=self.current_row, column=1, sticky='W', pady=2)
813
+ self.current_row += 1
781
814
 
782
- ttk.Label(features_frame, text="Open Anki Note in Browser:").grid(row=self.current_row, column=0, sticky='W')
815
+ HoverInfoLabelWidget(features_frame, text="Open Anki Note in Browser:",
816
+ tooltip="Open Anki note in browser after updating.", row=self.current_row, column=0)
783
817
  self.open_anki_browser = tk.BooleanVar(value=self.settings.features.open_anki_in_browser)
784
- ttk.Checkbutton(features_frame, variable=self.open_anki_browser).grid(row=self.current_row, column=1, sticky='W')
785
- self.add_label_and_increment_row(features_frame, "Open Anki note in browser after updating.", row=self.current_row,
786
- column=2)
818
+ ttk.Checkbutton(features_frame, variable=self.open_anki_browser, bootstyle="round-toggle").grid(
819
+ row=self.current_row, column=1, sticky='W', pady=2)
820
+ self.current_row += 1
787
821
 
788
- ttk.Label(features_frame, text="Browser Query:").grid(row=self.current_row, column=0, sticky='W')
822
+ HoverInfoLabelWidget(features_frame, text="Browser Query:",
823
+ tooltip="Query to use when opening Anki notes in the browser. Ex: 'Added:1'",
824
+ row=self.current_row, column=0)
789
825
  self.browser_query = ttk.Entry(features_frame, width=50)
790
826
  self.browser_query.insert(0, self.settings.features.browser_query)
791
- self.browser_query.grid(row=self.current_row, column=1)
792
- self.add_label_and_increment_row(features_frame, "Query to use when opening Anki notes in the browser. Ex: 'Added:1'", row=self.current_row,
793
- column=2)
827
+ self.browser_query.grid(row=self.current_row, column=1, sticky='EW', pady=2)
828
+ self.current_row += 1
794
829
 
795
- ttk.Label(features_frame, text="Backfill Audio:").grid(row=self.current_row, column=0, sticky='W')
830
+ HoverInfoLabelWidget(features_frame, text="Backfill Audio:", tooltip="Fill in audio data for existing entries.",
831
+ row=self.current_row, column=0)
796
832
  self.backfill_audio = tk.BooleanVar(value=self.settings.features.backfill_audio)
797
- ttk.Checkbutton(features_frame, variable=self.backfill_audio).grid(row=self.current_row, column=1, sticky='W')
798
- self.add_label_and_increment_row(features_frame, "Fill in audio data for existing entries.",
799
- row=self.current_row, column=2)
833
+ ttk.Checkbutton(features_frame, variable=self.backfill_audio, bootstyle="round-toggle").grid(
834
+ row=self.current_row, column=1, sticky='W', pady=2)
835
+ self.current_row += 1
800
836
 
801
- ttk.Label(features_frame, text="Full Auto Mode:").grid(row=self.current_row, column=0, sticky='W')
837
+ HoverInfoLabelWidget(features_frame, text="Full Auto Mode:", tooltip="Yomitan 1-click anki card creation.",
838
+ row=self.current_row, column=0)
802
839
  self.full_auto = tk.BooleanVar(
803
840
  value=self.settings.features.full_auto)
804
- ttk.Checkbutton(features_frame, variable=self.full_auto).grid(row=self.current_row, column=1, sticky='W')
805
- self.add_label_and_increment_row(features_frame, "Yomitan 1-click anki card creation.", row=self.current_row,
806
- column=2)
841
+ ttk.Checkbutton(features_frame, variable=self.full_auto, bootstyle="round-toggle").grid(row=self.current_row,
842
+ column=1, sticky='W',
843
+ pady=2)
844
+ self.current_row += 1
845
+
846
+ for col in range(3):
847
+ features_frame.grid_columnconfigure(col, weight=0)
848
+
849
+ for row in range(self.current_row):
850
+ features_frame.grid_rowconfigure(row, minsize=30)
807
851
 
808
852
  @new_tab
809
853
  def create_screenshot_tab(self):
810
- screenshot_frame = ttk.Frame(self.notebook)
854
+ screenshot_frame = ttk.Frame(self.notebook, padding=15)
811
855
  self.notebook.add(screenshot_frame, text='Screenshot')
812
856
 
813
- ttk.Label(screenshot_frame, text="Enabled:").grid(row=self.current_row, column=0, sticky='W')
857
+ HoverInfoLabelWidget(screenshot_frame, text="Enabled:", tooltip="Enable or disable screenshot processing.",
858
+ row=self.current_row, column=0)
814
859
  self.screenshot_enabled = tk.BooleanVar(value=self.settings.screenshot.enabled)
815
- ttk.Checkbutton(screenshot_frame, variable=self.screenshot_enabled).grid(row=self.current_row, column=1, sticky='W')
816
- self.add_label_and_increment_row(screenshot_frame, "Enable or disable screenshot processing.", row=self.current_row, column=2)
860
+ ttk.Checkbutton(screenshot_frame, variable=self.screenshot_enabled, bootstyle="round-toggle").grid(
861
+ row=self.current_row, column=1, sticky='W', pady=2)
862
+ self.current_row += 1
817
863
 
818
- ttk.Label(screenshot_frame, text="Width:").grid(row=self.current_row, column=0, sticky='W')
864
+ HoverInfoLabelWidget(screenshot_frame, text="Width:", tooltip="Width of the screenshot in pixels.",
865
+ row=self.current_row, column=0)
819
866
  self.screenshot_width = ttk.Entry(screenshot_frame)
820
867
  self.screenshot_width.insert(0, str(self.settings.screenshot.width))
821
- self.screenshot_width.grid(row=self.current_row, column=1)
822
- self.add_label_and_increment_row(screenshot_frame, "Width of the screenshot in pixels.", row=self.current_row,
823
- column=2)
868
+ self.screenshot_width.grid(row=self.current_row, column=1, sticky='EW', pady=2)
869
+ self.current_row += 1
824
870
 
825
- ttk.Label(screenshot_frame, text="Height:").grid(row=self.current_row, column=0, sticky='W')
871
+ HoverInfoLabelWidget(screenshot_frame, text="Height:", tooltip="Height of the screenshot in pixels.",
872
+ row=self.current_row, column=0)
826
873
  self.screenshot_height = ttk.Entry(screenshot_frame)
827
874
  self.screenshot_height.insert(0, str(self.settings.screenshot.height))
828
- self.screenshot_height.grid(row=self.current_row, column=1)
829
- self.add_label_and_increment_row(screenshot_frame, "Height of the screenshot in pixels.", row=self.current_row,
830
- column=2)
875
+ self.screenshot_height.grid(row=self.current_row, column=1, sticky='EW', pady=2)
876
+ self.current_row += 1
831
877
 
832
- ttk.Label(screenshot_frame, text="Quality:").grid(row=self.current_row, column=0, sticky='W')
878
+ HoverInfoLabelWidget(screenshot_frame, text="Quality:", tooltip="Quality of the screenshot (0-100).",
879
+ row=self.current_row, column=0)
833
880
  self.screenshot_quality = ttk.Entry(screenshot_frame)
834
881
  self.screenshot_quality.insert(0, str(self.settings.screenshot.quality))
835
- self.screenshot_quality.grid(row=self.current_row, column=1)
836
- self.add_label_and_increment_row(screenshot_frame, "Quality of the screenshot (0-100).", row=self.current_row,
837
- column=2)
838
-
839
- ttk.Label(screenshot_frame, text="Extension:").grid(row=self.current_row, column=0, sticky='W')
840
- self.screenshot_extension = ttk.Combobox(screenshot_frame, values=['webp', 'avif', 'png', 'jpeg'])
841
- self.screenshot_extension.insert(0, self.settings.screenshot.extension)
842
- self.screenshot_extension.grid(row=self.current_row, column=1)
843
- self.add_label_and_increment_row(screenshot_frame, "File extension for the screenshot format.",
844
- row=self.current_row, column=2)
845
-
846
- ttk.Label(screenshot_frame, text="FFmpeg Reencode Options:").grid(row=self.current_row, column=0, sticky='W')
847
- self.screenshot_custom_ffmpeg_settings = ttk.Entry(screenshot_frame, width=50)
848
- self.screenshot_custom_ffmpeg_settings.insert(0, self.settings.screenshot.custom_ffmpeg_settings)
849
- self.screenshot_custom_ffmpeg_settings.grid(row=self.current_row, column=1)
850
- self.add_label_and_increment_row(screenshot_frame, "Custom FFmpeg options for re-encoding screenshots.",
851
- row=self.current_row, column=2)
882
+ self.screenshot_quality.grid(row=self.current_row, column=1, sticky='EW', pady=2)
883
+ self.current_row += 1
852
884
 
885
+ HoverInfoLabelWidget(screenshot_frame, text="Extension:", tooltip="File extension for the screenshot format.",
886
+ row=self.current_row, column=0)
887
+ self.screenshot_extension = ttk.Combobox(screenshot_frame, values=['webp', 'avif', 'png', 'jpeg'],
888
+ state="readonly")
889
+ self.screenshot_extension.set(self.settings.screenshot.extension)
890
+ self.screenshot_extension.grid(row=self.current_row, column=1, sticky='EW', pady=2)
891
+ self.current_row += 1
853
892
 
854
- ttk.Label(screenshot_frame, text="Screenshot Hotkey Updates Anki:").grid(row=self.current_row, column=0, sticky='W')
855
- self.screenshot_hotkey_update_anki = tk.BooleanVar(value=self.settings.screenshot.screenshot_hotkey_updates_anki)
856
- ttk.Checkbutton(screenshot_frame, variable=self.screenshot_hotkey_update_anki).grid(row=self.current_row, column=1, sticky='W')
857
- self.add_label_and_increment_row(screenshot_frame, "Enable to allow Screenshot hotkey/button to update the latest anki card.", row=self.current_row,
858
- column=2)
893
+ HoverInfoLabelWidget(screenshot_frame, text="FFmpeg Reencode Options:",
894
+ tooltip="Custom FFmpeg options for re-encoding screenshots.", foreground="red",
895
+ font=("Helvetica", 10, "bold"), row=self.current_row, column=0)
896
+ self.screenshot_custom_ffmpeg_settings = ttk.Entry(screenshot_frame, width=50)
897
+ self.screenshot_custom_ffmpeg_settings.insert(0, self.settings.screenshot.custom_ffmpeg_settings)
898
+ self.screenshot_custom_ffmpeg_settings.grid(row=self.current_row, column=1, sticky='EW', pady=2)
899
+ self.current_row += 1
859
900
 
860
- ttk.Label(screenshot_frame, text="Screenshot Timing:").grid(row=self.current_row, column=0, sticky='W')
861
- self.screenshot_timing = ttk.Combobox(screenshot_frame, values=['beginning', 'middle', 'end'])
862
- self.screenshot_timing.insert(0, self.settings.screenshot.screenshot_timing_setting)
863
- self.screenshot_timing.grid(row=self.current_row, column=1)
864
- self.add_label_and_increment_row(screenshot_frame, "Select when to take the screenshot relative to the line: beginning, middle, or end.", row=self.current_row,
865
- column=2)
901
+ HoverInfoLabelWidget(screenshot_frame, text="Screenshot Timing:",
902
+ tooltip="Select when to take the screenshot relative to the line: beginning, middle, or end.",
903
+ row=self.current_row, column=0)
904
+ self.screenshot_timing = ttk.Combobox(screenshot_frame, values=['beginning', 'middle', 'end'], state="readonly")
905
+ self.screenshot_timing.set(self.settings.screenshot.screenshot_timing_setting)
906
+ self.screenshot_timing.grid(row=self.current_row, column=1, sticky='EW', pady=2)
907
+ self.current_row += 1
866
908
 
867
- ttk.Label(screenshot_frame, text="Screenshot Offset:").grid(row=self.current_row, column=0, sticky='W')
909
+ HoverInfoLabelWidget(screenshot_frame, text="Screenshot Offset:",
910
+ tooltip="Time in seconds to offset the screenshot based on the Timing setting above (should almost always be positive, can be negative if you use \"middle\")",
911
+ foreground="dark orange", font=("Helvetica", 10, "bold"), row=self.current_row, column=0)
868
912
  self.seconds_after_line = ttk.Entry(screenshot_frame)
869
913
  self.seconds_after_line.insert(0, str(self.settings.screenshot.seconds_after_line))
870
- self.seconds_after_line.grid(row=self.current_row, column=1)
871
- self.add_label_and_increment_row(screenshot_frame, "Time in seconds to offset the screenshot based on the Timing setting above (should almost always be positive, can be negative if you use \"middle\")", row=self.current_row,
872
- column=2)
914
+ self.seconds_after_line.grid(row=self.current_row, column=1, sticky='EW', pady=2)
915
+ self.current_row += 1
873
916
 
874
- ttk.Label(screenshot_frame, text="Use Screenshot Selector for every card:").grid(row=self.current_row, column=0, sticky='W')
917
+ HoverInfoLabelWidget(screenshot_frame, text="Use Screenshot Selector for every card:",
918
+ tooltip="Enable to use the screenshot selector to choose the screenshot point on every card.",
919
+ row=self.current_row, column=0)
875
920
  self.use_screenshot_selector = tk.BooleanVar(value=self.settings.screenshot.use_screenshot_selector)
876
- ttk.Checkbutton(screenshot_frame, variable=self.use_screenshot_selector).grid(row=self.current_row, column=1, sticky='W')
877
- self.add_label_and_increment_row(screenshot_frame, "Enable to use the screenshot selector to choose the screenshot point on every card.", row=self.current_row,
878
- column=2)
879
-
880
- # ttk.Label(screenshot_frame, text="Use Beginning of Line as Screenshot:").grid(row=self.current_row, column=0, sticky='W')
881
- # self.use_beginning_of_line_as_screenshot = tk.BooleanVar(value=self.settings.screenshot.use_beginning_of_line_as_screenshot)
882
- # ttk.Checkbutton(screenshot_frame, variable=self.use_beginning_of_line_as_screenshot).grid(row=self.current_row, column=1, sticky='W')
883
- # self.add_label_and_increment_row(screenshot_frame, "Enable to use the beginning of the line as the screenshot point. Adjust the above setting to fine-tine timing.", row=self.current_row, column=2)
884
- #
885
- # ttk.Label(screenshot_frame, text="Use alternative screenshot logic:").grid(row=self.current_row, column=0, sticky='W')
886
- # self.use_new_screenshot_logic = tk.BooleanVar(value=self.settings.screenshot.use_new_screenshot_logic)
887
- # ttk.Checkbutton(screenshot_frame, variable=self.use_new_screenshot_logic).grid(row=self.current_row, column=1, sticky='W')
888
- # self.add_label_and_increment_row(screenshot_frame, "Enable to use the new screenshot logic. This will try to take the screenshot in the middle of the voiceline, or middle of the line if no audio/vad.", row=self.current_row, column=2)
889
- #
921
+ ttk.Checkbutton(screenshot_frame, variable=self.use_screenshot_selector, bootstyle="round-toggle").grid(
922
+ row=self.current_row, column=1, sticky='W', pady=2)
923
+ self.current_row += 1
924
+
925
+ HoverInfoLabelWidget(screenshot_frame, text="Take Screenshot Hotkey:", tooltip="Hotkey to take a screenshot.",
926
+ row=self.current_row, column=0)
927
+ self.take_screenshot_hotkey = ttk.Entry(screenshot_frame)
928
+ self.take_screenshot_hotkey.insert(0, self.settings.hotkeys.take_screenshot)
929
+ self.take_screenshot_hotkey.grid(row=self.current_row, column=1, sticky='EW', pady=2)
930
+ self.current_row += 1
931
+
932
+ HoverInfoLabelWidget(screenshot_frame, text="Screenshot Hotkey Updates Anki:",
933
+ tooltip="Enable to allow Screenshot hotkey/button to update the latest anki card.",
934
+ row=self.current_row, column=0)
935
+ self.screenshot_hotkey_update_anki = tk.BooleanVar(
936
+ value=self.settings.screenshot.screenshot_hotkey_updates_anki)
937
+ ttk.Checkbutton(screenshot_frame, variable=self.screenshot_hotkey_update_anki, bootstyle="round-toggle").grid(
938
+ row=self.current_row, column=1, sticky='W', pady=2)
939
+ self.current_row += 1
940
+
941
+ for col in range(3):
942
+ screenshot_frame.grid_columnconfigure(col, weight=0)
943
+
944
+ for row in range(self.current_row):
945
+ screenshot_frame.grid_rowconfigure(row, minsize=30)
890
946
 
891
947
  def update_audio_ffmpeg_settings(self, event):
892
948
  selected_option = self.ffmpeg_audio_preset_options.get()
@@ -899,209 +955,281 @@ class ConfigApp:
899
955
 
900
956
  @new_tab
901
957
  def create_audio_tab(self):
902
- audio_frame = ttk.Frame(self.notebook)
958
+ audio_frame = ttk.Frame(self.notebook, padding=15)
903
959
  self.notebook.add(audio_frame, text='Audio')
904
960
 
905
- ttk.Label(audio_frame, text="Enabled:").grid(row=self.current_row, column=0, sticky='W')
961
+ HoverInfoLabelWidget(audio_frame, text="Enabled:", tooltip="Enable or disable audio processing.",
962
+ row=self.current_row, column=0)
906
963
  self.audio_enabled = tk.BooleanVar(value=self.settings.audio.enabled)
907
- ttk.Checkbutton(audio_frame, variable=self.audio_enabled).grid(row=self.current_row, column=1, sticky='W')
908
- self.add_label_and_increment_row(audio_frame, "Enable or disable audio processing.", row=self.current_row, column=2)
909
-
910
- ttk.Label(audio_frame, text="Audio Extension:").grid(row=self.current_row, column=0, sticky='W')
911
- self.audio_extension = ttk.Combobox(audio_frame, values=['opus', 'mp3', 'ogg', 'aac', 'm4a'])
912
- self.audio_extension.insert(0, self.settings.audio.extension)
913
- self.audio_extension.grid(row=self.current_row, column=1)
914
- self.add_label_and_increment_row(audio_frame, "File extension for audio files.", row=self.current_row, column=2)
964
+ ttk.Checkbutton(audio_frame, variable=self.audio_enabled, bootstyle="round-toggle").grid(row=self.current_row,
965
+ column=1, sticky='W',
966
+ pady=2)
967
+ self.current_row += 1
915
968
 
969
+ HoverInfoLabelWidget(audio_frame, text="Audio Extension:", tooltip="File extension for audio files.",
970
+ row=self.current_row, column=0)
971
+ self.audio_extension = ttk.Combobox(audio_frame, values=['opus', 'mp3', 'ogg', 'aac', 'm4a'], state="readonly")
972
+ self.audio_extension.set(self.settings.audio.extension)
973
+ self.audio_extension.grid(row=self.current_row, column=1, sticky='EW', pady=2)
974
+ self.current_row += 1
916
975
 
917
- ttk.Label(audio_frame, text="Audio Extraction Beginning Offset:").grid(row=self.current_row, column=0, sticky='W')
976
+ HoverInfoLabelWidget(audio_frame, text="Audio Extraction Beginning Offset:",
977
+ tooltip="Offset in seconds from beginning of the video to extract (Should Usually be negative or 0).",
978
+ foreground="dark orange", font=("Helvetica", 10, "bold"), row=self.current_row, column=0)
918
979
  self.beginning_offset = ttk.Entry(audio_frame)
919
980
  self.beginning_offset.insert(0, str(self.settings.audio.beginning_offset))
920
- self.beginning_offset.grid(row=self.current_row, column=1)
921
- self.add_label_and_increment_row(audio_frame, "Offset in seconds from beginning of the video to extract",
922
- row=self.current_row, column=2)
981
+ self.beginning_offset.grid(row=self.current_row, column=1, sticky='EW', pady=2)
982
+
983
+ ttk.Button(audio_frame, text="Find Offset (WIP)", command=self.call_audio_offset_selector, bootstyle="info").grid(
984
+ row=self.current_row, column=2, sticky='EW', pady=2, padx=5)
985
+
986
+ self.current_row += 1
923
987
 
924
- ttk.Label(audio_frame, text="Audio Extraction End Offset:").grid(row=self.current_row, column=0, sticky='W')
988
+
989
+ HoverInfoLabelWidget(audio_frame, text="Audio Extraction End Offset:",
990
+ tooltip="Offset in seconds to trim from the end before VAD processing starts. Warning: May Result in lost audio if negative.",
991
+ foreground="red", font=("Helvetica", 10, "bold"), row=self.current_row, column=0)
925
992
  self.pre_vad_audio_offset = ttk.Entry(audio_frame)
926
993
  self.pre_vad_audio_offset.insert(0, str(self.settings.audio.pre_vad_end_offset))
927
- self.pre_vad_audio_offset.grid(row=self.current_row, column=1)
928
- self.add_label_and_increment_row(audio_frame, "Offset in seconds to trim from the end before VAD processing starts. Negative = Less time on the end of the pre-vad trimmed audio (should usually be negative)",
929
- row=self.current_row, column=2)
930
-
994
+ self.pre_vad_audio_offset.grid(row=self.current_row, column=1, sticky='EW', pady=2)
995
+ self.current_row += 1
931
996
 
932
- ttk.Label(audio_frame, text="FFmpeg Preset Options:").grid(row=self.current_row, column=0, sticky='W')
997
+ HoverInfoLabelWidget(audio_frame, text="FFmpeg Preset Options:",
998
+ tooltip="Select a preset FFmpeg option for re-encoding screenshots.", row=self.current_row,
999
+ column=0)
933
1000
 
934
1001
  # Define display names and their corresponding values
935
1002
  self.ffmpeg_audio_preset_options_map = {
936
- "No Re-encode" : "",
937
- "Simple Fade-in, Avoids Audio Clipping (Default)": "-c:a libopus -f opus -af \"afade=t=in:d=0.10\"",
938
- "Simple loudness normalization (Simplest, Start Here)": "-c:a libopus -f opus -af \"loudnorm=I=-23:TP=-2,afade=t=in:d=0.10\"",
939
- "Downmix to mono with normalization (Recommended(?))": "-c:a libopus -ac 1 -f opus -af \"loudnorm=I=-23:TP=-2:dual_mono=true,afade=t=in:d=0.10\"",
940
- "Downmix to mono, 30kbps, normalized (Optimal(?))": "-c:a libopus -b:a 30k -ac 1 -f opus -af \"loudnorm=I=-23:TP=-2:dual_mono=true,afade=t=in:d=0.10\"",
1003
+ "No Re-encode": "",
1004
+ "Simple Fade-in, Avoids Audio Clipping (Default)": "-c:a {encoder} -f {format} -af \"afade=t=in:d=0.10\"",
1005
+ "Simple loudness normalization (Simplest, Start Here)": "-c:a {encoder} -f {format} -af \"loudnorm=I=-23:TP=-2,afade=t=in:d=0.10\"",
1006
+ "Downmix to mono with normalization (Recommended(?))": "-c:a {encoder} -ac 1 -f {format} -af \"loudnorm=I=-23:TP=-2:dual_mono=true,afade=t=in:d=0.10\"",
1007
+ "Downmix to mono, 30kbps, normalized (Optimal(?))": "-c:a {encoder} -b:a 30k -ac 1 -f {format} -af \"loudnorm=I=-23:TP=-2:dual_mono=true,afade=t=in:d=0.10\"",
941
1008
  "Custom": get_config().audio.custom_encode_settings,
942
1009
  }
943
1010
 
944
1011
  # Create a Combobox with display names
945
- self.ffmpeg_audio_preset_options = ttk.Combobox(audio_frame, values=list(self.ffmpeg_audio_preset_options_map.keys()), width=50)
1012
+ self.ffmpeg_audio_preset_options = ttk.Combobox(audio_frame,
1013
+ values=list(self.ffmpeg_audio_preset_options_map.keys()),
1014
+ width=50, state="readonly")
946
1015
  # self.ffmpeg_preset_options.set("Downmix to mono with normalization") # Set default display name
947
- self.ffmpeg_audio_preset_options.grid(row=self.current_row, column=1)
1016
+ self.ffmpeg_audio_preset_options.grid(row=self.current_row, column=1, sticky='EW', pady=2)
948
1017
 
949
1018
  # Bind selection to update settings
950
1019
  self.ffmpeg_audio_preset_options.bind("<<ComboboxSelected>>", self.update_audio_ffmpeg_settings)
1020
+ self.current_row += 1
951
1021
 
952
- self.add_label_and_increment_row(audio_frame, "Select a preset FFmpeg option for re-encoding screenshots.",
953
- row=self.current_row, column=2)
954
-
955
- ttk.Label(audio_frame, text="FFmpeg Reencode Options:").grid(row=self.current_row, column=0, sticky='W')
1022
+ HoverInfoLabelWidget(audio_frame, text="FFmpeg Reencode Options:",
1023
+ tooltip="Custom FFmpeg options for re-encoding audio files.", foreground="red",
1024
+ font=("Helvetica", 10, "bold"), row=self.current_row, column=0)
956
1025
  self.audio_ffmpeg_reencode_options = ttk.Entry(audio_frame, width=50)
957
1026
  self.audio_ffmpeg_reencode_options.insert(0, self.settings.audio.ffmpeg_reencode_options)
958
- self.audio_ffmpeg_reencode_options.grid(row=self.current_row, column=1)
959
- self.add_label_and_increment_row(audio_frame, "Custom FFmpeg options for re-encoding audio files.",
960
- row=self.current_row, column=2)
961
-
1027
+ self.audio_ffmpeg_reencode_options.grid(row=self.current_row, column=1, sticky='EW', pady=2)
1028
+ self.current_row += 1
962
1029
 
963
- ttk.Label(audio_frame, text="Anki Media Collection:").grid(row=self.current_row, column=0, sticky='W')
1030
+ HoverInfoLabelWidget(audio_frame, text="Anki Media Collection:",
1031
+ tooltip="Path of the Anki Media Collection, used for external Trimming tool. NO TRAILING SLASH",
1032
+ foreground="green", font=("Helvetica", 10, "bold"), row=self.current_row, column=0)
964
1033
  self.anki_media_collection = ttk.Entry(audio_frame)
965
1034
  self.anki_media_collection.insert(0, self.settings.audio.anki_media_collection)
966
- self.anki_media_collection.grid(row=self.current_row, column=1)
967
- self.add_label_and_increment_row(audio_frame,
968
- "Path of the Anki Media Collection, used for external Trimming tool. NO TRAILING SLASH",
969
- row=self.current_row,
970
- column=2)
1035
+ self.anki_media_collection.grid(row=self.current_row, column=1, sticky='EW', pady=2)
1036
+ self.current_row += 1
971
1037
 
972
- ttk.Label(audio_frame, text="External Audio Editing Tool:").grid(row=self.current_row, column=0, sticky='W')
1038
+ HoverInfoLabelWidget(audio_frame, text="External Audio Editing Tool:",
1039
+ tooltip="Path to External tool that opens the audio up for manual trimming. I recommend OcenAudio for in-place Editing.",
1040
+ foreground="green", font=("Helvetica", 10, "bold"), row=self.current_row, column=0)
973
1041
  self.external_tool = ttk.Entry(audio_frame)
974
1042
  self.external_tool.insert(0, self.settings.audio.external_tool)
975
- self.external_tool.grid(row=self.current_row, column=1)
976
- ttk.Label(audio_frame, text="Enabled:").grid(row=self.current_row, column=2, sticky='W')
1043
+ self.external_tool.grid(row=self.current_row, column=1, sticky='EW', pady=2)
977
1044
  self.external_tool_enabled = tk.BooleanVar(value=self.settings.audio.external_tool_enabled)
978
- ttk.Checkbutton(audio_frame, variable=self.external_tool_enabled).grid(row=self.current_row, column=3, sticky='W')
979
- self.add_label_and_increment_row(audio_frame,
980
- "Path to External tool that opens the audio up for manual trimming. I recommend OcenAudio for in-place Editing.",
981
- row=self.current_row,
982
- column=4)
1045
+ HoverInfoLabelWidget(audio_frame, text="Enabled:", tooltip="Send Audio to External Tool for Editing.",
1046
+ row=self.current_row, column=2, foreground="green", font=("Helvetica", 10, "bold"))
1047
+ ttk.Checkbutton(audio_frame, variable=self.external_tool_enabled, bootstyle="round-toggle").grid(
1048
+ row=self.current_row, column=3, sticky='W', padx=10, pady=5)
1049
+ self.current_row += 1
983
1050
 
984
- ttk.Button(audio_frame, text="Install Ocenaudio", command=self.download_and_install_ocen).grid(
1051
+ ttk.Button(audio_frame, text="Install Ocenaudio", command=self.download_and_install_ocen,
1052
+ bootstyle="info").grid(
985
1053
  row=self.current_row, column=0, pady=5)
986
1054
  ttk.Button(audio_frame, text="Get Anki Media Collection",
987
- command=self.set_default_anki_media_collection).grid(row=self.current_row, column=1, pady=5)
988
- self.add_label_and_increment_row(audio_frame,
989
- "These Two buttons both help set up the External Audio Editing Tool. The first one downloads and installs OcenAudio, a free audio editing software. The second one sets the default Anki media collection path.",
990
- row=self.current_row,
991
- column=3)
1055
+ command=self.set_default_anki_media_collection, bootstyle="info").grid(row=self.current_row,
1056
+ column=1, pady=5)
1057
+ self.current_row += 1
1058
+
1059
+ for col in range(5):
1060
+ audio_frame.grid_columnconfigure(col, weight=0)
1061
+
1062
+ for row in range(self.current_row):
1063
+ audio_frame.grid_rowconfigure(row, minsize=30)
1064
+
1065
+
1066
+ def call_audio_offset_selector(self):
1067
+ try:
1068
+ # if is_dev:
1069
+ # path, beginning_offset, end_offset = r"C:\Users\Beangate\GSM\Electron App\test\tmphd01whan_untrimmed.opus", 500, 0
1070
+ # else:
1071
+ path, beginning_offset, end_offset = gsm_state.previous_trim_args
1072
+ # Get the directory of the current script
1073
+ current_dir = os.path.dirname(os.path.abspath(__file__))
1074
+ # Construct the path to the audio offset selector script
1075
+ script_path = os.path.join(current_dir,
1076
+ "audio_offset_selector.py") # Replace with the actual script name if different
1077
+
1078
+ logger.info(' '.join([sys.executable, "-m", "GameSentenceMiner.util.audio_offset_selector",
1079
+ "--path", path, "--beginning_offset", str(beginning_offset), "--end_offset",
1080
+ str(end_offset)]))
1081
+
1082
+ # Run the script using subprocess.run()
1083
+ result = subprocess.run(
1084
+ [sys.executable, "-m", "GameSentenceMiner.util.audio_offset_selector",
1085
+ "--path", path, "--beginning_offset", str(beginning_offset), "--end_offset", str(end_offset)],
1086
+ capture_output=True,
1087
+ text=True, # Get output as text
1088
+ check=False # Raise an exception for non-zero exit codes
1089
+ )
1090
+ if result.returncode != 0:
1091
+ logger.error(f"Script failed with return code: {result.returncode}")
1092
+ return None
1093
+ logger.info(result)
1094
+ logger.info(f"Audio offset selector script output: {result.stdout.strip()}")
1095
+ pyperclip.copy(result.stdout.strip()) # Copy the output to clipboard
1096
+ messagebox.showinfo("Clipboard", "Offset copied to clipboard!")
1097
+ return result.stdout.strip() # Return the output
1098
+
1099
+ except subprocess.CalledProcessError as e:
1100
+ logger.error(f"Error calling script: {e}")
1101
+ logger.error(f"Script output (stderr): {e.stderr.strip()}")
1102
+ return None
1103
+ except FileNotFoundError:
1104
+ logger.error(f"Error: Script not found at {script_path}. Make sure the script name is correct.")
1105
+ return None
1106
+ except Exception as e:
1107
+ logger.error(f"An unexpected error occurred: {e}")
1108
+ return None
1109
+
992
1110
 
993
1111
  @new_tab
994
1112
  def create_obs_tab(self):
995
- obs_frame = ttk.Frame(self.notebook)
1113
+ obs_frame = ttk.Frame(self.notebook, padding=15)
996
1114
  self.notebook.add(obs_frame, text='OBS')
997
1115
 
998
- ttk.Label(obs_frame, text="Enabled:").grid(row=self.current_row, column=0, sticky='W')
1116
+ HoverInfoLabelWidget(obs_frame, text="Enabled:", tooltip="Enable or disable OBS integration.",
1117
+ row=self.current_row, column=0)
999
1118
  self.obs_enabled = tk.BooleanVar(value=self.settings.obs.enabled)
1000
- ttk.Checkbutton(obs_frame, variable=self.obs_enabled).grid(row=self.current_row, column=1, sticky='W')
1001
- self.add_label_and_increment_row(obs_frame, "Enable or disable OBS integration.", row=self.current_row,
1002
- column=2)
1119
+ ttk.Checkbutton(obs_frame, variable=self.obs_enabled, bootstyle="round-toggle").grid(row=self.current_row,
1120
+ column=1, sticky='W',
1121
+ pady=2)
1122
+ self.current_row += 1
1003
1123
 
1004
- ttk.Label(obs_frame, text="Open OBS:").grid(row=self.current_row, column=0, sticky='W')
1124
+ HoverInfoLabelWidget(obs_frame, text="Open OBS:", tooltip="Open OBS when the GSM starts.", row=self.current_row,
1125
+ column=0)
1005
1126
  self.open_obs = tk.BooleanVar(value=self.settings.obs.open_obs)
1006
- ttk.Checkbutton(obs_frame, variable=self.open_obs).grid(row=self.current_row, column=1, sticky='W')
1007
- self.add_label_and_increment_row(obs_frame, "Open OBS when the GSM starts.", row=self.current_row,
1008
- column=2)
1127
+ ttk.Checkbutton(obs_frame, variable=self.open_obs, bootstyle="round-toggle").grid(row=self.current_row,
1128
+ column=1, sticky='W', pady=2)
1129
+ self.current_row += 1
1009
1130
 
1010
- ttk.Label(obs_frame, text="Close OBS:").grid(row=self.current_row, column=0, sticky='W')
1131
+ HoverInfoLabelWidget(obs_frame, text="Close OBS:", tooltip="Close OBS when the GSM closes.",
1132
+ row=self.current_row, column=0)
1011
1133
  self.close_obs = tk.BooleanVar(value=self.settings.obs.close_obs)
1012
- ttk.Checkbutton(obs_frame, variable=self.close_obs).grid(row=self.current_row, column=1, sticky='W')
1013
- self.add_label_and_increment_row(obs_frame, "Close OBS when the GSM closes.", row=self.current_row,
1014
- column=2)
1134
+ ttk.Checkbutton(obs_frame, variable=self.close_obs, bootstyle="round-toggle").grid(row=self.current_row,
1135
+ column=1, sticky='W', pady=2)
1136
+ self.current_row += 1
1015
1137
 
1016
- ttk.Label(obs_frame, text="Host:").grid(row=self.current_row, column=0, sticky='W')
1138
+ HoverInfoLabelWidget(obs_frame, text="Host:", tooltip="Host address for the OBS WebSocket server.",
1139
+ row=self.current_row, column=0)
1017
1140
  self.obs_host = ttk.Entry(obs_frame)
1018
1141
  self.obs_host.insert(0, self.settings.obs.host)
1019
- self.obs_host.grid(row=self.current_row, column=1)
1020
- self.add_label_and_increment_row(obs_frame, "Host address for the OBS WebSocket server.", row=self.current_row,
1021
- column=2)
1142
+ self.obs_host.grid(row=self.current_row, column=1, sticky='EW', pady=2)
1143
+ self.current_row += 1
1022
1144
 
1023
- ttk.Label(obs_frame, text="Port:").grid(row=self.current_row, column=0, sticky='W')
1145
+ HoverInfoLabelWidget(obs_frame, text="Port:", tooltip="Port number for the OBS WebSocket server.",
1146
+ row=self.current_row, column=0)
1024
1147
  self.obs_port = ttk.Entry(obs_frame)
1025
1148
  self.obs_port.insert(0, str(self.settings.obs.port))
1026
- self.obs_port.grid(row=self.current_row, column=1)
1027
- self.add_label_and_increment_row(obs_frame, "Port number for the OBS WebSocket server.", row=self.current_row,
1028
- column=2)
1149
+ self.obs_port.grid(row=self.current_row, column=1, sticky='EW', pady=2)
1150
+ self.current_row += 1
1029
1151
 
1030
- ttk.Label(obs_frame, text="Password:").grid(row=self.current_row, column=0, sticky='W')
1031
- self.obs_password = ttk.Entry(obs_frame)
1152
+ HoverInfoLabelWidget(obs_frame, text="Password:", tooltip="Password for the OBS WebSocket server.",
1153
+ row=self.current_row, column=0)
1154
+ self.obs_password = ttk.Entry(obs_frame, show="*")
1032
1155
  self.obs_password.insert(0, self.settings.obs.password)
1033
- self.obs_password.grid(row=self.current_row, column=1)
1034
- self.add_label_and_increment_row(obs_frame, "Password for the OBS WebSocket server.", row=self.current_row,
1035
- column=2)
1156
+ self.obs_password.grid(row=self.current_row, column=1, sticky='EW', pady=2)
1157
+ self.current_row += 1
1036
1158
 
1037
- ttk.Label(obs_frame, text="Get Game From Scene Name:").grid(row=self.current_row, column=0, sticky='W')
1159
+ HoverInfoLabelWidget(obs_frame, text="Get Game From Scene Name:", tooltip="Changes Current Game to Scene Name",
1160
+ row=self.current_row, column=0)
1038
1161
  self.get_game_from_scene_name = tk.BooleanVar(value=self.settings.obs.get_game_from_scene)
1039
- ttk.Checkbutton(obs_frame, variable=self.get_game_from_scene_name).grid(row=self.current_row, column=1,
1040
- sticky='W')
1041
- self.add_label_and_increment_row(obs_frame, "Changes Current Game to Scene Name", row=self.current_row,
1042
- column=2)
1162
+ ttk.Checkbutton(obs_frame, variable=self.get_game_from_scene_name, bootstyle="round-toggle").grid(
1163
+ row=self.current_row, column=1,
1164
+ sticky='W', pady=2)
1165
+ self.current_row += 1
1043
1166
 
1044
- ttk.Label(obs_frame, text="Minimum Replay Size (KB):").grid(row=self.current_row, column=0, sticky='W')
1167
+ HoverInfoLabelWidget(obs_frame, text="Minimum Replay Size (KB):",
1168
+ tooltip="Minimum Replay Size for OBS Replays in KB. If Replay is Under this, Audio/Screenshot Will not be grabbed.",
1169
+ row=self.current_row, column=0)
1045
1170
  self.minimum_replay_size = ttk.Entry(obs_frame)
1046
1171
  self.minimum_replay_size.insert(0, str(self.settings.obs.minimum_replay_size))
1047
- self.minimum_replay_size.grid(row=self.current_row, column=1)
1048
- self.add_label_and_increment_row(obs_frame, "Minimum Replay Size for OBS Replays in KB. If Replay is Under this, "
1049
- "Audio/Screenshot Will not be grabbed.", row=self.current_row,
1050
- column=2)
1051
-
1052
- @new_tab
1053
- def create_hotkeys_tab(self):
1054
- hotkeys_frame = ttk.Frame(self.notebook)
1055
- self.notebook.add(hotkeys_frame, text='Hotkeys')
1056
-
1057
- ttk.Label(hotkeys_frame, text="Reset Line Hotkey:").grid(row=self.current_row, column=0, sticky='W')
1058
- self.reset_line_hotkey = ttk.Entry(hotkeys_frame)
1059
- self.reset_line_hotkey.insert(0, self.settings.hotkeys.reset_line)
1060
- self.reset_line_hotkey.grid(row=self.current_row, column=1)
1061
- self.add_label_and_increment_row(hotkeys_frame, "Hotkey to reset the current line of dialogue.",
1062
- row=self.current_row, column=2)
1172
+ self.minimum_replay_size.grid(row=self.current_row, column=1, sticky='EW', pady=2)
1173
+ self.current_row += 1
1063
1174
 
1064
- ttk.Label(hotkeys_frame, text="Take Screenshot Hotkey:").grid(row=self.current_row, column=0, sticky='W')
1065
- self.take_screenshot_hotkey = ttk.Entry(hotkeys_frame)
1066
- self.take_screenshot_hotkey.insert(0, self.settings.hotkeys.take_screenshot)
1067
- self.take_screenshot_hotkey.grid(row=self.current_row, column=1)
1068
- self.add_label_and_increment_row(hotkeys_frame, "Hotkey to take a screenshot.", row=self.current_row, column=2)
1175
+ for col in range(3):
1176
+ obs_frame.grid_columnconfigure(col, weight=0)
1069
1177
 
1178
+ for row in range(self.current_row):
1179
+ obs_frame.grid_rowconfigure(row, minsize=30)
1070
1180
 
1071
1181
  @new_tab
1072
1182
  def create_profiles_tab(self):
1073
- profiles_frame = ttk.Frame(self.notebook)
1074
-
1183
+ profiles_frame = ttk.Frame(self.notebook, padding=15)
1075
1184
  self.notebook.add(profiles_frame, text='Profiles')
1076
1185
 
1077
- ttk.Label(profiles_frame, text="Select Profile:").grid(row=self.current_row, column=0, sticky='W')
1186
+ HoverInfoLabelWidget(profiles_frame, text="Select Profile:", tooltip="Select a profile to load its settings.",
1187
+ row=self.current_row, column=0)
1078
1188
  self.profile_var = tk.StringVar(value=self.settings.name)
1079
- self.profile_combobox = ttk.Combobox(profiles_frame, textvariable=self.profile_var, values=list(self.master_config.configs.keys()))
1080
- self.profile_combobox.grid(row=self.current_row, column=1)
1189
+ self.profile_combobox = ttk.Combobox(profiles_frame, textvariable=self.profile_var,
1190
+ values=list(self.master_config.configs.keys()), state="readonly")
1191
+ self.profile_combobox.grid(row=self.current_row, column=1, sticky='EW', pady=2)
1081
1192
  self.profile_combobox.bind("<<ComboboxSelected>>", self.on_profile_change)
1082
- self.add_label_and_increment_row(profiles_frame, "Select a profile to load its settings.", row=self.current_row, column=2)
1193
+ self.current_row += 1
1083
1194
 
1084
- ttk.Button(profiles_frame, text="Add Profile", command=self.add_profile).grid(row=self.current_row, column=0, pady=5)
1085
- ttk.Button(profiles_frame, text="Copy Profile", command=self.copy_profile).grid(row=self.current_row, column=1, pady=5)
1086
- self.delete_profile_button = ttk.Button(profiles_frame, text="Delete Config", command=self.delete_profile)
1195
+ button_row = self.current_row
1196
+ ttk.Button(profiles_frame, text="Add Profile", command=self.add_profile, bootstyle="primary").grid(
1197
+ row=button_row, column=0, pady=5)
1198
+ ttk.Button(profiles_frame, text="Copy Profile", command=self.copy_profile, bootstyle="secondary").grid(
1199
+ row=button_row, column=1, pady=5)
1200
+ self.delete_profile_button = ttk.Button(profiles_frame, text="Delete Config", command=self.delete_profile,
1201
+ bootstyle="danger")
1087
1202
  if self.master_config.current_profile != DEFAULT_CONFIG:
1088
- self.delete_profile_button.grid(row=1, column=2, pady=5)
1203
+ self.delete_profile_button.grid(row=button_row, column=2, pady=5)
1089
1204
  else:
1090
1205
  self.delete_profile_button.grid_remove()
1091
1206
  self.current_row += 1
1092
1207
 
1093
- ttk.Label(profiles_frame, text="OBS Scene:").grid(row=self.current_row, column=0, sticky='W')
1208
+ HoverInfoLabelWidget(profiles_frame, text="OBS Scene (Auto Switch Profile):",
1209
+ tooltip="Select an OBS scene to associate with this profile. (Optional)",
1210
+ row=self.current_row, column=0)
1094
1211
  self.obs_scene_var = tk.StringVar(value="")
1095
- self.obs_scene_listbox = tk.Listbox(profiles_frame, listvariable=self.obs_scene_var, selectmode=tk.MULTIPLE, height=10, width=50)
1096
- self.obs_scene_listbox.grid(row=self.current_row, column=1)
1212
+ self.obs_scene_listbox = tk.Listbox(profiles_frame, listvariable=self.obs_scene_var, selectmode=tk.MULTIPLE,
1213
+ height=10, width=50, selectbackground=ttk.Style().colors.primary)
1214
+ self.obs_scene_listbox.grid(row=self.current_row, column=1, sticky='EW', pady=2)
1097
1215
  self.obs_scene_listbox.bind("<<ListboxSelect>>", self.on_obs_scene_select)
1098
- ttk.Button(profiles_frame, text="Refresh Scenes", command=self.refresh_obs_scenes).grid(row=self.current_row, column=2, pady=5)
1099
- self.add_label_and_increment_row(profiles_frame, "Select an OBS scene to associate with this profile. (Optional)", row=self.current_row, column=3)
1216
+ ttk.Button(profiles_frame, text="Refresh Scenes", command=self.refresh_obs_scenes, bootstyle="outline").grid(
1217
+ row=self.current_row, column=2, pady=5)
1218
+ self.current_row += 1
1100
1219
 
1101
- ttk.Label(profiles_frame, text="Switch To Default If Not Found:").grid(row=self.current_row, column=0, sticky='W')
1220
+ HoverInfoLabelWidget(profiles_frame, text="Switch To Default If Not Found:",
1221
+ tooltip="Enable to switch to the default profile if the selected OBS scene is not found.",
1222
+ row=self.current_row, column=0)
1102
1223
  self.switch_to_default_if_not_found = tk.BooleanVar(value=self.master_config.switch_to_default_if_not_found)
1103
- ttk.Checkbutton(profiles_frame, variable=self.switch_to_default_if_not_found).grid(row=self.current_row, column=1, sticky='W')
1104
- self.add_label_and_increment_row(profiles_frame, "Enable to switch to the default profile if the selected OBS scene is not found.", row=self.current_row, column=2)
1224
+ ttk.Checkbutton(profiles_frame, variable=self.switch_to_default_if_not_found, bootstyle="round-toggle").grid(
1225
+ row=self.current_row, column=1, sticky='W', pady=2)
1226
+ self.current_row += 1
1227
+
1228
+ for col in range(4):
1229
+ profiles_frame.grid_columnconfigure(col, weight=0)
1230
+
1231
+ for row in range(self.current_row):
1232
+ profiles_frame.grid_rowconfigure(row, minsize=30)
1105
1233
 
1106
1234
  def on_obs_scene_select(self, event):
1107
1235
  self.obs_scene_listbox_changed = True
@@ -1112,158 +1240,218 @@ class ConfigApp:
1112
1240
  self.obs_scene_listbox.delete(0, tk.END) # Clear existing items
1113
1241
  for scene_name in obs_scene_names:
1114
1242
  self.obs_scene_listbox.insert(tk.END, scene_name) # Add each scene to the Listbox
1115
- for i, scene in enumerate(eval(self.obs_scene_var.get())): # Parse the string as a tuple
1116
- if scene.strip() in self.settings.scenes: # Use strip() to remove extra spaces
1243
+ for i, scene in enumerate(obs_scene_names): # Iterate through actual scene names
1244
+ if scene.strip() in self.settings.scenes: # Check if scene is in current settings
1117
1245
  self.obs_scene_listbox.select_set(i) # Select the item in the Listbox
1118
1246
  self.obs_scene_listbox.activate(i)
1119
- self.obs_scene_listbox.update_idletasks() # Ensure the GUI reflects the changes
1247
+ self.obs_scene_listbox.update_idletasks() # Ensure the GUI reflects the changes
1120
1248
 
1121
1249
  @new_tab
1122
1250
  def create_advanced_tab(self):
1123
- advanced_frame = ttk.Frame(self.notebook)
1251
+ advanced_frame = ttk.Frame(self.notebook, padding=15)
1124
1252
  self.notebook.add(advanced_frame, text='Advanced')
1125
1253
 
1126
- ttk.Label(advanced_frame, text="Note: Only one of these will take effect, prioritizing audio.", foreground="red").grid(row=self.current_row, column=0, columnspan=3, sticky='W')
1254
+ ttk.Label(advanced_frame, text="Note: Only one of these will take effect, prioritizing audio.",
1255
+ foreground="red", font=("Helvetica", 10, "bold")).grid(row=self.current_row, column=0, columnspan=3,
1256
+ sticky='W', pady=5)
1127
1257
  self.current_row += 1
1128
1258
 
1129
- ttk.Label(advanced_frame, text="Audio Player Path:").grid(row=self.current_row, column=0, sticky='W')
1259
+ HoverInfoLabelWidget(advanced_frame, text="Audio Player Path:",
1260
+ tooltip="Path to the audio player executable. Will open the trimmed Audio",
1261
+ row=self.current_row, column=0)
1130
1262
  self.audio_player_path = ttk.Entry(advanced_frame, width=50)
1131
1263
  self.audio_player_path.insert(0, self.settings.advanced.audio_player_path)
1132
- self.audio_player_path.grid(row=self.current_row, column=1)
1133
- ttk.Button(advanced_frame, text="Browse", command=lambda: self.browse_file(self.audio_player_path)).grid(row=self.current_row, column=2)
1134
- self.add_label_and_increment_row(advanced_frame, "Path to the audio player executable. Will open the trimmed Audio", row=self.current_row, column=3)
1264
+ self.audio_player_path.grid(row=self.current_row, column=1, sticky='EW', pady=2)
1265
+ ttk.Button(advanced_frame, text="Browse", command=lambda: self.browse_file(self.audio_player_path),
1266
+ bootstyle="outline").grid(row=self.current_row, column=2, padx=5, pady=2)
1267
+ self.current_row += 1
1135
1268
 
1136
- ttk.Label(advanced_frame, text="Video Player Path:").grid(row=self.current_row, column=0, sticky='W')
1269
+ HoverInfoLabelWidget(advanced_frame, text="Video Player Path:",
1270
+ tooltip="Path to the video player executable. Will seek to the location of the line in the replay",
1271
+ row=self.current_row, column=0)
1137
1272
  self.video_player_path = ttk.Entry(advanced_frame, width=50)
1138
1273
  self.video_player_path.insert(0, self.settings.advanced.video_player_path)
1139
- self.video_player_path.grid(row=self.current_row, column=1)
1140
- ttk.Button(advanced_frame, text="Browse", command=lambda: self.browse_file(self.video_player_path)).grid(row=self.current_row, column=2)
1141
- self.add_label_and_increment_row(advanced_frame, "Path to the video player executable. Will seek to the location of the line in the replay", row=self.current_row, column=3)
1274
+ self.video_player_path.grid(row=self.current_row, column=1, sticky='EW', pady=2)
1275
+ ttk.Button(advanced_frame, text="Browse", command=lambda: self.browse_file(self.video_player_path),
1276
+ bootstyle="outline").grid(row=self.current_row, column=2, padx=5, pady=2)
1277
+ self.current_row += 1
1142
1278
 
1143
- ttk.Label(advanced_frame, text="Play Latest Video/Audio Hotkey:").grid(row=self.current_row, column=0, sticky='W')
1279
+ HoverInfoLabelWidget(advanced_frame, text="Play Latest Video/Audio Hotkey:",
1280
+ tooltip="Hotkey to trim and play the latest audio.", row=self.current_row, column=0)
1144
1281
  self.play_latest_audio_hotkey = ttk.Entry(advanced_frame)
1145
1282
  self.play_latest_audio_hotkey.insert(0, self.settings.hotkeys.play_latest_audio)
1146
- self.play_latest_audio_hotkey.grid(row=self.current_row, column=1)
1147
- self.add_label_and_increment_row(advanced_frame, "Hotkey to trim and play the latest audio.", row=self.current_row, column=2)
1283
+ self.play_latest_audio_hotkey.grid(row=self.current_row, column=1, sticky='EW', pady=2)
1284
+ self.current_row += 1
1148
1285
 
1149
- ttk.Label(advanced_frame, text="Multi-line Line-Break:").grid(row=self.current_row, column=0, sticky='W')
1286
+ HoverInfoLabelWidget(advanced_frame, text="Multi-line Line-Break:",
1287
+ tooltip="Line break for multi-line mining. This goes between each sentence",
1288
+ row=self.current_row, column=0)
1150
1289
  self.multi_line_line_break = ttk.Entry(advanced_frame)
1151
1290
  self.multi_line_line_break.insert(0, self.settings.advanced.multi_line_line_break)
1152
- self.multi_line_line_break.grid(row=self.current_row, column=1)
1153
- self.add_label_and_increment_row(advanced_frame, "Line break for multi-line mining. This goes between each sentence", row=self.current_row, column=2)
1291
+ self.multi_line_line_break.grid(row=self.current_row, column=1, sticky='EW', pady=2)
1292
+ self.current_row += 1
1154
1293
 
1155
- ttk.Label(advanced_frame, text="Multi-Line Sentence Storage Field:").grid(row=self.current_row, column=0, sticky='W')
1294
+ HoverInfoLabelWidget(advanced_frame, text="Multi-Line Sentence Storage Field:",
1295
+ tooltip="Field in Anki for storing the multi-line sentence temporarily.",
1296
+ row=self.current_row, column=0)
1156
1297
  self.multi_line_sentence_storage_field = ttk.Entry(advanced_frame)
1157
1298
  self.multi_line_sentence_storage_field.insert(0, self.settings.advanced.multi_line_sentence_storage_field)
1158
- self.multi_line_sentence_storage_field.grid(row=self.current_row, column=1)
1159
- self.add_label_and_increment_row(advanced_frame, "Field in Anki for storing the multi-line sentence temporarily.", row=self.current_row, column=2)
1160
-
1161
- ttk.Label(advanced_frame, text="OCR Sends to Clipboard:").grid(row=self.current_row, column=0, sticky='W')
1162
- self.ocr_sends_to_clipboard = tk.BooleanVar(value=self.settings.advanced.ocr_sends_to_clipboard)
1163
- ttk.Checkbutton(advanced_frame, variable=self.ocr_sends_to_clipboard).grid(row=self.current_row, column=1, sticky='W')
1164
- self.add_label_and_increment_row(advanced_frame, "Enable to send OCR results to clipboard.", row=self.current_row, column=2)
1299
+ self.multi_line_sentence_storage_field.grid(row=self.current_row, column=1, sticky='EW', pady=2)
1300
+ self.current_row += 1
1165
1301
 
1302
+ HoverInfoLabelWidget(advanced_frame, text="OCR WebSocket Port:",
1303
+ tooltip="Port for OCR WebSocket communication. GSM will also listen on this port",
1304
+ row=self.current_row, column=0)
1166
1305
  self.ocr_websocket_port = ttk.Entry(advanced_frame)
1167
1306
  self.ocr_websocket_port.insert(0, str(self.settings.advanced.ocr_websocket_port))
1168
- self.ocr_websocket_port.grid(row=self.current_row, column=1)
1169
- self.add_label_and_increment_row(advanced_frame, "Port for OCR WebSocket communication. GSM will also listen on this port", row=self.current_row, column=2)
1307
+ self.ocr_websocket_port.grid(row=self.current_row, column=1, sticky='EW', pady=2)
1308
+ self.current_row += 1
1170
1309
 
1171
- ttk.Label(advanced_frame, text="Texthooker Communication WebSocket Port:").grid(row=self.current_row, column=0, sticky='W')
1310
+ HoverInfoLabelWidget(advanced_frame, text="Texthooker Communication WebSocket Port:",
1311
+ tooltip="Port for GSM Texthooker WebSocket communication. Does nothing right now, hardcoded to 55001",
1312
+ row=self.current_row, column=0)
1172
1313
  self.texthooker_communication_websocket_port = ttk.Entry(advanced_frame)
1173
- self.texthooker_communication_websocket_port.insert(0, str(self.settings.advanced.texthooker_communication_websocket_port))
1174
- self.texthooker_communication_websocket_port.grid(row=self.current_row, column=1)
1175
- self.add_label_and_increment_row(advanced_frame, "Port for GSM Texthooker WebSocket communication. Does nothing right now, hardcoded to 55001", row=self.current_row, column=2)
1176
-
1314
+ self.texthooker_communication_websocket_port.insert(0,
1315
+ str(self.settings.advanced.texthooker_communication_websocket_port))
1316
+ self.texthooker_communication_websocket_port.grid(row=self.current_row, column=1, sticky='EW', pady=2)
1317
+ self.current_row += 1
1177
1318
 
1178
- ttk.Label(advanced_frame, text="Use Anki Creation Date for Audio Timing:").grid(row=self.current_row, column=0, sticky='W')
1319
+ HoverInfoLabelWidget(advanced_frame, text="Use Anki Creation Date for Audio Timing:",
1320
+ tooltip="Use the Anki note creation date for audio timing instead of the OBS replay time.",
1321
+ row=self.current_row, column=0)
1179
1322
  self.use_anki_note_creation_time = tk.BooleanVar(value=self.settings.advanced.use_anki_note_creation_time)
1180
- ttk.Checkbutton(advanced_frame, variable=self.use_anki_note_creation_time).grid(row=self.current_row, column=1, sticky='W')
1181
- self.add_label_and_increment_row(advanced_frame, "Use the Anki note creation date for audio timing instead of the OBS replay time.", row=self.current_row, column=2)
1323
+ ttk.Checkbutton(advanced_frame, variable=self.use_anki_note_creation_time, bootstyle="round-toggle").grid(
1324
+ row=self.current_row, column=1, sticky='W', pady=2)
1325
+ self.current_row += 1
1182
1326
 
1327
+ HoverInfoLabelWidget(advanced_frame, text="Reset Line Hotkey:",
1328
+ tooltip="Hotkey to reset the current line of dialogue.", row=self.current_row, column=0)
1329
+ self.reset_line_hotkey = ttk.Entry(advanced_frame)
1330
+ self.reset_line_hotkey.insert(0, self.settings.hotkeys.reset_line)
1331
+ self.reset_line_hotkey.grid(row=self.current_row, column=1, sticky='EW', pady=2)
1332
+ self.current_row += 1
1333
+
1334
+ HoverInfoLabelWidget(advanced_frame, text="Polling Rate:",
1335
+ tooltip="Rate at which Anki will check for updates (in milliseconds).",
1336
+ row=self.current_row, column=0)
1337
+ self.polling_rate = ttk.Entry(advanced_frame)
1338
+ self.polling_rate.insert(0, str(self.settings.anki.polling_rate))
1339
+ self.polling_rate.grid(row=self.current_row, column=1, sticky='EW', pady=2)
1340
+ self.current_row += 1
1341
+
1342
+ HoverInfoLabelWidget(advanced_frame, text="Vosk URL:", tooltip="URL for connecting to the Vosk server.",
1343
+ row=self.current_row, column=0)
1344
+ self.vosk_url = ttk.Combobox(advanced_frame, values=[VOSK_BASE, VOSK_SMALL], state="readonly")
1345
+ self.vosk_url.set(VOSK_BASE if self.settings.vad.vosk_url == 'https://alphacephei.com/vosk/models/vosk-model-ja-0.22.zip' else VOSK_SMALL)
1346
+ self.vosk_url.grid(row=self.current_row, column=1, sticky='EW', pady=2)
1347
+ self.current_row += 1
1183
1348
 
1349
+ for col in range(4):
1350
+ advanced_frame.grid_columnconfigure(col, weight=0)
1351
+
1352
+ for row in range(self.current_row):
1353
+ advanced_frame.grid_rowconfigure(row, minsize=30)
1184
1354
 
1185
1355
  @new_tab
1186
1356
  def create_ai_tab(self):
1187
- ai_frame = ttk.Frame(self.notebook)
1357
+ ai_frame = ttk.Frame(self.notebook, padding=15)
1188
1358
  self.notebook.add(ai_frame, text='AI')
1189
1359
 
1190
- ttk.Label(ai_frame, text="Enabled:").grid(row=self.current_row, column=0, sticky='W')
1360
+ HoverInfoLabelWidget(ai_frame, text="Enabled:", tooltip="Enable or disable AI integration.",
1361
+ row=self.current_row, column=0)
1191
1362
  self.ai_enabled = tk.BooleanVar(value=self.settings.ai.enabled)
1192
- ttk.Checkbutton(ai_frame, variable=self.ai_enabled).grid(row=self.current_row, column=1, sticky='W')
1193
- self.add_label_and_increment_row(ai_frame, "Enable or disable AI integration.", row=self.current_row, column=2)
1363
+ ttk.Checkbutton(ai_frame, variable=self.ai_enabled, bootstyle="round-toggle").grid(row=self.current_row,
1364
+ column=1, sticky='W', pady=2)
1365
+ self.current_row += 1
1194
1366
 
1195
- ttk.Label(ai_frame, text="Provider:").grid(row=self.current_row, column=0, sticky='W')
1196
- self.ai_provider = ttk.Combobox(ai_frame, values=['Gemini', 'Groq'])
1367
+ HoverInfoLabelWidget(ai_frame, text="Provider:", tooltip="Select the AI provider.", row=self.current_row,
1368
+ column=0)
1369
+ self.ai_provider = ttk.Combobox(ai_frame, values=['Gemini', 'Groq'], state="readonly")
1197
1370
  self.ai_provider.set(self.settings.ai.provider)
1198
- self.ai_provider.grid(row=self.current_row, column=1)
1199
- self.add_label_and_increment_row(ai_frame, "Select the AI provider.", row=self.current_row, column=2)
1371
+ self.ai_provider.grid(row=self.current_row, column=1, sticky='EW', pady=2)
1372
+ self.current_row += 1
1200
1373
 
1201
- ttk.Label(ai_frame, text="Gemini AI Model:").grid(row=self.current_row, column=0, sticky='W')
1202
- self.gemini_model = ttk.Combobox(ai_frame, values=['gemini-2.0-flash', 'gemini-2.0-flash-lite', 'gemini-2.5-pro-preview-05-06', 'gemini-2.5-flash-preview-05-20'])
1374
+ HoverInfoLabelWidget(ai_frame, text="Gemini AI Model:", tooltip="Select the AI model to use.",
1375
+ row=self.current_row, column=0)
1376
+ self.gemini_model = ttk.Combobox(ai_frame, values=['gemini-2.0-flash', 'gemini-2.0-flash-lite',
1377
+ 'gemini-2.5-pro-preview-05-06',
1378
+ 'gemini-2.5-flash-preview-05-20'], state="readonly")
1203
1379
  self.gemini_model.set(self.settings.ai.gemini_model)
1204
- self.gemini_model.grid(row=self.current_row, column=1)
1205
- self.add_label_and_increment_row(ai_frame, "Select the AI model to use.", row=self.current_row, column=2)
1206
-
1207
-
1208
- # ttk.Label(ai_frame, text="Provider:").grid(row=self.current_row, column=0, sticky='W')
1209
- # self.provider = ttk.Combobox(ai_frame,
1210
- # values=[AI_GEMINI])
1211
- # self.provider.set(self.settings.ai.provider)
1212
- # self.provider.grid(row=self.current_row, column=1)
1213
- # self.add_label_and_increment_row(ai_frame, "Select the AI provider. Currently only Gemini is supported.", row=self.current_row, column=2)
1380
+ self.gemini_model.grid(row=self.current_row, column=1, sticky='EW', pady=2)
1381
+ self.current_row += 1
1214
1382
 
1215
- ttk.Label(ai_frame, text="Gemini API Key:").grid(row=self.current_row, column=0, sticky='W')
1383
+ HoverInfoLabelWidget(ai_frame, text="Gemini API Key:",
1384
+ tooltip="API key for the selected AI provider (Gemini only currently).",
1385
+ foreground="green", font=("Helvetica", 10, "bold"), row=self.current_row, column=0)
1216
1386
  self.gemini_api_key = ttk.Entry(ai_frame, show="*") # Mask the API key for security
1217
1387
  self.gemini_api_key.insert(0, self.settings.ai.gemini_api_key)
1218
- self.gemini_api_key.grid(row=self.current_row, column=1)
1219
- self.add_label_and_increment_row(ai_frame, "API key for the selected AI provider (Gemini only currently).", row=self.current_row,
1220
- column=2)
1388
+ self.gemini_api_key.grid(row=self.current_row, column=1, sticky='EW', pady=2)
1389
+ self.current_row += 1
1221
1390
 
1222
- ttk.Label(ai_frame, text="Groq AI Model:").grid(row=self.current_row, column=0, sticky='W')
1223
- self.groq_model = ttk.Combobox(ai_frame, values=['meta-llama/llama-4-maverick-17b-128e-instruct', 'meta-llama/llama-4-scout-17b-16e-instruct', 'llama-3.1-8b-instant'])
1391
+ HoverInfoLabelWidget(ai_frame, text="Groq AI Model:", tooltip="Select the Groq AI model to use.",
1392
+ row=self.current_row, column=0)
1393
+ self.groq_model = ttk.Combobox(ai_frame, values=['meta-llama/llama-4-maverick-17b-128e-instruct',
1394
+ 'meta-llama/llama-4-scout-17b-16e-instruct',
1395
+ 'llama-3.1-8b-instant'], state="readonly")
1224
1396
  self.groq_model.set(self.settings.ai.groq_model)
1225
- self.groq_model.grid(row=self.current_row, column=1)
1226
- self.add_label_and_increment_row(ai_frame, "Select the Groq AI model to use.", row=self.current_row, column=2)
1227
-
1397
+ self.groq_model.grid(row=self.current_row, column=1, sticky='EW', pady=2)
1398
+ self.current_row += 1
1228
1399
 
1229
- ttk.Label(ai_frame, text="Groq API Key:").grid(row=self.current_row, column=0, sticky='W')
1400
+ HoverInfoLabelWidget(ai_frame, text="Groq API Key:", tooltip="API key for Groq AI provider.",
1401
+ row=self.current_row, column=0)
1230
1402
  self.groq_api_key = ttk.Entry(ai_frame, show="*") # Mask the API key for security
1231
1403
  self.groq_api_key.insert(0, self.settings.ai.groq_api_key)
1232
- self.groq_api_key.grid(row=self.current_row, column=1)
1233
- self.add_label_and_increment_row(ai_frame, "API key for Groq AI provider.", row=self.current_row,
1234
- column=2)
1404
+ self.groq_api_key.grid(row=self.current_row, column=1, sticky='EW', pady=2)
1405
+ self.current_row += 1
1235
1406
 
1236
- ttk.Label(ai_frame, text="Anki Field:").grid(row=self.current_row, column=0, sticky='W')
1407
+ HoverInfoLabelWidget(ai_frame, text="Anki Field:", tooltip="Field in Anki for AI-generated content.",
1408
+ row=self.current_row, column=0)
1237
1409
  self.ai_anki_field = ttk.Entry(ai_frame)
1238
1410
  self.ai_anki_field.insert(0, self.settings.ai.anki_field)
1239
- self.ai_anki_field.grid(row=self.current_row, column=1)
1240
- self.add_label_and_increment_row(ai_frame, "Field in Anki for AI-generated content.", row=self.current_row,
1241
- column=2)
1411
+ self.ai_anki_field.grid(row=self.current_row, column=1, sticky='EW', pady=2)
1412
+ self.current_row += 1
1242
1413
 
1243
- ttk.Label(ai_frame, text="Use Canned Translation Prompt:").grid(row=self.current_row, column=0, sticky='W')
1414
+ HoverInfoLabelWidget(ai_frame, text="Use Canned Translation Prompt:",
1415
+ tooltip="Use a pre-defined translation prompt for AI.", row=self.current_row, column=0)
1244
1416
  self.use_canned_translation_prompt = tk.BooleanVar(value=self.settings.ai.use_canned_translation_prompt)
1245
- ttk.Checkbutton(ai_frame, variable=self.use_canned_translation_prompt).grid(row=self.current_row, column=1,
1246
- sticky='W')
1247
- self.add_label_and_increment_row(ai_frame, "Use a pre-defined translation prompt for AI.", row=self.current_row,
1248
- column=2)
1417
+ ttk.Checkbutton(ai_frame, variable=self.use_canned_translation_prompt, bootstyle="round-toggle").grid(
1418
+ row=self.current_row, column=1,
1419
+ sticky='W', pady=2)
1420
+ self.current_row += 1
1249
1421
 
1250
- ttk.Label(ai_frame, text="Use Canned Context Prompt:").grid(row=self.current_row, column=0, sticky='W')
1422
+ HoverInfoLabelWidget(ai_frame, text="Use Canned Context Prompt:",
1423
+ tooltip="Use a pre-defined context prompt for AI.", row=self.current_row, column=0)
1251
1424
  self.use_canned_context_prompt = tk.BooleanVar(value=self.settings.ai.use_canned_context_prompt)
1252
- ttk.Checkbutton(ai_frame, variable=self.use_canned_context_prompt).grid(row=self.current_row, column=1,
1253
- sticky='W')
1254
- self.add_label_and_increment_row(ai_frame, "Use a pre-defined context prompt for AI.", row=self.current_row,
1255
- column=2)
1256
-
1257
- ttk.Label(ai_frame, text="Custom Prompt:").grid(row=self.current_row, column=0, sticky='W')
1425
+ ttk.Checkbutton(ai_frame, variable=self.use_canned_context_prompt, bootstyle="round-toggle").grid(
1426
+ row=self.current_row, column=1,
1427
+ sticky='W', pady=2)
1428
+ self.current_row += 1
1258
1429
 
1259
- self.custom_prompt = scrolledtext.ScrolledText(ai_frame, width=50, height=5) # Adjust height as needed
1430
+ HoverInfoLabelWidget(ai_frame, text="Custom Prompt:", tooltip="Custom prompt for AI processing.",
1431
+ row=self.current_row, column=0)
1432
+ self.custom_prompt = scrolledtext.ScrolledText(ai_frame, width=50, height=5, font=("TkDefaultFont", 9),
1433
+ relief="solid", borderwidth=1,
1434
+ highlightbackground=ttk.Style().colors.border) # Adjust height as needed
1260
1435
  self.custom_prompt.insert(tk.END, self.settings.ai.custom_prompt)
1261
- self.custom_prompt.grid(row=self.current_row, column=1)
1436
+ self.custom_prompt.grid(row=self.current_row, column=1, sticky='EW', pady=2)
1437
+ self.current_row += 1
1262
1438
 
1263
- self.add_label_and_increment_row(ai_frame, "Custom prompt for AI processing.", row=self.current_row, column=2)
1439
+ for col in range(3):
1440
+ ai_frame.grid_columnconfigure(col, weight=0)
1441
+
1442
+ for row in range(self.current_row):
1443
+ ai_frame.grid_rowconfigure(row, minsize=30)
1264
1444
 
1265
1445
  return ai_frame
1266
1446
 
1447
+ # @new_tab
1448
+ # def create_help_tab(self):
1449
+ # help_frame = ttk.Frame(self.notebook, padding=15)
1450
+ # self.notebook.add(help_frame, text='Help')
1451
+ #
1452
+ #
1453
+ #
1454
+ # help_frame.grid_columnconfigure(0, weight=1)
1267
1455
 
1268
1456
  def on_profile_change(self, event):
1269
1457
  self.save_settings(profile_change=True)
@@ -1285,9 +1473,12 @@ class ConfigApp:
1285
1473
 
1286
1474
  def copy_profile(self):
1287
1475
  source_profile = self.profile_combobox.get()
1288
- new_profile_name = simpledialog.askstring("Input", "Enter new profile name:")
1476
+ new_profile_name = simpledialog.askstring("Input", "Enter new profile name:", parent=self.window)
1289
1477
  if new_profile_name and source_profile in self.master_config.configs:
1290
- self.master_config.configs[new_profile_name] = self.master_config.configs[source_profile]
1478
+ # Deep copy the configuration to avoid shared references
1479
+ import copy
1480
+ self.master_config.configs[new_profile_name] = copy.deepcopy(self.master_config.configs[source_profile])
1481
+ self.master_config.configs[new_profile_name].name = new_profile_name # Update the name in the copied config
1291
1482
  self.profile_combobox['values'] = list(self.master_config.configs.keys())
1292
1483
  self.profile_combobox.set(new_profile_name)
1293
1484
  self.save_settings()
@@ -1300,7 +1491,9 @@ class ConfigApp:
1300
1491
  return
1301
1492
 
1302
1493
  if profile_to_delete and profile_to_delete in self.master_config.configs:
1303
- confirm = messagebox.askyesno("Confirm Delete", f"Are you sure you want to delete the profile '{profile_to_delete}'?")
1494
+ confirm = messagebox.askyesno("Confirm Delete",
1495
+ f"Are you sure you want to delete the profile '{profile_to_delete}'?",
1496
+ parent=self.window, icon='warning')
1304
1497
  if confirm:
1305
1498
  del self.master_config.configs[profile_to_delete]
1306
1499
  self.profile_combobox['values'] = list(self.master_config.configs.keys())
@@ -1309,26 +1502,31 @@ class ConfigApp:
1309
1502
  save_full_config(self.master_config)
1310
1503
  self.reload_settings()
1311
1504
 
1312
-
1313
1505
  def show_error_box(self, title, message):
1314
1506
  messagebox.showerror(title, message)
1315
1507
 
1316
1508
  def download_and_install_ocen(self):
1317
- confirm = messagebox.askyesno("Download OcenAudio?", "Would you like to download and install OcenAudio? It is a free audio editing software that works extremely well with GSM.")
1509
+ confirm = messagebox.askyesno("Download OcenAudio?",
1510
+ "Would you like to download and install OcenAudio? It is a free audio editing software that works extremely well with GSM.",
1511
+ parent=self.window, icon='question')
1318
1512
  if confirm:
1319
1513
  self.external_tool.delete(0, tk.END)
1320
1514
  self.external_tool.insert(0, "Downloading OcenAudio...")
1321
1515
  exe_path = download_ocenaudio_if_needed()
1322
- messagebox.showinfo("OcenAudio Downloaded", f"OcenAudio has been downloaded and installed. You can find it at {exe_path}.")
1516
+ messagebox.showinfo("OcenAudio Downloaded",
1517
+ f"OcenAudio has been downloaded and installed. You can find it at {exe_path}.",
1518
+ parent=self.window)
1323
1519
  self.external_tool.delete(0, tk.END)
1324
1520
  self.external_tool.insert(0, exe_path)
1325
1521
  self.save_settings()
1326
1522
 
1327
1523
  def set_default_anki_media_collection(self):
1328
- confirm = messagebox.askyesno("Set Default Anki Media Collection?", "Would you like to set the default Anki media collection path? This will help the script find the media collection for external trimming.\n\nDefault: %APPDATA%/Anki2/User 1/collection.media")
1524
+ confirm = messagebox.askyesno("Set Default Anki Media Collection?",
1525
+ "Would you like to set the default Anki media collection path? This will help the script find the media collection for external trimming.\n\nDefault: %APPDATA%/Anki2/User 1/collection.media",
1526
+ parent=self.window, icon='question')
1329
1527
  if confirm:
1330
1528
  default_path = get_default_anki_media_collection_path()
1331
- if default_path != self.settings.audio.external_tool:
1529
+ if default_path != self.settings.audio.anki_media_collection: # Check against anki_media_collection
1332
1530
  self.anki_media_collection.delete(0, tk.END)
1333
1531
  self.anki_media_collection.insert(0, default_path)
1334
1532
  self.save_settings()
@@ -1338,4 +1536,4 @@ if __name__ == '__main__':
1338
1536
  root = ttk.Window(themename='darkly')
1339
1537
  window = ConfigApp(root)
1340
1538
  window.show()
1341
- window.window.mainloop()
1539
+ window.window.mainloop()