fix: Implement drag-and-drop functionality with QWebChannel integration

This commit is contained in:
claudi 2026-01-30 08:17:01 +01:00
parent b2681a9cbd
commit f701247fab
4 changed files with 165 additions and 59 deletions

View file

@ -0,0 +1,73 @@
// WebDrop Bridge - Injected Script
// Automatically converts Z:\ path drags to native file drags via QWebChannel bridge
(function() {
if (window.__webdrop_bridge_injected) return;
window.__webdrop_bridge_injected = true;
function ensureChannel(cb) {
if (window.bridge) { cb(); return; }
function init() {
if (window.QWebChannel && window.qt && window.qt.webChannelTransport) {
new QWebChannel(window.qt.webChannelTransport, function(channel) {
window.bridge = channel.objects.bridge;
cb();
});
}
}
if (window.QWebChannel) {
init();
return;
}
var s = document.createElement('script');
s.src = 'qrc:///qtwebchannel/qwebchannel.js';
s.onload = init;
document.documentElement.appendChild(s);
}
function hook() {
document.addEventListener('dragstart', function(e) {
var dt = e.dataTransfer;
if (!dt) return;
// Get path from existing payload or from the card markup.
var path = dt.getData('text/plain');
if (!path) {
var card = e.target.closest && e.target.closest('.drag-item');
if (card) {
var pathEl = card.querySelector('p');
if (pathEl) {
path = (pathEl.textContent || '').trim();
}
}
}
if (!path) return;
// Ensure text payload exists for non-file drags and downstream targets.
if (!dt.getData('text/plain')) {
dt.setData('text/plain', path);
}
// Check if path is Z:\ — if yes, trigger native file drag. Otherwise, stay as text.
var isZDrive = /^z:/i.test(path);
if (!isZDrive) return;
// Z:\ detected — prevent default browser drag and convert to native file drag
e.preventDefault();
ensureChannel(function() {
if (window.bridge && typeof window.bridge.start_file_drag === 'function') {
window.bridge.start_file_drag(path);
}
});
}, false);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', hook);
} else {
hook();
}
})();

View file

