QtBrowser splitted

parent 7f478654
import sys, os
sys.path.append(os.path.abspath(os.path.dirname(__file__)))
print(sys.path)
......@@ -2,52 +2,38 @@
import os
import sys
import subprocess
import tempfile
import json
import shutil
import asyncio
import platform
import signal
from pathlib import Path
from typing import Dict, List, Optional, Union, Callable, Any
import configparser
from functools import partial
# Import our RuntimeBridge for chrome.runtime API emulation
from assets.browser.js.runtime_bridge import RuntimeBridge, create_runtime_api_script
import re
from shmcs.qtbrowser.runtime_bridge import RuntimeBridge, create_runtime_api_script
from PyQt6.QtCore import Qt, QUrl, QProcess, pyqtSignal, QSize, QRect, QTimer, QMimeData, QPoint, QByteArray, QBuffer
from PyQt6.QtCore import Qt, QUrl, QSize, QPoint
from PyQt6.QtWebChannel import QWebChannel
from PyQt6.QtGui import QIcon, QAction, QKeySequence, QFont, QColor, QPainter, QDrag, QPixmap
from PyQt6.QtGui import QIcon, QAction, QKeySequence, QFont, QColor, QPainter
from PyQt6.QtWidgets import (
QApplication, QMainWindow, QTabWidget, QToolBar, QLineEdit,
QPushButton, QWidget, QVBoxLayout, QHBoxLayout, QDialog,
QLabel, QListWidget, QListWidgetItem, QCheckBox, QMenu,
QSizePolicy, QStyle, QFrame, QSplitter, QMessageBox, QStatusBar, QTabBar, QStyleOptionTab, QFileDialog,
QLabel, QMenu, QStyle, QMessageBox, QStatusBar,
QInputDialog, QTextEdit
)
from PyQt6.QtWebEngineWidgets import QWebEngineView
from PyQt6.QtWebEngineCore import (
QWebEngineProfile, QWebEngineSettings, QWebEnginePage, QWebEngineUrlSchemeHandler,
QWebEngineUrlRequestJob, QWebEngineUrlRequestInterceptor, QWebEngineUrlScheme,
QWebEngineProfile, QWebEngineSettings, QWebEnginePage, QWebEngineUrlScheme,
QWebEngineScript
)
import mimetypes
# Import Playwright for API compatibility
from playwright.async_api import async_playwright, Browser as PlaywrightBrowser
from playwright.async_api import Page as PlaywrightPage
from playwright.async_api import BrowserContext as PlaywrightBrowserContext
from playwright.async_api import async_playwright
from page import ChromeWebEnginePage
from tabs import BrowserTab, DetachableTabBar
from injector import ContentScriptInjector
from chrome import ChromeUrlInterceptor
from extensions import ExtensionSchemeHandler, ExtensionDialog
from profiles import ProfileDialog
from shmcs.qtbrowser.tabs import BrowserTab, DetachableTabBar
from shmcs.qtbrowser.chrome import ChromeUrlInterceptor, ChromeWebEnginePage
from shmcs.qtbrowser.extensions import ExtensionSchemeHandler, ExtensionDialog, ContentScriptInjector
from shmcs.qtbrowser.profiles import ProfileDialog
# Register URL schemes before any QApplication is created
def register_url_schemes():
......@@ -1633,191 +1619,7 @@ class Browser(QMainWindow):
dialog.exec()
class QPlaywrightBrowser:
"""
A class that wraps the Playwright browser API and uses Qt6Browser internally.
This class exposes the Playwright API while using the Qt6 browser for display.
"""
def __init__(self):
self.app = QApplication.instance() or QApplication(sys.argv)
self.browser_ui = None
self.playwright = None
self.browser = None
self.contexts = {}
self.event_loop = None
async def launch(self, **kwargs):
"""Launch a new browser instance."""
# Initialize Playwright if not already done
if not self.playwright:
self.playwright = await async_playwright().start()
# Create the Qt browser UI
if not self.browser_ui:
self.browser_ui = Browser()
self.browser_ui.show()
# Launch the actual Playwright browser (hidden)
# We'll use this for API compatibility but display in Qt
self.browser = await self.playwright.chromium.launch(
headless=True, # Run headless since we're displaying in Qt
args = [
'--headless=new',
'--enable-features=Autofill',
]+os.environ.get('QTWEBENGINE_CHROMIUM_FLAGS').split(),
**kwargs
)
#
# Create a default context
default_context = await self.browser.new_context()
self.contexts["default"] = default_context
return self
async def new_context(self, **kwargs):
"""Create a new browser context."""
if not self.browser:
await self.launch()
# Create a new context in the Playwright browser
context_id = f"context_{len(self.contexts)}"
context = await self.browser.new_context(**kwargs)
self.contexts[context_id] = context
# Return a wrapper that provides both Playwright API and Qt UI
return QPlaywrightBrowserContext(self, context, context_id)
async def new_page(self, url="about:blank"):
"""Create a new page in the default context."""
if "default" not in self.contexts:
await self.launch()
# Create a new page in the Playwright browser
pw_page = await self.contexts["default"].new_page()
await pw_page.goto(url)
# Create a new tab in the Qt browser
qt_tab = self.browser_ui.new_page(url)
qt_tab.page = pw_page
# Return a wrapper that provides both Playwright API and Qt UI
return QPlaywrightPage(pw_page, qt_tab)
async def close(self):
"""Close the browser."""
# Close all Playwright contexts and browser
if self.browser:
for context_id, context in self.contexts.items():
await context.close()
await self.browser.close()
self.contexts = {}
self.browser = None
# Close the Qt browser UI
if self.browser_ui:
self.browser_ui.close()
self.browser_ui = None
# Close Playwright
if self.playwright:
await self.playwright.stop()
self.playwright = None
def run(self):
"""Run the application event loop."""
return self.app.exec()
def run_async(self, coro):
"""Run an async coroutine in the Qt event loop."""
if not self.event_loop:
self.event_loop = asyncio.new_event_loop()
asyncio.set_event_loop(self.event_loop)
# Create a QTimer to process asyncio events
timer = QTimer()
timer.timeout.connect(lambda: self.event_loop.run_until_complete(asyncio.sleep(0)))
timer.start(10) # 10ms interval
# Run the coroutine
future = asyncio.run_coroutine_threadsafe(coro, self.event_loop)
return future
class QPlaywrightBrowserContext:
"""
A wrapper around a Playwright BrowserContext that provides both
Playwright API and Qt UI integration.
"""
def __init__(self, qbrowser, context, context_id):
self.qbrowser = qbrowser
self.context = context
self.context_id = context_id
self.pages = []
async def new_page(self):
"""Create a new page in this context."""
# Create a new page in the Playwright context
pw_page = await self.context.new_page()
# Create a new tab in the Qt browser
qt_tab = self.qbrowser.browser_ui.new_page("about:blank")
qt_tab.page = pw_page
# Create a wrapper page
page = QPlaywrightPage(pw_page, qt_tab)
self.pages.append(page)
return page
async def close(self):
"""Close this context."""
# Close all pages
for page in self.pages:
await page.close()
# Close the Playwright context
await self.context.close()
# Remove from the qbrowser contexts
if self.context_id in self.qbrowser.contexts:
del self.qbrowser.contexts[self.context_id]
class QPlaywrightPage:
"""
A wrapper around a Playwright Page that provides both
Playwright API and Qt UI integration.
"""
def __init__(self, pw_page, qt_tab):
self.pw_page = pw_page
self.qt_tab = qt_tab
# Forward Playwright page methods and properties
self.goto = pw_page.goto
self.click = pw_page.click
self.fill = pw_page.fill
self.type = pw_page.type
self.press = pw_page.press
self.wait_for_selector = pw_page.wait_for_selector
self.wait_for_navigation = pw_page.wait_for_navigation
self.wait_for_load_state = pw_page.wait_for_load_state
self.evaluate = pw_page.evaluate
self.screenshot = pw_page.screenshot
self.content = pw_page.content
self.title = pw_page.title
self.url = pw_page.url
async def close(self):
"""Close this page."""
# Close the Playwright page
await self.pw_page.close()
# Close the Qt tab
# We need to use the Qt event loop for this
index = self.qt_tab.parent.indexOf(self.qt_tab)
if index >= 0:
self.qt_tab.parent.close_tab(index)
async def main_async():
......
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
qbrowser.py - A Qt6-based browser that wraps and extends the Playwright browser class
This module provides a Qt6-based browser interface that wraps the Playwright browser
API and launches Chromium browser instances without decorations using the --app flag.
Each new page is opened as a new tab in the Qt6 window, and the interface includes
URL input, home button, and browser extension management.
"""
import os
import sys
import subprocess
import tempfile
import json
import shutil
import asyncio
import platform
import signal
from pathlib import Path
from typing import Dict, List, Optional, Union, Callable, Any
import configparser
from functools import partial
import re
from PyQt6.QtCore import Qt, QUrl, QProcess, pyqtSignal, QSize, QRect, QTimer, QMimeData, QPoint, QByteArray, QBuffer
from PyQt6.QtWebChannel import QWebChannel
from PyQt6.QtGui import QIcon, QAction, QKeySequence, QFont, QColor, QPainter, QDrag, QPixmap
from PyQt6.QtWidgets import (
QApplication, QMainWindow, QTabWidget, QToolBar, QLineEdit,
QPushButton, QWidget, QVBoxLayout, QHBoxLayout, QDialog,
QLabel, QListWidget, QListWidgetItem, QCheckBox, QMenu,
QSizePolicy, QStyle, QFrame, QSplitter, QMessageBox, QStatusBar, QTabBar, QStyleOptionTab, QFileDialog,
QInputDialog, QTextEdit
)
from PyQt6.QtWebEngineWidgets import QWebEngineView
from PyQt6.QtWebEngineCore import (
QWebEngineProfile, QWebEngineSettings, QWebEnginePage, QWebEngineUrlSchemeHandler,
QWebEngineUrlRequestJob, QWebEngineUrlRequestInterceptor, QWebEngineUrlScheme,
QWebEngineScript
)
import mimetypes
# Import Playwright for API compatibility
from playwright.async_api import async_playwright, Browser as PlaywrightBrowser
from playwright.async_api import Page as PlaywrightPage
from playwright.async_api import BrowserContext as PlaywrightBrowserContext
class QPlaywrightBrowser:
"""
A class that wraps the Playwright browser API and uses Qt6Browser internally.
This class exposes the Playwright API while using the Qt6 browser for display.
"""
def __init__(self):
self.app = QApplication.instance() or QApplication(sys.argv)
self.browser_ui = None
self.playwright = None
self.browser = None
self.contexts = {}
self.event_loop = None
async def launch(self, **kwargs):
"""Launch a new browser instance."""
# Initialize Playwright if not already done
if not self.playwright:
self.playwright = await async_playwright().start()
# Create the Qt browser UI
if not self.browser_ui:
self.browser_ui = Browser()
self.browser_ui.show()
# Launch the actual Playwright browser (hidden)
# We'll use this for API compatibility but display in Qt
self.browser = await self.playwright.chromium.launch(
headless=True, # Run headless since we're displaying in Qt
args = [
'--headless=new',
'--enable-features=Autofill',
]+os.environ.get('QTWEBENGINE_CHROMIUM_FLAGS').split(),
**kwargs
)
#
# Create a default context
default_context = await self.browser.new_context()
self.contexts["default"] = default_context
return self
async def new_context(self, **kwargs):
"""Create a new browser context."""
if not self.browser:
await self.launch()
# Create a new context in the Playwright browser
context_id = f"context_{len(self.contexts)}"
context = await self.browser.new_context(**kwargs)
self.contexts[context_id] = context
# Return a wrapper that provides both Playwright API and Qt UI
return QPlaywrightBrowserContext(self, context, context_id)
async def new_page(self, url="about:blank"):
"""Create a new page in the default context."""
if "default" not in self.contexts:
await self.launch()
# Create a new page in the Playwright browser
pw_page = await self.contexts["default"].new_page()
await pw_page.goto(url)
# Create a new tab in the Qt browser
qt_tab = self.browser_ui.new_page(url)
qt_tab.page = pw_page
# Return a wrapper that provides both Playwright API and Qt UI
return QPlaywrightPage(pw_page, qt_tab)
async def close(self):
"""Close the browser."""
# Close all Playwright contexts and browser
if self.browser:
for context_id, context in self.contexts.items():
await context.close()
await self.browser.close()
self.contexts = {}
self.browser = None
# Close the Qt browser UI
if self.browser_ui:
self.browser_ui.close()
self.browser_ui = None
# Close Playwright
if self.playwright:
await self.playwright.stop()
self.playwright = None
def run(self):
"""Run the application event loop."""
return self.app.exec()
def run_async(self, coro):
"""Run an async coroutine in the Qt event loop."""
if not self.event_loop:
self.event_loop = asyncio.new_event_loop()
asyncio.set_event_loop(self.event_loop)
# Create a QTimer to process asyncio events
timer = QTimer()
timer.timeout.connect(lambda: self.event_loop.run_until_complete(asyncio.sleep(0)))
timer.start(10) # 10ms interval
# Run the coroutine
future = asyncio.run_coroutine_threadsafe(coro, self.event_loop)
return future
class QPlaywrightBrowserContext:
"""
A wrapper around a Playwright BrowserContext that provides both
Playwright API and Qt UI integration.
"""
def __init__(self, qbrowser, context, context_id):
self.qbrowser = qbrowser
self.context = context
self.context_id = context_id
self.pages = []
async def new_page(self):
"""Create a new page in this context."""
# Create a new page in the Playwright context
pw_page = await self.context.new_page()
# Create a new tab in the Qt browser
qt_tab = self.qbrowser.browser_ui.new_page("about:blank")
qt_tab.page = pw_page
# Create a wrapper page
page = QPlaywrightPage(pw_page, qt_tab)
self.pages.append(page)
return page
async def close(self):
"""Close this context."""
# Close all pages
for page in self.pages:
await page.close()
# Close the Playwright context
await self.context.close()
# Remove from the qbrowser contexts
if self.context_id in self.qbrowser.contexts:
del self.qbrowser.contexts[self.context_id]
class QPlaywrightPage:
"""
A wrapper around a Playwright Page that provides both
Playwright API and Qt UI integration.
"""
def __init__(self, pw_page, qt_tab):
self.pw_page = pw_page
self.qt_tab = qt_tab
# Forward Playwright page methods and properties
self.goto = pw_page.goto
self.click = pw_page.click
self.fill = pw_page.fill
self.type = pw_page.type
self.press = pw_page.press
self.wait_for_selector = pw_page.wait_for_selector
self.wait_for_navigation = pw_page.wait_for_navigation
self.wait_for_load_state = pw_page.wait_for_load_state
self.evaluate = pw_page.evaluate
self.screenshot = pw_page.screenshot
self.content = pw_page.content
self.title = pw_page.title
self.url = pw_page.url
async def close(self):
"""Close this page."""
# Close the Playwright page
await self.pw_page.close()
# Close the Qt tab
# We need to use the Qt event loop for this
index = self.qt_tab.parent.indexOf(self.qt_tab)
if index >= 0:
self.qt_tab.parent.close_tab(index)
This source diff could not be displayed because it is too large. You can view the blob instead.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
RuntimeBridge - A QObject that bridges between the Python browser and JavaScript chrome.runtime API
"""
import os
import json
import time
from PyQt6.QtCore import QObject, pyqtSignal, pyqtSlot, QUrl, QTimer
from PyQt6.QtWebEngineCore import QWebEngineScript
class RuntimeBridge(QObject):
"""
A QObject that provides a bridge between the Python browser and the JavaScript chrome.runtime API.
This class is exposed to JavaScript via QWebChannel.
"""
# Signals to communicate with JavaScript
messageReceived = pyqtSignal(str)
portMessageReceived = pyqtSignal(str, str)
def __init__(self, browser, extensions_dir, parent=None):
"""Initialize the RuntimeBridge."""
super().__init__(parent)
self.browser = browser
self.extensions_dir = extensions_dir
self.extension_ports = {} # Dictionary to track active port connections
self.extension_pages = {} # Dictionary to track active extension pages
self.manifest_cache = {} # Cache for extension manifests
@pyqtSlot(str, str)
def extensionLoaded(self, extension_id, page_path):
"""Called when an extension page is loaded and the runtime API is initialized."""
print(f"Extension {extension_id} loaded page {page_path}")
if extension_id not in self.extension_pages:
self.extension_pages[extension_id] = []
self.extension_pages[extension_id].append(page_path)
# If this is a background page, we might want to trigger onInstalled or onStartup events
if page_path.endswith('background.html') or page_path.endswith('background.js'):
# In a real implementation, we would check if this is a first install or update
# For now, we'll just simulate an install event
self.triggerOnInstalledEvent(extension_id)
def triggerOnInstalledEvent(self, extension_id):
"""Trigger the onInstalled event for an extension."""
# This would be called when an extension is first installed or updated
# For now, we'll just print a message
print(f"Triggering onInstalled event for extension {extension_id}")
# In a real implementation, we would send a message to the extension
@pyqtSlot(str, result=str)
def getManifest(self, extension_id):
"""Get the manifest for an extension."""
if extension_id in self.manifest_cache:
return self.manifest_cache[extension_id]
# First try to load from the profile's extensions directory
manifest_path = os.path.join(self.extensions_dir, extension_id, 'manifest.json')
# If not found, try the assets extensions directory
if not os.path.exists(manifest_path):
assets_manifest_path = os.path.join("assets/browser/extensions", extension_id, 'manifest.json')
if os.path.exists(assets_manifest_path):
manifest_path = assets_manifest_path
if os.path.exists(manifest_path):
try:
with open(manifest_path, 'r', encoding='utf-8') as f:
manifest_data = json.load(f)
# For test-extension, ensure it has the right permissions
if extension_id == "test-extension":
# Make sure permissions are set correctly
if "permissions" not in manifest_data:
manifest_data["permissions"] = []
# Add necessary permissions if not already present
required_permissions = ["storage"]
for perm in required_permissions:
if perm not in manifest_data["permissions"]:
manifest_data["permissions"].append(perm)
manifest_json = json.dumps(manifest_data)
self.manifest_cache[extension_id] = manifest_json
return manifest_json
except Exception as e:
print(f"Error loading manifest for {extension_id}: {e}")
return "{}"
else:
print(f"Manifest not found for {extension_id}")
return "{}"
@pyqtSlot(str, result=bool)
def getBackgroundPage(self, extension_id):
"""Get the background page for an extension."""
print(f"Getting background page for {extension_id}")
# In a real implementation, we would return a reference to the background page
# For now, we'll just simulate success for the test extension
if extension_id == "test-extension":
# Emit a message to simulate the background page responding
background_message = {
"action": "background_accessed",
"from": "background",
"timestamp": int(time.time() * 1000)
}
# Emit after a short delay to simulate async behavior
QTimer.singleShot(100, lambda: self.messageReceived.emit(json.dumps(background_message)))
return True
return False
@pyqtSlot(str, str, str, result=str)
def sendMessage(self, target_extension_id, message, options, callback=None):
"""Send a message to an extension."""
print(f"Sending message to {target_extension_id}: {message}")
# Parse the message and options
try:
message_obj = json.loads(message)
options_obj = json.loads(options)
# Emit the messageReceived signal to notify all listeners
# This will be received by any extension page that has registered a listener
self.messageReceived.emit(message)
# For our test extension, we'll simulate a response from the background script
if target_extension_id == "test-extension":
response = {
"success": True,
"from": "background",
"received": True,
"timestamp": int(__import__('time').time() * 1000),
"echo": message_obj
}
else:
# Default response for other extensions
response = {"success": True, "echo": message_obj}
if callback:
callback(json.dumps(response))
return json.dumps(response)
except Exception as e:
print(f"Error sending message: {e}")
if callback:
callback('{"error": "Failed to send message"}')
return '{"error": "Failed to send message"}'
@pyqtSlot(str, str)
def connectPort(self, extension_id, port_name):
"""Connect a port for message passing."""
print(f"Connecting port {port_name} for extension {extension_id}")
if extension_id not in self.extension_ports:
self.extension_ports[extension_id] = {}
self.extension_ports[extension_id][port_name] = True
# For test extension, send an initial connection message
if extension_id == "test-extension":
# Send a welcome message after a short delay to simulate async behavior
QTimer.singleShot(100, lambda: self.send_port_welcome_message(extension_id, port_name))
def send_port_welcome_message(self, extension_id, port_name):
"""Send a welcome message to a newly connected port."""
welcome_message = {
"action": "connected",
"from": "background",
"timestamp": int(__import__('time').time() * 1000),
"message": f"Welcome to port {port_name}!"
}
self.portMessageReceived.emit(port_name, json.dumps(welcome_message))
@pyqtSlot(str, str)
def disconnectPort(self, extension_id, port_name):
"""Disconnect a port."""
print(f"Disconnecting port {port_name} for extension {extension_id}")
if extension_id in self.extension_ports and port_name in self.extension_ports[extension_id]:
del self.extension_ports[extension_id][port_name]
@pyqtSlot(str, str, str)
def postPortMessage(self, extension_id, port_name, message):
"""Post a message to a port."""
print(f"Posting message to port {port_name} for extension {extension_id}: {message}")
# Check if the port exists
if extension_id in self.extension_ports and port_name in self.extension_ports[extension_id]:
# For test extension, echo the message back with additional data
if extension_id == "test-extension":
try:
message_obj = json.loads(message)
response = {
"received": True,
"from": "background",
"timestamp": int(__import__('time').time() * 1000),
"echo": message_obj
}
# Emit the response after a short delay to simulate async processing
QTimer.singleShot(200, lambda: self.portMessageReceived.emit(port_name, json.dumps(response)))
except Exception as e:
print(f"Error processing port message: {e}")
self.portMessageReceived.emit(port_name, json.dumps({"error": str(e)}))
else:
# For other extensions, just echo the message back
self.portMessageReceived.emit(port_name, message)
else:
print(f"Port {port_name} for extension {extension_id} not found")
@pyqtSlot(str, str)
def sendResponse(self, extension_id, response):
"""Send a response to a message."""
print(f"Sending response for extension {extension_id}: {response}")
# In a real implementation, we would route this response to the appropriate sender
# For now, we'll just print it
@pyqtSlot(str)
def reloadExtension(self, extension_id):
"""Reload an extension."""
print(f"Reloading extension {extension_id}")
# In a real implementation, we would reload the extension
# For now, we'll just print a message
# If the browser has a reload_profile_and_tabs method, call it
if hasattr(self.browser, 'reload_profile_and_tabs'):
self.browser.reload_profile_and_tabs()
def create_runtime_api_script():
"""Create a QWebEngineScript to inject the chrome.runtime API emulation."""
script = QWebEngineScript()
# Load the chrome-runtime-api.js script
runtime_api_path = 'assets/browser/js/chrome-runtime-api.js'
with open(runtime_api_path, 'r', encoding='utf-8') as f:
runtime_api_js = f.read()
# Create a wrapper script that loads QWebChannel.js from qrc and then runs the runtime api script.
wrapper_js = """
// Wrapper to ensure proper initialization sequence
(function() {
var qwc_script = document.createElement('script');
qwc_script.src = 'qrc:///qtwebchannel/qwebchannel.js';
qwc_script.onload = function() {
// Now that QWebChannel is loaded, run the rest of the initialization.
// Create global initialization tracking variables
window.__qtWebChannelTransportReady = false;
window.__chromeRuntimeInitialized = false;
window.__initializationAttempts = 0;
// Add helper functions to check initialization status
window.isQWebChannelReady = function() {
return typeof QWebChannel !== 'undefined' &&
typeof qt !== 'undefined' &&
qt.webChannelTransport !== undefined;
};
window.isChromeRuntimeInitialized = function() {
return window.__chromeRuntimeInitialized === true ||
(typeof chrome !== 'undefined' &&
typeof chrome.runtime !== 'undefined' &&
window.__qtWebChannelTransportReady === true);
};
// Function to handle initialization failure
window.reportInitializationStatus = function() {
console.log('Initialization Status:');
console.log('- QWebChannel available: ' + (typeof QWebChannel !== 'undefined'));
console.log('- qt object available: ' + (typeof qt !== 'undefined'));
console.log('- qt.webChannelTransport available: ' + (typeof qt !== 'undefined' && qt.webChannelTransport !== undefined));
console.log('- chrome object available: ' + (typeof chrome !== 'undefined'));
console.log('- chrome.runtime available: ' + (typeof chrome !== 'undefined' && chrome.runtime !== undefined));
console.log('- __qtWebChannelTransportReady: ' + window.__qtWebChannelTransportReady);
console.log('- __chromeRuntimeInitialized: ' + window.__chromeRuntimeInitialized);
console.log('- __initializationAttempts: ' + window.__initializationAttempts);
};
// Set up event listeners for initialization events
document.addEventListener('webChannelReady', function(event) {
console.log('webChannelReady event received');
window.__qtWebChannelTransportReady = true;
window.reportInitializationStatus();
});
document.addEventListener('chrome.runtime.initialized', function(event) {
console.log('chrome.runtime.initialized event received for extension: ' + event.detail.extensionId);
window.__chromeRuntimeInitialized = true;
window.reportInitializationStatus();
});
// Then initialize the chrome.runtime API
%s
// Log the initialization status
console.log('Runtime API script injected. QWebChannel ready: ' + window.isQWebChannelReady());
window.reportInitializationStatus();
// Set up a periodic check for initialization status
var statusCheckInterval = setInterval(function() {
window.__initializationAttempts++;
if (window.isChromeRuntimeInitialized()) {
console.log('Chrome runtime API successfully initialized');
clearInterval(statusCheckInterval);
} else if (window.__initializationAttempts >= 50) {
console.error('Failed to initialize Chrome runtime API after 50 attempts');
window.reportInitializationStatus();
clearInterval(statusCheckInterval);
} else if (window.__initializationAttempts %% 10 === 0) {
console.log('Still waiting for Chrome runtime API initialization (attempt ' + window.__initializationAttempts + ')');
window.reportInitializationStatus();
// Try to trigger initialization if not already initialized
if (!window.__qtWebChannelTransportReady) {
console.log('Attempting to trigger initialization');
document.dispatchEvent(new CustomEvent('requestWebChannelTransport', {
detail: { timestamp: Date.now() }
}));
}
}
}, 500);
};
document.head.appendChild(qwc_script);
})();
""" % (runtime_api_js)
script.setSourceCode(wrapper_js)
script.setName("chrome-runtime-api")
script.setWorldId(QWebEngineScript.ScriptWorldId.ApplicationWorld)
script.setInjectionPoint(QWebEngineScript.InjectionPoint.DocumentCreation)
script.setRunsOnSubFrames(True)
return script
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -1609,123 +1609,85 @@ class ExtensionSchemeHandler(QWebEngineUrlSchemeHandler):
# Decode the HTML content
html_content = content.decode('utf-8')
# Load QWebChannel.js
qwebchannel_path = os.path.join("assets/browser/js", "qwebchannel.js")
with open(qwebchannel_path, 'r', encoding='utf-8') as f:
qwebchannel_js = f.read()
# Create the WebChannel initialization script with direct QWebChannel.js injection
# Create the WebChannel initialization script.
# We load qwebchannel.js from Qt's resource system (qrc)
# and then run our initialization code once the DOM is loaded.
init_script = f"""
<script type="text/javascript" src="qrc:///qtwebchannel/qwebchannel.js"></script>
<script>
// Directly define QWebChannel in the global scope
{qwebchannel_js}
console.log("QWebChannel.js directly injected into extension page");
// Verify QWebChannel is defined
if (typeof QWebChannel === 'undefined') {{
console.error("QWebChannel is still undefined after direct injection!");
}} else {{
console.log("QWebChannel is successfully defined in global scope");
}}
// WebChannel initialization
window.__qtWebChannelTransportReady = false;
function initializeWebChannel() {{
if (typeof qt !== 'undefined' && qt.webChannelTransport) {{
try {{
// QWebChannel should be defined now
new QWebChannel(qt.webChannelTransport, function(channel) {{
window.channel = channel;
window.runtimeBridge = channel.objects.runtimeBridge;
window.__qtWebChannelTransportReady = true;
console.log("QWebChannel initialized successfully for extension: {ext_id}");
// Dispatch an event to notify that the channel is ready
document.dispatchEvent(new CustomEvent('webChannelReady', {{
detail: {{ channel: channel }}
}}));
// Also dispatch the chrome.runtime.initialized event
document.dispatchEvent(new CustomEvent('chrome.runtime.initialized', {{
detail: {{ extensionId: "{ext_id}" }}
}}));
// Make chrome.runtime available globally
if (window.runtimeBridge) {{
console.log("RuntimeBridge is available, initializing chrome.runtime API");
// Expose chrome.runtime API
if (typeof chrome === 'undefined') {{
window.chrome = {{}};
}}
if (typeof chrome.runtime === 'undefined') {{
chrome.runtime = {{}};
document.addEventListener('DOMContentLoaded', function() {{
console.log("DOM content loaded, initializing WebChannel for extension: {ext_id}");
// Verify QWebChannel is defined
if (typeof QWebChannel === 'undefined') {{
console.error("QWebChannel is not defined. Check if qrc:///qtwebchannel/qwebchannel.js loaded correctly.");
return;
}}
// WebChannel initialization
window.__qtWebChannelTransportReady = false;
function initializeWebChannel() {{
if (typeof qt !== 'undefined' && qt.webChannelTransport) {{
try {{
new QWebChannel(qt.webChannelTransport, function(channel) {{
window.channel = channel;
window.runtimeBridge = channel.objects.runtimeBridge;
window.__qtWebChannelTransportReady = true;
console.log("QWebChannel initialized successfully for extension: {ext_id}");
// Dispatch an event to notify that the channel is ready
document.dispatchEvent(new CustomEvent('webChannelReady', {{
detail: {{ channel: channel }}
}}));
// Also dispatch the chrome.runtime.initialized event
document.dispatchEvent(new CustomEvent('chrome.runtime.initialized', {{
detail: {{ extensionId: "{ext_id}" }}
}}));
// Make chrome.runtime available globally
if (window.runtimeBridge) {{
console.log("RuntimeBridge is available, initializing chrome.runtime API");
if (typeof chrome === 'undefined') {{ window.chrome = {{}}; }}
if (typeof chrome.runtime === 'undefined') {{ chrome.runtime = {{}}; }}
chrome.runtime.id = "{ext_id}";
chrome.runtime.sendMessage = function(extensionId, message, options, callback) {{
if (typeof extensionId !== 'string') {{
callback = options;
options = message;
message = extensionId;
extensionId = "{ext_id}";
}}
if (typeof options === 'undefined') {{ options = {{}}; }}
return window.runtimeBridge.sendMessage(extensionId, JSON.stringify(message), JSON.stringify(options));
}};
}}
// Define basic chrome.runtime API methods
chrome.runtime.id = "{ext_id}";
chrome.runtime.sendMessage = function(extensionId, message, options, callback) {{
if (typeof extensionId !== 'string') {{
callback = options;
options = message;
message = extensionId;
extensionId = "{ext_id}";
}}
// Always pass an options parameter, even if it's empty
if (typeof options === 'undefined') {{
options = {{}};
}}
return window.runtimeBridge.sendMessage(extensionId, JSON.stringify(message), JSON.stringify(options));
}};
}}
}});
return true;
}} catch (e) {{
console.error("Error initializing QWebChannel:", e);
}});
return true;
}} catch (e) {{
console.error("Error initializing QWebChannel:", e);
return false;
}}
}} else {{
console.log("qt.webChannelTransport not available yet, will retry later");
return false;
}}
}} else {{
console.log("qt.webChannelTransport not available yet, will retry later");
return false;
}}
}}
// Create a global variable to track initialization attempts
window.__qwebchannel_init_attempts = 0;
// Try to initialize immediately
if (!initializeWebChannel()) {{
// If it fails, set up a retry mechanism
// Retry mechanism
let attempts = 0;
const MAX_RETRIES = 50;
const retryInterval = setInterval(function() {{
window.__qwebchannel_init_attempts++;
console.log("Retrying QWebChannel initialization... (attempt " + window.__qwebchannel_init_attempts + " of " + MAX_RETRIES + ")");
// Check if qt object exists, if not, try to create it
if (typeof qt === 'undefined') {{
console.log("qt object is undefined, creating placeholder");
window.qt = {{}};
}}
if (initializeWebChannel() || window.__qwebchannel_init_attempts >= MAX_RETRIES) {{
if (initializeWebChannel()) {{
clearInterval(retryInterval);
if (window.__qwebchannel_init_attempts >= MAX_RETRIES) {{
console.error("Failed to initialize QWebChannel after " + MAX_RETRIES + " attempts");
}}
}}
}}, 200);
// Add an event listener for manual transport requests
document.addEventListener('requestWebChannelTransport', function() {{
console.log("Received request for webChannelTransport");
if (typeof qt !== 'undefined' && qt.webChannelTransport) {{
console.log("qt.webChannelTransport is available, initializing QWebChannel");
initializeWebChannel();
}} else if (attempts++ >= MAX_RETRIES) {{
clearInterval(retryInterval);
console.error("Failed to initialize QWebChannel after " + MAX_RETRIES + " attempts.");
}}
}});
}}
}}, 100);
}});
</script>
"""
......@@ -1824,8 +1786,7 @@ class ExtensionSchemeHandler(QWebEngineUrlSchemeHandler):
# Fix URL handling in extensions
if ext_id:
html_content = self.rewrite_src_urls(html_content, ext_id)
html_content = self.rewrite_src_urls(html_content, ext_id)
# Inject the script right before the closing </head> tag
if '</head>' in html_content:
......@@ -2083,123 +2044,69 @@ class ExtensionDialog(QDialog):
# Set up QWebChannel for the page to enable chrome.runtime API
page.setWebChannel(browser_window.web_channel)
# First load QWebChannel.js content
qwebchannel_path = os.path.join("assets/browser/js", "qwebchannel.js")
qwebchannel_js = ""
try:
with open(qwebchannel_path, 'r', encoding='utf-8') as f:
qwebchannel_js = f.read()
except Exception as e:
print(f"Error loading QWebChannel.js: {e}")
qwebchannel_js = "console.error('Failed to load QWebChannel.js');"
# Explicitly initialize the QWebChannel with a script that includes QWebChannel.js
init_script = f"""
def on_popup_load_finished(ok):
if not ok:
print(f"Popup failed to load: {web_view.url().toString()}")
return
print(f"Popup loaded, injecting QWebChannel init script for {ext_name}")
# Load qwebchannel.js content
qwebchannel_js = ""
try:
qwebchannel_path = os.path.join("assets/browser/js", "qwebchannel.js")
with open(qwebchannel_path, 'r', encoding='utf-8') as f:
qwebchannel_js = f.read()
except Exception as e:
print(f"Error loading QWebChannel.js for injection: {e}")
init_script = f'''
// Directly inject QWebChannel.js content
{qwebchannel_js}
console.log("QWebChannel.js directly injected into popup");
// Verify QWebChannel is defined
if (typeof QWebChannel === 'undefined') {{
console.error("QWebChannel is still undefined after direct injection!");
}} else {{
console.log("QWebChannel is successfully defined in global scope");
}}
// Create a global variable to track if transport is ready
window.__qtWebChannelTransportReady = false;
function initializeWebChannel() {{
if (typeof qt !== 'undefined' && qt.webChannelTransport) {{
try {{
// QWebChannel should be defined now
new QWebChannel(qt.webChannelTransport, function(channel) {{
window.channel = channel;
window.runtimeBridge = channel.objects.runtimeBridge;
window.__qtWebChannelTransportReady = true;
console.log("QWebChannel initialized successfully for extension popup");
// Dispatch an event to notify that the channel is ready
document.dispatchEvent(new CustomEvent('webChannelReady', {{
detail: {{ channel: channel }}
}}));
// Also dispatch the chrome.runtime.initialized event
document.dispatchEvent(new CustomEvent('chrome.runtime.initialized', {{
detail: {{ extensionId: "{ext_name}" }}
}}));
// Make chrome.runtime available globally
if (window.runtimeBridge) {{
console.log("RuntimeBridge is available, initializing chrome.runtime API");
// Expose chrome.runtime API
if (typeof chrome === 'undefined') {{
window.chrome = {{}};
}}
if (typeof chrome.runtime === 'undefined') {{
chrome.runtime = {{}};
}}
// Define basic chrome.runtime API methods
chrome.runtime.id = "{ext_name}";
chrome.runtime.sendMessage = function(extensionId, message, options, callback) {{
if (typeof extensionId !== 'string') {{
callback = options;
options = message;
message = extensionId;
extensionId = "{ext_name}";
}}
// Always pass an options parameter, even if it's empty
if (typeof options === 'undefined') {{
options = {{}};
}}
return window.runtimeBridge.sendMessage(extensionId, JSON.stringify(message), JSON.stringify(options));
}};
}}
}});
return true;
}} catch (e) {{
console.error("Error initializing QWebChannel:", e);
return false;
}}
}} else {{
console.log("qt.webChannelTransport not available yet, will retry later");
return false;
}}
}}
// Create a global variable to track initialization attempts
window.__qwebchannel_init_attempts = 0;
// Try to initialize immediately
if (!initializeWebChannel()) {{
// If it fails, set up a retry mechanism
const MAX_RETRIES = 50;
(function() {{
console.log("Popup DOM loaded, ensuring WebChannel is initialized for {ext_name}");
let attempts = 0;
const MAX_RETRIES = 100;
const retryInterval = setInterval(function() {{
window.__qwebchannel_init_attempts++;
console.log("Retrying QWebChannel initialization... (attempt " + window.__qwebchannel_init_attempts + " of " + MAX_RETRIES + ")");
// Check if qt object exists, if not, try to create it
if (typeof qt === 'undefined') {{
console.log("qt object is undefined, creating placeholder");
window.qt = {{}};
function initialize() {{
if (typeof QWebChannel === 'undefined') {{
attempts++;
if (attempts < MAX_RETRIES) {{
console.log("QWebChannel not ready, retrying...");
setTimeout(initialize, 100);
}} else {{
console.error("QWebChannel not defined after all retries.");
}}
return;
}}
if (initializeWebChannel() || window.__qwebchannel_init_attempts >= MAX_RETRIES) {{
clearInterval(retryInterval);
if (window.__qwebchannel_init_attempts >= MAX_RETRIES) {{
console.error("Failed to initialize QWebChannel after " + MAX_RETRIES + " attempts");
if (typeof qt === 'undefined' || !qt.webChannelTransport) {{
attempts++;
if (attempts < MAX_RETRIES) {{
console.log("qt.webChannelTransport not ready, retrying...");
setTimeout(initialize, 100);
}} else {{
console.error("qt.webChannelTransport not available after all retries.");
}}
return;
}}
}}, 200);
}}
"""
# Run the initialization script before loading the URL
page.runJavaScript(init_script, lambda result: print("QWebChannel initialization script executed for popup"))
console.log("QWebChannel and transport are ready, initializing.");
new QWebChannel(qt.webChannelTransport, function(channel) {{
window.channel = channel;
window.runtimeBridge = channel.objects.runtimeBridge;
console.log("QWebChannel initialized for popup: {ext_name}");
document.dispatchEvent(new CustomEvent('webChannelReady', {{ "detail": {{ "channel": channel }} }}));
}});
}}
initialize();
}})();
'''
page.runJavaScript(init_script)
web_view.loadFinished.connect(on_popup_load_finished)
popup_url = QUrl(f"qextension://{ext_name}/{popup_path}")
web_view.load(popup_url)
......@@ -3591,74 +3498,81 @@ class Browser(QMainWindow):
# Set up QWebChannel for the page to enable chrome.runtime API
page.setWebChannel(self.web_channel)
# Explicitly initialize the QWebChannel with a script
init_script = """
// Create a global variable to track if transport is ready
window.__qtWebChannelTransportReady = false;
function initializeWebChannel() {
if (typeof qt !== 'undefined' && qt.webChannelTransport) {
try {
// Check if QWebChannel is defined
if (typeof QWebChannel === 'undefined') {
console.log("QWebChannel is not defined yet, will retry later");
return false;
}
new QWebChannel(qt.webChannelTransport, function(channel) {
window.channel = channel;
window.runtimeBridge = channel.objects.runtimeBridge;
window.__qtWebChannelTransportReady = true;
console.log("QWebChannel initialized successfully for extension popup");
// Dispatch an event to notify that the channel is ready
document.dispatchEvent(new CustomEvent('webChannelReady', {
detail: { channel: channel }
}));
// Also dispatch the chrome.runtime.initialized event
document.dispatchEvent(new CustomEvent('chrome.runtime.initialized', {
detail: { extensionId: "%s" }
}));
});
return true;
} catch (e) {
console.error("Error initializing QWebChannel:", e);
return false;
}
} else {
console.log("qt.webChannelTransport not available yet, will retry later");
return false;
}
}
#popup_url = QUrl(f"qextension://{ext_name}/{popup_path}")
#web_view.load(popup_url)
#
#layout.addWidget(web_view)
#)popup_dialog.setLayout(layout)
def on_popup_load_finished(ok):
if not ok:
print(f"Popup failed to load: {web_view.url().toString()}")
return
print(f"Popup loaded, injecting QWebChannel init script for {ext_name}")
// Try to initialize immediately
if (!initializeWebChannel()) {
// If it fails, set up a retry mechanism
let retryCount = 0;
const MAX_RETRIES = 50;
# Load qwebchannel.js content
qwebchannel_js = ""
try:
qwebchannel_path = os.path.join("assets/browser/js", "qwebchannel.js")
with open(qwebchannel_path, 'r', encoding='utf-8') as f:
qwebchannel_js = f.read()
except Exception as e:
print(f"Error loading QWebChannel.js for injection: {e}")
init_script = f'''
// Directly inject QWebChannel.js content
{qwebchannel_js}
(function() {{
console.log("Popup DOM loaded, ensuring WebChannel is initialized for {ext_name}");
let attempts = 0;
const MAX_RETRIES = 100;
const retryInterval = setInterval(function() {
console.log("Retrying QWebChannel initialization... (attempt " + (retryCount + 1) + " of " + MAX_RETRIES + ")");
if (initializeWebChannel() || retryCount >= MAX_RETRIES) {
clearInterval(retryInterval);
if (retryCount >= MAX_RETRIES) {
console.error("Failed to initialize QWebChannel after " + MAX_RETRIES + " attempts");
}
}
retryCount++;
}, 200);
}
""" % ext_name
# Run the initialization script before loading the URL
page.runJavaScript(init_script, lambda result: print("QWebChannel initialization script executed for popup"))
function initialize() {{
if (typeof QWebChannel === 'undefined') {{
attempts++;
if (attempts < MAX_RETRIES) {{
console.log("QWebChannel not ready, retrying...");
setTimeout(initialize, 100);
}} else {{
console.error("QWebChannel not defined after all retries.");
}}
return;
}}
if (typeof qt === 'undefined' || !qt.webChannelTransport) {{
attempts++;
if (attempts < MAX_RETRIES) {{
console.log("qt.webChannelTransport not ready, retrying...");
setTimeout(initialize, 100);
}} else {{
console.error("qt.webChannelTransport not available after all retries.");
}}
return;
}}
console.log("QWebChannel and transport are ready, initializing.");
new QWebChannel(qt.webChannelTransport, function(channel) {{
window.channel = channel;
window.runtimeBridge = channel.objects.runtimeBridge;
console.log("QWebChannel initialized for popup: {ext_name}");
document.dispatchEvent(new CustomEvent('webChannelReady', {{ "detail": {{ "channel": channel }} }}));
}});
}}
initialize();
}})();
'''
page.runJavaScript(init_script)
web_view.loadFinished.connect(on_popup_load_finished)
popup_url = QUrl(f"qextension://{ext_name}/{popup_path}")
web_view.load(popup_url)
layout.addWidget(web_view)
popup_dialog.setLayout(layout)
popup_dialog.resize(400, 600)
popup_dialog.show()
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment