GameSentenceMiner 2.7.11__py3-none-any.whl → 2.7.13__py3-none-any.whl

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