@ -5,7 +5,9 @@ import logging
from pathlib import Path from pathlib import Path
from typing import Optional from typing import Optional
from PySide6.QtCore import QObject, QSize, Qt, QThread, QUrl, Signal from PySide6.QtCore import QObject, QPoint, QSize, Qt, QThread, QTimer, QUrl, Signal, Slot
from PySide6.QtWebChannel import QWebChannel
from PySide6.QtWebEngineCore import QWebEngineScript
from PySide6.QtWidgets import QLabel, QMainWindow, QStatusBar, QToolBar, QVBoxLayout, QWidget from PySide6.QtWidgets import QLabel, QMainWindow, QStatusBar, QToolBar, QVBoxLayout, QWidget
from webdrop_bridge.config import Config from webdrop_bridge.config import Config
@ -170,6 +172,39 @@ DEFAULT_WELCOME_PAGE = """
""" """
class _DragBridge(QObject):
"""JavaScript bridge for drag operations via QWebChannel.
Exposed to JavaScript as 'bridge' object.
"""
def __init__(self, window: 'MainWindow', parent: Optional[QObject] = None):
"""Initialize the drag bridge.
Args:
window: MainWindow instance
parent: Parent QObject
"""
super().__init__(parent)
self.window = window
@Slot(str)
def start_file_drag(self, path_text: str) -> None:
"""Start a native file drag for the given path.
Called from JavaScript when user drags a Z:\ path item.
Defers execution to avoid Qt drag manager state issues.
Args:
path_text: File path string to drag
"""
logger.debug(f"Bridge: start_file_drag called for {path_text}")
# Defer to avoid drag manager state issues
# initiate_drag() handles validation internally
QTimer.singleShot(0, lambda: self.window.drag_interceptor.initiate_drag([path_text]))
class MainWindow(QMainWindow): class MainWindow(QMainWindow):
"""Main application window for WebDrop Bridge. """Main application window for WebDrop Bridge.
@ -221,7 +256,6 @@ class MainWindow(QMainWindow):
# Create drag interceptor # Create drag interceptor
self.drag_interceptor = DragInterceptor() self.drag_interceptor = DragInterceptor()
# Set up path validator # Set up path validator
validator = PathValidator(config.allowed_roots) validator = PathValidator(config.allowed_roots)
self.drag_interceptor.set_validator(validator) self.drag_interceptor.set_validator(validator)
@ -230,6 +264,15 @@ class MainWindow(QMainWindow):
self.drag_interceptor.drag_started.connect(self._on_drag_started) self.drag_interceptor.drag_started.connect(self._on_drag_started)
self.drag_interceptor.drag_failed.connect(self._on_drag_failed) self.drag_interceptor.drag_failed.connect(self._on_drag_failed)
# Set up JavaScript bridge with QWebChannel
self._drag_bridge = _DragBridge(self)
web_channel = QWebChannel(self)
web_channel.registerObject("bridge", self._drag_bridge)
self.web_view.page().setWebChannel(web_channel)
# Install the drag bridge script
self._install_bridge_script()
# Set up central widget with layout # Set up central widget with layout
central_widget = QWidget() central_widget = QWidget()
layout = QVBoxLayout() layout = QVBoxLayout()
@ -248,6 +291,7 @@ class MainWindow(QMainWindow):
"""Load the web application. """Load the web application.
Loads HTML from the configured webapp URL or from local file. Loads HTML from the configured webapp URL or from local file.
Injects the WebChannel bridge JavaScript for drag-and-drop.
Supports both bundled apps (PyInstaller) and development mode. Supports both bundled apps (PyInstaller) and development mode.
Falls back to default welcome page if webapp not found. Falls back to default welcome page if webapp not found.
""" """
@ -282,15 +326,55 @@ class MainWindow(QMainWindow):
self.web_view.setHtml(welcome_html) self.web_view.setHtml(welcome_html)
return return
# Load local file as file:// URL # Load local file
file_url = file_path.as_uri() html_content = file_path.read_text(encoding='utf-8')
self.web_view.load(QUrl(file_url))
# Inject WebChannel bridge JavaScript
injected_html = self._inject_drag_bridge(html_content)
# Load the modified HTML
self.web_view.setHtml(injected_html, QUrl.fromLocalFile(file_path.parent))
except (OSError, ValueError) as e: except (OSError, ValueError) as e:
# Show welcome page on error # Show welcome page on error
welcome_html = DEFAULT_WELCOME_PAGE.format(version=self.config.app_version) welcome_html = DEFAULT_WELCOME_PAGE.format(version=self.config.app_version)
self.web_view.setHtml(welcome_html) self.web_view.setHtml(welcome_html)
def _install_bridge_script(self) -> None:
"""Install the drag bridge JavaScript via QWebEngineScript.
Follows the POC pattern for proper script injection and QWebChannel setup.
"""
script = QWebEngineScript()
script.setName("webdrop-bridge")
script.setInjectionPoint(QWebEngineScript.InjectionPoint.DocumentReady)
script.setWorldId(QWebEngineScript.ScriptWorldId.MainWorld)
script.setRunsOnSubFrames(False)
# Load bridge script from file
script_path = Path(__file__).parent / "bridge_script.js"
try:
with open(script_path, 'r', encoding='utf-8') as f:
script.setSourceCode(f.read())
self.web_view.page().scripts().insert(script)
logger.debug(f"Installed bridge script from {script_path}")
except (OSError, IOError) as e:
logger.warning(f"Failed to load bridge script: {e}")
def _inject_drag_bridge(self, html_content: str) -> str:
"""Return HTML content unmodified.
The drag bridge script is now injected via QWebEngineScript in _install_bridge_script().
This method is kept for compatibility but does nothing.
Args:
html_content: Original HTML content
Returns:
HTML unchanged
"""
return html_content
def _apply_stylesheet(self) -> None: def _apply_stylesheet(self) -> None:
"""Apply application stylesheet if available.""" """Apply application stylesheet if available."""
stylesheet_path = Path(__file__).parent.parent.parent.parent / \ stylesheet_path = Path(__file__).parent.parent.parent.parent / \

View file

@ -98,3 +98,4 @@ class RestrictedWebEngineView(QWebEngineView):
return True return True
return False return False

View file

@ -163,13 +163,13 @@
<div class="drag-item" draggable="true" id="dragItem1"> <div class="drag-item" draggable="true" id="dragItem1">
<div class="icon">🖼️</div> <div class="icon">🖼️</div>
<h3>Sample Image</h3> <h3>Sample Image</h3>
<p id="path1">Z:\samples\image.psd</p> <p id="path1">Z:\data\test-image.jpg</p>
</div> </div>
<div class="drag-item" draggable="true" id="dragItem2"> <div class="drag-item" draggable="true" id="dragItem2">
<div class="icon">📄</div> <div class="icon">📄</div>
<h3>Sample Document</h3> <h3>Sample Document</h3>
<p id="path2">Z:\samples\document.indd</p> <p id="path2">Z:\data\API_DOCUMENTATION.pdf</p>
</div> </div>
<div class="drag-item" draggable="true" id="dragItem3"> <div class="drag-item" draggable="true" id="dragItem3">
@ -193,57 +193,5 @@
<p>WebDrop Bridge v1.0.0 | Built with Qt and PySide6</p> <p>WebDrop Bridge v1.0.0 | Built with Qt and PySide6</p>
</div> </div>
</div> </div>
<script>
const items = document.querySelectorAll('.drag-item');
const statusMessage = document.getElementById('statusMessage');
items.forEach(item => {
item.addEventListener('dragstart', (e) => {
const pathElement = item.querySelector('p');
const path = pathElement.textContent.trim();
e.dataTransfer.effectAllowed = 'copy';
e.dataTransfer.setData('text/plain', path);
statusMessage.textContent = `Dragging: ${path}`;
statusMessage.className = 'status-message info';
console.log('🚀 Drag started:', path);
console.log('📋 DataTransfer types:', e.dataTransfer.types);
});
item.addEventListener('dragend', (e) => {
const pathElement = item.querySelector('p');
const path = pathElement.textContent.trim();
if (e.dataTransfer.dropEffect === 'none') {
statusMessage.textContent = `❌ Drop failed or cancelled`;
statusMessage.className = 'status-message info';
} else {
statusMessage.textContent = `✅ Drop completed: ${e.dataTransfer.dropEffect}`;
statusMessage.className = 'status-message success';
}
console.log('🏁 Drag ended:', e.dataTransfer.dropEffect);
});
// Visual feedback
item.addEventListener('dragstart', () => {
item.style.opacity = '0.5';
item.style.transform = 'scale(0.95)';
});
item.addEventListener('dragend', () => {
item.style.opacity = '1';
item.style.transform = 'scale(1)';
});
});
// Application info
console.log('%cWebDrop Bridge', 'font-size: 18px; font-weight: bold; color: #667eea;');
console.log('Ready for testing. Drag items to other applications.');
console.log('Check the path values in the drag items above.');
</script>
</body> </body>
</html> </html>