Add drag & drop script variants and enhanced debugging tools
- Introduced multiple JavaScript scripts for handling drag & drop functionality: - `bridge_script.js`: Original implementation with popup prevention. - `bridge_script_debug.js`: Debug version with extensive logging for troubleshooting. - `bridge_script_v2.js`: Enhanced version extending DataTransfer for better integration. - `bridge_script_hybrid.js`: Hybrid approach allowing parallel native file drag. - `bridge_script_drop_intercept.js`: Intercepts drop events for custom handling. - `bridge_script_intercept.js`: Prevents browser drag for ALT+drag, using Qt for file drag. - Added detailed documentation in `SCRIPT_VARIANTS.md` outlining usage, status, and recommended workflows for each script. - Implemented logging features to capture drag events, DataTransfer modifications, and network requests for better debugging. - Enhanced DataTransfer handling to support Windows-specific file formats and improve user experience during drag & drop operations.
This commit is contained in:
parent
88dc358894
commit
dee02ad600
12 changed files with 2244 additions and 65 deletions
|
|
@ -1,7 +1,9 @@
|
|||
"""Main application window with web engine integration."""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
|
@ -426,7 +428,8 @@ class MainWindow(QMainWindow):
|
|||
logger.warning("Failed to load qwebchannel.js from resources")
|
||||
|
||||
# Load bridge script from file
|
||||
script_path = Path(__file__).parent / "bridge_script.js"
|
||||
# Using intercept script - prevents browser drag, hands off to Qt
|
||||
script_path = Path(__file__).parent / "bridge_script_intercept.js"
|
||||
try:
|
||||
with open(script_path, 'r', encoding='utf-8') as f:
|
||||
bridge_code = f.read()
|
||||
|
|
@ -492,7 +495,119 @@ class MainWindow(QMainWindow):
|
|||
local_path: Local file path that is being dragged
|
||||
"""
|
||||
logger.info(f"Drag started: {source} -> {local_path}")
|
||||
# Can be extended with status bar updates or user feedback
|
||||
|
||||
# Ask user if they want to check out the asset
|
||||
if source.startswith('http'):
|
||||
self._prompt_checkout(source, local_path)
|
||||
|
||||
def _prompt_checkout(self, azure_url: str, local_path: str) -> None:
|
||||
"""Prompt user to check out the asset.
|
||||
|
||||
Args:
|
||||
azure_url: Azure Blob Storage URL
|
||||
local_path: Local file path
|
||||
"""
|
||||
from PySide6.QtWidgets import QMessageBox
|
||||
|
||||
# Extract filename for display
|
||||
filename = Path(local_path).name
|
||||
|
||||
# Show confirmation dialog
|
||||
reply = QMessageBox.question(
|
||||
self,
|
||||
"Checkout Asset",
|
||||
f"Do you want to check out this asset?\n\n{filename}",
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||||
QMessageBox.StandardButton.Yes
|
||||
)
|
||||
|
||||
if reply == QMessageBox.StandardButton.Yes:
|
||||
logger.info(f"User confirmed checkout for {filename}")
|
||||
self._trigger_checkout_api(azure_url)
|
||||
else:
|
||||
logger.info(f"User declined checkout for {filename}")
|
||||
|
||||
def _trigger_checkout_api(self, azure_url: str) -> None:
|
||||
"""Trigger checkout via API call using JavaScript.
|
||||
|
||||
Calls the checkout API from JavaScript so HttpOnly cookies are automatically included.
|
||||
Example URL: https://devagravitystg.file.core.windows.net/devagravitysync/anPGZszKzgKaSz1SIx2HFgduy/filename
|
||||
Asset ID: anPGZszKzgKaSz1SIx2HFgduy
|
||||
|
||||
Args:
|
||||
azure_url: Azure Blob Storage URL containing asset ID
|
||||
"""
|
||||
try:
|
||||
# Extract asset ID from URL (middle segment between domain and filename)
|
||||
# Format: https://domain/container/ASSET_ID/filename
|
||||
match = re.search(r'/([^/]+)/[^/]+$', azure_url)
|
||||
if not match:
|
||||
logger.warning(f"Could not extract asset ID from URL: {azure_url}")
|
||||
return
|
||||
|
||||
asset_id = match.group(1)
|
||||
logger.info(f"Extracted asset ID: {asset_id}")
|
||||
|
||||
# Call API from JavaScript with Authorization header
|
||||
js_code = f"""
|
||||
(async function() {{
|
||||
try {{
|
||||
// Get captured auth token (from intercepted XHR)
|
||||
const authToken = window.capturedAuthToken;
|
||||
|
||||
if (!authToken) {{
|
||||
console.error('No authorization token available');
|
||||
return {{ success: false, error: 'No auth token' }};
|
||||
}}
|
||||
|
||||
const headers = {{
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'Accept-Language': 'de',
|
||||
'Authorization': authToken
|
||||
}};
|
||||
|
||||
const response = await fetch(
|
||||
'https://devagravityprivate.azurewebsites.net/api/assets/checkout/bulk?checkout=true',
|
||||
{{
|
||||
method: 'PUT',
|
||||
headers: headers,
|
||||
body: JSON.stringify({{asset_ids: ['{asset_id}']}})
|
||||
}}
|
||||
);
|
||||
|
||||
if (response.ok) {{
|
||||
console.log('✅ Checkout API successful for asset {asset_id}');
|
||||
return {{ success: true, status: response.status }};
|
||||
}} else {{
|
||||
const text = await response.text();
|
||||
console.warn('Checkout API returned status ' + response.status + ': ' + text.substring(0, 200));
|
||||
return {{ success: false, status: response.status, error: text }};
|
||||
}}
|
||||
}} catch (error) {{
|
||||
console.error('Checkout API call failed:', error);
|
||||
return {{ success: false, error: error.toString() }};
|
||||
}}
|
||||
}})();
|
||||
"""
|
||||
|
||||
def on_result(result):
|
||||
"""Callback when JavaScript completes."""
|
||||
if result and isinstance(result, dict):
|
||||
if result.get('success'):
|
||||
logger.info(f"✅ Checkout successful for asset {asset_id}")
|
||||
else:
|
||||
status = result.get('status', 'unknown')
|
||||
error = result.get('error', 'unknown error')
|
||||
logger.warning(f"Checkout API returned status {status}: {error}")
|
||||
else:
|
||||
logger.debug(f"Checkout API call completed (result: {result})")
|
||||
|
||||
# Execute JavaScript (async, non-blocking)
|
||||
self.web_view.page().runJavaScript(js_code, on_result)
|
||||
|
||||
except Exception as e:
|
||||
logger.exception(f"Error triggering checkout API: {e}")
|
||||
|
||||
def _on_drag_failed(self, source: str, error: str) -> None:
|
||||
"""Handle drag operation failure.
|
||||
|
|
@ -605,33 +720,13 @@ class MainWindow(QMainWindow):
|
|||
def dragEnterEvent(self, event):
|
||||
"""Handle drag entering the main window (from WebView or external).
|
||||
|
||||
When a drag from the WebView enters the MainWindow area, we can read
|
||||
the drag data and potentially convert Azure URLs to file drags.
|
||||
Note: With intercept script, ALT-drags are prevented in JavaScript
|
||||
and handled via bridge.start_file_drag(). This just handles any
|
||||
remaining drag events.
|
||||
|
||||
Args:
|
||||
event: QDragEnterEvent
|
||||
"""
|
||||
from PySide6.QtCore import QMimeData
|
||||
|
||||
mime_data = event.mimeData()
|
||||
|
||||
# Check if we have text data (URL from web app)
|
||||
if mime_data.hasText():
|
||||
url_text = mime_data.text()
|
||||
logger.debug(f"Drag entered main window with text: {url_text[:100]}")
|
||||
|
||||
# Store for potential conversion
|
||||
self._current_drag_url = url_text
|
||||
|
||||
# Check if it's convertible
|
||||
is_azure = url_text.startswith('https://') and 'file.core.windows.net' in url_text
|
||||
is_z_drive = url_text.lower().startswith('z:')
|
||||
|
||||
if is_azure or is_z_drive:
|
||||
logger.info(f"Convertible URL detected in drag: {url_text[:60]}")
|
||||
event.acceptProposedAction()
|
||||
return
|
||||
|
||||
event.ignore()
|
||||
|
||||
def dragMoveEvent(self, event):
|
||||
|
|
@ -640,10 +735,7 @@ class MainWindow(QMainWindow):
|
|||
Args:
|
||||
event: QDragMoveEvent
|
||||
"""
|
||||
if self._current_drag_url:
|
||||
event.acceptProposedAction()
|
||||
else:
|
||||
event.ignore()
|
||||
event.ignore()
|
||||
|
||||
def dragLeaveEvent(self, event):
|
||||
"""Handle drag leaving the main window.
|
||||
|
|
@ -651,33 +743,15 @@ class MainWindow(QMainWindow):
|
|||
Args:
|
||||
event: QDragLeaveEvent
|
||||
"""
|
||||
logger.debug("Drag left main window")
|
||||
# Reset tracking
|
||||
self._current_drag_url = None
|
||||
event.ignore()
|
||||
|
||||
def dropEvent(self, event):
|
||||
"""Handle drop on the main window.
|
||||
|
||||
This captures drops on the MainWindow area (outside WebView).
|
||||
If the user drops an Azure URL here, we convert it to a file operation.
|
||||
|
||||
Args:
|
||||
event: QDropEvent
|
||||
"""
|
||||
if self._current_drag_url:
|
||||
logger.info(f"Drop on main window with URL: {self._current_drag_url[:60]}")
|
||||
|
||||
# Handle via drag interceptor (converts Azure URL to local path)
|
||||
success = self.drag_interceptor.handle_drag(self._current_drag_url)
|
||||
|
||||
if success:
|
||||
event.acceptProposedAction()
|
||||
else:
|
||||
event.ignore()
|
||||
|
||||
self._current_drag_url = None
|
||||
else:
|
||||
event.ignore()
|
||||
event.ignore()
|
||||
|
||||
def _on_js_console_message(self, level, message, line_number, source_id):
|
||||
"""Redirect JavaScript console messages to Python logger.
|
||||
|
|
@ -863,10 +937,33 @@ class MainWindow(QMainWindow):
|
|||
def closeEvent(self, event) -> None:
|
||||
"""Handle window close event.
|
||||
|
||||
Properly cleanup WebEnginePage before closing to avoid
|
||||
"Release of profile requested but WebEnginePage still not deleted" warning.
|
||||
This ensures session data (cookies, login state) is properly saved.
|
||||
|
||||
Args:
|
||||
event: Close event
|
||||
"""
|
||||
# Can be extended with save operations or cleanup
|
||||
logger.debug("Closing application - cleaning up web engine resources")
|
||||
|
||||
# Properly delete WebEnginePage before the profile is released
|
||||
# This ensures cookies and session data are saved correctly
|
||||
if hasattr(self, 'web_view') and self.web_view:
|
||||
page = self.web_view.page()
|
||||
if page:
|
||||
# Disconnect signals to prevent callbacks during shutdown
|
||||
try:
|
||||
page.loadFinished.disconnect()
|
||||
except RuntimeError:
|
||||
pass # Already disconnected or never connected
|
||||
|
||||
# Delete the page explicitly
|
||||
page.deleteLater()
|
||||
logger.debug("WebEnginePage scheduled for deletion")
|
||||
|
||||
# Clear the page from the view
|
||||
self.web_view.setPage(None)
|
||||
|
||||
event.accept()
|
||||
|
||||
def check_for_updates_startup(self) -> None:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue