From 67106ae64eaf5cb4bb5f7d40ba8767b03be5ae62 Mon Sep 17 00:00:00 2001 From: LemonNexus Date: Sun, 15 Feb 2026 01:20:54 +0000 Subject: [PATCH] feat: Add Snipping Tool-style area selection to Skill Scanner NEW FEATURE - Snipping Tool Area Selection: WORKFLOW: 1. Click 'Select Area' button in Skill Scanner 2. Screen dims with semi-transparent overlay 3. Drag to draw rectangle over your Skills window 4. Release to confirm (right-click or Escape to cancel) 5. Selected area is saved for all future scans 6. Click 'Start Smart Scan' to begin SNIPPING WIDGET FEATURES: - Fullscreen overlay with darkened background - Drag to draw selection rectangle - White border around selection - Dimensions displayed (e.g., '800 x 600') - Right-click to cancel - Escape key to cancel - Minimum 50x50 pixels required UI UPDATES: - Added 'Select Area' button with blue color (#4a9eff) - Area label shows current selection: '800x600 at (100, 200)' - All mode instructions updated to mention Select Area step TECHNICAL: - SnippingWidget inherits from QWidget - Uses Qt.TranslucentBackground for transparency - QPainter.CompositionMode_Clear for cutout effect - Selected area stored as self.scan_area (x, y, w, h) - SkillOCRThread accepts scan_area parameter - Both single scan and multi-page scan use selected area BENEFITS: - No more window detection errors - User has full control over scan region - Works regardless of window title or process name - Precise selection of Skills window area If no area selected, falls back to full game window capture. --- plugins/skill_scanner/plugin.py | 223 +++++++++++++++++++++++++++++--- 1 file changed, 206 insertions(+), 17 deletions(-) diff --git a/plugins/skill_scanner/plugin.py b/plugins/skill_scanner/plugin.py index 9b05bc0..101f0bf 100644 --- a/plugins/skill_scanner/plugin.py +++ b/plugins/skill_scanner/plugin.py @@ -172,23 +172,31 @@ class SkillOCRThread(QThread): scan_error = pyqtSignal(str) progress_update = pyqtSignal(str) - def __init__(self, ocr_service): + def __init__(self, ocr_service, scan_area=None): super().__init__() self.ocr_service = ocr_service + self.scan_area = scan_area # (x, y, width, height) or None def run(self): - """Perform OCR using core service - only on Entropia window.""" + """Perform OCR using core service - only on selected area or Entropia window.""" try: - self.progress_update.emit("Finding Entropia window...") - - # Capture only the Entropia game window - screenshot = capture_entropia_region() + if self.scan_area: + # Use user-selected area + x, y, w, h = self.scan_area + self.progress_update.emit(f"Capturing selected area ({w}x{h})...") + + from PIL import ImageGrab + screenshot = ImageGrab.grab(bbox=(x, y, x + w, y + h)) + else: + # Capture Entropia game window + self.progress_update.emit("Finding Entropia window...") + screenshot = capture_entropia_region() if screenshot is None: - self.scan_error.emit("Could not capture game window") + self.scan_error.emit("Could not capture screen") return - self.progress_update.emit("Running OCR on game window...") + self.progress_update.emit("Running OCR...") # Use core OCR service with the captured image result = self.ocr_service.recognize_image(screenshot) @@ -197,12 +205,22 @@ class SkillOCRThread(QThread): self.scan_error.emit(result['error']) return - self.progress_update.emit("Filtering and parsing skills...") + self.progress_update.emit("Parsing skills...") - # Filter out non-game text and parse skills + # Parse skills from text raw_text = result.get('text', '') skills_data = self._parse_skills_filtered(raw_text) + if not skills_data: + self.progress_update.emit("No skills found. Make sure Skills window is visible.") + else: + self.progress_update.emit(f"Found {len(skills_data)} skills") + + self.scan_complete.emit(skills_data) + + except Exception as e: + self.scan_error.emit(str(e)) + if not skills_data: self.progress_update.emit("No valid skills found. Make sure Skills window is open.") else: @@ -348,6 +366,108 @@ class SkillOCRThread(QThread): return skills +class SnippingWidget(QWidget): + """Fullscreen overlay for snipping tool-style area selection.""" + + area_selected = pyqtSignal(int, int, int, int) # x, y, width, height + cancelled = pyqtSignal() + + def __init__(self): + super().__init__() + self.setWindowFlags( + Qt.WindowType.FramelessWindowHint | + Qt.WindowType.WindowStaysOnTopHint | + Qt.WindowType.Tool + ) + self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground) + + # Get screen geometry + from PyQt6.QtWidgets import QApplication + screen = QApplication.primaryScreen().geometry() + self.setGeometry(screen) + + self.begin = None + self.end = None + self.drawing = False + + # Semi-transparent dark overlay + self.overlay_color = Qt.GlobalColor.black + self.overlay_opacity = 160 # 0-255 + + def paintEvent(self, event): + from PyQt6.QtGui import QPainter, QPen, QColor, QBrush + + painter = QPainter(self) + + # Draw semi-transparent overlay + overlay = QColor(0, 0, 0, self.overlay_opacity) + painter.fillRect(self.rect(), overlay) + + if self.begin and self.end: + # Clear overlay in selected area (make it transparent) + x = min(self.begin.x(), self.end.x()) + y = min(self.begin.y(), self.end.y()) + w = abs(self.end.x() - self.begin.x()) + h = abs(self.end.y() - self.begin.y()) + + # Draw the clear rectangle + painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_Clear) + painter.fillRect(x, y, w, h, Qt.GlobalColor.transparent) + painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_SourceOver) + + # Draw border around selection + pen = QPen(Qt.GlobalColor.white, 2, Qt.PenStyle.SolidLine) + painter.setPen(pen) + painter.drawRect(x, y, w, h) + + # Draw dimensions text + painter.setPen(Qt.GlobalColor.white) + from PyQt6.QtGui import QFont + font = QFont("Arial", 10) + painter.setFont(font) + painter.drawText(x, y - 5, f"{w} x {h}") + + def mousePressEvent(self, event): + if event.button() == Qt.MouseButton.LeftButton: + self.begin = event.pos() + self.end = event.pos() + self.drawing = True + self.update() + + def mouseMoveEvent(self, event): + if self.drawing: + self.end = event.pos() + self.update() + + def mouseReleaseEvent(self, event): + if event.button() == Qt.MouseButton.LeftButton and self.drawing: + self.drawing = False + self.end = event.pos() + + # Calculate selection rectangle + x = min(self.begin.x(), self.end.x()) + y = min(self.begin.y(), self.end.y()) + w = abs(self.end.x() - self.begin.x()) + h = abs(self.end.y() - self.begin.y()) + + # Minimum size check + if w > 50 and h > 50: + self.area_selected.emit(x, y, w, h) + else: + self.cancelled.emit() + + self.close() + elif event.button() == Qt.MouseButton.RightButton: + # Right click to cancel + self.cancelled.emit() + self.close() + + def keyPressEvent(self, event): + if event.key() == Qt.Key.Key_Escape: + self.cancelled.emit() + self.close() + + class SignalHelper(QObject): """Helper QObject to hold signals since BasePlugin doesn't inherit from QObject.""" hotkey_triggered = pyqtSignal() @@ -383,6 +503,9 @@ class SkillScannerPlugin(BasePlugin): self.current_scan_session = {} # Skills collected in current multi-page scan self.pages_scanned = 0 + # Scan area selection (x, y, width, height) - None means auto-detect game window + self.scan_area = None + # Connect signals (using signal helper QObject) self._signals.hotkey_triggered.connect(self._scan_page_for_multi) self._signals.update_status_signal.connect(self._update_multi_page_status_slot) @@ -447,6 +570,33 @@ class SkillScannerPlugin(BasePlugin): scan_group = QGroupBox("OCR Scan (Core Service)") scan_layout = QVBoxLayout(scan_group) + # Area selection row + area_layout = QHBoxLayout() + + self.select_area_btn = QPushButton("📐 Select Area") + self.select_area_btn.setStyleSheet(""" + QPushButton { + background-color: #4a9eff; + color: white; + padding: 10px 20px; + border: none; + border-radius: 4px; + font-weight: bold; + } + QPushButton:hover { + background-color: #3a8eef; + } + """) + self.select_area_btn.clicked.connect(self._start_area_selection) + area_layout.addWidget(self.select_area_btn) + + self.area_label = QLabel("Area: Not selected (will scan full game window)") + self.area_label.setStyleSheet("color: #888; font-size: 11px;") + area_layout.addWidget(self.area_label) + area_layout.addStretch() + + scan_layout.addLayout(area_layout) + # Buttons row buttons_layout = QHBoxLayout() @@ -510,7 +660,7 @@ class SkillScannerPlugin(BasePlugin): # Instructions self.instructions_label = QLabel( "🤖 SMART MODE:\n" - "1. Position Skills window\n" + "1. Click 'Select Area' above and drag over your Skills window\n" "2. Click 'Start Smart Scan'\n" "3. Navigate pages in EU - auto-detect will scan for you\n" "4. If auto fails, use hotkey F12 to scan manually\n" @@ -668,7 +818,8 @@ class SkillScannerPlugin(BasePlugin): self.scan_progress.setText("Error: OCR service not available") return - self.scanner = SkillOCRThread(ocr_service) + # Pass scan_area if user has selected one + self.scanner = SkillOCRThread(ocr_service, scan_area=self.scan_area) self.scanner.scan_complete.connect(self._on_scan_complete) self.scanner.scan_error.connect(self._on_scan_error) self.scanner.progress_update.connect(self._on_scan_progress) @@ -732,6 +883,7 @@ class SkillScannerPlugin(BasePlugin): def do_scan(): try: from core.ocr_service import get_ocr_service + from PIL import ImageGrab import re from datetime import datetime @@ -740,8 +892,19 @@ class SkillScannerPlugin(BasePlugin): self._signals.update_status_signal.emit("Error: OCR not available", False, True) return - # Capture and OCR - result = ocr_service.recognize() + # Capture based on scan_area setting + if self.scan_area: + x, y, w, h = self.scan_area + screenshot = ImageGrab.grab(bbox=(x, y, x + w, y + h)) + else: + screenshot = capture_entropia_region() + + if screenshot is None: + self._signals.update_status_signal.emit("Error: Could not capture screen", False, True) + return + + # OCR + result = ocr_service.recognize_image(screenshot) text = result.get('text', '') # Parse skills @@ -774,6 +937,32 @@ class SkillScannerPlugin(BasePlugin): thread.daemon = True thread.start() + def _start_area_selection(self): + """Open snipping tool for user to select scan area.""" + self.select_area_btn.setEnabled(False) + self.select_area_btn.setText("📐 Selecting...") + + # Create and show snipping widget + self.snipping_widget = SnippingWidget() + self.snipping_widget.area_selected.connect(self._on_area_selected) + self.snipping_widget.cancelled.connect(self._on_area_cancelled) + self.snipping_widget.show() + + def _on_area_selected(self, x, y, w, h): + """Handle area selection from snipping tool.""" + self.scan_area = (x, y, w, h) + self.area_label.setText(f"Area: {w}x{h} at ({x}, {y})") + self.area_label.setStyleSheet("color: #4ecdc4; font-size: 11px;") + self.select_area_btn.setEnabled(True) + self.select_area_btn.setText("📐 Select Area") + self._signals.update_status_signal.emit(f"✅ Scan area selected: {w}x{h}", True, False) + + def _on_area_cancelled(self): + """Handle cancelled area selection.""" + self.select_area_btn.setEnabled(True) + self.select_area_btn.setText("📐 Select Area") + self._signals.update_status_signal.emit("Area selection cancelled", False, False) + def _parse_skills_from_text(self, text): """Parse skills from OCR text.""" skills = {} @@ -893,9 +1082,9 @@ class SkillScannerPlugin(BasePlugin): def _on_scan_mode_changed(self, index): """Handle scan mode change.""" modes = [ - "🤖 SMART MODE:\n1. Position Skills window\n2. Click 'Start Smart Scan'\n3. Navigate pages - auto-detect will scan\n4. If auto fails, press F12\n5. Click 'Save All' when done", - "⌨️ HOTKEY MODE:\n1. Position Skills window\n2. Navigate to page 1 in EU\n3. Press F12 to scan each page\n4. Click Next Page in EU\n5. Repeat F12 for each page", - "🖱️ MANUAL MODE:\n1. Position Skills window\n2. Click 'Scan Current Page'\n3. Wait for beep\n4. Click Next Page in EU\n5. Repeat" + "🤖 SMART MODE:\n1. Click 'Select Area' and drag over Skills window\n2. Click 'Start Smart Scan'\n3. Navigate pages - auto-detect will scan\n4. If auto fails, press F12\n5. Click 'Save All' when done", + "⌨️ HOTKEY MODE:\n1. Click 'Select Area' and drag over Skills window\n2. Navigate to page 1 in EU\n3. Press F12 to scan each page\n4. Click Next Page in EU\n5. Repeat F12 for each page", + "🖱️ MANUAL MODE:\n1. Click 'Select Area' and drag over Skills window\n2. Click 'Scan Current Page'\n3. Wait for beep\n4. Click Next Page in EU\n5. Repeat" ] self.instructions_label.setText(modes[index])