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.
This commit is contained in:
parent
4d9699c1a3
commit
67106ae64e
|
|
@ -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...")
|
||||
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})...")
|
||||
|
||||
# Capture only the Entropia game window
|
||||
screenshot = capture_entropia_region()
|
||||
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])
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue