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 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 vadget_config().audio.ffmpeg_reencode_options_to_use and os.path.exists(vad_trimmed_audio):
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, ImageDraw
8
+ from PIL import Image, ImageTk
9
9
 
10
- from GameSentenceMiner import obs # Import your actual obs module
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 # Handle missing library
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 # Minimum width in pixels
29
- MIN_RECT_HEIGHT = 25 # Minimum height in pixels
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 gw:
44
- raise ImportError("pygetwindow is required but not installed.")
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() # Connect to OBS (using mock or real)
42
+ obs.connect_to_obs_sync()
47
43
  self.window_name = window_name
48
- print(f"Target window name: {window_name or 'None (Absolute Mode)'}")
44
+ print(f"Targeting window: '{window_name}'")
45
+
49
46
  self.sct = mss.mss()
50
- self.monitors = self.sct.monitors[1:] # Skip the 'all monitors' entry
51
- if not self.monitors: # If only one monitor exists
52
- if len(self.sct.monitors) >= 1:
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 # Manager dict for results
62
- # Internal storage ALWAYS uses absolute pixel coordinates
63
- self.rectangles = [] # List to store (monitor_dict, (abs_x, abs_y, abs_w, abs_h), is_excluded) tuples
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 = None # Canvas coordinates
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.monitor_windows = {} # Stores {'window': tk.Toplevel, 'canvas': tk.Canvas, 'bg_img_id': int} per monitor index
71
- self.redo_stack = [] # Stores undone actions: (monitor_dict, abs_coords, is_excluded, canvas_id)
68
+ self.redo_stack = []
69
+ self.bounding_box = {} # Geometry of the single large canvas window
72
70
 
