Compare commits
2 commits
4b252da572
...
530e7f92a3
| Author | SHA1 | Date | |
|---|---|---|---|
| 530e7f92a3 | |||
| f7111896b5 |
3 changed files with 136 additions and 6 deletions
|
|
@ -82,7 +82,9 @@
|
||||||
originalAddEventListener.call(document, 'dragstart', function(e) {
|
originalAddEventListener.call(document, 'dragstart', function(e) {
|
||||||
currentDragUrl = null; // Reset
|
currentDragUrl = null; // Reset
|
||||||
|
|
||||||
console.log('%c[Intercept] dragstart', 'background: #FF9800; color: white; padding: 2px 6px;', 'ALT:', e.altKey);
|
// Check once per drag if native DnD is disabled via URL param (e.g. ?disablednd=true)
|
||||||
|
var disabledNativeDnD = /[?&]disablednd=true/i.test(window.location.search);
|
||||||
|
console.log('%c[Intercept] dragstart', 'background: #FF9800; color: white; padding: 2px 6px;', 'ALT:', e.altKey, '| disablednd:', disabledNativeDnD);
|
||||||
|
|
||||||
// Call Angular's handlers first to let them set the data
|
// Call Angular's handlers first to let them set the data
|
||||||
var handled = 0;
|
var handled = 0;
|
||||||
|
|
@ -102,7 +104,8 @@
|
||||||
console.log('[Intercept] Called', handled, 'Angular handlers, URL:', currentDragUrl ? currentDragUrl.substring(0, 60) : 'none');
|
console.log('[Intercept] Called', handled, 'Angular handlers, URL:', currentDragUrl ? currentDragUrl.substring(0, 60) : 'none');
|
||||||
|
|
||||||
// NOW check if we should intercept
|
// NOW check if we should intercept
|
||||||
if (e.altKey && currentDragUrl) {
|
// Intercept when: Alt key held (normal mode) OR native DnD disabled via URL param
|
||||||
|
if ((e.altKey || disabledNativeDnD) && currentDragUrl) {
|
||||||
var shouldIntercept = false;
|
var shouldIntercept = false;
|
||||||
|
|
||||||
// Check against configured URL mappings
|
// Check against configured URL mappings
|
||||||
|
|
@ -188,7 +191,7 @@
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('%c[WebDrop Intercept] Ready! ALT-drag will use Qt file drag.',
|
console.log('%c[WebDrop Intercept] Ready! ALT-drag or ?disablednd=true will use Qt file drag.',
|
||||||
'background: #4CAF50; color: white; font-weight: bold; padding: 4px 8px;');
|
'background: #4CAF50; color: white; font-weight: bold; padding: 4px 8px;');
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
console.error('[WebDrop Intercept] FATAL ERROR in bridge script:', e);
|
console.error('[WebDrop Intercept] FATAL ERROR in bridge script:', e);
|
||||||
|
|
|
||||||
|
|
@ -200,6 +200,101 @@ DEFAULT_WELCOME_PAGE = """
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class OpenDropZone(QWidget):
|
||||||
|
"""Drop target widget that opens dragged files with their system default application.
|
||||||
|
|
||||||
|
Displays an 'open folder' icon in the navigation toolbar. When a file is
|
||||||
|
dragged from the web view and dropped here, the file's URL is passed to
|
||||||
|
``QDesktopServices.openUrl()`` so the OS opens it with the associated
|
||||||
|
programme — exactly the same way double-clicking a file in Explorer/Finder
|
||||||
|
would behave.
|
||||||
|
|
||||||
|
Visual feedback is provided on drag-enter (green border highlight) so the
|
||||||
|
user can see the drop target is active.
|
||||||
|
|
||||||
|
Signals:
|
||||||
|
file_opened (str): Emitted with the local file path when successfully opened.
|
||||||
|
file_open_failed (str, str): Emitted with (path, error_message) on failure.
|
||||||
|
"""
|
||||||
|
|
||||||
|
file_opened = Signal(str)
|
||||||
|
file_open_failed = Signal(str, str)
|
||||||
|
|
||||||
|
_NORMAL_STYLE = "QLabel { padding: 4px; border: 2px solid transparent; border-radius: 4px; }"
|
||||||
|
_HOVER_STYLE = (
|
||||||
|
"QLabel { padding: 4px; border: 2px solid #4CAF50; border-radius: 4px;"
|
||||||
|
" background: #E8F5E9; }"
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, parent: Optional[QWidget] = None) -> None:
|
||||||
|
"""Initialize the OpenDropZone widget.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
parent: Parent widget.
|
||||||
|
"""
|
||||||
|
super().__init__(parent)
|
||||||
|
self.setAcceptDrops(True)
|
||||||
|
|
||||||
|
layout = QVBoxLayout(self)
|
||||||
|
layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
|
||||||
|
self._icon_label = QLabel(self)
|
||||||
|
icon = self.style().standardIcon(self.style().StandardPixmap.SP_DirOpenIcon)
|
||||||
|
pixmap = icon.pixmap(QSize(32, 32))
|
||||||
|
self._icon_label.setPixmap(pixmap)
|
||||||
|
self._icon_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
self._icon_label.setStyleSheet(self._NORMAL_STYLE)
|
||||||
|
self._icon_label.setToolTip("Drop a file here to open it with its default application")
|
||||||
|
layout.addWidget(self._icon_label)
|
||||||
|
|
||||||
|
self.setMinimumSize(QSize(44, 44))
|
||||||
|
self.setMaximumSize(QSize(48, 48))
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Drop handling
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
def dragEnterEvent(self, event) -> None: # type: ignore[override]
|
||||||
|
"""Accept drag events that carry file URLs."""
|
||||||
|
if event.mimeData().hasUrls():
|
||||||
|
event.acceptProposedAction()
|
||||||
|
self._icon_label.setStyleSheet(self._HOVER_STYLE)
|
||||||
|
else:
|
||||||
|
event.ignore()
|
||||||
|
|
||||||
|
def dragLeaveEvent(self, event) -> None: # type: ignore[override]
|
||||||
|
"""Reset appearance when drag leaves the widget."""
|
||||||
|
self._icon_label.setStyleSheet(self._NORMAL_STYLE)
|
||||||
|
super().dragLeaveEvent(event)
|
||||||
|
|
||||||
|
def dropEvent(self, event) -> None: # type: ignore[override]
|
||||||
|
"""Open each dropped file with the system default application.
|
||||||
|
|
||||||
|
Accepts the drop action so that the originating ``QDrag`` reports
|
||||||
|
success (preserving normal drag-started accounting), then immediately
|
||||||
|
opens the file via ``QDesktopServices.openUrl()``.
|
||||||
|
"""
|
||||||
|
self._icon_label.setStyleSheet(self._NORMAL_STYLE)
|
||||||
|
mime = event.mimeData()
|
||||||
|
if not mime.hasUrls():
|
||||||
|
event.ignore()
|
||||||
|
return
|
||||||
|
|
||||||
|
event.acceptProposedAction()
|
||||||
|
for url in mime.urls():
|
||||||
|
if url.isLocalFile():
|
||||||
|
file_path = url.toLocalFile()
|
||||||
|
logger.info(f"OpenDropZone: opening '{file_path}' with system default app")
|
||||||
|
if QDesktopServices.openUrl(url):
|
||||||
|
self.file_opened.emit(file_path)
|
||||||
|
else:
|
||||||
|
msg = "OS could not open the file"
|
||||||
|
logger.warning(f"OpenDropZone: {msg}: {file_path}")
|
||||||
|
self.file_open_failed.emit(file_path, msg)
|
||||||
|
else:
|
||||||
|
logger.debug(f"OpenDropZone: skipping non-local URL {url.toString()}")
|
||||||
|
|
||||||
|
|
||||||
class _DragBridge(QObject):
|
class _DragBridge(QObject):
|
||||||
"""JavaScript bridge for drag operations via QWebChannel.
|
"""JavaScript bridge for drag operations via QWebChannel.
|
||||||
|
|
||||||
|
|
@ -899,6 +994,30 @@ class MainWindow(QMainWindow):
|
||||||
f"Could not complete the drag-and-drop operation.\n\nError: {error}",
|
f"Could not complete the drag-and-drop operation.\n\nError: {error}",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _on_file_opened_via_drop(self, file_path: str) -> None:
|
||||||
|
"""Handle a file successfully opened via the OpenDropZone.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
file_path: Local file path that was opened.
|
||||||
|
"""
|
||||||
|
logger.info(f"Opened via drop zone: {file_path}")
|
||||||
|
self.statusBar().showMessage(f"Opened: {Path(file_path).name}", 4000)
|
||||||
|
|
||||||
|
def _on_file_open_failed_via_drop(self, file_path: str, error: str) -> None:
|
||||||
|
"""Handle a failure to open a file dropped on the OpenDropZone.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
file_path: Local file path that could not be opened.
|
||||||
|
error: Error description.
|
||||||
|
"""
|
||||||
|
logger.warning(f"Failed to open via drop zone '{file_path}': {error}")
|
||||||
|
QMessageBox.warning(
|
||||||
|
self,
|
||||||
|
"Open File Error",
|
||||||
|
f"Could not open the file with its default application.\n\n"
|
||||||
|
f"File: {file_path}\nError: {error}",
|
||||||
|
)
|
||||||
|
|
||||||
def _on_download_requested(self, download: QWebEngineDownloadRequest) -> None:
|
def _on_download_requested(self, download: QWebEngineDownloadRequest) -> None:
|
||||||
"""Handle download requests from the embedded web view.
|
"""Handle download requests from the embedded web view.
|
||||||
|
|
||||||
|
|
@ -1102,6 +1221,14 @@ class MainWindow(QMainWindow):
|
||||||
refresh_action = self.web_view.pageAction(self.web_view.page().WebAction.Reload)
|
refresh_action = self.web_view.pageAction(self.web_view.page().WebAction.Reload)
|
||||||
toolbar.addAction(refresh_action)
|
toolbar.addAction(refresh_action)
|
||||||
|
|
||||||
|
# Open-with-default-app drop zone (right of Reload)
|
||||||
|
self._open_drop_zone = OpenDropZone()
|
||||||
|
self._open_drop_zone.file_opened.connect(self._on_file_opened_via_drop)
|
||||||
|
self._open_drop_zone.file_open_failed.connect(self._on_file_open_failed_via_drop)
|
||||||
|
open_drop_action = QWidgetAction(toolbar)
|
||||||
|
open_drop_action.setDefaultWidget(self._open_drop_zone)
|
||||||
|
toolbar.addAction(open_drop_action)
|
||||||
|
|
||||||
# Add stretch spacer to push help buttons to the right
|
# Add stretch spacer to push help buttons to the right
|
||||||
spacer = QWidget()
|
spacer = QWidget()
|
||||||
spacer.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
|
spacer.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
|
||||||
|
|
|
||||||
|
|
@ -204,7 +204,7 @@ class RestrictedWebEngineView(QWebEngineView):
|
||||||
- Exact domain matches: example.com
|
- Exact domain matches: example.com
|
||||||
- Wildcard patterns: *.example.com
|
- Wildcard patterns: *.example.com
|
||||||
- Localhost variations: localhost, 127.0.0.1
|
- Localhost variations: localhost, 127.0.0.1
|
||||||
- File URLs: file://...
|
- Internal/local URLs: file://, data:, about:, blob:, qrc:
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
url: QUrl to check
|
url: QUrl to check
|
||||||
|
|
@ -216,8 +216,8 @@ class RestrictedWebEngineView(QWebEngineView):
|
||||||
host = url.host()
|
host = url.host()
|
||||||
scheme = url.scheme()
|
scheme = url.scheme()
|
||||||
|
|
||||||
# Allow file:// URLs (local webapp)
|
# Allow internal browser/Qt schemes (never send these to the OS)
|
||||||
if scheme == "file":
|
if scheme in ("file", "data", "about", "blob", "qrc"):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# If no whitelist, allow all URLs
|
# If no whitelist, allow all URLs
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue