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)
|
scan_error = pyqtSignal(str)
|
||||||
progress_update = pyqtSignal(str)
|
progress_update = pyqtSignal(str)
|
||||||
|
|
||||||
def __init__(self, ocr_service):
|
def __init__(self, ocr_service, scan_area=None):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.ocr_service = ocr_service
|
self.ocr_service = ocr_service
|
||||||
|
self.scan_area = scan_area # (x, y, width, height) or None
|
||||||
|
|
||||||
def run(self):
|
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:
|
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
|
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()
|
screenshot = capture_entropia_region()
|
||||||
|
|
||||||
if screenshot is None:
|
if screenshot is None:
|
||||||
self.scan_error.emit("Could not capture game window")
|
self.scan_error.emit("Could not capture screen")
|
||||||
return
|
return
|
||||||
|
|
||||||
self.progress_update.emit("Running OCR on game window...")
|
self.progress_update.emit("Running OCR...")
|
||||||
|
|
||||||
# Use core OCR service with the captured image
|
# Use core OCR service with the captured image
|
||||||
result = self.ocr_service.recognize_image(screenshot)
|
result = self.ocr_service.recognize_image(screenshot)
|
||||||
|
|
@ -197,12 +205,22 @@ class SkillOCRThread(QThread):
|
||||||
self.scan_error.emit(result['error'])
|
self.scan_error.emit(result['error'])
|
||||||
return
|
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', '')
|
raw_text = result.get('text', '')
|
||||||
skills_data = self._parse_skills_filtered(raw_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:
|
if not skills_data:
|
||||||
self.progress_update.emit("No valid skills found. Make sure Skills window is open.")
|
self.progress_update.emit("No valid skills found. Make sure Skills window is open.")
|
||||||
else:
|
else:
|
||||||
|
|
@ -348,6 +366,108 @@ class SkillOCRThread(QThread):
|
||||||
return skills
|
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):
|
class SignalHelper(QObject):
|
||||||
"""Helper QObject to hold signals since BasePlugin doesn't inherit from QObject."""
|
"""Helper QObject to hold signals since BasePlugin doesn't inherit from QObject."""
|
||||||
hotkey_triggered = pyqtSignal()
|
hotkey_triggered = pyqtSignal()
|
||||||
|
|
@ -383,6 +503,9 @@ class SkillScannerPlugin(BasePlugin):
|
||||||
self.current_scan_session = {} # Skills collected in current multi-page scan
|
self.current_scan_session = {} # Skills collected in current multi-page scan
|
||||||
self.pages_scanned = 0
|
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)
|
# Connect signals (using signal helper QObject)
|
||||||
self._signals.hotkey_triggered.connect(self._scan_page_for_multi)
|
self._signals.hotkey_triggered.connect(self._scan_page_for_multi)
|
||||||
self._signals.update_status_signal.connect(self._update_multi_page_status_slot)
|
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_group = QGroupBox("OCR Scan (Core Service)")
|
||||||
scan_layout = QVBoxLayout(scan_group)
|
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 row
|
||||||
buttons_layout = QHBoxLayout()
|
buttons_layout = QHBoxLayout()
|
||||||
|
|
||||||
|
|
@ -510,7 +660,7 @@ class SkillScannerPlugin(BasePlugin):
|
||||||
# Instructions
|
# Instructions
|
||||||
self.instructions_label = QLabel(
|
self.instructions_label = QLabel(
|
||||||
"🤖 SMART MODE:\n"
|
"🤖 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"
|
"2. Click 'Start Smart Scan'\n"
|
||||||
"3. Navigate pages in EU - auto-detect will scan for you\n"
|
"3. Navigate pages in EU - auto-detect will scan for you\n"
|
||||||
"4. If auto fails, use hotkey F12 to scan manually\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")
|
self.scan_progress.setText("Error: OCR service not available")
|
||||||
return
|
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_complete.connect(self._on_scan_complete)
|
||||||
self.scanner.scan_error.connect(self._on_scan_error)
|
self.scanner.scan_error.connect(self._on_scan_error)
|
||||||
self.scanner.progress_update.connect(self._on_scan_progress)
|
self.scanner.progress_update.connect(self._on_scan_progress)
|
||||||
|
|
@ -732,6 +883,7 @@ class SkillScannerPlugin(BasePlugin):
|
||||||
def do_scan():
|
def do_scan():
|
||||||
try:
|
try:
|
||||||
from core.ocr_service import get_ocr_service
|
from core.ocr_service import get_ocr_service
|
||||||
|
from PIL import ImageGrab
|
||||||
import re
|
import re
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
@ -740,8 +892,19 @@ class SkillScannerPlugin(BasePlugin):
|
||||||
self._signals.update_status_signal.emit("Error: OCR not available", False, True)
|
self._signals.update_status_signal.emit("Error: OCR not available", False, True)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Capture and OCR
|
# Capture based on scan_area setting
|
||||||
result = ocr_service.recognize()
|
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', '')
|
text = result.get('text', '')
|
||||||
|
|
||||||
# Parse skills
|
# Parse skills
|
||||||
|
|
@ -774,6 +937,32 @@ class SkillScannerPlugin(BasePlugin):
|
||||||
thread.daemon = True
|
thread.daemon = True
|
||||||
thread.start()
|
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):
|
def _parse_skills_from_text(self, text):
|
||||||
"""Parse skills from OCR text."""
|
"""Parse skills from OCR text."""
|
||||||
skills = {}
|
skills = {}
|
||||||
|
|
@ -893,9 +1082,9 @@ class SkillScannerPlugin(BasePlugin):
|
||||||
def _on_scan_mode_changed(self, index):
|
def _on_scan_mode_changed(self, index):
|
||||||
"""Handle scan mode change."""
|
"""Handle scan mode change."""
|
||||||
modes = [
|
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",
|
"🤖 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. 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",
|
"⌨️ 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. Position Skills window\n2. Click 'Scan Current Page'\n3. Wait for beep\n4. Click Next Page in EU\n5. Repeat"
|
"🖱️ 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])
|
self.instructions_label.setText(modes[index])
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue