GameSentenceMiner 2.10.8__py3-none-any.whl → 2.10.10__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.
- GameSentenceMiner/gsm.py +1 -1
- GameSentenceMiner/ocr/owocr_area_selector.py +227 -758
- {gamesentenceminer-2.10.8.dist-info → gamesentenceminer-2.10.10.dist-info}/METADATA +1 -1
- {gamesentenceminer-2.10.8.dist-info → gamesentenceminer-2.10.10.dist-info}/RECORD +8 -8
- {gamesentenceminer-2.10.8.dist-info → gamesentenceminer-2.10.10.dist-info}/WHEEL +0 -0
- {gamesentenceminer-2.10.8.dist-info → gamesentenceminer-2.10.10.dist-info}/entry_points.txt +0 -0
- {gamesentenceminer-2.10.8.dist-info → gamesentenceminer-2.10.10.dist-info}/licenses/LICENSE +0 -0
- {gamesentenceminer-2.10.8.dist-info → gamesentenceminer-2.10.10.dist-info}/top_level.txt +0 -0
GameSentenceMiner/gsm.py
CHANGED
@@ -200,7 +200,7 @@ class VideoToAudioHandler(FileSystemEventHandler):
|
|
200
200
|
return vad_result
|
201
201
|
if vad_result.output_audio:
|
202
202
|
vad_trimmed_audio = vad_result.output_audio
|
203
|
-
if
|
203
|
+
if get_config().audio.ffmpeg_reencode_options_to_use and os.path.exists(vad_trimmed_audio):
|
204
204
|
ffmpeg.reencode_file_with_user_config(vad_trimmed_audio, final_audio_output,
|
205
205
|
get_config().audio.ffmpeg_reencode_options_to_use)
|
206
206
|
elif os.path.exists(vad_trimmed_audio):
|
@@ -5,9 +5,10 @@ from multiprocessing import Process, Manager
|
|
5
5
|
from pathlib import Path
|
6
6
|
|
7
7
|
import mss
|
8
|
-
from PIL import Image, ImageTk
|
8
|
+
from PIL import Image, ImageTk
|
9
9
|
|
10
|
-
|
10
|
+
# Assuming a mock or real obs module exists in this path
|
11
|
+
from GameSentenceMiner import obs
|
11
12
|
from GameSentenceMiner.ocr.gsm_ocr_config import set_dpi_awareness, get_window
|
12
13
|
from GameSentenceMiner.util.gsm_utils import sanitize_filename
|
13
14
|
|
@@ -15,7 +16,7 @@ try:
|
|
15
16
|
import pygetwindow as gw
|
16
17
|
except ImportError:
|
17
18
|
print("Error: pygetwindow library not found. Please install it: pip install pygetwindow")
|
18
|
-
gw = None
|
19
|
+
gw = None
|
19
20
|
|
20
21
|
try:
|
21
22
|
import tkinter as tk
|
@@ -25,68 +26,51 @@ except ImportError:
|
|
25
26
|
print("Error: tkinter library not found. GUI selection is unavailable.")
|
26
27
|
selector_available = False
|
27
28
|
|
28
|
-
MIN_RECT_WIDTH = 25
|
29
|
-
MIN_RECT_HEIGHT = 25
|
29
|
+
MIN_RECT_WIDTH = 25
|
30
|
+
MIN_RECT_HEIGHT = 25
|
30
31
|
|
31
|
-
# --- Constants for coordinate systems ---
|
32
|
-
COORD_SYSTEM_ABSOLUTE = "absolute_pixels"
|
33
|
-
COORD_SYSTEM_RELATIVE = "relative_pixels" # Kept for potential backward compatibility loading
|
34
32
|
COORD_SYSTEM_PERCENTAGE = "percentage"
|
35
33
|
|
36
34
|
|
37
|
-
# --- ---
|
38
|
-
|
39
35
|
class ScreenSelector:
|
40
36
|
def __init__(self, result, window_name):
|
41
|
-
if not selector_available:
|
42
|
-
raise RuntimeError("tkinter is not available.")
|
43
|
-
if not
|
44
|
-
raise
|
37
|
+
if not selector_available or not gw:
|
38
|
+
raise RuntimeError("tkinter or pygetwindow is not available.")
|
39
|
+
if not window_name:
|
40
|
+
raise ValueError("A target window name is required for percentage-based coordinates.")
|
45
41
|
|
46
|
-
obs.connect_to_obs_sync()
|
42
|
+
obs.connect_to_obs_sync()
|
47
43
|
self.window_name = window_name
|
48
|
-
print(f"
|
44
|
+
print(f"Targeting window: '{window_name}'")
|
45
|
+
|
49
46
|
self.sct = mss.mss()
|
50
|
-
self.monitors = self.sct.monitors[1:]
|
51
|
-
if not self.monitors:
|
52
|
-
|
53
|
-
self.monitors = self.sct.monitors[0:1] # Use the primary monitor entry
|
54
|
-
else:
|
55
|
-
raise RuntimeError("No monitors found by mss.")
|
56
|
-
# Assign index to each monitor dictionary
|
47
|
+
self.monitors = self.sct.monitors[1:]
|
48
|
+
if not self.monitors:
|
49
|
+
raise RuntimeError("No monitors found by mss.")
|
57
50
|
for i, monitor in enumerate(self.monitors):
|
58
51
|
monitor['index'] = i
|
59
52
|
|
53
|
+
# --- Window Awareness is now critical ---
|
54
|
+
self.target_window = self._find_target_window()
|
55
|
+
self.target_window_geometry = self._get_window_geometry(self.target_window)
|
56
|
+
if not self.target_window_geometry:
|
57
|
+
raise RuntimeError(f"Could not find or get geometry for window '{self.window_name}'.")
|
58
|
+
print(f"Found target window at: {self.target_window_geometry}")
|
59
|
+
# ---
|
60
|
+
|
60
61
|
self.root = None
|
61
|
-
self.result = result
|
62
|
-
# Internal storage ALWAYS
|
63
|
-
self.
|
64
|
-
self.drawn_rect_ids = [] # List to store canvas rectangle IDs *per canvas* (needs careful management)
|
62
|
+
self.result = result
|
63
|
+
self.rectangles = [] # Internal storage is ALWAYS absolute pixels for drawing
|
64
|
+
self.drawn_rect_ids = []
|
65
65
|
self.current_rect_id = None
|
66
|
-
self.start_x =
|
67
|
-
self.start_y = None # Canvas coordinates
|
68
|
-
self.canvas_windows = {} # Dictionary mapping monitor_index -> canvas widget
|
66
|
+
self.start_x = self.start_y = None
|
69
67
|
self.image_mode = True
|
70
|
-
self.
|
71
|
-
self.
|
68
|
+
self.redo_stack = []
|
69
|
+
self.bounding_box = {} # Geometry of the single large canvas window
|
72
70
|
|
73
|
-
|
74
|
-
self.target_window = self._find_target_window()
|
75
|
-
self.target_window_geometry = self._get_window_geometry(self.target_window)
|
76
|
-
if self.target_window_geometry:
|
77
|
-
print(f"Found target window '{self.window_name}' at: {self.target_window_geometry}")
|
78
|
-
elif self.window_name:
|
79
|
-
print(f"Warning: Could not find window '{self.window_name}'. Coordinates will be absolute.")
|
80
|
-
else:
|
81
|
-
print("No target window specified. Using absolute coordinates.")
|
82
|
-
# --- End Window Awareness ---
|
83
|
-
|
84
|
-
self.load_existing_rectangles() # Load AFTER finding the window
|
71
|
+
self.load_existing_rectangles()
|
85
72
|
|
86
73
|
def _find_target_window(self):
|
87
|
-
"""Finds the window matching self.window_name."""
|
88
|
-
if not self.window_name:
|
89
|
-
return None
|
90
74
|
try:
|
91
75
|
return get_window(self.window_name)
|
92
76
|
except Exception as e:
|
@@ -94,812 +78,297 @@ class ScreenSelector:
|
|
94
78
|
return None
|
95
79
|
|
96
80
|
def _get_window_geometry(self, window):
|
97
|
-
"""Gets the geometry (left, top, width, height) of a pygetwindow object."""
|
98
81
|
if window:
|
99
82
|
try:
|
100
|
-
# Ensure width/height are positive
|
83
|
+
# Ensure width/height are positive and non-zero
|
101
84
|
width = max(1, window.width)
|
102
85
|
height = max(1, window.height)
|
103
86
|
return {"left": window.left, "top": window.top, "width": width, "height": height}
|
104
|
-
except Exception
|
105
|
-
|
106
|
-
# Handle specific states gracefully if possible
|
107
|
-
try:
|
108
|
-
if window.isMinimized or window.isMaximized:
|
109
|
-
print(f"Window '{window.title}' might be minimized or maximized, geometry may be inaccurate.")
|
110
|
-
# Attempt to get geometry anyway, might work depending on OS/lib version
|
111
|
-
width = max(1, window.width)
|
112
|
-
height = max(1, window.height)
|
113
|
-
return {"left": window.left, "top": window.top, "width": width, "height": height}
|
114
|
-
except: # Catch potential errors accessing window state attributes
|
115
|
-
pass # Fall through to return None
|
87
|
+
except Exception:
|
88
|
+
return None
|
116
89
|
return None
|
117
90
|
|
118
91
|
def get_scene_ocr_config(self):
|
119
|
-
"""Return the path to the OCR config file (scene.json)."""
|
120
92
|
app_dir = Path.home() / "AppData" / "Roaming" / "GameSentenceMiner"
|
121
93
|
ocr_config_dir = app_dir / "ocr_config"
|
122
94
|
ocr_config_dir.mkdir(parents=True, exist_ok=True)
|
123
95
|
try:
|
124
|
-
|
125
|
-
current_scene = obs.get_current_scene()
|
126
|
-
scene = sanitize_filename(current_scene or "default_scene")
|
96
|
+
scene = sanitize_filename(obs.get_current_scene() or "default_scene")
|
127
97
|
except Exception as e:
|
128
98
|
print(f"Error getting OBS scene: {e}. Using default config name.")
|
129
99
|
scene = "default_scene"
|
130
|
-
|
131
|
-
# Use only the scene name for the config file
|
132
|
-
config_filename = f"{scene}.json"
|
133
|
-
config_path = ocr_config_dir / config_filename
|
134
|
-
return config_path
|
100
|
+
return ocr_config_dir / f"{scene}.json"
|
135
101
|
|
136
102
|
def load_existing_rectangles(self):
|
137
|
-
"""Loads rectangles from
|
103
|
+
"""Loads rectangles from config, converting from percentage to absolute pixels for use."""
|
138
104
|
config_path = self.get_scene_ocr_config()
|
139
|
-
#
|
140
|
-
|
105
|
+
win_geom = self.target_window_geometry # Use current geometry for conversion
|
106
|
+
win_w, win_h, win_l, win_t = win_geom['width'], win_geom['height'], win_geom['left'], win_geom['top']
|
141
107
|
|
142
108
|
try:
|
143
109
|
with open(config_path, 'r', encoding='utf-8') as f:
|
144
110
|
config_data = json.load(f)
|
145
111
|
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
saved_window_geometry = config_data.get("window_geometry")
|
151
|
-
|
152
|
-
# Backward compatibility and system determination logic
|
153
|
-
if saved_coord_system:
|
154
|
-
coordinate_system = saved_coord_system
|
155
|
-
elif saved_window_geometry: # If geo exists but system doesn't, assume old relative format
|
156
|
-
coordinate_system = COORD_SYSTEM_RELATIVE
|
157
|
-
print(f"Loading using inferred '{COORD_SYSTEM_RELATIVE}' system (for backward compatibility).")
|
158
|
-
else: # Otherwise assume old absolute format
|
159
|
-
coordinate_system = COORD_SYSTEM_ABSOLUTE
|
160
|
-
print(f"Loading using inferred '{COORD_SYSTEM_ABSOLUTE}' system (for backward compatibility).")
|
161
|
-
# --- ---
|
162
|
-
|
163
|
-
print(f"Using coordinate system: {coordinate_system} from config {config_path}")
|
164
|
-
|
165
|
-
rectangles_data = config_data.get("rectangles", [])
|
166
|
-
self.rectangles = [] # Clear existing internal rectangles
|
167
|
-
loaded_count = 0
|
168
|
-
skipped_count = 0
|
112
|
+
if config_data.get("coordinate_system") != COORD_SYSTEM_PERCENTAGE:
|
113
|
+
print(
|
114
|
+
f"Warning: Config file '{config_path}' does not use '{COORD_SYSTEM_PERCENTAGE}' system. Please re-create selections.")
|
115
|
+
return
|
169
116
|
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
is_excluded = rect_data.get("is_excluded", False)
|
117
|
+
print(f"Loading rectangles from {config_path}...")
|
118
|
+
self.rectangles = []
|
119
|
+
loaded_count = 0
|
174
120
|
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
skipped_count += 1
|
180
|
-
continue
|
121
|
+
for rect_data in config_data.get("rectangles", []):
|
122
|
+
try:
|
123
|
+
coords_pct = rect_data["coordinates"]
|
124
|
+
x_pct, y_pct, w_pct, h_pct = map(float, coords_pct)
|
181
125
|
|
182
|
-
|
126
|
+
# Convert from percentage to absolute pixel coordinates
|
127
|
+
x_abs = (x_pct * win_w) + win_l
|
128
|
+
y_abs = (y_pct * win_h) + win_t
|
129
|
+
w_abs = w_pct * win_w
|
130
|
+
h_abs = h_pct * win_h
|
131
|
+
abs_coords = (int(x_abs), int(y_abs), int(w_abs), int(h_abs))
|
183
132
|
|
184
|
-
|
185
|
-
try:
|
186
|
-
if coordinate_system == COORD_SYSTEM_PERCENTAGE:
|
187
|
-
if current_window_geometry:
|
188
|
-
win_w = current_window_geometry['width']
|
189
|
-
win_h = current_window_geometry['height']
|
190
|
-
win_l = current_window_geometry['left']
|
191
|
-
win_t = current_window_geometry['top']
|
192
|
-
# Ensure dimensions are valid for calculation
|
193
|
-
if win_w <= 0 or win_h <= 0:
|
194
|
-
raise ValueError("Current window dimensions are invalid for percentage calculation.")
|
195
|
-
|
196
|
-
x_pct, y_pct, w_pct, h_pct = map(float, coords) # Ensure float
|
197
|
-
|
198
|
-
# Calculate absolute pixel values
|
199
|
-
x_abs = (x_pct * win_w) + win_l
|
200
|
-
y_abs = (y_pct * win_h) + win_t
|
201
|
-
w_abs = w_pct * win_w
|
202
|
-
h_abs = h_pct * win_h
|
203
|
-
abs_coords = (int(x_abs), int(y_abs), int(w_abs), int(h_abs))
|
204
|
-
else:
|
205
|
-
raise ValueError(f"Cannot convert percentage coords {coords}, target window not found now.")
|
206
|
-
|
207
|
-
elif coordinate_system == COORD_SYSTEM_RELATIVE:
|
208
|
-
if current_window_geometry:
|
209
|
-
x_rel, y_rel, w_pix, h_pix = map(float, coords) # Read as float first
|
210
|
-
x_abs = x_rel + current_window_geometry['left']
|
211
|
-
y_abs = y_rel + current_window_geometry['top']
|
212
|
-
abs_coords = (int(x_abs), int(y_abs), int(w_pix), int(h_pix))
|
213
|
-
else:
|
214
|
-
raise ValueError(
|
215
|
-
f"Cannot convert relative pixel coords {coords}, target window not found now.")
|
216
|
-
|
217
|
-
elif coordinate_system == COORD_SYSTEM_ABSOLUTE:
|
218
|
-
# Assume absolute pixels
|
219
|
-
abs_coords = tuple(int(float(c)) for c in coords) # Allow float->int conversion
|
220
|
-
else:
|
221
|
-
# Fallback for unknown system: treat as absolute
|
222
|
-
print(
|
223
|
-
f"Warning: Unknown coordinate system '{coordinate_system}'. Treating coords {coords} as absolute.")
|
224
|
-
abs_coords = tuple(int(float(c)) for c in coords)
|
225
|
-
|
226
|
-
except (ValueError, TypeError, KeyError, IndexError) as e:
|
227
|
-
print(f"Error processing coords {coords} with system '{coordinate_system}': {e}. Skipping rect.")
|
228
|
-
skipped_count += 1
|
229
|
-
continue # Skip this rectangle if conversion failed
|
230
|
-
# --- End Conversion ---
|
231
|
-
|
232
|
-
# Validate size using the final absolute pixel coordinates
|
233
|
-
if coordinate_system == COORD_SYSTEM_PERCENTAGE or (abs_coords and abs_coords[2] >= MIN_RECT_WIDTH and abs_coords[3] >= MIN_RECT_HEIGHT):
|
234
|
-
# Find the correct monitor dict from self.monitors based on index
|
235
|
-
monitor_index = monitor_data['index']
|
133
|
+
monitor_index = rect_data["monitor"]['index']
|
236
134
|
target_monitor = next((m for m in self.monitors if m['index'] == monitor_index), None)
|
237
135
|
if target_monitor:
|
238
|
-
|
239
|
-
self.rectangles.append((target_monitor, abs_coords, is_excluded))
|
136
|
+
self.rectangles.append((target_monitor, abs_coords, rect_data["is_excluded"]))
|
240
137
|
loaded_count += 1
|
241
|
-
|
242
|
-
|
243
|
-
f"Warning: Monitor with index {monitor_index} not found in current setup. Skipping rect {abs_coords}.")
|
244
|
-
skipped_count += 1
|
245
|
-
elif abs_coords:
|
246
|
-
print(
|
247
|
-
f"Skipping small rectangle (pixels): W={abs_coords[2]}, H={abs_coords[3]} (from original {coords})")
|
248
|
-
skipped_count += 1
|
249
|
-
# else: conversion failed, already printed message
|
250
|
-
|
251
|
-
print(f"Loaded {loaded_count} rectangles, skipped {skipped_count} from {config_path}")
|
138
|
+
except (KeyError, ValueError, TypeError) as e:
|
139
|
+
print(f"Skipping malformed rectangle data: {rect_data}, Error: {e}")
|
252
140
|
|
141
|
+
print(f"Loaded {loaded_count} valid rectangles.")
|
253
142
|
except FileNotFoundError:
|
254
|
-
print(f"No
|
255
|
-
except json.JSONDecodeError:
|
256
|
-
print(f"Error decoding JSON from {config_path}. Check file format. Starting fresh.")
|
143
|
+
print(f"No config found at {config_path}. Starting fresh.")
|
257
144
|
except Exception as e:
|
258
|
-
print(f"
|
259
|
-
import traceback
|
260
|
-
traceback.print_exc() # More detail on unexpected errors
|
145
|
+
print(f"Error loading config: {e}. Starting fresh.")
|
261
146
|
|
262
147
|
def save_rects(self, event=None):
|
263
|
-
"""Saves rectangles to
|
264
|
-
# Use the window geometry found during __init__ for consistency during save
|
265
|
-
window_geom_to_save = self.target_window_geometry
|
266
|
-
save_coord_system = COORD_SYSTEM_PERCENTAGE # Default if no window
|
267
|
-
|
148
|
+
"""Saves rectangles to config, converting from absolute pixels to percentages."""
|
268
149
|
config_path = self.get_scene_ocr_config()
|
269
|
-
|
150
|
+
win_geom = self.target_window_geometry
|
151
|
+
win_l, win_t, win_w, win_h = win_geom['left'], win_geom['top'], win_geom['width'], win_geom['height']
|
152
|
+
print(f"Saving rectangles to: {config_path} relative to window: {win_geom}")
|
153
|
+
|
154
|
+
serializable_rects = []
|
155
|
+
for monitor_dict, abs_coords, is_excluded in self.rectangles:
|
156
|
+
x_abs, y_abs, w_abs, h_abs = abs_coords
|
157
|
+
|
158
|
+
# Convert absolute pixel coordinates to percentages
|
159
|
+
x_pct = (x_abs - win_l) / win_w
|
160
|
+
y_pct = (y_abs - win_t) / win_h
|
161
|
+
w_pct = w_abs / win_w
|
162
|
+
h_pct = h_abs / win_h
|
163
|
+
coords_to_save = [x_pct, y_pct, w_pct, h_pct]
|
164
|
+
|
165
|
+
serializable_rects.append({
|
166
|
+
"monitor": {'index': monitor_dict['index']},
|
167
|
+
"coordinates": coords_to_save,
|
168
|
+
"is_excluded": is_excluded
|
169
|
+
})
|
170
|
+
|
171
|
+
save_data = {
|
172
|
+
"scene": obs.get_current_scene() or "default_scene",
|
173
|
+
"window": self.window_name,
|
174
|
+
"coordinate_system": COORD_SYSTEM_PERCENTAGE, # Always save as percentage
|
175
|
+
"window_geometry": win_geom, # Save the geometry used for conversion
|
176
|
+
"rectangles": serializable_rects
|
177
|
+
}
|
270
178
|
|
271
|
-
|
272
|
-
|
273
|
-
if window_geom_to_save:
|
274
|
-
# We have a window, try to save as percentages
|
275
|
-
win_l = window_geom_to_save['left']
|
276
|
-
win_t = window_geom_to_save['top']
|
277
|
-
win_w = window_geom_to_save['width']
|
278
|
-
win_h = window_geom_to_save['height']
|
279
|
-
# Basic check for valid dimensions needed for percentage calculation
|
280
|
-
if win_w > 0 and win_h > 0:
|
281
|
-
save_coord_system = COORD_SYSTEM_PERCENTAGE
|
282
|
-
win_l = window_geom_to_save['left']
|
283
|
-
win_t = window_geom_to_save['top']
|
284
|
-
print(f"Saving using coordinate system: {save_coord_system} relative to {window_geom_to_save}")
|
285
|
-
else:
|
286
|
-
print(
|
287
|
-
f"Warning: Window dimensions are invalid ({win_w}x{win_h}). Saving as absolute pixels instead.")
|
288
|
-
save_coord_system = COORD_SYSTEM_ABSOLUTE
|
289
|
-
window_geom_to_save = None # Don't save invalid geometry
|
290
|
-
else:
|
291
|
-
# No window found, save as absolute pixels
|
292
|
-
save_coord_system = COORD_SYSTEM_ABSOLUTE
|
293
|
-
print(f"Saving using coordinate system: {save_coord_system}")
|
294
|
-
# --- ---
|
295
|
-
|
296
|
-
serializable_rects = []
|
297
|
-
for monitor_dict, abs_coords, is_excluded in self.rectangles:
|
298
|
-
# abs_coords are the internal absolute pixels (x_abs, y_abs, w_abs, h_abs)
|
299
|
-
x_abs, y_abs, w_abs, h_abs = abs_coords
|
300
|
-
coords_to_save = []
|
301
|
-
|
302
|
-
# --- Convert absolute pixels to the chosen system ---
|
303
|
-
if save_coord_system == COORD_SYSTEM_PERCENTAGE and window_geom_to_save:
|
304
|
-
# Calculate percentages (handle potential float precision issues if necessary)
|
305
|
-
x_pct = (x_abs - win_l) / win_w
|
306
|
-
y_pct = (y_abs - win_t) / win_h
|
307
|
-
w_pct = w_abs / win_w
|
308
|
-
h_pct = h_abs / win_h
|
309
|
-
# Round percentages slightly to avoid overly long floats? Optional.
|
310
|
-
# precision = 6+
|
311
|
-
# coords_to_save = [round(x_pct, precision), round(y_pct, precision), round(w_pct, precision), round(h_pct, precision)]
|
312
|
-
coords_to_save = [x_pct, y_pct, w_pct, h_pct]
|
313
|
-
else:
|
314
|
-
# Save absolute pixel coordinates
|
315
|
-
coords_to_save = list(abs_coords)
|
316
|
-
save_coord_system = COORD_SYSTEM_ABSOLUTE # Ensure we note this system
|
317
|
-
|
318
|
-
# --- End Conversion ---
|
319
|
-
|
320
|
-
# Create serializable monitor info (e.g., just index)
|
321
|
-
monitor_info_to_save = {'index': monitor_dict['index']} # Save minimal info
|
322
|
-
|
323
|
-
rect_data = {
|
324
|
-
"monitor": monitor_info_to_save,
|
325
|
-
"coordinates": coords_to_save,
|
326
|
-
"is_excluded": is_excluded
|
327
|
-
}
|
328
|
-
serializable_rects.append(rect_data)
|
329
|
-
|
330
|
-
# Prepare final data structure for JSON
|
331
|
-
if not self.rectangles or len(self.rectangles) == 0:
|
332
|
-
save_coord_system = COORD_SYSTEM_PERCENTAGE
|
333
|
-
save_data = {
|
334
|
-
"scene": obs.get_current_scene() or "default_scene",
|
335
|
-
"window": self.window_name, # Store targeted window name
|
336
|
-
"coordinate_system": save_coord_system, # Explicitly save the system used
|
337
|
-
"rectangles": serializable_rects
|
338
|
-
}
|
339
|
-
# Only add window_geometry if it was valid and used for non-absolute saving
|
340
|
-
if window_geom_to_save and save_coord_system != COORD_SYSTEM_ABSOLUTE:
|
341
|
-
save_data["window_geometry"] = window_geom_to_save # Save geometry used for % calc
|
342
|
-
|
343
|
-
# Write to JSON file
|
344
|
-
with open(config_path, 'w', encoding="utf-8") as f:
|
345
|
-
json.dump(save_data, f, indent=4, ensure_ascii=False)
|
346
|
-
|
347
|
-
print(f"Successfully saved {len(serializable_rects)} rectangles.")
|
348
|
-
# Pass back the internally stored ABSOLUTE coordinates and context
|
349
|
-
# Need to convert internal tuples to lists for the manager dict
|
350
|
-
abs_rects_list = [(r[0], list(r[1]), r[2]) for r in self.rectangles]
|
351
|
-
self.result['rectangles'] = abs_rects_list
|
352
|
-
self.result['window_geometry'] = window_geom_to_save # Pass back geometry used (or None)
|
353
|
-
self.result['coordinate_system'] = save_coord_system # Pass back system used for saving
|
354
|
-
|
355
|
-
self.quit_app() # Close the selector UI after saving
|
179
|
+
with open(config_path, 'w', encoding="utf-8") as f:
|
180
|
+
json.dump(save_data, f, indent=4, ensure_ascii=False)
|
356
181
|
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
# self.root.destroy()
|
182
|
+
print(f"Successfully saved {len(serializable_rects)} rectangles.")
|
183
|
+
# Pass back the internal absolute coords for any immediate post-processing
|
184
|
+
self.result['rectangles'] = [(r[0], list(r[1]), r[2]) for r in self.rectangles]
|
185
|
+
self.result['window_geometry'] = win_geom
|
186
|
+
self.result['coordinate_system'] = COORD_SYSTEM_PERCENTAGE
|
187
|
+
self.quit_app()
|
364
188
|
|
365
189
|
def undo_last_rect(self, event=None):
|
366
|
-
"""Removes the last drawn rectangle."""
|
367
190
|
if self.rectangles and self.drawn_rect_ids:
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
# A better approach links the canvas ID directly to the rectangle data.
|
374
|
-
# For now, we'll assume simple append/pop correspondence.
|
375
|
-
|
376
|
-
# Find the canvas ID associated with this rectangle
|
377
|
-
monitor_index = last_rect_tuple[0]['index']
|
378
|
-
canvas = self.canvas_windows.get(monitor_index)
|
379
|
-
|
380
|
-
# Find the *specific* ID on that canvas to delete
|
381
|
-
# We need a way to map the internal rect tuple to the canvas ID
|
382
|
-
# Let's store ID with the rect temporarily: self.rectangles stores (mon, coords, excluded, canvas_id)
|
383
|
-
# Redo stack needs update too. Simpler for now: Assume last ID is correct.
|
384
|
-
|
385
|
-
if self.drawn_rect_ids: # Check if the list is not empty
|
386
|
-
last_rect_id = self.drawn_rect_ids.pop() # Get the assumed corresponding ID
|
387
|
-
|
388
|
-
# Add to redo stack (including the ID)
|
389
|
-
self.redo_stack.append((*last_rect_tuple, last_rect_id)) # (mon, coords, excluded, id)
|
390
|
-
|
391
|
-
if canvas:
|
392
|
-
try:
|
393
|
-
# Check if ID exists on this canvas before deleting
|
394
|
-
if last_rect_id in canvas.find_all():
|
395
|
-
canvas.delete(last_rect_id)
|
396
|
-
print(f"Undo: Deleted rectangle ID {last_rect_id} from canvas {monitor_index}")
|
397
|
-
else:
|
398
|
-
print(f"Warning: Undo - Rect ID {last_rect_id} not found on canvas {monitor_index}.")
|
399
|
-
except tk.TclError as e:
|
400
|
-
print(f"Warning: TclError during undo delete: {e}")
|
401
|
-
pass # Ignore if already deleted or canvas gone
|
402
|
-
else:
|
403
|
-
print(f"Warning: Undo - Canvas for monitor index {monitor_index} not found.")
|
404
|
-
else:
|
405
|
-
print("Warning: Undo failed - drawn_rect_ids list is empty.")
|
406
|
-
# Put the rectangle back if ID list was empty?
|
407
|
-
self.rectangles.append(last_rect_tuple)
|
408
|
-
|
409
|
-
|
410
|
-
elif self.current_rect_id is not None:
|
411
|
-
# If undoing during drag (before rectangle is finalized in self.rectangles)
|
412
|
-
active_canvas = None
|
413
|
-
# Find which canvas holds the temporary rectangle
|
414
|
-
for canvas in self.canvas_windows.values():
|
415
|
-
if self.current_rect_id in canvas.find_all():
|
416
|
-
active_canvas = canvas
|
417
|
-
break
|
418
|
-
if active_canvas:
|
419
|
-
try:
|
420
|
-
active_canvas.delete(self.current_rect_id)
|
421
|
-
print("Undo: Deleted temporary rectangle.")
|
422
|
-
except tk.TclError:
|
423
|
-
pass # Ignore if already deleted
|
424
|
-
self.current_rect_id = None
|
425
|
-
self.start_x = None
|
426
|
-
self.start_y = None
|
191
|
+
last_rect_tuple = self.rectangles.pop()
|
192
|
+
last_rect_id = self.drawn_rect_ids.pop()
|
193
|
+
self.redo_stack.append((*last_rect_tuple, last_rect_id))
|
194
|
+
event.widget.winfo_toplevel().winfo_children()[0].delete(last_rect_id)
|
195
|
+
print("Undo: Removed last rectangle.")
|
427
196
|
|
428
197
|
def redo_last_rect(self, event=None):
|
429
|
-
|
430
|
-
if not self.redo_stack:
|
431
|
-
print("Redo: Nothing to redo.")
|
432
|
-
return
|
433
|
-
|
434
|
-
# Pop monitor, absolute coords, is_excluded, and the original canvas ID
|
198
|
+
if not self.redo_stack: return
|
435
199
|
monitor, abs_coords, is_excluded, old_rect_id = self.redo_stack.pop()
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
canvas_x = x_abs - monitor['left']
|
445
|
-
canvas_y = y_abs - monitor['top']
|
446
|
-
|
447
|
-
try:
|
448
|
-
# Draw using coordinates relative to the canvas/monitor window
|
449
|
-
# IMPORTANT: This creates a *new* canvas ID
|
450
|
-
new_rect_id = canvas.create_rectangle(
|
451
|
-
canvas_x, canvas_y, canvas_x + w_abs, canvas_y + h_abs,
|
452
|
-
outline=outline_color, width=2
|
453
|
-
)
|
454
|
-
|
455
|
-
# Store the absolute coordinates and the *new* ID internally again
|
456
|
-
self.rectangles.append((monitor, abs_coords, is_excluded))
|
457
|
-
self.drawn_rect_ids.append(new_rect_id) # Add the NEW ID
|
458
|
-
print(f"Redo: Restored rectangle with new ID {new_rect_id} on canvas {monitor_index}")
|
459
|
-
|
460
|
-
except tk.TclError as e:
|
461
|
-
print(f"Warning: TclError during redo draw: {e}")
|
462
|
-
# If drawing fails, put the item back on the redo stack?
|
463
|
-
self.redo_stack.append((monitor, abs_coords, is_excluded, old_rect_id))
|
464
|
-
else:
|
465
|
-
print(f"Warning: Redo - Canvas for monitor index {monitor_index} not found.")
|
466
|
-
# Put the item back on the redo stack if canvas not found
|
467
|
-
self.redo_stack.append((monitor, abs_coords, is_excluded, old_rect_id))
|
468
|
-
|
469
|
-
def create_window(self, monitor):
|
470
|
-
"""Creates the transparent overlay window for a single monitor."""
|
471
|
-
monitor_index = monitor['index'] # Assumes index is set
|
472
|
-
monitor_left, monitor_top = monitor['left'], monitor['top']
|
473
|
-
monitor_width, monitor_height = monitor['width'], monitor['height']
|
200
|
+
canvas = event.widget.winfo_toplevel().winfo_children()[0]
|
201
|
+
x_abs, y_abs, w_abs, h_abs = abs_coords
|
202
|
+
canvas_x, canvas_y = x_abs - self.bounding_box['left'], y_abs - self.bounding_box['top']
|
203
|
+
new_rect_id = canvas.create_rectangle(canvas_x, canvas_y, canvas_x + w_abs, canvas_y + h_abs,
|
204
|
+
outline='orange' if is_excluded else 'green', width=2)
|
205
|
+
self.rectangles.append((monitor, abs_coords, is_excluded))
|
206
|
+
self.drawn_rect_ids.append(new_rect_id)
|
207
|
+
print("Redo: Restored rectangle.")
|
474
208
|
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
img = Image.frombytes('RGB', (screenshot.width, screenshot.height), screenshot.rgb)
|
209
|
+
def start(self):
|
210
|
+
self.root = tk.Tk()
|
211
|
+
self.root.withdraw()
|
479
212
|
|
480
|
-
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
213
|
+
# Calculate bounding box of all monitors
|
214
|
+
left = min(m['left'] for m in self.monitors)
|
215
|
+
top = min(m['top'] for m in self.monitors)
|
216
|
+
right = max(m['left'] + m['width'] for m in self.monitors)
|
217
|
+
bottom = max(m['top'] + m['height'] for m in self.monitors)
|
218
|
+
self.bounding_box = {'left': left, 'top': top, 'width': right - left, 'height': bottom - top}
|
485
219
|
|
486
|
-
|
487
|
-
|
488
|
-
# Create a blank placeholder image on error
|
489
|
-
img = Image.new('RGB', (monitor_width, monitor_height), color='grey')
|
490
|
-
draw = ImageDraw.Draw(img)
|
491
|
-
draw.text((10, 10), f"Error grabbing screen {monitor_index}", fill="red")
|
220
|
+
sct_img = self.sct.grab(self.sct.monitors[0])
|
221
|
+
img = Image.frombytes("RGB", sct_img.size, sct_img.bgra, "raw", "BGRX")
|
492
222
|
|
493
|
-
# Create the Toplevel window
|
494
223
|
window = tk.Toplevel(self.root)
|
495
|
-
window.geometry(f"{
|
496
|
-
window.overrideredirect(1)
|
497
|
-
window.attributes('-topmost', 1)
|
498
|
-
window.attributes("-alpha", 1.0 if self.image_mode else 0.2) # Initial transparency
|
224
|
+
window.geometry(f"{self.bounding_box['width']}x{self.bounding_box['height']}+{left}+{top}")
|
225
|
+
window.overrideredirect(1)
|
226
|
+
window.attributes('-topmost', 1)
|
499
227
|
|
500
|
-
|
501
|
-
|
502
|
-
# Create the canvas covering the entire window
|
503
|
-
canvas = tk.Canvas(window, cursor='cross', highlightthickness=0,
|
504
|
-
width=monitor_width, height=monitor_height)
|
228
|
+
self.photo_image = ImageTk.PhotoImage(img)
|
229
|
+
canvas = tk.Canvas(window, cursor='cross', highlightthickness=0)
|
505
230
|
canvas.pack(fill=tk.BOTH, expand=True)
|
506
|
-
|
507
|
-
|
508
|
-
# Draw
|
509
|
-
|
510
|
-
|
511
|
-
|
512
|
-
|
513
|
-
|
514
|
-
|
515
|
-
|
516
|
-
'canvas': canvas,
|
517
|
-
'bg_img_id': bg_img_id, # ID of the background image item
|
518
|
-
}
|
231
|
+
canvas.create_image(0, 0, image=self.photo_image, anchor=tk.NW)
|
232
|
+
|
233
|
+
# Draw existing rectangles (which were converted to absolute pixels on load)
|
234
|
+
for _, abs_coords, is_excluded in self.rectangles:
|
235
|
+
x_abs, y_abs, w_abs, h_abs = abs_coords
|
236
|
+
canvas_x = x_abs - self.bounding_box['left']
|
237
|
+
canvas_y = y_abs - self.bounding_box['top']
|
238
|
+
rect_id = canvas.create_rectangle(canvas_x, canvas_y, canvas_x + w_abs, canvas_y + h_abs,
|
239
|
+
outline='orange' if is_excluded else 'green', width=2)
|
240
|
+
self.drawn_rect_ids.append(rect_id)
|
519
241
|
|
520
|
-
# --- Draw existing rectangles loaded from config ---
|
521
|
-
# These are already converted to absolute pixel coordinates in self.rectangles
|
522
|
-
drawn_on_this_canvas = []
|
523
|
-
for mon_data, abs_coords, is_excluded in self.rectangles:
|
524
|
-
if mon_data['index'] == monitor_index:
|
525
|
-
x_abs, y_abs, w_abs, h_abs = abs_coords
|
526
|
-
# Convert absolute screen coords to canvas-local coords for drawing
|
527
|
-
canvas_x = x_abs - monitor_left
|
528
|
-
canvas_y = y_abs - monitor_top
|
529
|
-
outline_color = 'green' if not is_excluded else 'orange'
|
530
|
-
try:
|
531
|
-
rect_id = canvas.create_rectangle(
|
532
|
-
canvas_x, canvas_y, canvas_x + w_abs, canvas_y + h_abs,
|
533
|
-
outline=outline_color, width=2
|
534
|
-
)
|
535
|
-
# IMPORTANT: Store the generated ID. Needs careful mapping back to self.rectangles later
|
536
|
-
# For simplicity now, just add to the global list.
|
537
|
-
self.drawn_rect_ids.append(rect_id)
|
538
|
-
drawn_on_this_canvas.append(rect_id)
|
539
|
-
except tk.TclError as e:
|
540
|
-
print(f"Warning: TclError drawing existing rectangle {abs_coords} on canvas {monitor_index}: {e}")
|
541
|
-
print(f"Drew {len(drawn_on_this_canvas)} existing rectangles on monitor {monitor_index}")
|
542
|
-
|
543
|
-
# --- Define Event Handlers specific to this canvas instance ---
|
544
242
|
def on_click(event):
|
545
|
-
|
546
|
-
if
|
547
|
-
|
548
|
-
|
549
|
-
is_exclusion = bool(event.state & 0x0001)
|
550
|
-
outline_color = 'purple' if is_exclusion else 'red' # Temp color while drawing
|
551
|
-
try:
|
552
|
-
# Create rectangle on *this* specific canvas
|
553
|
-
self.current_rect_id = canvas.create_rectangle(
|
554
|
-
self.start_x, self.start_y, self.start_x, self.start_y,
|
555
|
-
outline=outline_color, width=2
|
556
|
-
)
|
557
|
-
except tk.TclError as e:
|
558
|
-
print(f"Warning: TclError creating rectangle on canvas {monitor_index}: {e}")
|
559
|
-
self.current_rect_id = None # Reset state if creation failed
|
243
|
+
self.start_x, self.start_y = event.x, event.y
|
244
|
+
outline = 'purple' if bool(event.state & 0x0001) else 'red'
|
245
|
+
self.current_rect_id = canvas.create_rectangle(self.start_x, self.start_y, self.start_x, self.start_y,
|
246
|
+
outline=outline, width=2)
|
560
247
|
|
561
248
|
def on_drag(event):
|
562
|
-
|
563
|
-
if self.current_rect_id:
|
564
|
-
try:
|
565
|
-
# Use canvas coords directly
|
566
|
-
canvas.coords(self.current_rect_id, self.start_x, self.start_y, event.x, event.y)
|
567
|
-
except tk.TclError as e:
|
568
|
-
print(f"Warning: TclError updating coords during drag on canvas {monitor_index}: {e}")
|
569
|
-
# Option: delete the rect and stop drag?
|
570
|
-
# canvas.delete(self.current_rect_id)
|
571
|
-
# self.current_rect_id = None
|
572
|
-
pass
|
249
|
+
if self.current_rect_id: canvas.coords(self.current_rect_id, self.start_x, self.start_y, event.x, event.y)
|
573
250
|
|
574
251
|
def on_release(event):
|
575
|
-
|
576
|
-
|
577
|
-
|
578
|
-
|
579
|
-
|
580
|
-
|
581
|
-
|
582
|
-
|
583
|
-
|
584
|
-
|
585
|
-
|
586
|
-
|
587
|
-
|
588
|
-
|
589
|
-
|
590
|
-
|
591
|
-
|
592
|
-
|
593
|
-
|
594
|
-
|
595
|
-
|
596
|
-
|
597
|
-
|
598
|
-
|
599
|
-
canvas.itemconfig(current_rect_id_local, outline=final_outline)
|
600
|
-
|
601
|
-
# Store the absolute coordinates and monitor info internally
|
602
|
-
abs_coords_tuple = (abs_x1, abs_y1, width_abs, height_abs)
|
603
|
-
# Add to internal list (using the correct monitor dictionary)
|
604
|
-
self.rectangles.append((monitor, abs_coords_tuple, is_excluded))
|
605
|
-
# Add the ID of the rectangle just drawn on this canvas
|
606
|
-
self.drawn_rect_ids.append(current_rect_id_local)
|
607
|
-
# Clear redo stack on new action
|
608
|
-
self.redo_stack.clear()
|
609
|
-
print(
|
610
|
-
f"Stored rectangle: Abs={abs_coords_tuple}, Excluded={is_excluded}, ID={current_rect_id_local}")
|
611
|
-
else:
|
612
|
-
# Rectangle too small, delete it from the canvas
|
613
|
-
canvas.delete(current_rect_id_local)
|
614
|
-
print(f"Skipping small rectangle: W={width_abs}, H={height_abs}")
|
615
|
-
|
616
|
-
except tk.TclError as e:
|
617
|
-
print(f"Warning: TclError processing rectangle on release on canvas {monitor_index}: {e}")
|
618
|
-
# Attempt cleanup if ID still exists
|
619
|
-
try:
|
620
|
-
if current_rect_id_local in canvas.find_all():
|
621
|
-
canvas.delete(current_rect_id_local)
|
622
|
-
except tk.TclError:
|
623
|
-
pass # Ignore cleanup error
|
624
|
-
|
625
|
-
finally:
|
626
|
-
# Always reset start coordinates
|
627
|
-
self.start_x = None
|
628
|
-
self.start_y = None
|
252
|
+
if not self.current_rect_id: return
|
253
|
+
coords = canvas.coords(self.current_rect_id)
|
254
|
+
x_abs = int(min(coords[0], coords[2]) + self.bounding_box['left'])
|
255
|
+
y_abs = int(min(coords[1], coords[3]) + self.bounding_box['top'])
|
256
|
+
w, h = int(abs(coords[2] - coords[0])), int(abs(coords[3] - coords[1]))
|
257
|
+
|
258
|
+
if w >= MIN_RECT_WIDTH and h >= MIN_RECT_HEIGHT:
|
259
|
+
is_excl = bool(event.state & 0x0001)
|
260
|
+
canvas.itemconfig(self.current_rect_id, outline='orange' if is_excl else 'green')
|
261
|
+
|
262
|
+
center_x, center_y = x_abs + w / 2, y_abs + h / 2
|
263
|
+
target_mon = self.monitors[0]
|
264
|
+
for mon in self.monitors:
|
265
|
+
if mon['left'] <= center_x < mon['left'] + mon['width'] and mon['top'] <= center_y < mon['top'] + \
|
266
|
+
mon['height']:
|
267
|
+
target_mon = mon
|
268
|
+
break
|
269
|
+
|
270
|
+
self.rectangles.append((target_mon, (x_abs, y_abs, w, h), is_excl))
|
271
|
+
self.drawn_rect_ids.append(self.current_rect_id)
|
272
|
+
self.redo_stack.clear()
|
273
|
+
else:
|
274
|
+
canvas.delete(self.current_rect_id)
|
275
|
+
self.current_rect_id = self.start_x = self.start_y = None
|
629
276
|
|
630
277
|
def on_right_click(event):
|
631
|
-
# Find rectangle item under cursor on *this* canvas
|
632
|
-
# Use find_closest initially, then check if it's overlapping and a rectangle we manage
|
633
|
-
# find_closest returns a tuple, possibly empty
|
634
278
|
items = canvas.find_closest(event.x, event.y)
|
635
|
-
|
636
|
-
|
637
|
-
|
638
|
-
|
639
|
-
|
640
|
-
|
641
|
-
|
642
|
-
|
643
|
-
|
644
|
-
if
|
645
|
-
|
646
|
-
|
647
|
-
|
648
|
-
|
649
|
-
|
650
|
-
|
651
|
-
|
652
|
-
|
653
|
-
|
654
|
-
|
655
|
-
|
656
|
-
|
657
|
-
|
658
|
-
|
659
|
-
if mon['index'] == monitor_index and \
|
660
|
-
abs(abs_coords[0] - rect_abs_x) < 2 and \
|
661
|
-
abs(abs_coords[1] - rect_abs_y) < 2 and \
|
662
|
-
abs(abs_coords[2] - rect_w) < 2 and \
|
663
|
-
abs(abs_coords[3] - rect_h) < 2:
|
664
|
-
found_rect_index = i
|
665
|
-
break
|
666
|
-
|
667
|
-
# --- Delete if found ---
|
668
|
-
if found_rect_index != -1:
|
669
|
-
print(
|
670
|
-
f"Deleting rectangle index {found_rect_index} (ID {target_id_to_delete}) on canvas {monitor_index}")
|
671
|
-
# Remove from internal list
|
672
|
-
del self.rectangles[found_rect_index]
|
673
|
-
# Remove from the ID list
|
674
|
-
self.drawn_rect_ids.remove(target_id_to_delete)
|
675
|
-
# Clear redo stack on new action
|
676
|
-
self.redo_stack.clear()
|
677
|
-
# Delete from canvas
|
678
|
-
canvas.delete(target_id_to_delete)
|
679
|
-
else:
|
680
|
-
print(
|
681
|
-
f"Warning: Could not find internal rectangle data matching canvas ID {target_id_to_delete} for deletion.")
|
682
|
-
# Optionally still delete from canvas if found, but lists will be inconsistent
|
683
|
-
# canvas.delete(target_id_to_delete)
|
684
|
-
# if target_id_to_delete in self.drawn_rect_ids: self.drawn_rect_ids.remove(target_id_to_delete)
|
685
|
-
|
686
|
-
except tk.TclError as e:
|
687
|
-
print(f"Warning: TclError deleting rectangle ID {target_id_to_delete} on right click: {e}")
|
688
|
-
except ValueError:
|
689
|
-
# This happens if ID was already removed from drawn_rect_ids somehow
|
690
|
-
print(
|
691
|
-
f"Warning: Rectangle ID {target_id_to_delete} not found in drawn_rect_ids list during delete.")
|
692
|
-
# Still try to delete from canvas if possible
|
693
|
-
try:
|
694
|
-
canvas.delete(target_id_to_delete)
|
695
|
-
except:
|
696
|
-
pass
|
697
|
-
|
698
|
-
def toggle_image_mode(event=None):
|
699
|
-
"""Toggles the transparency of the overlay window."""
|
700
|
-
self.image_mode = not self.image_mode
|
701
|
-
alpha = 1.0 if self.image_mode else 0.25 # Adjust alpha value as needed
|
702
|
-
# Apply alpha to all monitor windows
|
703
|
-
for mon_idx in self.monitor_windows:
|
704
|
-
try:
|
705
|
-
self.monitor_windows[mon_idx]['window'].attributes("-alpha", alpha)
|
706
|
-
except Exception as e:
|
707
|
-
print(f"Error setting alpha for monitor {mon_idx}: {e}")
|
708
|
-
|
709
|
-
# --- Bind Events to the canvas ---
|
710
|
-
canvas.bind('<ButtonPress-1>', on_click) # Left click start
|
711
|
-
canvas.bind('<B1-Motion>', on_drag) # Left drag
|
712
|
-
canvas.bind('<ButtonRelease-1>', on_release) # Left click release
|
713
|
-
canvas.bind('<Button-3>', on_right_click) # Right click delete
|
714
|
-
canvas.bind('<Control-s>', self.save_rects) # Save
|
715
|
-
canvas.bind('<Control-z>', self.undo_last_rect) # Undo
|
716
|
-
canvas.bind('<Control-y>', self.redo_last_rect) # Redo
|
717
|
-
canvas.bind("<m>", toggle_image_mode) # Toggle image mode (alpha)
|
718
|
-
|
719
|
-
|
720
|
-
# --- Bind Global Actions to the window (apply to all windows) ---
|
721
|
-
# Use lambdas to ensure the correct toggle function is called if needed,
|
722
|
-
# but here toggle_image_mode already affects all windows.
|
723
|
-
window.bind('<Control-s>', self.save_rects) # Save
|
724
|
-
window.bind('<Control-z>', self.undo_last_rect) # Undo
|
725
|
-
window.bind('<Control-y>', self.redo_last_rect) # Redo
|
726
|
-
# # Optional: Add non-Ctrl versions if desired
|
727
|
-
window.bind('<s>', self.save_rects)
|
728
|
-
window.bind('<z>', self.undo_last_rect)
|
729
|
-
window.bind('<y>', self.redo_last_rect)
|
730
|
-
window.bind("<Escape>", self.quit_app) # Quit
|
731
|
-
window.bind('<Button-3>', on_right_click) # Right click delete
|
732
|
-
window.bind("<m>", toggle_image_mode) # Toggle image mode (alpha)
|
279
|
+
if items and items[0] in self.drawn_rect_ids:
|
280
|
+
item_id = items[0]
|
281
|
+
idx_to_del = self.drawn_rect_ids.index(item_id)
|
282
|
+
del self.drawn_rect_ids[idx_to_del]
|
283
|
+
del self.rectangles[idx_to_del]
|
284
|
+
self.redo_stack.clear()
|
285
|
+
canvas.delete(item_id)
|
286
|
+
|
287
|
+
def toggle_image_mode(e=None):
|
288
|
+
self.image_mode = not self.image_mode; window.attributes("-alpha", 1.0 if self.image_mode else 0.25)
|
289
|
+
|
290
|
+
def on_enter(e=None):
|
291
|
+
canvas.focus_set()
|
292
|
+
|
293
|
+
canvas.bind('<Enter>', on_enter)
|
294
|
+
canvas.bind('<ButtonPress-1>', on_click)
|
295
|
+
canvas.bind('<B1-Motion>', on_drag)
|
296
|
+
canvas.bind('<ButtonRelease-1>', on_release)
|
297
|
+
canvas.bind('<Button-3>', on_right_click)
|
298
|
+
canvas.bind('<Control-s>', self.save_rects)
|
299
|
+
canvas.bind('<Control-z>', self.undo_last_rect)
|
300
|
+
canvas.bind('<Control-y>', self.redo_last_rect)
|
301
|
+
canvas.bind("<Escape>", self.quit_app)
|
302
|
+
canvas.bind("<m>", toggle_image_mode)
|
733
303
|
|
734
304
|
canvas.focus_set()
|
735
|
-
|
736
|
-
|
737
|
-
"""Initializes the Tkinter root and creates windows for each monitor."""
|
738
|
-
self.root = tk.Tk()
|
739
|
-
self.root.withdraw() # Hide the main useless root window
|
740
|
-
|
741
|
-
if not self.monitors:
|
742
|
-
print("Error: No monitors available to display.")
|
743
|
-
self.result['error'] = "No monitors found" # Report back error
|
744
|
-
self.root.destroy()
|
745
|
-
return
|
746
|
-
|
747
|
-
print(f"Creating selection windows for {len(self.monitors)} monitor(s)...")
|
748
|
-
for monitor in self.monitors:
|
749
|
-
self.create_window(monitor) # Create overlay for each monitor
|
750
|
-
|
751
|
-
# Check if any canvas windows were actually created
|
752
|
-
if not self.canvas_windows:
|
753
|
-
print("Error: Failed to create any monitor selection windows.")
|
754
|
-
self.result['error'] = "Failed to create monitor windows"
|
755
|
-
self.root.destroy()
|
756
|
-
return
|
757
|
-
|
758
|
-
print("Starting Tkinter main loop. Press Esc to quit, Ctrl+S to save.")
|
759
|
-
self.root.mainloop() # Start the event loop
|
305
|
+
print("Starting UI. Press Esc to quit, Ctrl+S to save, M to toggle background.")
|
306
|
+
self.root.mainloop()
|
760
307
|
|
761
308
|
def quit_app(self, event=None):
|
762
|
-
|
763
|
-
|
764
|
-
self.on_close() # Trigger cleanup
|
765
|
-
|
766
|
-
def on_close(self):
|
767
|
-
"""Cleans up Tkinter resources."""
|
768
|
-
# Check if root exists and hasn't been destroyed already
|
769
|
-
if self.root and self.root.winfo_exists():
|
770
|
-
print("Destroying Tkinter windows...")
|
771
|
-
try:
|
772
|
-
# Explicitly destroy child windows first (Toplevels)
|
773
|
-
for monitor_index in list(self.monitor_windows.keys()): # Iterate over keys copy
|
774
|
-
win_info = self.monitor_windows.pop(monitor_index, None)
|
775
|
-
if win_info and win_info.get('window'):
|
776
|
-
try:
|
777
|
-
if win_info['window'].winfo_exists():
|
778
|
-
win_info['window'].destroy()
|
779
|
-
except tk.TclError:
|
780
|
-
pass # Ignore if already destroyed
|
781
|
-
# Now destroy the root window
|
782
|
-
self.root.quit() # Exit mainloop first
|
783
|
-
self.root.destroy()
|
784
|
-
print("Tkinter windows destroyed.")
|
785
|
-
except Exception as e:
|
786
|
-
print(f"Error during Tkinter cleanup: {e}")
|
787
|
-
finally:
|
788
|
-
self.root = None # Ensure root is marked as gone
|
789
|
-
self.canvas_windows.clear()
|
790
|
-
self.monitor_windows.clear()
|
791
|
-
else:
|
792
|
-
print("Cleanup: Root window already destroyed or not initialized.")
|
793
|
-
|
309
|
+
if self.root and self.root.winfo_exists(): self.root.destroy()
|
310
|
+
self.root = None
|
794
311
|
|
795
|
-
# --- Multiprocessing Functions ---
|
796
312
|
|
797
313
|
def run_screen_selector(result_dict, window_name):
|
798
|
-
"""Target function for the separate process. Handles setup and running."""
|
799
314
|
try:
|
800
315
|
selector = ScreenSelector(result_dict, window_name)
|
801
316
|
selector.start()
|
802
|
-
# Result dictionary is updated directly by selector.save_rects or on error
|
803
|
-
print("ScreenSelector process finished.")
|
804
|
-
except ImportError as e:
|
805
|
-
print(f"Import error in subprocess: {e}")
|
806
|
-
result_dict['error'] = str(e) # Report error back via manager dict
|
807
317
|
except Exception as e:
|
808
|
-
print(f"Error
|
318
|
+
print(f"Error in selector process: {e}", file=sys.stderr)
|
809
319
|
import traceback
|
810
|
-
traceback.print_exc()
|
811
|
-
result_dict['error'] =
|
320
|
+
traceback.print_exc()
|
321
|
+
result_dict['error'] = str(e)
|
812
322
|
|
813
323
|
|
814
324
|
def get_screen_selection(window_name):
|
815
|
-
|
816
|
-
|
817
|
-
|
818
|
-
{'rectangles': list_of_abs_coords, 'window_geometry': dict_or_None, 'coordinate_system': str}
|
819
|
-
"""
|
820
|
-
if not selector_available:
|
821
|
-
print('Fatal Error: tkinter is not installed or available.')
|
822
|
-
return None
|
823
|
-
if not gw:
|
824
|
-
print('Fatal Error: pygetwindow is not installed or available.')
|
325
|
+
if not selector_available or not gw: return None
|
326
|
+
if not window_name:
|
327
|
+
print("Error: A target window name must be provided.", file=sys.stderr)
|
825
328
|
return None
|
826
329
|
|
827
330
|
with Manager() as manager:
|
828
|
-
# Use a Manager dictionary for safe inter-process communication
|
829
331
|
result_data = manager.dict()
|
830
332
|
process = Process(target=run_screen_selector, args=(result_data, window_name))
|
831
|
-
|
832
|
-
print(f"Starting ScreenSelector process for window: '{window_name or 'None'}'...")
|
333
|
+
print(f"Starting ScreenSelector process...")
|
833
334
|
process.start()
|
834
|
-
process.join()
|
835
|
-
print("ScreenSelector process joined.")
|
335
|
+
process.join()
|
836
336
|
|
837
|
-
# Process results from the Manager dictionary
|
838
337
|
if 'error' in result_data:
|
839
|
-
print(f"
|
840
|
-
return None
|
338
|
+
print(f"Selector process failed: {result_data['error']}", file=sys.stderr)
|
339
|
+
return None
|
841
340
|
elif 'rectangles' in result_data:
|
842
341
|
print("Screen selection successful.")
|
843
|
-
|
844
|
-
return {
|
845
|
-
"rectangles": result_data.get('rectangles'), # List of (monitor, [abs_coords], excluded)
|
846
|
-
"window_geometry": result_data.get('window_geometry'), # Dict or None
|
847
|
-
"coordinate_system": result_data.get('coordinate_system') # String constant
|
848
|
-
}
|
342
|
+
return dict(result_data)
|
849
343
|
else:
|
850
|
-
|
851
|
-
|
852
|
-
return {} # Return empty dict to indicate cancellation without error
|
344
|
+
print("Selection was cancelled by the user.")
|
345
|
+
return {}
|
853
346
|
|
854
347
|
|
855
|
-
# --- Main Execution Block ---
|
856
348
|
if __name__ == "__main__":
|
857
|
-
target_window_title = None # Default to absolute coordinates
|
858
|
-
# Check for command line arguments to specify window title
|
859
349
|
set_dpi_awareness()
|
350
|
+
target_window_title = "Windowed Projector (Preview)" # Default
|
860
351
|
if len(sys.argv) > 1:
|
861
352
|
target_window_title = sys.argv[1]
|
862
|
-
|
863
|
-
|
864
|
-
|
865
|
-
|
866
|
-
# Example: uncomment below to target Calculator on Windows by default if no arg given
|
867
|
-
# if sys.platform == "win32": target_window_title = "Calculator"
|
868
|
-
|
869
|
-
# if not target_window_title:
|
870
|
-
# target_window_title = get_ocr_config().window
|
871
|
-
|
872
|
-
if not target_window_title:
|
873
|
-
target_window_title = "Windowed Projector (Preview)"
|
353
|
+
# else:
|
354
|
+
# print("Usage: python your_script_name.py \"Target Window Title\"", file=sys.stderr)
|
355
|
+
# print("Example: python selector.py \"Windowed Projector (Preview)\"", file=sys.stderr)
|
356
|
+
# sys.exit(1)
|
874
357
|
|
875
|
-
# Get the selection result
|
876
358
|
selection_result = get_screen_selection(target_window_title)
|
877
359
|
|
878
|
-
# --- Process and display the result ---
|
879
360
|
if selection_result is None:
|
880
|
-
print("\n--- Screen selection failed
|
881
|
-
elif not selection_result:
|
882
|
-
print("\n--- Screen selection
|
361
|
+
print("\n--- Screen selection failed. ---")
|
362
|
+
elif not selection_result:
|
363
|
+
print("\n--- Screen selection cancelled. ---")
|
883
364
|
elif 'rectangles' in selection_result:
|
884
365
|
print("\n--- Selection Result ---")
|
885
|
-
|
886
|
-
|
887
|
-
|
888
|
-
|
889
|
-
|
890
|
-
|
891
|
-
|
892
|
-
|
893
|
-
|
894
|
-
print(f"Selected rectangles returned ({len(rectangles)}):")
|
895
|
-
# The returned coordinates are always absolute pixels for external use
|
896
|
-
for i, (monitor, coords, is_excluded) in enumerate(rectangles):
|
897
|
-
# Safely access monitor info
|
898
|
-
monitor_info = f"Idx:{monitor.get('index', 'N/A')} Pos:({monitor.get('left', '?')},{monitor.get('top', '?')}) W:{monitor.get('width', '?')} H:{monitor.get('height', '?')}"
|
899
|
-
# Format absolute pixel coordinates
|
900
|
-
coord_str = f"({coords[0]}, {coords[1]}, W:{coords[2]}, H:{coords[3]})"
|
901
|
-
print(f" Rect {i + 1}: Monitor={monitor_info}, Coords={coord_str}, Excluded={is_excluded}")
|
902
|
-
else:
|
903
|
-
# Should not happen if get_screen_selection returns correctly, but handles unexpected cases
|
904
|
-
print("\n--- Screen selection returned an unexpected result. ---")
|
905
|
-
print(selection_result)
|
366
|
+
rects = selection_result.get('rectangles', [])
|
367
|
+
win_geom = selection_result.get('window_geometry')
|
368
|
+
print(f"Saved relative to window: {win_geom}")
|
369
|
+
print(f"Selected rectangles ({len(rects)}):")
|
370
|
+
# The returned coordinates are absolute pixels for immediate use
|
371
|
+
for i, (monitor, coords, is_excluded) in enumerate(rects):
|
372
|
+
coord_str = f"(X:{coords[0]}, Y:{coords[1]}, W:{coords[2]}, H:{coords[3]})"
|
373
|
+
print(
|
374
|
+
f" Rect {i + 1}: On Monitor Idx:{monitor.get('index', 'N/A')}, Coords={coord_str}, Excluded={is_excluded}")
|
@@ -2,7 +2,7 @@ GameSentenceMiner/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU
|
|
2
2
|
GameSentenceMiner/anki.py,sha256=kWw3PV_Jj5-lHcttCB3lRXejHlaAbiJ2Ag_NAGX-RI8,16632
|
3
3
|
GameSentenceMiner/config_gui.py,sha256=h-vDxpFCC347iK_mDJAjwKm7Qubeu-NWaxvd9SvzqzY,90942
|
4
4
|
GameSentenceMiner/gametext.py,sha256=6VkjmBeiuZfPk8T6PHFdIAElBH2Y_oLVYvmcafqN7RM,6747
|
5
|
-
GameSentenceMiner/gsm.py,sha256=
|
5
|
+
GameSentenceMiner/gsm.py,sha256=PSL_J723k23SIfgeNhoXgTqlG-V3MQTFJtLDcrZDFqs,24625
|
6
6
|
GameSentenceMiner/obs.py,sha256=ZV9Vk39hrsJLT-AlIxa3qgncKxXaL3Myl33vVJEDEoA,14670
|
7
7
|
GameSentenceMiner/vad.py,sha256=G0NkaWFJaIfKQAV7LOFxyKoih7pPNYHDuy4SzeFVCkI,16389
|
8
8
|
GameSentenceMiner/ai/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
@@ -18,7 +18,7 @@ GameSentenceMiner/assets/pickaxe.png,sha256=VfIGyXyIZdzEnVcc4PmG3wszPMO1W4KCT7Q_
|
|
18
18
|
GameSentenceMiner/ocr/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
19
19
|
GameSentenceMiner/ocr/gsm_ocr_config.py,sha256=0hZmNIvZmlAEcy_NaTukG_ALUORULUT7sQ8q5VlDJU4,4047
|
20
20
|
GameSentenceMiner/ocr/ocrconfig.py,sha256=_tY8mjnzHMJrLS8E5pHqYXZjMuLoGKYgJwdhYgN-ny4,6466
|
21
|
-
GameSentenceMiner/ocr/owocr_area_selector.py,sha256=
|
21
|
+
GameSentenceMiner/ocr/owocr_area_selector.py,sha256=GEqIIhRc3WCIAx3HunuYo6ayJsCnZWT-x9fwZMCy2e8,16183
|
22
22
|
GameSentenceMiner/ocr/owocr_helper.py,sha256=YHhG3PuJsPWP4352TAu4dtdX7itRiOybngzZVT4B50c,20184
|
23
23
|
GameSentenceMiner/ocr/ss_picker.py,sha256=0IhxUdaKruFpZyBL-8SpxWg7bPrlGpy3lhTcMMZ5rwo,5224
|
24
24
|
GameSentenceMiner/owocr/owocr/__init__.py,sha256=87hfN5u_PbL_onLfMACbc0F5j4KyIK9lKnRCj6oZgR0,49
|
@@ -62,9 +62,9 @@ GameSentenceMiner/web/templates/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm
|
|
62
62
|
GameSentenceMiner/web/templates/index.html,sha256=n0J-dV8eksj8JXUuaCTIh0fIxIjfgm2EvxGBdQ6gWoM,214113
|
63
63
|
GameSentenceMiner/web/templates/text_replacements.html,sha256=tV5c8mCaWSt_vKuUpbdbLAzXZ3ATZeDvQ9PnnAfqY0M,8598
|
64
64
|
GameSentenceMiner/web/templates/utility.html,sha256=3flZinKNqUJ7pvrZk6xu__v67z44rXnaK7UTZ303R-8,16946
|
65
|
-
gamesentenceminer-2.10.
|
66
|
-
gamesentenceminer-2.10.
|
67
|
-
gamesentenceminer-2.10.
|
68
|
-
gamesentenceminer-2.10.
|
69
|
-
gamesentenceminer-2.10.
|
70
|
-
gamesentenceminer-2.10.
|
65
|
+
gamesentenceminer-2.10.10.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
66
|
+
gamesentenceminer-2.10.10.dist-info/METADATA,sha256=KJtMtM6AUz0qc8xsuSrNxd_I53gcYtAPNWO9VkDGSsY,7355
|
67
|
+
gamesentenceminer-2.10.10.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
68
|
+
gamesentenceminer-2.10.10.dist-info/entry_points.txt,sha256=2APEP25DbfjSxGeHtwBstMH8mulVhLkqF_b9bqzU6vQ,65
|
69
|
+
gamesentenceminer-2.10.10.dist-info/top_level.txt,sha256=V1hUY6xVSyUEohb0uDoN4UIE6rUZ_JYx8yMyPGX4PgQ,18
|
70
|
+
gamesentenceminer-2.10.10.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|