feat: implement mouse event emulator for Qt WebEngineView to enhance hover effects
Some checks are pending
Tests & Quality Checks / Test on Python 3.11 (push) Waiting to run
Tests & Quality Checks / Test on Python 3.12 (push) Waiting to run
Tests & Quality Checks / Test on Python 3.11-1 (push) Waiting to run
Tests & Quality Checks / Test on Python 3.12-1 (push) Waiting to run
Tests & Quality Checks / Test on Python 3.10 (push) Waiting to run
Tests & Quality Checks / Test on Python 3.11-2 (push) Waiting to run
Tests & Quality Checks / Test on Python 3.12-2 (push) Waiting to run
Tests & Quality Checks / Build Artifacts (push) Blocked by required conditions
Tests & Quality Checks / Build Artifacts-1 (push) Blocked by required conditions

This commit is contained in:
claudi 2026-03-04 14:49:40 +01:00
parent c612072dc8
commit 810baf65d9
5 changed files with 482 additions and 25 deletions

View file

@ -32,25 +32,24 @@
} else {
console.log('[Intercept] Auth token capture disabled (checkout feature inactive)');
}
// ============================================================================
// PART 1: Intercept Angular's dragstart listener registration
// ============================================================================
EventTarget.prototype.addEventListener = function(type, listener, options) {
if (listenerPatchActive && type === 'dragstart' && listener) {
// Store Angular's dragstart handler instead of registering it
console.log('[Intercept] Storing Angular dragstart listener for', this.tagName || this.constructor.name);
angularDragHandlers.push({
target: this,
listener: listener,
options: options
});
return; // Don't actually register it yet
}
// All other events: use original
return originalAddEventListener.call(this, type, listener, options);
};
// Only patch addEventListener for dragstart events
// This minimizes impact on other event listeners (mouseover, mouseenter, etc.)
EventTarget.prototype.addEventListener = function(type, listener, options) {
if (listenerPatchActive && type === 'dragstart' && listener) {
// Store Angular's dragstart handler instead of registering it
console.log('[Intercept] Storing Angular dragstart listener for', this.tagName || this.constructor.name);
angularDragHandlers.push({
target: this,
listener: listener,
options: options
});
return; // Don't actually register it yet
}
// All other events (mouseover, mouseenter, mousedown, etc.): use original
// This is critical to ensure mouseover/hover events work properly
return originalAddEventListener.call(this, type, listener, options);
};
// ============================================================================
// PART 2: Intercept DataTransfer.setData to capture URL

View file

