diff --git a/WebView2/Plugin.cpp b/WebView2/Plugin.cpp index 239d3aa..dbab4cd 100644 --- a/WebView2/Plugin.cpp +++ b/WebView2/Plugin.cpp @@ -51,8 +51,8 @@ BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved) // Measure constructor Measure::Measure() : rm(nullptr), skin(nullptr), skinWindow(nullptr), measureName(nullptr), - width(800), height(600), x(0), y(0), - visible(true), initialized(false), clickthrough(false), webMessageToken{} + width(800), height(600), x(0), y(0), zoomFactor(1.0), + visible(true), initialized(false), clickthrough(false), allowDualControl(true), webMessageToken{} { // Initialize COM for this thread if not already done if (!g_comInitialized) @@ -129,6 +129,54 @@ void UpdateClickthrough(Measure* measure) } } +// Inject AllowDualControl script into the WebView +void InjectAllowDualControl(Measure* measure) +{ + if (!measure->webView) return; + // Inject script to capture page load events for drag/move and context menu + measure->webView->ExecuteScript( + L"let rm_AllowDualControl=false,rm_AllowDualControlOn=false,rm_AllowDualControlClientX=0,rm_AllowDualControlClientY=0;function rm_SetAllowDualControl(v){rm_AllowDualControl=!!v;if(!rm_AllowDualControl)rm_AllowDualControlOn=false;}document.body.onpointerdown=e=>{if(!rm_AllowDualControl)return;if(e.button===0&&e.ctrlKey){e.preventDefault();e.stopImmediatePropagation();rm_AllowDualControlOn=true;rm_AllowDualControlClientX=e.clientX;rm_AllowDualControlClientY=e.clientY;try{document.body.setPointerCapture(e.pointerId);}catch{}}};document.body.onpointermove=e=>{if(!rm_AllowDualControl||!rm_AllowDualControlOn)return;e.preventDefault();RainmeterAPI.Bang('[!Move '+(e.screenX-RainmeterAPI.ReadFormula('X',0)-rm_AllowDualControlClientX)+' '+(e.screenY-RainmeterAPI.ReadFormula('Y',0)-rm_AllowDualControlClientY)+']');};document.body.onpointerup=e=>{if(!rm_AllowDualControl)return;if(e.button===0){e.preventDefault();rm_AllowDualControlOn=false;try{document.body.releasePointerCapture(e.pointerId);}catch{}}};document.body.oncontextmenu=e=>{if(!rm_AllowDualControl)return;if(e.button===2&&e.ctrlKey){e.preventDefault();RainmeterAPI.Bang('[!SkinMenu]');}};", + Callback( + [measure](HRESULT errorCode, LPCWSTR resultObjectAsJson) -> HRESULT + { + return S_OK; + } + ).Get() + ); + measure->isAllowDualControlInjected = true; + UpdateAllowDualControl(measure); +} + +// Update AllowDualControl state in the WebView +void UpdateAllowDualControl(Measure* measure) +{ + if (!measure->webView) return; + + if (measure->allowDualControl) + { + measure->webView->ExecuteScript( + L"rm_SetAllowDualControl(true);", + Callback( + [measure](HRESULT errorCode, LPCWSTR resultObjectAsJson) -> HRESULT + { + return S_OK; + } + ).Get() + ); + } + else { + measure->webView->ExecuteScript( + L"rm_SetAllowDualControl(false);", + Callback( + [measure](HRESULT errorCode, LPCWSTR resultObjectAsJson) -> HRESULT + { + return S_OK; + } + ).Get() + ); + } +} + PLUGIN_EXPORT void Reload(void* data, void* rm, double* maxValue) { Measure* measure = (Measure*)data; @@ -182,12 +230,72 @@ PLUGIN_EXPORT void Reload(void* data, void* rm, double* maxValue) int newHeight = RmReadInt(rm, L"H", 600); int newX = RmReadInt(rm, L"X", 0); int newY = RmReadInt(rm, L"Y", 0); - bool newVisible = RmReadInt(rm, L"Hidden", 0) == 0; - bool newClickthrough = RmReadInt(rm, L"Clickthrough", 0) != 0; - + double newZoomFactor = RmReadFormula(rm, L"ZoomFactor", 1.0); + bool newVisible = RmReadInt(rm, L"Hidden", 0) <= 0; + bool newClickthrough = RmReadInt(rm, L"Clickthrough", 0) >= 1; + + // Read AllowDualControl for Yincognito's script injection + bool newAllowDualControl = RmReadInt(rm, L"AllowDualControl", 1) >= 1; + + // Read OnWebViewLoadAction + std::wstring newOnWebViewLoadAction; + LPCWSTR onWebViewLoadOption = RmReadString(rm, L"OnWebViewLoadAction", L"", FALSE); + if (onWebViewLoadOption && wcslen(onWebViewLoadOption) > 0) + { + newOnWebViewLoadAction = onWebViewLoadOption; + } + + // Read OnWebViewFailAction + std::wstring newOnWebViewFailAction; + LPCWSTR onWebViewFailOption = RmReadString(rm, L"OnWebViewFailAction", L"", FALSE); + if (onWebViewFailOption && wcslen(onWebViewFailOption) > 0) + { + newOnWebViewFailAction = onWebViewFailOption; + } + + // Read OnPageLoadStartAction + std::wstring newOnPageLoadStartAction; + LPCWSTR onPageLoadStartOption = RmReadString(rm, L"OnPageLoadStartAction", L"", FALSE); + if (onPageLoadStartOption && wcslen(onPageLoadStartOption) > 0) + { + newOnPageLoadStartAction = onPageLoadStartOption; + } + + // Read OnPageLoadingAction + std::wstring newOnPageLoadingAction; + LPCWSTR onPageLoadingOption = RmReadString(rm, L"OnPageLoadingAction", L"", FALSE); + if (onPageLoadingOption && wcslen(onPageLoadingOption) > 0) + { + newOnPageLoadingAction = onPageLoadingOption; + } + + // Read OnPageLoadFinishAction + std::wstring newOnPageLoadFinishAction; + LPCWSTR onPageLoadFinishOption = RmReadString(rm, L"OnPageLoadFinishAction", L"", FALSE); + if (onPageLoadFinishOption && wcslen(onPageLoadFinishOption) > 0) + { + newOnPageLoadFinishAction = onPageLoadFinishOption; + } + + // Read OnPageFirstLoadAction + std::wstring newOnPageFirstLoadAction; + LPCWSTR onPageFirstLoadOption = RmReadString(rm, L"OnPageFirstLoadAction", L"", FALSE); + if (onPageFirstLoadOption && wcslen(onPageFirstLoadOption) > 0) + { + newOnPageFirstLoadAction = onPageFirstLoadOption; + } + + // Read OnPageReloadAction + std::wstring newOnPageReloadAction; + LPCWSTR onPageReloadOption = RmReadString(rm, L"OnPageReloadAction", L"", FALSE); + if (onPageReloadOption && wcslen(onPageReloadOption) > 0) + { + newOnPageReloadAction = onPageReloadOption; + } + // Check if URL has changed (requires recreation) bool urlChanged = (newUrl != measure->url); - + // Check if dimensions or position changed (can be updated dynamically) bool dimensionsChanged = (newWidth != measure->width || newHeight != measure->height || @@ -196,16 +304,27 @@ PLUGIN_EXPORT void Reload(void* data, void* rm, double* maxValue) bool visibilityChanged = (newVisible != measure->visible); bool clickthroughChanged = (newClickthrough != measure->clickthrough); - + bool allowDualControlChanged = (newAllowDualControl != measure->allowDualControl); + bool zoomFactorChanged = (newZoomFactor != measure->zoomFactor); + // Update stored values measure->url = newUrl; measure->width = newWidth; measure->height = newHeight; measure->x = newX; measure->y = newY; + measure->zoomFactor = newZoomFactor; measure->visible = newVisible; measure->clickthrough = newClickthrough; - + measure->allowDualControl = newAllowDualControl; + measure->onWebViewLoadAction = newOnWebViewLoadAction; + measure->onWebViewFailAction = newOnWebViewFailAction; + measure->onPageLoadStartAction = newOnPageLoadStartAction; + measure->onPageLoadingAction = newOnPageLoadingAction; + measure->onPageLoadFinishAction = newOnPageLoadFinishAction; + measure->onPageFirstLoadAction = newOnPageFirstLoadAction; + measure->onPageReloadAction = newOnPageReloadAction; + // Only create WebView2 if not initialized OR if URL changed if (!measure->initialized || urlChanged) { @@ -220,6 +339,11 @@ PLUGIN_EXPORT void Reload(void* data, void* rm, double* maxValue) else { // First initialization - create WebView2 + if (measure->isCreationInProgress) + { + // Avoid re-entrancy if creation is already in progress + return; + } CreateWebView2(measure); } } @@ -241,11 +365,26 @@ PLUGIN_EXPORT void Reload(void* data, void* rm, double* maxValue) { measure->webViewController->put_IsVisible(measure->visible ? TRUE : FALSE); } + + if (zoomFactorChanged && measure->webViewController) + { + measure->webViewController->put_ZoomFactor(measure->zoomFactor); + } if (clickthroughChanged) { UpdateClickthrough(measure); } + + if (allowDualControlChanged) + { + if (!measure->isAllowDualControlInjected) + { + InjectAllowDualControl(measure); + } + else + UpdateAllowDualControl(measure); + } } } @@ -275,13 +414,6 @@ PLUGIN_EXPORT double Update(void* data) { measure->callbackResult = result; } - - // Trigger Rainmeter redraw after callback completes - if (measure->skin) - { - RmExecute(measure->skin, L"!UpdateMeter *"); - RmExecute(measure->skin, L"!Redraw"); - } } return S_OK; } @@ -304,6 +436,7 @@ PLUGIN_EXPORT LPCWSTR GetString(void* data) return L"0"; } + PLUGIN_EXPORT void ExecuteBang(void* data, LPCWSTR args) { Measure* measure = (Measure*)data; diff --git a/WebView2/Plugin.h b/WebView2/Plugin.h index 0ea5939..c51e5d6 100644 --- a/WebView2/Plugin.h +++ b/WebView2/Plugin.h @@ -20,27 +20,41 @@ struct Measure void* skin; HWND skinWindow; LPCWSTR measureName; - + std::wstring url; + std::wstring currentUrl; int width; int height; int x; int y; + double zoomFactor; bool visible; bool initialized; bool clickthrough; - + bool isCreationInProgress = false; + bool isFirstLoad = true; + bool allowDualControl; + bool isAllowDualControlInjected = false; + + std::wstring onWebViewLoadAction; + std::wstring onWebViewFailAction; + std::wstring onPageFirstLoadAction; + std::wstring onPageLoadStartAction; + std::wstring onPageLoadingAction; + std::wstring onPageLoadFinishAction; + std::wstring onPageReloadAction; + wil::com_ptr webViewController; wil::com_ptr webView; EventRegistrationToken webMessageToken; - + std::wstring buffer; // Buffer for section variable return values std::wstring callbackResult; // Stores return value from OnInitialize/OnUpdate callbacks std::map jsResults; // Cache for CallJS results - + Measure(); ~Measure(); - + // Member callback functions for WebView2 creation HRESULT CreateEnvironmentHandler(HRESULT result, ICoreWebView2Environment* env); HRESULT CreateControllerHandler(HRESULT result, ICoreWebView2Controller* controller); @@ -49,4 +63,7 @@ struct Measure // WebView2 functions void CreateWebView2(Measure* measure); void UpdateClickthrough(Measure* measure); +void InjectAllowDualControl(Measure* measure); +void UpdateAllowDualControl(Measure* measure); + diff --git a/WebView2/WebView2.cpp b/WebView2/WebView2.cpp index a3db632..6eb16b6 100644 --- a/WebView2/WebView2.cpp +++ b/WebView2/WebView2.cpp @@ -12,6 +12,18 @@ void CreateWebView2(Measure* measure) RmLog(measure->rm, LOG_ERROR, L"WebView2: Invalid measure or skin window"); return; } + + if (measure->initialized) + { + return; + } + + if (measure->isCreationInProgress) + { + return; + } + + measure->isCreationInProgress = true; // Create user data folder in TEMP directory to avoid permission issues wchar_t tempPath[MAX_PATH]; @@ -28,6 +40,7 @@ void CreateWebView2(Measure* measure) measure, &Measure::CreateEnvironmentHandler ).Get() + ); if (FAILED(hr)) @@ -38,6 +51,11 @@ void CreateWebView2(Measure* measure) swprintf_s(errorMsg, L"WebView2: Failed to start creation process (HRESULT: 0x%08X). Make sure WebView2 Runtime is installed.", hr); RmLog(measure->rm, LOG_ERROR, errorMsg); } + if (measure->skin && wcslen(measure->onWebViewFailAction.c_str()) > 0) + { + RmExecute(measure->skin, measure->onWebViewFailAction.c_str()); + } + measure->isCreationInProgress = false; } } @@ -52,6 +70,11 @@ HRESULT Measure::CreateEnvironmentHandler(HRESULT result, ICoreWebView2Environme swprintf_s(errorMsg, L"WebView2: Failed to create environment (HRESULT: 0x%08X)", result); RmLog(rm, LOG_ERROR, errorMsg); } + if (skin && wcslen(onWebViewFailAction.c_str()) > 0) + { + RmExecute(skin, onWebViewFailAction.c_str()); + } + isCreationInProgress = false; return result; } @@ -70,6 +93,7 @@ HRESULT Measure::CreateEnvironmentHandler(HRESULT result, ICoreWebView2Environme // Controller creation callback HRESULT Measure::CreateControllerHandler(HRESULT result, ICoreWebView2Controller* controller) { + if (FAILED(result)) { if (rm) @@ -78,6 +102,11 @@ HRESULT Measure::CreateControllerHandler(HRESULT result, ICoreWebView2Controller swprintf_s(errorMsg, L"WebView2: Failed to create controller (HRESULT: 0x%08X)", result); RmLog(rm, LOG_ERROR, errorMsg); } + if (skin && wcslen(onWebViewFailAction.c_str()) > 0) + { + RmExecute(skin, onWebViewFailAction.c_str()); + } + isCreationInProgress = false; return result; } @@ -85,6 +114,11 @@ HRESULT Measure::CreateControllerHandler(HRESULT result, ICoreWebView2Controller { if (rm) RmLog(rm, LOG_ERROR, L"WebView2: Controller is null"); + if (skin && wcslen(onWebViewFailAction.c_str()) > 0) + { + RmExecute(skin, onWebViewFailAction.c_str()); + } + isCreationInProgress = false; return S_FALSE; } @@ -108,7 +142,10 @@ HRESULT Measure::CreateControllerHandler(HRESULT result, ICoreWebView2Controller // Set initial visibility webViewController->put_IsVisible(visible ? TRUE : FALSE); - + + // Set initial zoom factor + webViewController->put_ZoomFactor(zoomFactor); + // Transparent background auto controller2 = webViewController.query(); if (controller2) @@ -142,12 +179,81 @@ HRESULT Measure::CreateControllerHandler(HRESULT result, ICoreWebView2Controller L"window.RainmeterAPI = chrome.webview.hostObjects.sync.RainmeterAPI", nullptr ); - - // Add NavigationCompleted event to call OnInitialize after page loads + + // Add SourceChanged event to detect changes in URL + webView->add_SourceChanged( + Callback( + [this](ICoreWebView2* sender, ICoreWebView2SourceChangedEventArgs* args) -> HRESULT + { + wil::unique_cotaskmem_string updatedUri; + + if (SUCCEEDED(sender->get_Source(&updatedUri)) && updatedUri.get() != nullptr) + { + std::wstring newUrl = updatedUri.get(); + + if (currentUrl != newUrl) + { + // URL changed + isFirstLoad = true; + currentUrl = newUrl; + } + else + { + // URL did not change + isFirstLoad = false; + } + } + return S_OK; + } + ).Get(), + nullptr + ); + + // Add NavigationStarting event to call action when navigation starts + webView->add_NavigationStarting( + Callback( + [this](ICoreWebView2* sender, ICoreWebView2NavigationStartingEventArgs* args) -> HRESULT + { + if (wcslen(onPageLoadStartAction.c_str()) > 0) + { + if (skin) + RmExecute(skin, onPageLoadStartAction.c_str()); + } + return S_OK; + } + ).Get(), + nullptr + ); + + // Add ContentLoading event to call action when page starts loading + webView->add_ContentLoading( + Callback( + [this](ICoreWebView2* sender, ICoreWebView2ContentLoadingEventArgs* args) -> HRESULT + { + if (wcslen(onPageLoadingAction.c_str()) > 0) + { + if (skin) + RmExecute(skin, onPageLoadingAction.c_str()); + } + return S_OK; + } + ).Get(), + nullptr + ); + + // Add NavigationCompleted event to call OnInitialize after page loads and handle load actions webView->add_NavigationCompleted( Callback( [this](ICoreWebView2* sender, ICoreWebView2NavigationCompletedEventArgs* args) -> HRESULT { + isAllowDualControlInjected = false; + + // Inject script to capture page load events for drag/move and context menu + if (allowDualControl) + { + InjectAllowDualControl(this); + } + // Call JavaScript OnInitialize callback if it exists and capture return value webView->ExecuteScript( L"(function() { if (typeof window.OnInitialize === 'function') { var result = window.OnInitialize(); return result !== undefined ? String(result) : ''; } return ''; })();", @@ -162,30 +268,47 @@ HRESULT Measure::CreateControllerHandler(HRESULT result, ICoreWebView2Controller { result = result.substr(1, result.length() - 2); } - + // Store the callback result if (!result.empty() && result != L"null") { callbackResult = result; - - // Trigger Rainmeter redraw after callback completes - if (skin) - { - RmExecute(skin, L"!UpdateMeter *"); - RmExecute(skin, L"!Redraw"); - } } } return S_OK; } ).Get() ); + + if (isFirstLoad) // First load + { + if (wcslen(onPageFirstLoadAction.c_str()) > 0) + { + if (skin) + RmExecute(skin, onPageFirstLoadAction.c_str()); + } + isFirstLoad = false; + } + else // Page reload + { + if (wcslen(onPageReloadAction.c_str()) > 0) + { + if (skin) + RmExecute(skin, onPageReloadAction.c_str()); + } + } + // Common action after any page load + if (wcslen(onPageLoadFinishAction.c_str()) > 0) + { + if (skin) + RmExecute(skin, onPageLoadFinishAction.c_str()); + } return S_OK; } ).Get(), nullptr ); - + // Navigate to URL if (!url.empty()) { @@ -194,9 +317,16 @@ HRESULT Measure::CreateControllerHandler(HRESULT result, ICoreWebView2Controller initialized = true; + isCreationInProgress = false; + if (rm) RmLog(rm, LOG_NOTICE, L"WebView2: Initialized successfully with COM Host Objects"); + if (wcslen(onWebViewLoadAction.c_str()) > 0) + { + RmExecute(skin, onWebViewLoadAction.c_str()); + } + // Apply initial clickthrough state UpdateClickthrough(this);