talks-reducer 0.6.1__py3-none-any.whl → 0.7.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.
- talks_reducer/__about__.py +1 -1
- talks_reducer/cli.py +9 -3
- talks_reducer/{gui.py → gui/__init__.py} +384 -1216
- talks_reducer/gui/__main__.py +8 -0
- talks_reducer/gui/discovery.py +126 -0
- talks_reducer/gui/layout.py +526 -0
- talks_reducer/gui/preferences.py +113 -0
- talks_reducer/gui/remote.py +356 -0
- talks_reducer/gui/theme.py +269 -0
- talks_reducer/models.py +1 -1
- talks_reducer/pipeline.py +142 -92
- talks_reducer/resources/__init__.py +0 -0
- talks_reducer/server.py +60 -6
- talks_reducer/server_tray.py +4 -6
- talks_reducer/service_client.py +56 -4
- {talks_reducer-0.6.1.dist-info → talks_reducer-0.7.0.dist-info}/METADATA +23 -13
- talks_reducer-0.7.0.dist-info/RECORD +29 -0
- talks_reducer-0.6.1.dist-info/RECORD +0 -22
- {talks_reducer-0.6.1.dist-info → talks_reducer-0.7.0.dist-info}/WHEEL +0 -0
- {talks_reducer-0.6.1.dist-info → talks_reducer-0.7.0.dist-info}/entry_points.txt +0 -0
- {talks_reducer-0.6.1.dist-info → talks_reducer-0.7.0.dist-info}/licenses/LICENSE +0 -0
- {talks_reducer-0.6.1.dist-info → talks_reducer-0.7.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,126 @@
|
|
1
|
+
"""Discovery helpers for the Talks Reducer GUI."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
import threading
|
6
|
+
from typing import TYPE_CHECKING, List
|
7
|
+
|
8
|
+
from ..discovery import discover_servers as core_discover_servers
|
9
|
+
|
10
|
+
if TYPE_CHECKING: # pragma: no cover - imported for type checking only
|
11
|
+
from . import TalksReducerGUI
|
12
|
+
|
13
|
+
|
14
|
+
def start_discovery(gui: "TalksReducerGUI") -> None:
|
15
|
+
"""Search the local network for running Talks Reducer servers."""
|
16
|
+
|
17
|
+
if gui._discovery_thread and gui._discovery_thread.is_alive():
|
18
|
+
return
|
19
|
+
|
20
|
+
gui.server_discover_button.configure(state=gui.tk.DISABLED, text="Discovering…")
|
21
|
+
gui._append_log("Discovering Talks Reducer servers on port 9005…")
|
22
|
+
|
23
|
+
def worker() -> None:
|
24
|
+
try:
|
25
|
+
urls = core_discover_servers(
|
26
|
+
progress_callback=lambda current, total: gui._schedule_on_ui_thread(
|
27
|
+
lambda c=current, t=total: on_discovery_progress(gui, c, t)
|
28
|
+
)
|
29
|
+
)
|
30
|
+
except Exception as exc: # pragma: no cover - network failure safeguard
|
31
|
+
gui._schedule_on_ui_thread(lambda: on_discovery_failed(gui, exc))
|
32
|
+
return
|
33
|
+
gui._schedule_on_ui_thread(lambda: on_discovery_complete(gui, urls))
|
34
|
+
|
35
|
+
gui._discovery_thread = threading.Thread(target=worker, daemon=True)
|
36
|
+
gui._discovery_thread.start()
|
37
|
+
|
38
|
+
|
39
|
+
def on_discovery_failed(gui: "TalksReducerGUI", exc: Exception) -> None:
|
40
|
+
gui.server_discover_button.configure(state=gui.tk.NORMAL, text="Discover")
|
41
|
+
message = f"Discovery failed: {exc}"
|
42
|
+
gui._append_log(message)
|
43
|
+
gui.messagebox.showerror("Discovery failed", message)
|
44
|
+
|
45
|
+
|
46
|
+
def on_discovery_progress(gui: "TalksReducerGUI", current: int, total: int) -> None:
|
47
|
+
if total > 0:
|
48
|
+
bounded = max(0, min(current, total))
|
49
|
+
label = f"{bounded} / {total}"
|
50
|
+
else:
|
51
|
+
label = "Discovering…"
|
52
|
+
gui.server_discover_button.configure(text=label)
|
53
|
+
|
54
|
+
|
55
|
+
def on_discovery_complete(gui: "TalksReducerGUI", urls: List[str]) -> None:
|
56
|
+
gui.server_discover_button.configure(state=gui.tk.NORMAL, text="Discover")
|
57
|
+
if not urls:
|
58
|
+
gui._append_log("No Talks Reducer servers were found.")
|
59
|
+
gui.messagebox.showinfo(
|
60
|
+
"No servers found",
|
61
|
+
"No Talks Reducer servers responded on port 9005.",
|
62
|
+
)
|
63
|
+
return
|
64
|
+
|
65
|
+
gui._append_log(f"Discovered {len(urls)} server{'s' if len(urls) != 1 else ''}.")
|
66
|
+
|
67
|
+
if len(urls) == 1:
|
68
|
+
gui.server_url_var.set(urls[0])
|
69
|
+
return
|
70
|
+
|
71
|
+
show_discovery_results(gui, urls)
|
72
|
+
|
73
|
+
|
74
|
+
def show_discovery_results(gui: "TalksReducerGUI", urls: List[str]) -> None:
|
75
|
+
dialog = gui.tk.Toplevel(gui.root)
|
76
|
+
dialog.title("Select server")
|
77
|
+
dialog.transient(gui.root)
|
78
|
+
dialog.grab_set()
|
79
|
+
|
80
|
+
gui.ttk.Label(dialog, text="Select a Talks Reducer server:").grid(
|
81
|
+
row=0, column=0, columnspan=2, sticky="w", padx=gui.PADDING, pady=(12, 4)
|
82
|
+
)
|
83
|
+
|
84
|
+
listbox = gui.tk.Listbox(
|
85
|
+
dialog,
|
86
|
+
height=min(10, len(urls)),
|
87
|
+
selectmode=gui.tk.SINGLE,
|
88
|
+
)
|
89
|
+
listbox.grid(
|
90
|
+
row=1,
|
91
|
+
column=0,
|
92
|
+
columnspan=2,
|
93
|
+
padx=gui.PADDING,
|
94
|
+
sticky="nsew",
|
95
|
+
)
|
96
|
+
dialog.columnconfigure(0, weight=1)
|
97
|
+
dialog.columnconfigure(1, weight=1)
|
98
|
+
dialog.rowconfigure(1, weight=1)
|
99
|
+
|
100
|
+
for url in urls:
|
101
|
+
listbox.insert(gui.tk.END, url)
|
102
|
+
listbox.select_set(0)
|
103
|
+
|
104
|
+
def choose(_: object | None = None) -> None:
|
105
|
+
selection = listbox.curselection()
|
106
|
+
if not selection:
|
107
|
+
return
|
108
|
+
index = selection[0]
|
109
|
+
gui.server_url_var.set(urls[index])
|
110
|
+
dialog.grab_release()
|
111
|
+
dialog.destroy()
|
112
|
+
|
113
|
+
def cancel() -> None:
|
114
|
+
dialog.grab_release()
|
115
|
+
dialog.destroy()
|
116
|
+
|
117
|
+
listbox.bind("<Double-Button-1>", choose)
|
118
|
+
listbox.bind("<Return>", choose)
|
119
|
+
|
120
|
+
button_frame = gui.ttk.Frame(dialog)
|
121
|
+
button_frame.grid(row=2, column=0, columnspan=2, pady=(8, 12))
|
122
|
+
gui.ttk.Button(button_frame, text="Use server", command=choose).pack(
|
123
|
+
side=gui.tk.LEFT, padx=(0, 8)
|
124
|
+
)
|
125
|
+
gui.ttk.Button(button_frame, text="Cancel", command=cancel).pack(side=gui.tk.LEFT)
|
126
|
+
dialog.protocol("WM_DELETE_WINDOW", cancel)
|
@@ -0,0 +1,526 @@
|
|
1
|
+
"""Layout helpers for the Talks Reducer GUI."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
import sys
|
6
|
+
from pathlib import Path
|
7
|
+
from typing import TYPE_CHECKING, Callable
|
8
|
+
|
9
|
+
from ..models import default_temp_folder
|
10
|
+
|
11
|
+
if TYPE_CHECKING: # pragma: no cover - imported for type checking only
|
12
|
+
import tkinter as tk
|
13
|
+
|
14
|
+
from . import TalksReducerGUI
|
15
|
+
|
16
|
+
|
17
|
+
def build_layout(gui: "TalksReducerGUI") -> None:
|
18
|
+
"""Construct the main layout for the GUI."""
|
19
|
+
|
20
|
+
main = gui.ttk.Frame(gui.root, padding=gui.PADDING)
|
21
|
+
main.grid(row=0, column=0, sticky="nsew")
|
22
|
+
gui.root.columnconfigure(0, weight=1)
|
23
|
+
gui.root.rowconfigure(0, weight=1)
|
24
|
+
|
25
|
+
# Input selection frame
|
26
|
+
input_frame = gui.ttk.Frame(main, padding=gui.PADDING)
|
27
|
+
input_frame.grid(row=0, column=0, sticky="nsew")
|
28
|
+
main.rowconfigure(0, weight=1)
|
29
|
+
main.columnconfigure(0, weight=1)
|
30
|
+
input_frame.columnconfigure(0, weight=1)
|
31
|
+
input_frame.rowconfigure(0, weight=1)
|
32
|
+
|
33
|
+
gui.drop_zone = gui.tk.Label(
|
34
|
+
input_frame,
|
35
|
+
text="Drop video here",
|
36
|
+
relief=gui.tk.FLAT,
|
37
|
+
borderwidth=0,
|
38
|
+
padx=gui.PADDING,
|
39
|
+
pady=gui.PADDING,
|
40
|
+
highlightthickness=0,
|
41
|
+
)
|
42
|
+
gui.drop_zone.grid(row=0, column=0, sticky="nsew")
|
43
|
+
gui._configure_drop_targets(gui.drop_zone)
|
44
|
+
gui.drop_zone.configure(cursor="hand2", takefocus=1)
|
45
|
+
gui.drop_zone.bind("<Button-1>", gui._on_drop_zone_click)
|
46
|
+
gui.drop_zone.bind("<Return>", gui._on_drop_zone_click)
|
47
|
+
gui.drop_zone.bind("<space>", gui._on_drop_zone_click)
|
48
|
+
|
49
|
+
# Options frame
|
50
|
+
gui.options_frame = gui.ttk.Frame(main, padding=gui.PADDING)
|
51
|
+
gui.options_frame.grid(row=2, column=0, pady=(0, 0), sticky="ew")
|
52
|
+
gui.options_frame.columnconfigure(0, weight=1)
|
53
|
+
|
54
|
+
checkbox_frame = gui.ttk.Frame(gui.options_frame)
|
55
|
+
checkbox_frame.grid(row=0, column=0, columnspan=2, sticky="w")
|
56
|
+
|
57
|
+
gui.ttk.Checkbutton(
|
58
|
+
checkbox_frame,
|
59
|
+
text="Small video",
|
60
|
+
variable=gui.small_var,
|
61
|
+
).grid(row=0, column=0, sticky="w")
|
62
|
+
|
63
|
+
gui.ttk.Checkbutton(
|
64
|
+
checkbox_frame,
|
65
|
+
text="Open after convert",
|
66
|
+
variable=gui.open_after_convert_var,
|
67
|
+
).grid(row=0, column=1, sticky="w", padx=(12, 0))
|
68
|
+
|
69
|
+
gui.simple_mode_check = gui.ttk.Checkbutton(
|
70
|
+
checkbox_frame,
|
71
|
+
text="Simple mode",
|
72
|
+
variable=gui.simple_mode_var,
|
73
|
+
command=gui._toggle_simple_mode,
|
74
|
+
)
|
75
|
+
gui.simple_mode_check.grid(row=1, column=0, columnspan=3, sticky="w", pady=(8, 0))
|
76
|
+
|
77
|
+
gui.advanced_visible = gui.tk.BooleanVar(value=False)
|
78
|
+
|
79
|
+
basic_label_container = gui.ttk.Frame(gui.options_frame)
|
80
|
+
basic_label = gui.ttk.Label(basic_label_container, text="Basic options")
|
81
|
+
basic_label.pack(side=gui.tk.LEFT)
|
82
|
+
|
83
|
+
gui.reset_basic_button = gui.ttk.Button(
|
84
|
+
basic_label_container,
|
85
|
+
text="Reset to defaults",
|
86
|
+
command=gui._reset_basic_defaults,
|
87
|
+
state=gui.tk.DISABLED,
|
88
|
+
style="Link.TButton",
|
89
|
+
)
|
90
|
+
|
91
|
+
gui.basic_options_frame = gui.ttk.Labelframe(
|
92
|
+
gui.options_frame, padding=0, labelwidget=basic_label_container
|
93
|
+
)
|
94
|
+
gui.basic_options_frame.grid(
|
95
|
+
row=2, column=0, columnspan=2, sticky="ew", pady=(12, 0)
|
96
|
+
)
|
97
|
+
gui.basic_options_frame.columnconfigure(1, weight=1)
|
98
|
+
|
99
|
+
gui._reset_button_visible = False
|
100
|
+
|
101
|
+
gui.silent_speed_var = gui.tk.DoubleVar(
|
102
|
+
value=min(max(gui.preferences.get_float("silent_speed", 4.0), 1.0), 10.0)
|
103
|
+
)
|
104
|
+
add_slider(
|
105
|
+
gui,
|
106
|
+
gui.basic_options_frame,
|
107
|
+
"Silent speed",
|
108
|
+
gui.silent_speed_var,
|
109
|
+
row=0,
|
110
|
+
setting_key="silent_speed",
|
111
|
+
minimum=1.0,
|
112
|
+
maximum=10.0,
|
113
|
+
resolution=0.5,
|
114
|
+
display_format="{:.1f}×",
|
115
|
+
default_value=4.0,
|
116
|
+
)
|
117
|
+
|
118
|
+
gui.sounded_speed_var = gui.tk.DoubleVar(
|
119
|
+
value=min(max(gui.preferences.get_float("sounded_speed", 1.0), 0.75), 2.0)
|
120
|
+
)
|
121
|
+
add_slider(
|
122
|
+
gui,
|
123
|
+
gui.basic_options_frame,
|
124
|
+
"Sounded speed",
|
125
|
+
gui.sounded_speed_var,
|
126
|
+
row=1,
|
127
|
+
setting_key="sounded_speed",
|
128
|
+
minimum=0.75,
|
129
|
+
maximum=2.0,
|
130
|
+
resolution=0.25,
|
131
|
+
display_format="{:.2f}×",
|
132
|
+
default_value=1.0,
|
133
|
+
)
|
134
|
+
|
135
|
+
gui.silent_threshold_var = gui.tk.DoubleVar(
|
136
|
+
value=min(max(gui.preferences.get_float("silent_threshold", 0.05), 0.0), 1.0)
|
137
|
+
)
|
138
|
+
add_slider(
|
139
|
+
gui,
|
140
|
+
gui.basic_options_frame,
|
141
|
+
"Silent threshold",
|
142
|
+
gui.silent_threshold_var,
|
143
|
+
row=2,
|
144
|
+
setting_key="silent_threshold",
|
145
|
+
minimum=0.0,
|
146
|
+
maximum=1.0,
|
147
|
+
resolution=0.01,
|
148
|
+
display_format="{:.2f}",
|
149
|
+
default_value=0.05,
|
150
|
+
)
|
151
|
+
|
152
|
+
gui.ttk.Label(gui.basic_options_frame, text="Processing mode").grid(
|
153
|
+
row=3, column=0, sticky="w", pady=(8, 0)
|
154
|
+
)
|
155
|
+
mode_choice = gui.ttk.Frame(gui.basic_options_frame)
|
156
|
+
mode_choice.grid(row=3, column=1, sticky="w", pady=(8, 0))
|
157
|
+
|
158
|
+
gui.ttk.Radiobutton(
|
159
|
+
mode_choice,
|
160
|
+
text="Local",
|
161
|
+
value="local",
|
162
|
+
variable=gui.processing_mode_var,
|
163
|
+
).pack(side=gui.tk.LEFT, padx=(0, 8))
|
164
|
+
|
165
|
+
gui.remote_mode_button = gui.ttk.Radiobutton(
|
166
|
+
mode_choice,
|
167
|
+
text="Remote",
|
168
|
+
value="remote",
|
169
|
+
variable=gui.processing_mode_var,
|
170
|
+
)
|
171
|
+
gui.remote_mode_button.pack(side=gui.tk.LEFT, padx=(0, 8))
|
172
|
+
|
173
|
+
gui.ttk.Label(gui.basic_options_frame, text="Server URL").grid(
|
174
|
+
row=4, column=0, sticky="w", pady=(8, 0)
|
175
|
+
)
|
176
|
+
gui.server_entry = gui.ttk.Entry(
|
177
|
+
gui.basic_options_frame,
|
178
|
+
textvariable=gui.server_url_var,
|
179
|
+
width=40,
|
180
|
+
)
|
181
|
+
gui.server_entry.grid(row=4, column=1, sticky="ew", pady=(8, 0))
|
182
|
+
|
183
|
+
gui.server_discover_button = gui.ttk.Button(
|
184
|
+
gui.basic_options_frame, text="Discover", command=gui._start_discovery
|
185
|
+
)
|
186
|
+
gui.server_discover_button.grid(row=4, column=2, padx=(8, 0))
|
187
|
+
|
188
|
+
gui.ttk.Label(gui.basic_options_frame, text="Theme").grid(
|
189
|
+
row=5, column=0, sticky="w", pady=(8, 0)
|
190
|
+
)
|
191
|
+
theme_choice = gui.ttk.Frame(gui.basic_options_frame)
|
192
|
+
theme_choice.grid(row=5, column=1, columnspan=2, sticky="w", pady=(8, 0))
|
193
|
+
for value, label in ("os", "OS"), ("light", "Light"), ("dark", "Dark"):
|
194
|
+
gui.ttk.Radiobutton(
|
195
|
+
theme_choice,
|
196
|
+
text=label,
|
197
|
+
value=value,
|
198
|
+
variable=gui.theme_var,
|
199
|
+
command=gui._refresh_theme,
|
200
|
+
).pack(side=gui.tk.LEFT, padx=(0, 8))
|
201
|
+
|
202
|
+
gui.advanced_button = gui.ttk.Button(
|
203
|
+
gui.options_frame,
|
204
|
+
text="Advanced",
|
205
|
+
command=gui._toggle_advanced,
|
206
|
+
)
|
207
|
+
gui.advanced_button.grid(row=3, column=0, columnspan=2, sticky="w", pady=(12, 0))
|
208
|
+
|
209
|
+
gui.advanced_frame = gui.ttk.Frame(gui.options_frame, padding=0)
|
210
|
+
gui.advanced_frame.grid(row=4, column=0, columnspan=2, sticky="nsew")
|
211
|
+
gui.advanced_frame.columnconfigure(1, weight=1)
|
212
|
+
|
213
|
+
gui.output_var = gui.tk.StringVar()
|
214
|
+
add_entry(
|
215
|
+
gui,
|
216
|
+
gui.advanced_frame,
|
217
|
+
"Output file",
|
218
|
+
gui.output_var,
|
219
|
+
row=0,
|
220
|
+
browse=True,
|
221
|
+
)
|
222
|
+
|
223
|
+
gui.temp_var = gui.tk.StringVar(value=str(default_temp_folder()))
|
224
|
+
add_entry(
|
225
|
+
gui,
|
226
|
+
gui.advanced_frame,
|
227
|
+
"Temp folder",
|
228
|
+
gui.temp_var,
|
229
|
+
row=1,
|
230
|
+
browse=True,
|
231
|
+
)
|
232
|
+
|
233
|
+
gui.sample_rate_var = gui.tk.StringVar(value="48000")
|
234
|
+
add_entry(gui, gui.advanced_frame, "Sample rate", gui.sample_rate_var, row=2)
|
235
|
+
|
236
|
+
frame_margin_setting = gui.preferences.get("frame_margin", 2)
|
237
|
+
try:
|
238
|
+
frame_margin_default = int(frame_margin_setting)
|
239
|
+
except (TypeError, ValueError):
|
240
|
+
frame_margin_default = 2
|
241
|
+
gui.preferences.update("frame_margin", frame_margin_default)
|
242
|
+
|
243
|
+
gui.frame_margin_var = gui.tk.StringVar(value=str(frame_margin_default))
|
244
|
+
add_entry(gui, gui.advanced_frame, "Frame margin", gui.frame_margin_var, row=3)
|
245
|
+
|
246
|
+
gui._toggle_advanced(initial=True)
|
247
|
+
gui._update_processing_mode_state()
|
248
|
+
update_basic_reset_state(gui)
|
249
|
+
|
250
|
+
# Action buttons and log output
|
251
|
+
status_frame = gui.ttk.Frame(main, padding=gui.PADDING)
|
252
|
+
status_frame.grid(row=1, column=0, sticky="ew")
|
253
|
+
status_frame.columnconfigure(0, weight=0)
|
254
|
+
status_frame.columnconfigure(1, weight=1)
|
255
|
+
status_frame.columnconfigure(2, weight=0)
|
256
|
+
gui.status_frame = status_frame
|
257
|
+
|
258
|
+
gui.ttk.Label(status_frame, text="Status:").grid(row=0, column=0, sticky="w")
|
259
|
+
gui.status_label = gui.tk.Label(
|
260
|
+
status_frame, textvariable=gui.status_var, anchor="e"
|
261
|
+
)
|
262
|
+
gui.status_label.grid(row=0, column=1, sticky="e")
|
263
|
+
|
264
|
+
# Progress bar
|
265
|
+
gui.progress_bar = gui.ttk.Progressbar(
|
266
|
+
status_frame,
|
267
|
+
variable=gui.progress_var,
|
268
|
+
maximum=100,
|
269
|
+
mode="determinate",
|
270
|
+
style="Idle.Horizontal.TProgressbar",
|
271
|
+
)
|
272
|
+
gui.progress_bar.grid(row=1, column=0, columnspan=3, sticky="ew", pady=(0, 0))
|
273
|
+
|
274
|
+
gui.stop_button = gui.ttk.Button(
|
275
|
+
status_frame, text="Stop", command=gui._stop_processing
|
276
|
+
)
|
277
|
+
gui.stop_button.grid(row=2, column=0, columnspan=3, sticky="ew", pady=gui.PADDING)
|
278
|
+
gui.stop_button.grid_remove() # Hidden by default
|
279
|
+
|
280
|
+
gui.open_button = gui.ttk.Button(
|
281
|
+
status_frame,
|
282
|
+
text="Open last",
|
283
|
+
command=gui._open_last_output,
|
284
|
+
state=gui.tk.DISABLED,
|
285
|
+
)
|
286
|
+
gui.open_button.grid(row=2, column=0, columnspan=3, sticky="ew", pady=gui.PADDING)
|
287
|
+
gui.open_button.grid_remove()
|
288
|
+
|
289
|
+
# Button shown when no other action buttons are visible
|
290
|
+
gui.drop_hint_button = gui.ttk.Button(
|
291
|
+
status_frame,
|
292
|
+
text="Drop video to convert",
|
293
|
+
state=gui.tk.DISABLED,
|
294
|
+
)
|
295
|
+
gui.drop_hint_button.grid(
|
296
|
+
row=2, column=0, columnspan=3, sticky="ew", pady=gui.PADDING
|
297
|
+
)
|
298
|
+
gui.drop_hint_button.grid_remove() # Hidden by default
|
299
|
+
gui._configure_drop_targets(gui.drop_hint_button)
|
300
|
+
|
301
|
+
gui.log_frame = gui.ttk.Frame(main, padding=gui.PADDING)
|
302
|
+
gui.log_frame.grid(row=3, column=0, pady=(16, 0), sticky="nsew")
|
303
|
+
main.rowconfigure(4, weight=1)
|
304
|
+
gui.log_frame.columnconfigure(0, weight=1)
|
305
|
+
gui.log_frame.rowconfigure(0, weight=1)
|
306
|
+
|
307
|
+
gui.log_text = gui.tk.Text(
|
308
|
+
gui.log_frame, wrap="word", height=10, state=gui.tk.DISABLED
|
309
|
+
)
|
310
|
+
gui.log_text.grid(row=0, column=0, sticky="nsew")
|
311
|
+
log_scroll = gui.ttk.Scrollbar(
|
312
|
+
gui.log_frame, orient=gui.tk.VERTICAL, command=gui.log_text.yview
|
313
|
+
)
|
314
|
+
log_scroll.grid(row=0, column=1, sticky="ns")
|
315
|
+
gui.log_text.configure(yscrollcommand=log_scroll.set)
|
316
|
+
|
317
|
+
|
318
|
+
def add_entry(
|
319
|
+
gui: "TalksReducerGUI",
|
320
|
+
parent: "tk.Misc",
|
321
|
+
label: str,
|
322
|
+
variable: "tk.StringVar",
|
323
|
+
*,
|
324
|
+
row: int,
|
325
|
+
browse: bool = False,
|
326
|
+
) -> None:
|
327
|
+
"""Add a labeled entry widget to the given *parent* container."""
|
328
|
+
|
329
|
+
gui.ttk.Label(parent, text=label).grid(row=row, column=0, sticky="w", pady=4)
|
330
|
+
entry = gui.ttk.Entry(parent, textvariable=variable)
|
331
|
+
entry.grid(row=row, column=1, sticky="ew", pady=4)
|
332
|
+
if browse:
|
333
|
+
button = gui.ttk.Button(
|
334
|
+
parent,
|
335
|
+
text="Browse",
|
336
|
+
command=lambda var=variable: gui._browse_path(var, label),
|
337
|
+
)
|
338
|
+
button.grid(row=row, column=2, padx=(8, 0))
|
339
|
+
|
340
|
+
|
341
|
+
def add_slider(
|
342
|
+
gui: "TalksReducerGUI",
|
343
|
+
parent: "tk.Misc",
|
344
|
+
label: str,
|
345
|
+
variable: "tk.DoubleVar",
|
346
|
+
*,
|
347
|
+
row: int,
|
348
|
+
setting_key: str,
|
349
|
+
minimum: float,
|
350
|
+
maximum: float,
|
351
|
+
resolution: float,
|
352
|
+
display_format: str,
|
353
|
+
default_value: float,
|
354
|
+
) -> None:
|
355
|
+
"""Add a labeled slider to the given *parent* container."""
|
356
|
+
|
357
|
+
gui.ttk.Label(parent, text=label).grid(row=row, column=0, sticky="w", pady=4)
|
358
|
+
|
359
|
+
value_label = gui.ttk.Label(parent)
|
360
|
+
value_label.grid(row=row, column=2, sticky="e", pady=4)
|
361
|
+
|
362
|
+
def update(value: str) -> None:
|
363
|
+
numeric = float(value)
|
364
|
+
clamped = max(minimum, min(maximum, numeric))
|
365
|
+
steps = round((clamped - minimum) / resolution)
|
366
|
+
quantized = minimum + steps * resolution
|
367
|
+
if abs(variable.get() - quantized) > 1e-9:
|
368
|
+
variable.set(quantized)
|
369
|
+
value_label.configure(text=display_format.format(quantized))
|
370
|
+
gui.preferences.update(setting_key, float(f"{quantized:.6f}"))
|
371
|
+
update_basic_reset_state(gui)
|
372
|
+
|
373
|
+
slider = gui.tk.Scale(
|
374
|
+
parent,
|
375
|
+
variable=variable,
|
376
|
+
from_=minimum,
|
377
|
+
to=maximum,
|
378
|
+
orient=gui.tk.HORIZONTAL,
|
379
|
+
resolution=resolution,
|
380
|
+
showvalue=False,
|
381
|
+
command=update,
|
382
|
+
length=240,
|
383
|
+
highlightthickness=0,
|
384
|
+
)
|
385
|
+
slider.grid(row=row, column=1, sticky="ew", pady=4, padx=(0, 8))
|
386
|
+
|
387
|
+
update(str(variable.get()))
|
388
|
+
|
389
|
+
gui._slider_updaters[setting_key] = update
|
390
|
+
gui._basic_defaults[setting_key] = default_value
|
391
|
+
gui._basic_variables[setting_key] = variable
|
392
|
+
variable.trace_add("write", lambda *_: update_basic_reset_state(gui))
|
393
|
+
gui._sliders.append(slider)
|
394
|
+
|
395
|
+
|
396
|
+
def update_basic_reset_state(gui: "TalksReducerGUI") -> None:
|
397
|
+
"""Enable or disable the reset control based on slider values."""
|
398
|
+
|
399
|
+
if not hasattr(gui, "reset_basic_button"):
|
400
|
+
return
|
401
|
+
|
402
|
+
should_enable = False
|
403
|
+
for key, default_value in gui._basic_defaults.items():
|
404
|
+
variable = gui._basic_variables.get(key)
|
405
|
+
if variable is None:
|
406
|
+
continue
|
407
|
+
try:
|
408
|
+
current_value = float(variable.get())
|
409
|
+
except (TypeError, ValueError):
|
410
|
+
should_enable = True
|
411
|
+
break
|
412
|
+
if abs(current_value - default_value) > 1e-9:
|
413
|
+
should_enable = True
|
414
|
+
break
|
415
|
+
|
416
|
+
if should_enable:
|
417
|
+
if not getattr(gui, "_reset_button_visible", False):
|
418
|
+
gui.reset_basic_button.pack(side=gui.tk.LEFT, padx=(8, 0))
|
419
|
+
gui._reset_button_visible = True
|
420
|
+
gui.reset_basic_button.configure(state=gui.tk.NORMAL)
|
421
|
+
else:
|
422
|
+
if getattr(gui, "_reset_button_visible", False):
|
423
|
+
gui.reset_basic_button.pack_forget()
|
424
|
+
gui._reset_button_visible = False
|
425
|
+
gui.reset_basic_button.configure(state=gui.tk.DISABLED)
|
426
|
+
|
427
|
+
|
428
|
+
def reset_basic_defaults(gui: "TalksReducerGUI") -> None:
|
429
|
+
"""Restore the basic numeric controls to their default values."""
|
430
|
+
|
431
|
+
for key, default_value in gui._basic_defaults.items():
|
432
|
+
variable = gui._basic_variables.get(key)
|
433
|
+
if variable is None:
|
434
|
+
continue
|
435
|
+
|
436
|
+
try:
|
437
|
+
current_value = float(variable.get())
|
438
|
+
except (TypeError, ValueError):
|
439
|
+
current_value = default_value
|
440
|
+
|
441
|
+
if abs(current_value - default_value) <= 1e-9:
|
442
|
+
continue
|
443
|
+
|
444
|
+
variable.set(default_value)
|
445
|
+
updater: Callable[[str], None] | None = gui._slider_updaters.get(key)
|
446
|
+
if updater is not None:
|
447
|
+
updater(str(default_value))
|
448
|
+
else:
|
449
|
+
gui.preferences.update(key, float(f"{default_value:.6f}"))
|
450
|
+
|
451
|
+
update_basic_reset_state(gui)
|
452
|
+
|
453
|
+
|
454
|
+
def apply_window_icon(gui: "TalksReducerGUI") -> None:
|
455
|
+
"""Configure the application icon when the asset is available."""
|
456
|
+
|
457
|
+
base_path = Path(getattr(sys, "_MEIPASS", Path(__file__).resolve().parent.parent))
|
458
|
+
|
459
|
+
icon_candidates: list[tuple[Path, str]] = []
|
460
|
+
if sys.platform.startswith("win"):
|
461
|
+
icon_candidates.append(
|
462
|
+
(
|
463
|
+
base_path / "talks_reducer" / "resources" / "icons" / "icon.ico",
|
464
|
+
"ico",
|
465
|
+
)
|
466
|
+
)
|
467
|
+
icon_candidates.append(
|
468
|
+
(
|
469
|
+
base_path / "talks_reducer" / "resources" / "icons" / "icon.png",
|
470
|
+
"png",
|
471
|
+
)
|
472
|
+
)
|
473
|
+
|
474
|
+
for icon_path, icon_type in icon_candidates:
|
475
|
+
if not icon_path.is_file():
|
476
|
+
continue
|
477
|
+
|
478
|
+
try:
|
479
|
+
if icon_type == "ico" and sys.platform.startswith("win"):
|
480
|
+
# On Windows, iconbitmap works better without the 'default' parameter
|
481
|
+
gui.root.iconbitmap(str(icon_path))
|
482
|
+
else:
|
483
|
+
gui.root.iconphoto(False, gui.tk.PhotoImage(file=str(icon_path)))
|
484
|
+
return
|
485
|
+
except (gui.tk.TclError, Exception):
|
486
|
+
# Missing Tk image support or invalid icon format - try next candidate
|
487
|
+
continue
|
488
|
+
|
489
|
+
|
490
|
+
def apply_window_size(gui: "TalksReducerGUI", *, simple: bool) -> None:
|
491
|
+
"""Apply the appropriate window geometry for the current mode."""
|
492
|
+
|
493
|
+
width, height = gui._simple_size if simple else gui._full_size
|
494
|
+
gui.root.update_idletasks()
|
495
|
+
gui.root.minsize(width, height)
|
496
|
+
if simple:
|
497
|
+
gui.root.geometry(f"{width}x{height}")
|
498
|
+
else:
|
499
|
+
current_width = gui.root.winfo_width()
|
500
|
+
current_height = gui.root.winfo_height()
|
501
|
+
if current_width < width or current_height < height:
|
502
|
+
gui.root.geometry(f"{width}x{height}")
|
503
|
+
|
504
|
+
|
505
|
+
def apply_simple_mode(gui: "TalksReducerGUI", *, initial: bool = False) -> None:
|
506
|
+
"""Toggle between simple and full layouts."""
|
507
|
+
|
508
|
+
simple = gui.simple_mode_var.get()
|
509
|
+
if simple:
|
510
|
+
gui.basic_options_frame.grid_remove()
|
511
|
+
gui.log_frame.grid_remove()
|
512
|
+
gui.advanced_button.grid_remove()
|
513
|
+
gui.advanced_frame.grid_remove()
|
514
|
+
gui.run_after_drop_var.set(True)
|
515
|
+
apply_window_size(gui, simple=True)
|
516
|
+
else:
|
517
|
+
gui.basic_options_frame.grid()
|
518
|
+
gui.log_frame.grid()
|
519
|
+
gui.advanced_button.grid()
|
520
|
+
if gui.advanced_visible.get():
|
521
|
+
gui.advanced_frame.grid()
|
522
|
+
apply_window_size(gui, simple=False)
|
523
|
+
|
524
|
+
if initial and simple:
|
525
|
+
# Ensure the hidden widgets do not retain focus outlines on start.
|
526
|
+
gui.drop_zone.focus_set()
|