@ -534,8 +534,8 @@ class MainWindow(QMainWindow):
def _install_bridge_script(self) -> None:
"""Install the drag bridge JavaScript via QWebEngineScript.
Uses DocumentCreation injection point to ensure script runs as early as possible,
before any page scripts that might interfere with drag events.
Uses Deferred injection point to ensure script runs after the DOM is ready,
allowing proper event listener registration without race conditions.
Embeds qwebchannel.js inline to avoid CSP issues with qrc:// URLs.
Injects configuration that bridge script uses for dynamic URL pattern matching.
@ -544,7 +544,9 @@ class MainWindow(QMainWindow):
script = QWebEngineScript()
script.setName("webdrop-bridge")
script.setInjectionPoint(QWebEngineScript.InjectionPoint.DocumentCreation)
# Use Deferred instead of DocumentCreation to allow DOM to be ready first
# This prevents race conditions with JavaScript event listeners
script.setInjectionPoint(QWebEngineScript.InjectionPoint.Deferred)
script.setWorldId(QWebEngineScript.ScriptWorldId.MainWorld)
script.setRunsOnSubFrames(False)
@ -633,18 +635,47 @@ class MainWindow(QMainWindow):
else:
logger.debug("Download interceptor not found (optional)")
# Combine: qwebchannel.js + config + bridge script + download interceptor
# Load mouse event emulator for hover effect support
mouse_emulator_search_paths = []
mouse_emulator_search_paths.append(Path(__file__).parent / "mouse_event_emulator.js")
if hasattr(sys, "_MEIPASS"):
mouse_emulator_search_paths.append(
Path(sys._MEIPASS) / "webdrop_bridge" / "ui" / "mouse_event_emulator.js" # type: ignore
)
mouse_emulator_search_paths.append(
exe_dir / "webdrop_bridge" / "ui" / "mouse_event_emulator.js"
)
mouse_emulator_code = ""
for path in mouse_emulator_search_paths:
if path.exists():
try:
with open(path, "r", encoding="utf-8") as f:
mouse_emulator_code = f.read()
logger.debug(f"Loaded mouse event emulator from {path}")
break
except (OSError, IOError) as e:
logger.warning(f"Mouse event emulator exists but failed to load: {e}")
if not mouse_emulator_code:
logger.debug("Mouse event emulator not found (optional)")
# Combine: qwebchannel.js + config + bridge script + download interceptor + mouse emulator
combined_code = qwebchannel_code + "\n\n" + config_code + "\n\n" + bridge_code
if download_interceptor_code:
combined_code += "\n\n" + download_interceptor_code
# Add mouse event emulator last to ensure it runs after all other scripts
if mouse_emulator_code:
combined_code += "\n\n" + mouse_emulator_code
logger.debug(
f"Combined script size: {len(combined_code)} chars "
f"(qwebchannel: {len(qwebchannel_code)}, "
f"config: {len(config_code)}, "
f"bridge: {len(bridge_code)}, "
f"interceptor: {len(download_interceptor_code)})"
f"interceptor: {len(download_interceptor_code)}, "
f"mouse_emulator: {len(mouse_emulator_code)})"
)
logger.debug(f"URL mappings in config: {len(self.config.url_mappings)}")
for i, mapping in enumerate(self.config.url_mappings):

View file

@ -0,0 +1,186 @@
// Mouse Event Emulator for Qt WebEngineView
// Qt WebEngineView may not forward all mouse events to JavaScript properly
// This script uses polling with document.elementFromPoint() to detect hover changes
// and manually dispatches mouseover/mouseenter/mouseleave events.
// ALSO: Injects a CSS stylesheet that simulates :hover effects using classes
(function() {
try {
if (window.__mouse_emulator_injected) return;
window.__mouse_emulator_injected = true;
console.log("[MouseEventEmulator] Initialized - polling for hover state changes");
// ========================================================
// PART 1: Inject CSS stylesheet for hover simulation
// ========================================================
var style = document.createElement("style");
style.type = "text/css";
style.id = "__mouse_emulator_hover_styles";
style.innerHTML = `
/* Checkbox hover simulation */
input[type="checkbox"].__mouse_hover {
cursor: pointer;
}
/* Link hover simulation */
a.__mouse_hover {
text-decoration: underline;
}
`;
if (document.head) {
document.head.insertBefore(style, document.head.firstChild);
} else {
document.body.insertBefore(style, document.body.firstChild);
}
// ========================================================
// PART 2: Track hover state and apply hover class
// ========================================================
var lastElement = null;
var lastX = -1;
var lastY = -1;
// High-frequency polling to detect element changes at mouse position
var pollIntervalId = setInterval(function() {
if (!window.__lastMousePos) {
window.__lastMousePos = { x: 0, y: 0 };
}
var x = window.__lastMousePos.x;
var y = window.__lastMousePos.y;
lastX = x;
lastY = y;
var element = document.elementFromPoint(x, y);
if (!element || element === document || element.tagName === "HTML") {
if (lastElement && lastElement !== document) {
try {
lastElement.classList.remove("__mouse_hover");
var leaveEvent = new MouseEvent("mouseleave", {
bubbles: true,
cancelable: true,
view: window,
});
lastElement.dispatchEvent(leaveEvent);
} catch (err) {
console.warn("[MouseEventEmulator] Error in leave handler:", err);
}
lastElement = null;
}
return;
}
// Element changed
if (element !== lastElement) {
// Remove hover class from previous element
if (lastElement && lastElement !== document && lastElement !== element) {
try {
lastElement.classList.remove("__mouse_hover");
var leaveEvent = new MouseEvent("mouseleave", {
bubbles: true,
cancelable: true,
view: window,
clientX: x,
clientY: y,
});
lastElement.dispatchEvent(leaveEvent);
} catch (err) {
console.warn("[MouseEventEmulator] Error dispatching mouseleave:", err);
}
}
// Add hover class and dispatch events for new element
if (element) {
try {
element.classList.add("__mouse_hover");
var overEvent = new MouseEvent("mouseover", {
bubbles: true,
cancelable: true,
view: window,
clientX: x,
clientY: y,
});
element.dispatchEvent(overEvent);
var enterEvent = new MouseEvent("mouseenter", {
bubbles: false,
cancelable: true,
view: window,
clientX: x,
clientY: y,
});
element.dispatchEvent(enterEvent);
var moveEvent = new MouseEvent("mousemove", {
bubbles: true,
cancelable: true,
view: window,
clientX: x,
clientY: y,
});
element.dispatchEvent(moveEvent);
} catch (err) {
console.warn("[MouseEventEmulator] Error dispatching mouse events:", err);
}
}
lastElement = element;
}
}, 50);
// Track mouse position from all available events
document.addEventListener(
"mousemove",
function(e) {
window.__lastMousePos = { x: e.clientX, y: e.clientY };
},
true
);
document.addEventListener(
"mousedown",
function(e) {
window.__lastMousePos = { x: e.clientX, y: e.clientY };
},
true
);
document.addEventListener(
"mouseup",
function(e) {
window.__lastMousePos = { x: e.clientX, y: e.clientY };
},
true
);
document.addEventListener(
"mouseover",
function(e) {
window.__lastMousePos = { x: e.clientX, y: e.clientY };
},
true
);
document.addEventListener(
"mouseenter",
function(e) {
window.__lastMousePos = { x: e.clientX, y: e.clientY };
},
true
);
console.log("[MouseEventEmulator] Ready - polling enabled for hover state detection");
} catch (e) {
console.error("[MouseEventEmulator] FATAL ERROR:", e);
if (e.stack) {
console.error("[MouseEventEmulator] Stack:", e.stack);
}
}
})();

View file

@ -8,7 +8,12 @@ from typing import List, Optional, Union
from PySide6.QtCore import QStandardPaths, QUrl
from PySide6.QtGui import QDesktopServices
from PySide6.QtWebEngineCore import QWebEngineNavigationRequest, QWebEnginePage, QWebEngineProfile
from PySide6.QtWebEngineCore import (
QWebEngineNavigationRequest,
QWebEnginePage,
QWebEngineProfile,
QWebEngineSettings,
)
from PySide6.QtWebEngineWidgets import QWebEngineView
logger = logging.getLogger(__name__)
@ -128,6 +133,31 @@ class RestrictedWebEngineView(QWebEngineView):
# Profile is unique per domain to prevent cache corruption
self.profile = self._create_persistent_profile()
# Configure WebEngine settings on the profile for proper JavaScript and mouse event support
settings = self.profile.settings()
# Enable JavaScript (required for mouseover events and interactive features)
settings.setAttribute(QWebEngineSettings.WebAttribute.JavascriptEnabled, True)
# Enable JavaScript access to clipboard (some web apps need this)
settings.setAttribute(QWebEngineSettings.WebAttribute.JavascriptCanAccessClipboard, True)
# Enable JavaScript to open windows (for dialogs, popups)
settings.setAttribute(QWebEngineSettings.WebAttribute.JavascriptCanOpenWindows, True)
# Enable local content access (needed for drag operations)
settings.setAttribute(QWebEngineSettings.WebAttribute.LocalContentCanAccessFileUrls, True)
# Allow local content to access remote resources (some web apps may need this)
settings.setAttribute(
QWebEngineSettings.WebAttribute.LocalContentCanAccessRemoteUrls, False
)
logger.debug(
"RestrictedWebEngineView WebEngine settings configured: "
"JavaScript=enabled, Clipboard=enabled, WindowOpen=enabled, LocalFileAccess=enabled"
)
# Use custom page for better download handling with persistent profile
custom_page = CustomWebEnginePage(self.profile, self)
self.setPage(custom_page)
@ -136,6 +166,23 @@ class RestrictedWebEngineView(QWebEngineView):
"RestrictedWebEngineView initialized with CustomWebEnginePage and persistent profile"
)
# CRITICAL: Also configure settings on the page itself after setPage()
# This ensures Page-level settings override Profile defaults for event handling
page_settings = self.page().settings()
page_settings.setAttribute(QWebEngineSettings.WebAttribute.JavascriptEnabled, True)
page_settings.setAttribute(
QWebEngineSettings.WebAttribute.JavascriptCanAccessClipboard, True
)
page_settings.setAttribute(QWebEngineSettings.WebAttribute.JavascriptCanOpenWindows, True)
page_settings.setAttribute(
QWebEngineSettings.WebAttribute.LocalContentCanAccessFileUrls, True
)
page_settings.setAttribute(
QWebEngineSettings.WebAttribute.LocalContentCanAccessRemoteUrls, False
)
logger.debug("Page-level WebEngine settings configured for mouse event handling")
# Connect to navigation request handler
self.page().navigationRequested.connect(self._on_navigation_requested)