73
- # --- Window Awareness ---
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. Use max(1, ...) to avoid zero dimensions.
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 as e:
105
- print(f"Error getting geometry for window '{window.title}': {e}")
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
- # Get scene name (use mock or real OBS)
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 the config file, converting to absolute pixels."""
103
+ """Loads rectangles from config, converting from percentage to absolute pixels for use."""
138
104
  config_path = self.get_scene_ocr_config()
139
- # Get CURRENT window geometry for potential conversion
140
- current_window_geometry = self._get_window_geometry(self._find_target_window())
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
- # --- Determine coordinate system ---
147
- saved_coord_system = config_data.get("coordinate_system")
148
- # Check for window_name match if config stores it (optional enhancement)
149
- # saved_window_name = config_data.get("window")
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
- for rect_data in rectangles_data:
171
- monitor_data = rect_data.get("monitor") # Monitor info might be needed if not just index
172
- coords = rect_data.get("coordinates") # Could be %, relative px, or absolute px
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
- # Basic validation of loaded data structure
176
- if not (monitor_data and isinstance(monitor_data, dict) and 'index' in monitor_data and
177
- isinstance(coords, list) and len(coords) == 4):
178
- print(f"Skipping invalid rectangle data structure: {rect_data}")
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
- abs_coords = None # Will hold the final absolute pixel coordinates (x, y, w, h)
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
- # --- Convert loaded coords to absolute pixels ---
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
- # Store the monitor dict from *this* instance and absolute pixel coordinates internally
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
- else:
242
- print(
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 existing config found at {config_path}. Starting fresh.")
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"An unexpected error occurred while loading rectangles: {e}")
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 the config file, using percentages if window is targeted."""
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
- print(f"Saving rectangles to: {config_path}")
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
- try:
272
- # --- Determine coordinate system for saving ---
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
- except Exception as e:
358
- print(f"Failed to save rectangles: {e}")
359
- import traceback
360
- traceback.print_exc()
361
- # Optionally: Show an error message to the user in the UI
362
- # Do not destroy the root window on error, allow user to retry or quit
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
- # Pop the internal data
369
- last_rect_tuple = self.rectangles.pop() # (monitor_dict, abs_coords, is_excluded)
370
- # Pop the corresponding canvas ID (fragile if IDs aren't managed carefully)
371
- # This assumes drawn_rect_ids corresponds directly to rectangles, which might break
372
- # if deletes happened without updating both lists perfectly.
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
- """Redraws the last undone rectangle."""
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
- monitor_index = monitor['index']
437
- canvas = self.canvas_windows.get(monitor_index)
438
-
439
- if canvas:
440
- x_abs, y_abs, w_abs, h_abs = abs_coords # Use absolute coords for drawing
441
- outline_color = 'green' if not is_excluded else 'orange'
442
-
443
- # Convert absolute screen coords to canvas-local coords for drawing
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
- try:
476
- # Grab screenshot for this specific monitor
477
- screenshot = self.sct.grab(monitor)
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
- # Resize if screenshot dimensions don't match monitor info (e.g., DPI scaling)
481
- if img.width != monitor_width or img.height != monitor_height:
482
- print(
483
- f"Monitor {monitor_index}: Resizing screenshot from {img.size} to monitor size {monitor_width}x{monitor_height}")
484
- img = img.resize((monitor_width, monitor_height), Image.Resampling.LANCZOS)
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
- except Exception as e:
487
- print(f"Error grabbing screenshot for monitor {monitor_index}: {e}")
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"{monitor_width}x{monitor_height}+{monitor_left}+{monitor_top}")
496
- window.overrideredirect(1) # Frameless window
497
- window.attributes('-topmost', 1) # Keep on top
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
- img_tk = ImageTk.PhotoImage(img)
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
- # Keep a reference to the PhotoImage to prevent garbage collection!
507
- canvas.image = img_tk
508
- # Draw the background image onto the canvas
509
- bg_img_id = canvas.create_image(0, 0, image=img_tk, anchor=tk.NW)
510
- # Store the canvas widget, mapping from monitor index
511
- self.canvas_windows[monitor_index] = canvas
512
-
513
- # Store references for potential refreshing or cleanup
514
- self.monitor_windows[monitor_index] = {
515
- 'window': window,
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
- # event.x, event.y are relative to the canvas widget
546
- if self.current_rect_id is None:
547
- self.start_x, self.start_y = event.x, event.y # Store canvas coords
548
- # Determine color/state (Shift key for exclusion)
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
- # Update the temporary rectangle being drawn on *this* canvas
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
- # Finalize the rectangle drawn on *this* canvas
576
- if self.current_rect_id:
577
- current_rect_id_local = self.current_rect_id # Store locally in case it's reset
578
- self.current_rect_id = None # Reset global temp ID tracker
579
-
580
- try:
581
- # Get final coords relative to this canvas
582
- canvas_x1, canvas_y1, canvas_x2, canvas_y2 = canvas.coords(current_rect_id_local)
583
-
584
- # Convert canvas-local coords to absolute screen coords for storage
585
- # Ensure correct order (x1,y1 is top-left)
586
- abs_x1 = int(min(canvas_x1, canvas_x2) + monitor_left)
587
- abs_y1 = int(min(canvas_y1, canvas_y2) + monitor_top)
588
- abs_x2 = int(max(canvas_x1, canvas_x2) + monitor_left)
589
- abs_y2 = int(max(canvas_y1, canvas_y2) + monitor_top)
590
-
591
- # Calculate absolute width/height
592
- width_abs = abs_x2 - abs_x1
593
- height_abs = abs_y2 - abs_y1
594
-
595
- # Check against minimum size
596
- if width_abs >= MIN_RECT_WIDTH and height_abs >= MIN_RECT_HEIGHT:
597
- is_excluded = bool(event.state & 0x0001) # Check Shift key state at release
598
- final_outline = 'orange' if is_excluded else 'green' # Final color
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
- target_id_to_delete = None
636
- if items:
637
- item_id = items[0] # Get the closest item ID
638
- # Check if this ID is one we drew and if the click is actually inside its bbox
639
- if item_id in self.drawn_rect_ids and canvas.type(item_id) == 'rectangle':
640
- bbox = canvas.bbox(item_id)
641
- if bbox and bbox[0] <= event.x <= bbox[2] and bbox[1] <= event.y <= bbox[3]:
642
- target_id_to_delete = item_id
643
-
644
- if target_id_to_delete is not None:
645
- try:
646
- # --- Find the corresponding rectangle in self.rectangles ---
647
- # This requires matching the canvas ID or recalculating absolute coords
648
- found_rect_index = -1
649
-
650
- # Method 1: Match by recalculating absolute coords (more robust)
651
- canvas_coords = canvas.coords(target_id_to_delete) # canvas x1,y1,x2,y2
652
- rect_abs_x = int(min(canvas_coords[0], canvas_coords[2]) + monitor_left)
653
- rect_abs_y = int(min(canvas_coords[1], canvas_coords[3]) + monitor_top)
654
- rect_w = int(abs(canvas_coords[2] - canvas_coords[0]))
655
- rect_h = int(abs(canvas_coords[3] - canvas_coords[1]))
656
-
657
- for i, (mon, abs_coords, excluded) in enumerate(self.rectangles):
658
- # Compare recalculated coords with stored absolute coords (allow small tolerance)
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
- def start(self):
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
- """Initiates the cleanup and shutdown process."""
763
- print("Quit signal received, closing application...")
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 running ScreenSelector in subprocess: {e}")
318
+ print(f"Error in selector process: {e}", file=sys.stderr)
809
319
  import traceback
810
- traceback.print_exc() # Print detailed traceback in the subprocess console
811
- result_dict['error'] = f"Runtime error: {e}" # Report error back
320
+ traceback.print_exc()
321
+ result_dict['error'] = str(e)
812
322
 
813
323
 
814
324
  def get_screen_selection(window_name):
815
- """
816
- Launches the ScreenSelector in a separate process and returns the result.
817
- Returns None on failure or cancellation, otherwise a dict containing:
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() # Wait for the ScreenSelector process to complete
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"ScreenSelector process reported an error: {result_data['error']}")
840
- return None # Indicate failure
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
- # Return a standard dictionary copy from the Manager dict
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
- # This case usually means the user quit (Esc) without saving
851
- print("No selection saved or process was cancelled by user.")
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
- print(f"Attempting to target window title from args: '{target_window_title}'")
863
- else:
864
- print("Usage: python your_script_name.py [\"Target Window Title\"]")
865
- print("No window title provided. Using absolute screen coordinates.")
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 due to an error. ---")
881
- elif not selection_result: # Empty dict means cancelled
882
- print("\n--- Screen selection was cancelled by the user. ---")
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
- rectangles = selection_result.get('rectangles', [])
886
- window_geom = selection_result.get('window_geometry')
887
- coord_sys = selection_result.get('coordinate_system')
888
-
889
- print(f"Coordinate system used for saving: {coord_sys}")
890
- if window_geom:
891
- print(f"Saved relative to window geometry: {window_geom}")
892
- # else: It was COORD_SYSTEM_ABSOLUTE, no geometry saved/used
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}")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: GameSentenceMiner
3
- Version: 2.10.8
3
+ Version: 2.10.10
4
4
  Summary: A tool for mining sentences from games. Update: Full UI Re-design
5
5
  Author-email: Beangate <bpwhelan95@gmail.com>
6
6
  License: MIT License
@@ -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=BE4PGowAtC9vJlvO5DUypazJuKQmKwoIvAstWySS71w,24628
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=59zrzamPbBeU_Pfdeivc8RawlLXhXqNrhkBrhc69ZZo,47057
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.8.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
66
- gamesentenceminer-2.10.8.dist-info/METADATA,sha256=RbguN5KP3KaCpmklj4Yz5rg6WIGzHMMnDkp8mQBd1BI,7354
67
- gamesentenceminer-2.10.8.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
68
- gamesentenceminer-2.10.8.dist-info/entry_points.txt,sha256=2APEP25DbfjSxGeHtwBstMH8mulVhLkqF_b9bqzU6vQ,65
69
- gamesentenceminer-2.10.8.dist-info/top_level.txt,sha256=V1hUY6xVSyUEohb0uDoN4UIE6rUZ_JYx8yMyPGX4PgQ,18
70
- gamesentenceminer-2.10.8.dist-info/RECORD,,
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,,