From f71a734e11672462fd69fcb5acd505303676f1e7 Mon Sep 17 00:00:00 2001 From: RicardoTM05 <..> Date: Sun, 21 Dec 2025 00:06:43 -0600 Subject: [PATCH 1/6] WebView Lifecycle, Measure returns rework and iframes support --- WebView2/Plugin.cpp | 1091 ++++++++++++++++------------- WebView2/Plugin.h | 32 +- WebView2/WebView2.cpp | 953 +++++++++++++++++-------- WebView2/WebView2.vcxproj | 4 +- WebView2/WebView2.vcxproj.filters | 7 +- WebView2/packages.config | 4 +- 6 files changed, 1303 insertions(+), 788 deletions(-) diff --git a/WebView2/Plugin.cpp b/WebView2/Plugin.cpp index dbab4cd..248c564 100644 --- a/WebView2/Plugin.cpp +++ b/WebView2/Plugin.cpp @@ -14,55 +14,56 @@ wil::com_ptr g_typeLib; // DllMain to load TypeLib from embedded resources BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved) { - if (fdwReason == DLL_PROCESS_ATTACH) - { - // Extract TypeLib from embedded resource and load it - wchar_t tempPath[MAX_PATH]; - GetTempPath(MAX_PATH, tempPath); - wcscat_s(tempPath, L"WebView2.tlb"); - - // Read embedded resource: ID = 1, Type = TYPELIB - HRSRC hResInfo = FindResource(hinstDLL, MAKEINTRESOURCE(1), L"TYPELIB"); - if (hResInfo) - { - HGLOBAL hRes = LoadResource(hinstDLL, hResInfo); - if (hRes) // Check if LoadResource succeeded - { - LPVOID memRes = LockResource(hRes); - DWORD sizeRes = SizeofResource(hinstDLL, hResInfo); - - HANDLE hFile = CreateFile(tempPath, GENERIC_WRITE, 0, NULL, - CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); - if (hFile != INVALID_HANDLE_VALUE) - { - DWORD written; - WriteFile(hFile, memRes, sizeRes, &written, NULL); - CloseHandle(hFile); - - // Load the TypeLib - LoadTypeLib(tempPath, &g_typeLib); - } - } - } - } - return TRUE; + if (fdwReason == DLL_PROCESS_ATTACH) + { + // Extract TypeLib from embedded resource and load it + wchar_t tempPath[MAX_PATH]; + GetTempPath(MAX_PATH, tempPath); + wcscat_s(tempPath, L"WebView2.tlb"); + + // Read embedded resource: ID = 1, Type = TYPELIB + HRSRC hResInfo = FindResource(hinstDLL, MAKEINTRESOURCE(1), L"TYPELIB"); + if (hResInfo) + { + HGLOBAL hRes = LoadResource(hinstDLL, hResInfo); + if (hRes) // Check if LoadResource succeeded + { + LPVOID memRes = LockResource(hRes); + DWORD sizeRes = SizeofResource(hinstDLL, hResInfo); + + HANDLE hFile = CreateFile(tempPath, GENERIC_WRITE, 0, NULL, + CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); + if (hFile != INVALID_HANDLE_VALUE) + { + DWORD written; + WriteFile(hFile, memRes, sizeRes, &written, NULL); + CloseHandle(hFile); + + // Load the TypeLib + LoadTypeLib(tempPath, &g_typeLib); + } + } + } + } + return TRUE; } // Measure constructor -Measure::Measure() : rm(nullptr), skin(nullptr), skinWindow(nullptr), - measureName(nullptr), - width(800), height(600), x(0), y(0), zoomFactor(1.0), - visible(true), initialized(false), clickthrough(false), allowDualControl(true), webMessageToken{} +Measure::Measure() : rm(nullptr), skin(nullptr), skinWindow(nullptr), +measureName(nullptr), +width(800), height(600), x(0), y(0), zoomFactor(1.0), +autoStart(true), disabled(false), visible(true), initialized(false), clickthrough(false), allowDualControl(true), allowNotifications(false), allowNewWindow(false), +webMessageToken{} { - // Initialize COM for this thread if not already done - if (!g_comInitialized) - { - HRESULT hr = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); - if (SUCCEEDED(hr) || hr == RPC_E_CHANGED_MODE) - { - g_comInitialized = true; - } - } + // Initialize COM for this thread if not already done + if (!g_comInitialized) + { + HRESULT hr = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + if (SUCCEEDED(hr) || hr == RPC_E_CHANGED_MODE) + { + g_comInitialized = true; + } + } } // Measure destructor @@ -73,497 +74,635 @@ Measure::~Measure() // Rainmeter Plugin Exports PLUGIN_EXPORT void Initialize(void** data, void* rm) { - Measure* measure = new Measure; - *data = measure; - - measure->rm = rm; - measure->skin = RmGetSkin(rm); - measure->skinWindow = RmGetSkinWindow(rm); - measure->measureName = RmGetMeasureName(rm); + Measure* measure = new Measure; + *data = measure; + + measure->rm = rm; + measure->skin = RmGetSkin(rm); + measure->skinWindow = RmGetSkinWindow(rm); + measure->measureName = RmGetMeasureName(rm); + measure->autoStart = RmReadInt(rm, L"AutoStart", 1) >= 1; } // Helper to update clickthrough state void UpdateClickthrough(Measure* measure) { - if (!measure->skinWindow) return; - - // Find the WebView2 window (child of skin window) - // We iterate through children to find the one that matches our bounds - HWND child = GetWindow(measure->skinWindow, GW_CHILD); - while (child) - { - // Check if this is likely our window - // For simplicity, we assume the first child or check bounds if needed - // Since we can't easily map controller to HWND, we'll try to apply to all children - // that look like WebView windows (or just the first one if we assume 1 per skin for now) - - // Better approach: Check if the window rect matches our measure bounds - RECT rect; - GetWindowRect(child, &rect); - - // Convert to client coordinates of parent - POINT pt = { rect.left, rect.top }; - ScreenToClient(measure->skinWindow, &pt); - - // Allow some tolerance or just apply to all children? - // Applying to all children might be safer for "Clickthrough" if there are multiple WebViews - // and we want them all to respect their settings. - // But if we have multiple measures, we want to target ONLY ours. - - // For now, let's just apply to the child window found. - // EnableWindow(FALSE) makes it ignore mouse input (Clickthrough=1) - // EnableWindow(TRUE) makes it accept mouse input (Clickthrough=0) - EnableWindow(child, !measure->clickthrough); - - // If enabling clickthrough (disabling input), ensure it loses focus - if (measure->clickthrough) - { - HWND focusedWindow = GetFocus(); - if (focusedWindow && (focusedWindow == child || IsChild(child, focusedWindow))) - { - SetFocus(nullptr); - } - } - - child = GetWindow(child, GW_HWNDNEXT); - } + if (!measure->skinWindow) return; + + // Find the WebView2 window (child of skin window) + // We iterate through children to find the one that matches our bounds + HWND child = GetWindow(measure->skinWindow, GW_CHILD); + while (child) + { + // Check if this is likely our window + // For simplicity, we assume the first child or check bounds if needed + // Since we can't easily map controller to HWND, we'll try to apply to all children + // that look like WebView windows (or just the first one if we assume 1 per skin for now) + + // Better approach: Check if the window rect matches our measure bounds + RECT rect; + GetWindowRect(child, &rect); + + // Convert to client coordinates of parent + POINT pt = { rect.left, rect.top }; + ScreenToClient(measure->skinWindow, &pt); + + // Allow some tolerance or just apply to all children? + // Applying to all children might be safer for "Clickthrough" if there are multiple WebViews + // and we want them all to respect their settings. + // But if we have multiple measures, we want to target ONLY ours. + + // For now, let's just apply to the child window found. + // EnableWindow(FALSE) makes it ignore mouse input (Clickthrough=1) + // EnableWindow(TRUE) makes it accept mouse input (Clickthrough=0) + EnableWindow(child, !measure->clickthrough); + + // If enabling clickthrough (disabling input), ensure it loses focus + if (measure->clickthrough) + { + HWND focusedWindow = GetFocus(); + if (focusedWindow && (focusedWindow == child || IsChild(child, focusedWindow))) + { + SetFocus(nullptr); + } + } + + child = GetWindow(child, GW_HWNDNEXT); + } } +auto allowDualControlScript = LR"JS( + if (!window.__rmAllowDualControlInjected) { + window.__rmAllowDualControlInjected = true; + + 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 '+ + Math.round(e.screenX-RainmeterAPI.ReadFormula('X',0)-rm_AllowDualControlClientX*window.devicePixelRatio)+' '+ + Math.round(e.screenY-RainmeterAPI.ReadFormula('Y',0)-rm_AllowDualControlClientY*window.devicePixelRatio)+']' + ); + }; + + 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]'); + } + }; + } + )JS"; + // 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); + if (!measure->webView) return; + // Inject script to capture page load events for drag/move and context menu + HRESULT hr = measure->webView->ExecuteScript(allowDualControlScript, + Callback( + [measure](HRESULT errorCode, LPCWSTR resultObjectAsJson) -> HRESULT + { + return S_OK; + } + ).Get() + ); + if (SUCCEEDED(hr)) + { + 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() - ); - } + if (!measure->webView) return; + + measure->webView->ExecuteScript( + measure->allowDualControl + ? L"rm_SetAllowDualControl(true);" + : L"rm_SetAllowDualControl(false);", + Callback( + [](HRESULT, LPCWSTR) -> HRESULT { return S_OK; } + ).Get() + ); } -PLUGIN_EXPORT void Reload(void* data, void* rm, double* maxValue) +// Inject AllowDualControl script into the WebView frame +void InjectAllowDualControlFrame(Measure* measure, Frames* frame) { - Measure* measure = (Measure*)data; - - // Read URL - std::wstring newUrl; - LPCWSTR urlOption = RmReadString(rm, L"Url", L""); - if (urlOption && wcslen(urlOption) > 0) - { - std::wstring urlStr = urlOption; - - // Check if it's a web URL (http://, https://, etc.) - if (urlStr.find(L"://") != std::wstring::npos) - { - // Already has a protocol - use as-is - // This handles: http://, https://, file:///, etc. - newUrl = urlStr; - } - else - { - // No protocol found - treat as file path - // Check if it's a relative path or absolute path - if (urlStr[0] != L'/' && (urlStr.length() < 2 || urlStr[1] != L':')) - { - // Relative path - make it absolute using skin path - LPCWSTR absolutePath = RmPathToAbsolute(rm, urlStr.c_str()); - if (absolutePath) - { - urlStr = absolutePath; - } - } - - // Convert backslashes to forward slashes - for (size_t i = 0; i < urlStr.length(); i++) - { - if (urlStr[i] == L'\\') urlStr[i] = L'/'; - } - - // Add file:/// prefix if not already present - if (urlStr.find(L"file:///") != 0) - { - urlStr = L"file:///" + urlStr; - } - - newUrl = urlStr; - } - } - - // Read dimensions and visibility - int newWidth = RmReadInt(rm, L"W", 800); - int newHeight = RmReadInt(rm, L"H", 600); - int newX = RmReadInt(rm, L"X", 0); - int newY = RmReadInt(rm, L"Y", 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; + if (!frame || !frame->name) return; - // Read AllowDualControl for Yincognito's script injection - bool newAllowDualControl = RmReadInt(rm, L"AllowDualControl", 1) >= 1; + // Inject script to capture page load events for drag/move and context menu + HRESULT hr = frame->name->ExecuteScript(allowDualControlScript, + Callback( + [](HRESULT, LPCWSTR) -> HRESULT { return S_OK; } + ).Get() + ); + + if (SUCCEEDED(hr)) + { + frame->injected = true; + UpdateAllowDualControlFrame(measure, frame); + } +} + +// Update AllowDualControl state in the WebView frame +void UpdateAllowDualControlFrame(Measure* measure, Frames* frame) +{ + if (!frame || !frame->name || !frame->injected) return; + + frame->name->ExecuteScript( + measure->allowDualControl + ? L"rm_SetAllowDualControl(true);" + : L"rm_SetAllowDualControl(false);", + Callback( + [](HRESULT, LPCWSTR) -> HRESULT { return S_OK; } + ).Get() + ); +} - // Read OnWebViewLoadAction - std::wstring newOnWebViewLoadAction; - LPCWSTR onWebViewLoadOption = RmReadString(rm, L"OnWebViewLoadAction", L"", FALSE); - if (onWebViewLoadOption && wcslen(onWebViewLoadOption) > 0) +PLUGIN_EXPORT void Reload(void* data, void* rm, double* /*maxValue*/) +{ + Measure* measure = static_cast(data); + + // Disabled check + measure->disabled = RmReadInt(rm, L"Disabled", 0) >= 1; + if (measure->disabled) { - newOnWebViewLoadAction = onWebViewLoadOption; + return; } - // Read OnWebViewFailAction - std::wstring newOnWebViewFailAction; - LPCWSTR onWebViewFailOption = RmReadString(rm, L"OnWebViewFailAction", L"", FALSE); - if (onWebViewFailOption && wcslen(onWebViewFailOption) > 0) + // Read basic configuration + const int newWidth = RmReadInt(rm, L"W", 800); + const int newHeight = RmReadInt(rm, L"H", 600); + const int newX = RmReadInt(rm, L"X", 0); + const int newY = RmReadInt(rm, L"Y", 0); + const double newZoomFactor = RmReadFormula(rm, L"ZoomFactor", 1.0); + const bool newVisible = RmReadInt(rm, L"Hidden", 0) <= 0; + const bool newClickthrough = RmReadInt(rm, L"Clickthrough", 0) >= 1; + const bool newAllowDualControl = RmReadInt(rm, L"AllowDualControl", 1) >= 1; + const bool newAllowNotifications = RmReadInt(rm, L"AllowNotifications", 0) >= 1; + const bool newAllowNewWindow = RmReadInt(rm, L"AllowNewWindow", 0) >= 1; + + // URL handling + std::wstring newUrl; + LPCWSTR urlOption = RmReadString(rm, L"Url", L""); + + if (urlOption && *urlOption) { - newOnWebViewFailAction = onWebViewFailOption; + std::wstring urlStr = urlOption; + + // Protocol present - use as-is + if (urlStr.find(L"://") != std::wstring::npos) + { + newUrl = urlStr; + } + else + { + // Relative path - convert to absolute + if (urlStr[0] != L'/' && (urlStr.length() < 2 || urlStr[1] != L':')) + { + if (LPCWSTR absolutePath = RmPathToAbsolute(rm, urlStr.c_str())) + { + urlStr = absolutePath; + } + } + + // Normalize slashes + for (wchar_t& ch : urlStr) + { + if (ch == L'\\') ch = L'/'; + } + + // Ensure file:/// + if (urlStr.find(L"file:///") != 0) + { + urlStr = L"file:///" + urlStr; + } + + newUrl = urlStr; + } } - // Read OnPageLoadStartAction - std::wstring newOnPageLoadStartAction; - LPCWSTR onPageLoadStartOption = RmReadString(rm, L"OnPageLoadStartAction", L"", FALSE); - if (onPageLoadStartOption && wcslen(onPageLoadStartOption) > 0) + // Action strings + auto ReadAction = [&](LPCWSTR key) -> std::wstring + { + LPCWSTR value = RmReadString(rm, key, L"", FALSE); + return (value && *value) ? value : L""; + }; + + const std::wstring newOnWebViewLoadAction = ReadAction(L"OnWebViewLoadAction"); + const std::wstring newOnWebViewFailAction = ReadAction(L"OnWebViewFailAction"); + const std::wstring newOnWebViewStopAction = ReadAction(L"OnWebViewStopAction"); + const std::wstring newOnStateChangeAction = ReadAction(L"OnStateChangeAction"); + const std::wstring newOnUrlChangeAction = ReadAction(L"OnUrlChangeAction"); + const std::wstring newOnPageLoadStartAction = ReadAction(L"OnPageLoadStartAction"); + const std::wstring newOnPageLoadingAction = ReadAction(L"OnPageLoadingAction"); + const std::wstring newOnPageDOMLoadAction = ReadAction(L"OnPageDOMLoadAction"); + const std::wstring newOnPageLoadFinishAction = ReadAction(L"OnPageLoadFinishAction"); + const std::wstring newOnPageFirstLoadAction = ReadAction(L"OnPageFirstLoadAction"); + const std::wstring newOnPageReloadAction = ReadAction(L"OnPageReloadAction"); + + // Change detection + const bool urlChanged = (newUrl != measure->url); + const bool dimensionsChanged = (newWidth != measure->width || + newHeight != measure->height || + newX != measure->x || + newY != measure->y); + const bool visibilityChanged = (newVisible != measure->visible); + const bool clickthroughChanged = (newClickthrough != measure->clickthrough); + const bool allowDualControlChanged = (newAllowDualControl != measure->allowDualControl); + const bool zoomFactorChanged = (newZoomFactor != measure->zoomFactor); + + // Options + 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->allowNotifications = newAllowNotifications; + measure->allowNewWindow = newAllowNewWindow; + + // Actions + measure->onWebViewLoadAction = newOnWebViewLoadAction; + measure->onWebViewFailAction = newOnWebViewFailAction; + measure->onWebViewStopAction = newOnWebViewStopAction; + measure->onStateChangeAction = newOnStateChangeAction; + measure->onUrlChangeAction = newOnUrlChangeAction; + measure->onPageLoadStartAction = newOnPageLoadStartAction; + measure->onPageLoadingAction = newOnPageLoadingAction; + measure->onPageDOMLoadAction = newOnPageDOMLoadAction; + measure->onPageLoadFinishAction = newOnPageLoadFinishAction; + measure->onPageFirstLoadAction = newOnPageFirstLoadAction; + measure->onPageReloadAction = newOnPageReloadAction; + + // Initialization + if (!measure->initialized && measure->autoStart && !measure->disabled) { - newOnPageLoadStartAction = onPageLoadStartOption; + if (measure->isCreationInProgress) + { + return; + } + + CreateWebView2(measure); + measure->autoStart = false; + return; } - // Read OnPageLoadingAction - std::wstring newOnPageLoadingAction; - LPCWSTR onPageLoadingOption = RmReadString(rm, L"OnPageLoadingAction", L"", FALSE); - if (onPageLoadingOption && wcslen(onPageLoadingOption) > 0) + // Dynamic updates + if (dimensionsChanged && measure->webViewController) { - newOnPageLoadingAction = onPageLoadingOption; + RECT bounds; + GetClientRect(measure->skinWindow, &bounds); + + bounds.left = measure->x; + bounds.top = measure->y; + bounds.right = measure->x + measure->width; + bounds.bottom = measure->y + measure->height; + + measure->webViewController->put_Bounds(bounds); } - // Read OnPageLoadFinishAction - std::wstring newOnPageLoadFinishAction; - LPCWSTR onPageLoadFinishOption = RmReadString(rm, L"OnPageLoadFinishAction", L"", FALSE); - if (onPageLoadFinishOption && wcslen(onPageLoadFinishOption) > 0) + if (visibilityChanged && measure->webViewController) { - newOnPageLoadFinishAction = onPageLoadFinishOption; + measure->webViewController->put_IsVisible(measure->visible ? TRUE : FALSE); } - // Read OnPageFirstLoadAction - std::wstring newOnPageFirstLoadAction; - LPCWSTR onPageFirstLoadOption = RmReadString(rm, L"OnPageFirstLoadAction", L"", FALSE); - if (onPageFirstLoadOption && wcslen(onPageFirstLoadOption) > 0) + if (zoomFactorChanged && measure->webViewController) { - newOnPageFirstLoadAction = onPageFirstLoadOption; + measure->webViewController->put_ZoomFactor(measure->zoomFactor); } - // Read OnPageReloadAction - std::wstring newOnPageReloadAction; - LPCWSTR onPageReloadOption = RmReadString(rm, L"OnPageReloadAction", L"", FALSE); - if (onPageReloadOption && wcslen(onPageReloadOption) > 0) + if (clickthroughChanged) { - newOnPageReloadAction = onPageReloadOption; + UpdateClickthrough(measure); } - - // 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 || - newX != measure->x || - newY != measure->y); - - 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) - { - if (urlChanged && measure->initialized) - { - // URL changed - navigate to new URL instead of recreating - if (measure->webView && !newUrl.empty()) - { - measure->webView->Navigate(newUrl.c_str()); - } - } - else - { - // First initialization - create WebView2 - if (measure->isCreationInProgress) - { - // Avoid re-entrancy if creation is already in progress - return; - } - CreateWebView2(measure); - } - } - else - { - // WebView2 already exists - update properties dynamically - if (dimensionsChanged && measure->webViewController) - { - RECT bounds; - GetClientRect(measure->skinWindow, &bounds); - bounds.left = measure->x; - bounds.top = measure->y; - bounds.right = measure->x + measure->width; - bounds.bottom = measure->y + measure->height; - measure->webViewController->put_Bounds(bounds); - } - - if (visibilityChanged && measure->webViewController) - { - measure->webViewController->put_IsVisible(measure->visible ? TRUE : FALSE); - } - - if (zoomFactorChanged && measure->webViewController) - { - measure->webViewController->put_ZoomFactor(measure->zoomFactor); + + if (!allowDualControlChanged) + return; + + // Main document + if (!measure->isAllowDualControlInjected) + { + InjectAllowDualControl(measure); + } + else + { + UpdateAllowDualControl(measure); + } + + if (measure->webViewFrames.empty()) + return; + // Frames + for (Frames& frame : measure->webViewFrames) + { + if (!frame.name) + continue; + + if (!frame.injected) + { + InjectAllowDualControlFrame(measure, &frame); + } + else + { + UpdateAllowDualControlFrame(measure, &frame); } - - if (clickthroughChanged) - { - UpdateClickthrough(measure); - } - - if (allowDualControlChanged) - { - if (!measure->isAllowDualControlInjected) - { - InjectAllowDualControl(measure); - } - else - UpdateAllowDualControl(measure); - } - } + } } PLUGIN_EXPORT double Update(void* data) { - Measure* measure = (Measure*)data; - - // Call JavaScript OnUpdate callback if WebView is initialized - if (measure->initialized && measure->webView) - { - measure->webView->ExecuteScript( - L"(function() { if (typeof window.OnUpdate === 'function') { var result = window.OnUpdate(); return result !== undefined ? String(result) : ''; } return ''; })();", - Callback( - [measure](HRESULT errorCode, LPCWSTR resultObjectAsJson) -> HRESULT - { - if (SUCCEEDED(errorCode) && resultObjectAsJson) - { - // Remove quotes from JSON string result - std::wstring result = resultObjectAsJson; - if (result.length() >= 2 && result.front() == L'"' && result.back() == L'"') - { - result = result.substr(1, result.length() - 2); - } - - // Store the callback result - if (!result.empty() && result != L"null") - { - measure->callbackResult = result; - } - } - return S_OK; - } - ).Get() - ); - } - - return measure->initialized ? 1.0 : 0.0; + Measure* measure = (Measure*)data; + + // Call JavaScript OnUpdate callback if WebView is initialized + if (measure->initialized && measure->webView) + { + measure->webView->ExecuteScript( + L"(function() { if (typeof window.OnUpdate === 'function') { var result = window.OnUpdate(); return result !== undefined ? String(result) : ''; } return ''; })();", + Callback( + [measure](HRESULT errorCode, LPCWSTR resultObjectAsJson) -> HRESULT + { + if (SUCCEEDED(errorCode) && resultObjectAsJson) + { + // Remove quotes from JSON string result + std::wstring result = resultObjectAsJson; + if (result.length() >= 2 && result.front() == L'"' && result.back() == L'"') + { + result = result.substr(1, result.length() - 2); + } + + // Store the callback result + if (!result.empty() && result != L"null") + { + // measure->callbackResult = result; + } + } + return S_OK; + } + ).Get() + ); + } + + return measure->state; } PLUGIN_EXPORT LPCWSTR GetString(void* data) { - Measure* measure = (Measure*)data; - - // Return the callback result if available, otherwise return "0" - if (!measure->callbackResult.empty()) - { - return measure->callbackResult.c_str(); - } - - return L"0"; + Measure* measure = (Measure*)data; + + // Return the callback result if available, otherwise return "0" + if (!measure->callbackResult.empty()) + { + return measure->callbackResult.c_str(); + } + + return L""; } PLUGIN_EXPORT void ExecuteBang(void* data, LPCWSTR args) { - Measure* measure = (Measure*)data; - - if (!measure || !measure->webView) - return; - - std::wstring command = args; - - // Parse command - size_t spacePos = command.find(L' '); - std::wstring action = (spacePos != std::wstring::npos) ? - command.substr(0, spacePos) : command; - std::wstring param = (spacePos != std::wstring::npos) ? - command.substr(spacePos + 1) : L""; - - if (_wcsicmp(action.c_str(), L"Navigate") == 0) - { - if (!param.empty()) - { - measure->webView->Navigate(param.c_str()); - } - } - else if (_wcsicmp(action.c_str(), L"Reload") == 0) - { - measure->webView->Reload(); - } - else if (_wcsicmp(action.c_str(), L"GoBack") == 0) - { - measure->webView->GoBack(); - } - else if (_wcsicmp(action.c_str(), L"GoForward") == 0) - { - measure->webView->GoForward(); - } - else if (_wcsicmp(action.c_str(), L"ExecuteScript") == 0) - { - if (!param.empty()) - { - measure->webView->ExecuteScript( - param.c_str(), - Callback( - [](HRESULT errorCode, LPCWSTR resultObjectAsJson) -> HRESULT - { - return S_OK; - } - ).Get() - ); - } - } - else if (_wcsicmp(action.c_str(), L"OpenDevTools") == 0) - { - measure->webView->OpenDevToolsWindow(); - } + Measure* measure = (Measure*)data; + if (!measure) + return; + + if (measure->disabled) + { + RmLog(measure->rm, LOG_ERROR, L"WebView2: The measure is disabled"); + return; + } + + std::wstring command = args; + + // Parse command + size_t spacePos = command.find(L' '); + std::wstring action = (spacePos != std::wstring::npos) ? + command.substr(0, spacePos) : command; + std::wstring param = (spacePos != std::wstring::npos) ? + command.substr(spacePos + 1) : L""; + + // WebView Commands + if (_wcsicmp(action.c_str(), L"WebViewStart") == 0) + { + CreateWebView2(measure); + return; + } + if (_wcsicmp(action.c_str(), L"WebViewStop") == 0) + { + StopWebView2(measure); + return; + } + if (_wcsicmp(action.c_str(), L"WebViewRestart") == 0) + { + RestartWebView2(measure); + return; + } + + if (!measure->webView) + { + RmLog(measure->rm, LOG_ERROR, L"WebView2: Not running"); + return; + } + + // Navigation Commands + if (_wcsicmp(action.c_str(), L"Navigate") == 0) + { + measure->webView->Navigate(param.c_str()); + } + else if (_wcsicmp(action.c_str(), L"Stop") == 0) + { + measure->webView->Stop(); + } + else if (_wcsicmp(action.c_str(), L"Reload") == 0) + { + measure->webView->Reload(); + } + else if (_wcsicmp(action.c_str(), L"GoBack") == 0) + { + measure->webView->GoBack(); + } + else if (_wcsicmp(action.c_str(), L"GoForward") == 0) + { + measure->webView->GoForward(); + } + else if (_wcsicmp(action.c_str(), L"GoHome") == 0) + { + measure->webView->Navigate(measure->url.c_str()); + } + else if (_wcsicmp(action.c_str(), L"ExecuteScript") == 0) + { + if (!param.empty()) + { + measure->webView->ExecuteScript( + param.c_str(), + Callback( + [](HRESULT errorCode, LPCWSTR resultObjectAsJson) -> HRESULT + { + return S_OK; + } + ).Get() + ); + if (measure->webViewFrames.empty()) + return; + // Frames + for (Frames& frame : measure->webViewFrames) + { + if (!frame.name) + continue; + + frame.name->ExecuteScript( + param.c_str(), + Callback( + [](HRESULT errorCode, LPCWSTR resultObjectAsJson) -> HRESULT + { + return S_OK; + } + ).Get() + ); + } + } + } + else if (_wcsicmp(action.c_str(), L"OpenDevTools") == 0) + { + measure->webView->OpenDevToolsWindow(); + } } // Generic JavaScript function caller PLUGIN_EXPORT LPCWSTR CallJS(void* data, const int argc, const WCHAR* argv[]) { - Measure* measure = (Measure*)data; - - if (!measure || !measure->initialized || !measure->webView) - return L""; - - if (argc == 0 || !argv[0]) - return L""; - - // Build unique key for this call: functionName|arg1|arg2... - std::wstring key = argv[0]; - for (int i = 1; i < argc; i++) - { - key += L"|"; - key += argv[i]; - } - - // Build JavaScript call: functionName(arg1, arg2, ...) - std::wstring jsCode = L"(function() { try { if (typeof " + std::wstring(argv[0]) + L" === 'function') { var result = " + std::wstring(argv[0]) + L"("; - - // Add arguments if provided - for (int i = 1; i < argc; i++) - { - if (i > 1) jsCode += L", "; - jsCode += L"'" + std::wstring(argv[i]) + L"'"; - } - - jsCode += L"); return result !== undefined ? String(result) : ''; } return 'Function not found'; } catch(e) { return 'Error: ' + e.message; } })();"; - - // Execute asynchronously and update cache - measure->webView->ExecuteScript( - jsCode.c_str(), - Callback( - [measure, key](HRESULT errorCode, LPCWSTR resultObjectAsJson) -> HRESULT - { - if (SUCCEEDED(errorCode) && resultObjectAsJson) - { - std::wstring result = resultObjectAsJson; - if (result.length() >= 2 && result.front() == L'"' && result.back() == L'"') - { - result = result.substr(1, result.length() - 2); - } - - if (!result.empty() && result != L"null") - { - // Update cache for this specific call - measure->jsResults[key] = result; - } - } - return S_OK; - } - ).Get() - ); - - // Return cached result if available, otherwise "0" - if (measure->jsResults.find(key) != measure->jsResults.end()) - { - measure->buffer = measure->jsResults[key]; - } - else - { - measure->buffer = L"0"; - } - - return measure->buffer.c_str(); + Measure* measure = (Measure*)data; + + if (measure->disabled) + { + return L""; + } + + if (!measure || !measure->initialized || !measure->webView) + return L""; + + if (argc == 0 || !argv[0]) + return L""; + + // Build unique key for this call: functionName|arg1|arg2... + std::wstring key = argv[0]; + for (int i = 1; i < argc; i++) + { + key += L"|"; + key += argv[i]; + } + + // Build JavaScript call: functionName(arg1, arg2, ...) + std::wstring jsCode = L"(function() { try { if (typeof " + std::wstring(argv[0]) + L" === 'function') { var result = " + std::wstring(argv[0]) + L"("; + + // Add arguments if provided + for (int i = 1; i < argc; i++) + { + if (i > 1) jsCode += L", "; + jsCode += L"'" + std::wstring(argv[i]) + L"'"; + } + + jsCode += L"); return result !== undefined ? String(result) : ''; } return 'Function not found'; } catch(e) { return 'Error: ' + e.message; } })();"; + + // Execute asynchronously and update cache + measure->webView->ExecuteScript( + jsCode.c_str(), + Callback( + [measure, key](HRESULT errorCode, LPCWSTR resultObjectAsJson) -> HRESULT + { + if (SUCCEEDED(errorCode) && resultObjectAsJson) + { + std::wstring result = resultObjectAsJson; + if (result.length() >= 2 && result.front() == L'"' && result.back() == L'"') + { + result = result.substr(1, result.length() - 2); + } + + if (!result.empty() && result != L"null") + { + // Update cache for this specific call + measure->jsResults[key] = result; + } + } + return S_OK; + } + ).Get() + ); + + if (!measure->webViewFrames.empty()) + { + // Frames + for (Frames& frame : measure->webViewFrames) + { + if (!frame.name) + continue; + + frame.name->ExecuteScript( + jsCode.c_str(), + Callback( + [](HRESULT errorCode, LPCWSTR resultObjectAsJson) -> HRESULT + { + return S_OK; + } + ).Get() + ); + } + } + + // Return cached result if available, otherwise "0" + if (measure->jsResults.find(key) != measure->jsResults.end()) + { + measure->buffer = measure->jsResults[key]; + } + else + { + measure->buffer = L"0"; + } + + return measure->buffer.c_str(); } PLUGIN_EXPORT void Finalize(void* data) { - Measure* measure = (Measure*)data; - delete measure; + Measure* measure = (Measure*)data; + Frames* frames = (Frames*)data; + delete frames; + delete measure; } diff --git a/WebView2/Plugin.h b/WebView2/Plugin.h index c51e5d6..ad3b113 100644 --- a/WebView2/Plugin.h +++ b/WebView2/Plugin.h @@ -3,16 +3,24 @@ #include #include +#include #include #include #include #include +#include using namespace Microsoft::WRL; // Global TypeLib for COM objects extern wil::com_ptr g_typeLib; +struct Frames +{ + wil::com_ptr name; + bool injected = false; +}; + // Measure structure containing WebView2 state struct Measure { @@ -28,29 +36,43 @@ struct Measure int x; int y; double zoomFactor; + bool disabled; + bool autoStart = true; bool visible; bool initialized; bool clickthrough; - bool isCreationInProgress = false; bool isFirstLoad = true; bool allowDualControl; bool isAllowDualControlInjected = false; + bool allowNotifications; + bool allowNewWindow; + + bool isCreationInProgress = false; + bool isStopping = false; std::wstring onWebViewLoadAction; std::wstring onWebViewFailAction; + std::wstring onWebViewStopAction; + std::wstring onStateChangeAction; + std::wstring onUrlChangeAction; std::wstring onPageFirstLoadAction; std::wstring onPageLoadStartAction; std::wstring onPageLoadingAction; + std::wstring onPageDOMLoadAction; std::wstring onPageLoadFinishAction; std::wstring onPageReloadAction; + wil::com_ptr webViewEnvironment; wil::com_ptr webViewController; wil::com_ptr webView; + std::deque Measure::webViewFrames; + 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 + int state = -1; // Integer number to show the internal state of WebView and Navigation Measure(); ~Measure(); @@ -58,12 +80,16 @@ struct Measure // Member callback functions for WebView2 creation HRESULT CreateEnvironmentHandler(HRESULT result, ICoreWebView2Environment* env); HRESULT CreateControllerHandler(HRESULT result, ICoreWebView2Controller* controller); + void Measure::SetStateAndNotify(int newState); + HRESULT Measure::FailWebView(HRESULT hr, const wchar_t* logMessage, bool resetCreationFlag = true); }; // WebView2 functions void CreateWebView2(Measure* measure); +void StopWebView2(Measure* measure); +void RestartWebView2(Measure* measure); void UpdateClickthrough(Measure* measure); void InjectAllowDualControl(Measure* measure); void UpdateAllowDualControl(Measure* measure); - - +void InjectAllowDualControlFrame(Measure* measure, Frames* frame); +void UpdateAllowDualControlFrame(Measure* measure, Frames* frame); diff --git a/WebView2/WebView2.cpp b/WebView2/WebView2.cpp index 6eb16b6..7e8c921 100644 --- a/WebView2/WebView2.cpp +++ b/WebView2/WebView2.cpp @@ -2,333 +2,682 @@ #include "Plugin.h" #include "HostObjectRmAPI.h" #include "../API/RainmeterAPI.h" +#include + +std::wstring NormalizeUri(const std::wstring& uri) +{ + const std::wstring scheme_sep = L"://"; + auto scheme_pos = uri.find(scheme_sep); + if (scheme_pos == std::wstring::npos) + return uri; + + const std::wstring scheme = uri.substr(0, scheme_pos); + const size_t after_scheme = scheme_pos + scheme_sep.length(); + + if (scheme == L"file") + { + size_t last_slash = uri.find_last_of(L'/'); + if (last_slash != std::wstring::npos) + { + return uri.substr(0, last_slash + 1); + } + return uri; + } + + size_t path_start = uri.find(L'/', after_scheme); + if (path_start == std::wstring::npos) + { + return uri + L"/"; + } + + return uri.substr(0, path_start + 1); +} // Create WebView2 environment and controller void CreateWebView2(Measure* measure) { - if (!measure || !measure->skinWindow) - { - if (measure && measure->rm) - 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]; - GetTempPathW(MAX_PATH, tempPath); - std::wstring userDataFolder = std::wstring(tempPath) + L"RainmeterWebView2"; - - // Create the directory if it doesn't exist - CreateDirectoryW(userDataFolder.c_str(), nullptr); - - // Create WebView2 environment with user data folder - HRESULT hr = CreateCoreWebView2EnvironmentWithOptions( - nullptr, userDataFolder.c_str(), nullptr, - Callback( - measure, - &Measure::CreateEnvironmentHandler - ).Get() - - ); - - if (FAILED(hr)) - { - if (measure->rm) - { - wchar_t errorMsg[512]; - 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; - } + if (!measure || !measure->skinWindow) + { + if (measure && measure->rm) + RmLog(measure->rm, LOG_ERROR, L"WebView2: Invalid measure or skin window"); + return; + } + + if (measure->initialized) + { + RmLog(measure->rm, LOG_ERROR, L"WebView2: Already started"); + return; + } + + if (measure->isCreationInProgress) + { + RmLog(measure->rm, LOG_ERROR, L"WebView2: Initialization already in progress"); + return; + } + + measure->isCreationInProgress = true; + + // Create user data folder in TEMP directory to avoid permission issues + wchar_t tempPath[MAX_PATH]; + GetTempPathW(MAX_PATH, tempPath); + std::wstring userDataFolder = std::wstring(tempPath) + L"RainmeterWebView2"; + + // Create the directory if it doesn't exist + CreateDirectoryW(userDataFolder.c_str(), nullptr); + + // Add environment options + auto environmentOptions = Microsoft::WRL::Make(); + + // Available browser flags: https://learn.microsoft.com/en-us/microsoft-edge/webview2/concepts/webview-features-flags?tabs=win32cpp#available-webview2-browser-flags + std::wstring browserArgs; + browserArgs.append(L"--enable-features="); + + environmentOptions->put_AdditionalBrowserArguments(browserArgs.c_str()); // Add Flags + + Microsoft::WRL::ComPtr environmentOptions6; + if (environmentOptions.As(&environmentOptions6) == S_OK) + { + environmentOptions6->put_AreBrowserExtensionsEnabled(TRUE); // Enable Extensions + } + + // Create WebView2 environment with user data folder + HRESULT hr = CreateCoreWebView2EnvironmentWithOptions( + nullptr, userDataFolder.c_str(), environmentOptions.Get(), + Callback( + measure, + &Measure::CreateEnvironmentHandler + ).Get() + + ); + + if (FAILED(hr)) + { + measure->FailWebView(hr, L"WebView2: Failed to start creation process (HRESULT: 0x%08X). Make sure WebView2 Runtime is installed."); + } } // Environment creation callback HRESULT Measure::CreateEnvironmentHandler(HRESULT result, ICoreWebView2Environment* env) { - if (FAILED(result)) - { - if (rm) - { - wchar_t errorMsg[256]; - 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; - } - - // Create WebView2 controller using skin window directly - env->CreateCoreWebView2Controller( - skinWindow, - Callback( - this, - &Measure::CreateControllerHandler - ).Get() - ); - - return S_OK; + if (FAILED(result)) + { + return FailWebView(result, L"WebView2: Failed to create environment"); + } + + webViewEnvironment = env; + + // Create WebView2 controller with options. + auto webViewEnvironment10 = webViewEnvironment.query(); + if (webViewEnvironment10) + { + wil::com_ptr controllerOptions; + webViewEnvironment10->CreateCoreWebView2ControllerOptions(&controllerOptions); + + // OPTIONS + controllerOptions->put_IsInPrivateModeEnabled(FALSE); // Private/Incognito Mode + controllerOptions->put_ProfileName(L"Rainmeter"); // Profile Name + + // Get System's language + wchar_t osLocale[LOCALE_NAME_MAX_LENGTH] = { 0 }; + GetUserDefaultLocaleName(osLocale, LOCALE_NAME_MAX_LENGTH); + // Set language + auto controllerOptions2 = controllerOptions.query(); + if (controllerOptions2) + { + controllerOptions2->put_ScriptLocale(osLocale); // Script Locale + } + + // Set Transparent Background + auto controllerOptions3 = controllerOptions.query(); + if (controllerOptions3) + { + COREWEBVIEW2_COLOR transparentColor = { 0, 0, 0, 0 }; + + controllerOptions3->put_DefaultBackgroundColor(transparentColor); // Background Color + } + + // Allow Host Input Processing + auto controllerOptions4 = controllerOptions.query(); + if (controllerOptions4) + { + controllerOptions4->put_AllowHostInputProcessing(TRUE); // Allow Host Input Processing + } + + // Create Controller With Options. + webViewEnvironment10->CreateCoreWebView2ControllerWithOptions( + skinWindow, + controllerOptions.get(), + Callback( + this, + &Measure::CreateControllerHandler + ).Get() + ); + } + else // Create WebView Controller with no options. + { + webViewEnvironment->CreateCoreWebView2Controller( + skinWindow, + Callback( + this, + &Measure::CreateControllerHandler + ).Get() + ); + } + + return S_OK; } // Controller creation callback HRESULT Measure::CreateControllerHandler(HRESULT result, ICoreWebView2Controller* controller) { + if (FAILED(result)) + { + return FailWebView(result, L"WebView2: Failed to create controller"); + } + + if (!controller) + { + return FailWebView(S_FALSE, L"WebView2: Controller is null"); + } + + // WebView is initializing + SetStateAndNotify(0); - if (FAILED(result)) - { - if (rm) - { - wchar_t errorMsg[256]; - 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; - } - - if (controller == nullptr) - { - 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; - } - - webViewController = controller; - webViewController->get_CoreWebView2(&webView); - - // Set bounds within the skin window - RECT bounds; - GetClientRect(skinWindow, &bounds); - bounds.left = x; - bounds.top = y; - if (width > 0) - { - bounds.right = x + width; - } - if (height > 0) - { - bounds.bottom = y + height; - } - webViewController->put_Bounds(bounds); - - // Set initial visibility - webViewController->put_IsVisible(visible ? TRUE : FALSE); + webViewController = controller; + webViewController->get_CoreWebView2(&webView); + + // Set bounds within the skin window + RECT bounds; + GetClientRect(skinWindow, &bounds); + bounds.left = x; + bounds.top = y; + if (width > 0) + { + bounds.right = x + width; + } + if (height > 0) + { + bounds.bottom = y + height; + } + webViewController->put_Bounds(bounds); + + // 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) - { - COREWEBVIEW2_COLOR transparentColor = { 0, 0, 0, 0 }; - controller2->put_DefaultBackgroundColor(transparentColor); - } - - // Enable host objects and JavaScript in settings - wil::com_ptr settings; - webView->get_Settings(&settings); - settings->put_IsScriptEnabled(TRUE); - settings->put_AreDefaultScriptDialogsEnabled(TRUE); - settings->put_IsWebMessageEnabled(TRUE); - settings->put_AreHostObjectsAllowed(TRUE); - settings->put_AreDevToolsEnabled(TRUE); - settings->put_AreDefaultContextMenusEnabled(TRUE); - - // Create and inject COM Host Object for Rainmeter API - wil::com_ptr hostObject = - Microsoft::WRL::Make(this, g_typeLib); - - VARIANT variant = {}; - hostObject.query_to(&variant.pdispVal); - variant.vt = VT_DISPATCH; - webView->AddHostObjectToScript(L"RainmeterAPI", &variant); - variant.pdispVal->Release(); - - // Add script to make RainmeterAPI available globally - webView->AddScriptToExecuteOnDocumentCreated( - L"window.RainmeterAPI = chrome.webview.hostObjects.sync.RainmeterAPI", - nullptr - ); - - // 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 - ); + webViewController->put_ZoomFactor(zoomFactor); + + // Enable host objects and JavaScript in settings + wil::com_ptr settings; + webView->get_Settings(&settings); + settings->put_IsScriptEnabled(TRUE); + settings->put_AreDefaultScriptDialogsEnabled(TRUE); + settings->put_IsWebMessageEnabled(TRUE); + settings->put_AreHostObjectsAllowed(TRUE); + settings->put_AreDevToolsEnabled(TRUE); + settings->put_AreDefaultContextMenusEnabled(TRUE); + + // Create and inject COM Host Object for Rainmeter API + wil::com_ptr hostObject = + Microsoft::WRL::Make(this, g_typeLib); + + VARIANT variant = {}; + hostObject.query_to(&variant.pdispVal); + variant.vt = VT_DISPATCH; + webView->AddHostObjectToScript(L"RainmeterAPI", &variant); + variant.pdispVal->Release(); + + wil::com_ptr webView4; + webView->QueryInterface(IID_PPV_ARGS(&webView4)); + if (webView4) { + webView4->add_FrameCreated( + Microsoft::WRL::Callback( + [this](ICoreWebView2* sender, ICoreWebView2FrameCreatedEventArgs* args) -> HRESULT + { + wil::com_ptr frame; + RETURN_IF_FAILED(args->get_Frame(&frame)); + + wil::com_ptr frame2; + RETURN_IF_FAILED(frame->QueryInterface(IID_PPV_ARGS(&frame2))); + + if (frame2) { + // Add host object + wil::com_ptr hostObject = + Microsoft::WRL::Make(this, g_typeLib); + + wil::unique_variant hostObjectVariant; + hostObject.query_to(&hostObjectVariant.pdispVal); + hostObjectVariant.vt = VT_DISPATCH; + + std::wstring origin = NormalizeUri(currentUrl); + LPCWSTR origins = L"*"; // all-origins + + frame2->AddHostObjectToScriptWithOrigins(L"RainmeterAPI", &hostObjectVariant, 1, &origins); + + this->webViewFrames.push_back(Frames{ frame2, false }); + Frames* frameState = &this->webViewFrames.back(); + + // Inject frame ancestor to nested frames to allow framing websites. (Requires http-server). + frame2->add_NavigationStarting( + Microsoft::WRL::Callback( + [origin](ICoreWebView2Frame* sender, ICoreWebView2NavigationStartingEventArgs* args) -> HRESULT + { + wil::com_ptr navigationStartArgs; + if (SUCCEEDED(args->QueryInterface(IID_PPV_ARGS(&navigationStartArgs)))) + { + navigationStartArgs->put_AdditionalAllowedFrameAncestors(origin.c_str()); + } + return S_OK; + + } + ).Get(), nullptr + ); + + // Inject AllowDualControl script to frames. + frame2->add_DOMContentLoaded( + Callback( + [this, frameState](ICoreWebView2Frame* sender, ICoreWebView2DOMContentLoadedEventArgs* args) -> HRESULT { + + frameState->injected = false; + + if (this->allowDualControl) + { + InjectAllowDualControlFrame(this, frameState); + } + return S_OK; + } + ).Get(), nullptr + ); + } + + return S_OK; + }).Get(), nullptr + ); + } + + // Add script to make RainmeterAPI available globally + webView->AddScriptToExecuteOnDocumentCreated( + L"window.RainmeterAPI = chrome.webview.hostObjects.sync.RainmeterAPI;", + //L"chrome.webview.hostObjects.options.log = console.log.bind(console);" // This enables console debug mode. + nullptr + ); + + // Inject frame ancestor to allow framing websites. (Requires http-server). + webView->add_FrameNavigationStarting( + Microsoft::WRL::Callback( + [this](ICoreWebView2* sender, ICoreWebView2NavigationStartingEventArgs* args) -> HRESULT + { + std::wstring nUri = NormalizeUri(currentUrl); + + wil::com_ptr navigationStartArgs; + if (SUCCEEDED(args->QueryInterface(IID_PPV_ARGS(&navigationStartArgs)))) + { + navigationStartArgs->put_AdditionalAllowedFrameAncestors(nUri.c_str()); + } + return S_OK; + } + ).Get(),nullptr + ); + + // Avoid browser from opening links on different windows and block not user requested popups + webView->add_NewWindowRequested( + Callback( + [this](ICoreWebView2* sender, ICoreWebView2NewWindowRequestedEventArgs* args) -> HRESULT + { + BOOL isUserInitiated = FALSE; + args->get_IsUserInitiated(&isUserInitiated); + + // Block scripted popup + if (!isUserInitiated) + { + args->put_Handled(TRUE); + return S_OK; + } + + // Open in same window + if (!allowNewWindow) + { + wil::unique_cotaskmem_string uri; + if (SUCCEEDED(args->get_Uri(&uri)) && uri) + { + sender->Navigate(uri.get()); + } + args->put_Handled(TRUE); + } + else // Open in new window + { + args->put_Handled(FALSE); + } + return S_OK; + } + ).Get(), nullptr + ); + + // Handle permissions + webView->add_PermissionRequested( + Callback( + [this](ICoreWebView2* sender, ICoreWebView2PermissionRequestedEventArgs* args) -> HRESULT + { + COREWEBVIEW2_PERMISSION_KIND kind; + args->get_PermissionKind(&kind); + // Allow notifications + if (kind == COREWEBVIEW2_PERMISSION_KIND_NOTIFICATIONS) + { + if (allowNotifications) // Allow + { + args->put_State(COREWEBVIEW2_PERMISSION_STATE_ALLOW); + } + else // Deny + { + args->put_State(COREWEBVIEW2_PERMISSION_STATE_DENY); + } + } + + wil::com_ptr args3; + if (SUCCEEDED(args->QueryInterface(IID_PPV_ARGS(&args3)))) + { + args3->put_SavesInProfile(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 - ); + webView->add_NavigationStarting( + Callback( + [this](ICoreWebView2* sender, ICoreWebView2NavigationStartingEventArgs* args) -> HRESULT + { + // Navigation is starting + SetStateAndNotify(100); + + if (wcslen(onPageLoadStartAction.c_str()) > 0) + { + if (skin) + RmExecute(skin, onPageLoadStartAction.c_str()); + } + return S_OK; + } + ).Get(),nullptr + ); + + // 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; + callbackResult = currentUrl; + if (wcslen(onUrlChangeAction.c_str()) > 0) + { + if (skin) + RmExecute(skin, onUrlChangeAction.c_str()); + } + } + else + { + // URL did not change + isFirstLoad = false; + } + } + 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 - ); + webView->add_ContentLoading( + Callback( + [this](ICoreWebView2* sender, ICoreWebView2ContentLoadingEventArgs* args) -> HRESULT + { + // Navigation is loading + SetStateAndNotify(200); + + if (wcslen(onPageLoadingAction.c_str()) > 0) + { + if (skin) + RmExecute(skin, onPageLoadingAction.c_str()); + } + return S_OK; + } + ).Get(),nullptr + ); + + // Add DOMContentLoaded event to inject AllowDualControl + wil::com_ptr webView2; + webView->QueryInterface(IID_PPV_ARGS(&webView2)); + if (webView2) { + webView2->add_DOMContentLoaded( + Callback< ICoreWebView2DOMContentLoadedEventHandler>( + [this](ICoreWebView2* sender, ICoreWebView2DOMContentLoadedEventArgs* args) -> HRESULT + { + // DOM content is loaded + SetStateAndNotify(300); + + // Inject script to capture page load events for drag/move and context menu + isAllowDualControlInjected = false; + if (allowDualControl) + { + InjectAllowDualControl(this); + } + + if (wcslen(onPageDOMLoadAction.c_str()) > 0) + { + if (skin) + RmExecute(skin, onPageDOMLoadAction.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 ''; })();", - Callback( - [this](HRESULT errorCode, LPCWSTR resultObjectAsJson) -> HRESULT - { - if (SUCCEEDED(errorCode) && resultObjectAsJson) - { - // Remove quotes from JSON string result - std::wstring result = resultObjectAsJson; - if (result.length() >= 2 && result.front() == L'"' && result.back() == L'"') - { - result = result.substr(1, result.length() - 2); - } - - // Store the callback result - if (!result.empty() && result != L"null") - { - callbackResult = result; - } - } - return S_OK; - } - ).Get() - ); + webView->add_NavigationCompleted( + Callback( + [this](ICoreWebView2* sender, ICoreWebView2NavigationCompletedEventArgs* args) -> HRESULT + { + // Navigation is complete + SetStateAndNotify(400); + + // 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 ''; })();", + Callback( + [this](HRESULT errorCode, LPCWSTR resultObjectAsJson) -> HRESULT + { + if (SUCCEEDED(errorCode) && resultObjectAsJson) + { + // Remove quotes from JSON string result + std::wstring result = resultObjectAsJson; + if (result.length() >= 2 && result.front() == L'"' && result.back() == L'"') + { + result = result.substr(1, result.length() - 2); + } + + // Store the callback result + if (!result.empty() && result != L"null") + { + //callbackResult = result; + } + } + return S_OK; + } + ).Get() + ); if (isFirstLoad) // First load - { - if (wcslen(onPageFirstLoadAction.c_str()) > 0) - { - if (skin) - RmExecute(skin, onPageFirstLoadAction.c_str()); - } - isFirstLoad = false; - } + { + 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()); - } - } + { + 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()) - { - webView->Navigate(url.c_str()); - } - - 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); - - return S_OK; + if (wcslen(onPageLoadFinishAction.c_str()) > 0) + { + if (skin) + RmExecute(skin, onPageLoadFinishAction.c_str()); + } + return S_OK; + } + ).Get(), nullptr + ); + + initialized = true; + if (rm) + RmLog(rm, LOG_NOTICE, L"WebView2: Initialized successfully with COM Host Objects"); + + // WebView is initialized + SetStateAndNotify(1); + + if (wcslen(onWebViewLoadAction.c_str()) > 0) + { + RmExecute(skin, onWebViewLoadAction.c_str()); + } + + isCreationInProgress = false; + + // Navigate to URL + webView->Navigate(url.c_str()); + + // Apply initial clickthrough state + UpdateClickthrough(this); + + return S_OK; } + +void StopWebView2(Measure* measure) +{ + if (!measure) + return; + if (measure->isStopping) + return; + if (!measure->initialized && !measure->webView && !measure->webViewController) + { + RmLog(measure->rm, LOG_ERROR, L"WebView2: Already stopped"); + return; + } + + measure->isStopping = true; + + measure->isCreationInProgress = false; + + // Hide (prevents flicker) + if (measure->webViewController) + { + measure->webViewController->put_IsVisible(FALSE); + } + + // Stop navigation + if (measure->webView) + { + measure->webView->Stop(); + } + + // Release + measure->webView.reset(); + measure->webViewController.reset(); + measure->webViewEnvironment.reset(); + + // Reset flags + measure->initialized = false; + measure->isFirstLoad = true; + + // Clear url + measure->currentUrl.clear(); + measure->callbackResult.clear(); + + // WebView is stopped + measure->SetStateAndNotify(-1); + + if (wcslen(measure->onWebViewStopAction.c_str()) > 0) + { + if (measure->skin) + RmExecute(measure->skin, measure->onWebViewStopAction.c_str()); + } + if (measure->rm) + { + RmLog(measure->rm, LOG_NOTICE, L"WebView2: Stopped sucessfully"); + } + measure->isStopping = false; +} + +void RestartWebView2(Measure* measure) +{ + if (!measure || !measure->skinWindow) + return; + + if (!measure->initialized) + { + RmLog(measure->rm, LOG_ERROR, L"WebView2: Not running"); + return; + } + + // Stop WebView2 + StopWebView2(measure); + + // Start WebView2 + CreateWebView2(measure); +} + +void Measure::SetStateAndNotify(int newState) +{ + state = newState; + + if (!onStateChangeAction.empty() && skin) + { + RmExecute(skin, onStateChangeAction.c_str()); + } +} + +HRESULT Measure::FailWebView(HRESULT hr, const wchar_t* logMessage, bool resetCreationFlag) +{ + if (rm && logMessage) + { + wchar_t errorMsg[512]; + if (hr != S_OK) + { + swprintf_s(errorMsg, L"%s (HRESULT: 0x%08X)", logMessage, hr); + RmLog(rm, LOG_ERROR, errorMsg); + } + else + { + RmLog(rm, LOG_ERROR, logMessage); + } + } + + SetStateAndNotify(-2); + + if (!onWebViewFailAction.empty() && skin) + { + RmExecute(skin, onWebViewFailAction.c_str()); + } + + if (resetCreationFlag) + { + isCreationInProgress = false; + } + + return hr; +} \ No newline at end of file diff --git a/WebView2/WebView2.vcxproj b/WebView2/WebView2.vcxproj index dbdb55c..87955b7 100644 --- a/WebView2/WebView2.vcxproj +++ b/WebView2/WebView2.vcxproj @@ -201,7 +201,7 @@ - + @@ -211,7 +211,7 @@ This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - + \ No newline at end of file diff --git a/WebView2/WebView2.vcxproj.filters b/WebView2/WebView2.vcxproj.filters index 6b93abb..630c712 100644 --- a/WebView2/WebView2.vcxproj.filters +++ b/WebView2/WebView2.vcxproj.filters @@ -8,9 +8,6 @@ - - - @@ -23,4 +20,8 @@ + + + + \ No newline at end of file diff --git a/WebView2/packages.config b/WebView2/packages.config index 4662d01..d9b0570 100644 --- a/WebView2/packages.config +++ b/WebView2/packages.config @@ -1,5 +1,5 @@ - + - + From d4bb2344409eb449ed0fb63f08e7e183aa54e49e Mon Sep 17 00:00:00 2001 From: RicardoTM05 <168048518+RicardoTM05@users.noreply.github.com> Date: Sun, 21 Dec 2025 13:24:36 -0600 Subject: [PATCH 2/6] set put_AllowHostInputProcessing to false Turned off put_AllowHostInputProcessing due to it messing with AllowDualControl. Should be turned on once we start implementing a Non-JS AllowDualControl version. --- WebView2/WebView2.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/WebView2/WebView2.cpp b/WebView2/WebView2.cpp index 7e8c921..6262403 100644 --- a/WebView2/WebView2.cpp +++ b/WebView2/WebView2.cpp @@ -140,7 +140,7 @@ HRESULT Measure::CreateEnvironmentHandler(HRESULT result, ICoreWebView2Environme auto controllerOptions4 = controllerOptions.query(); if (controllerOptions4) { - controllerOptions4->put_AllowHostInputProcessing(TRUE); // Allow Host Input Processing + controllerOptions4->put_AllowHostInputProcessing(FALSE); // Allow Host Input Processing } // Create Controller With Options. @@ -680,4 +680,4 @@ HRESULT Measure::FailWebView(HRESULT hr, const wchar_t* logMessage, bool resetCr } return hr; -} \ No newline at end of file +} From 591ceedb1a359aabe38d55fdca789c642e4e9dea Mon Sep 17 00:00:00 2001 From: RicardoTM05 <168048518+RicardoTM05@users.noreply.github.com> Date: Sun, 21 Dec 2025 13:24:36 -0600 Subject: [PATCH 3/6] Correctly stop webview on finalize. set put_AllowHostInputProcessing to false Turned off put_AllowHostInputProcessing due to it messing with AllowDualControl. Should be turned on once we start implementing a Non-JS AllowDualControl version. Stop WebView on Finalize to avoid crashes. --- WebView2/Plugin.cpp | 3 +-- WebView2/WebView2.cpp | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/WebView2/Plugin.cpp b/WebView2/Plugin.cpp index 248c564..0df5ff2 100644 --- a/WebView2/Plugin.cpp +++ b/WebView2/Plugin.cpp @@ -702,7 +702,6 @@ PLUGIN_EXPORT LPCWSTR CallJS(void* data, const int argc, const WCHAR* argv[]) PLUGIN_EXPORT void Finalize(void* data) { Measure* measure = (Measure*)data; - Frames* frames = (Frames*)data; - delete frames; + StopWebView2(measure); delete measure; } diff --git a/WebView2/WebView2.cpp b/WebView2/WebView2.cpp index 7e8c921..6262403 100644 --- a/WebView2/WebView2.cpp +++ b/WebView2/WebView2.cpp @@ -140,7 +140,7 @@ HRESULT Measure::CreateEnvironmentHandler(HRESULT result, ICoreWebView2Environme auto controllerOptions4 = controllerOptions.query(); if (controllerOptions4) { - controllerOptions4->put_AllowHostInputProcessing(TRUE); // Allow Host Input Processing + controllerOptions4->put_AllowHostInputProcessing(FALSE); // Allow Host Input Processing } // Create Controller With Options. @@ -680,4 +680,4 @@ HRESULT Measure::FailWebView(HRESULT hr, const wchar_t* logMessage, bool resetCr } return hr; -} \ No newline at end of file +} From fbc5044ad7e71acb6f1aa9e07da2e7c37ee0f6ff Mon Sep 17 00:00:00 2001 From: RicardoTM05 <168048518+RicardoTM05@users.noreply.github.com> Date: Mon, 22 Dec 2025 19:24:05 -0600 Subject: [PATCH 4/6] Update Plugin.h --- WebView2/Plugin.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/WebView2/Plugin.h b/WebView2/Plugin.h index ad3b113..4ae66dc 100644 --- a/WebView2/Plugin.h +++ b/WebView2/Plugin.h @@ -8,7 +8,7 @@ #include #include #include -#include +#include using namespace Microsoft::WRL; @@ -17,7 +17,7 @@ extern wil::com_ptr g_typeLib; struct Frames { - wil::com_ptr name; + wil::com_ptr frame; bool injected = false; }; @@ -65,7 +65,7 @@ struct Measure wil::com_ptr webViewEnvironment; wil::com_ptr webViewController; wil::com_ptr webView; - std::deque Measure::webViewFrames; + std::vector> Measure::webViewFrames; EventRegistrationToken webMessageToken; From e2a98e10b63eebda8b8c15230a629212bb7e3731 Mon Sep 17 00:00:00 2001 From: RicardoTM05 <168048518+RicardoTM05@users.noreply.github.com> Date: Mon, 22 Dec 2025 19:35:40 -0600 Subject: [PATCH 5/6] Update Plugin.cpp --- WebView2/Plugin.cpp | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/WebView2/Plugin.cpp b/WebView2/Plugin.cpp index 0df5ff2..d600b87 100644 --- a/WebView2/Plugin.cpp +++ b/WebView2/Plugin.cpp @@ -222,12 +222,12 @@ void UpdateAllowDualControl(Measure* measure) } // Inject AllowDualControl script into the WebView frame -void InjectAllowDualControlFrame(Measure* measure, Frames* frame) +void InjectAllowDualControlFrame(Measure* measure, Frames* webViewFrame) { - if (!frame || !frame->name) return; - + if (!webViewFrame || !webViewFrame->frame) return; + // Inject script to capture page load events for drag/move and context menu - HRESULT hr = frame->name->ExecuteScript(allowDualControlScript, + HRESULT hr = webViewFrame->frame->ExecuteScript(allowDualControlScript, Callback( [](HRESULT, LPCWSTR) -> HRESULT { return S_OK; } ).Get() @@ -235,17 +235,17 @@ void InjectAllowDualControlFrame(Measure* measure, Frames* frame) if (SUCCEEDED(hr)) { - frame->injected = true; - UpdateAllowDualControlFrame(measure, frame); + webViewFrame->injected = true; + UpdateAllowDualControlFrame(measure, webViewFrame); } } // Update AllowDualControl state in the WebView frame -void UpdateAllowDualControlFrame(Measure* measure, Frames* frame) +void UpdateAllowDualControlFrame(Measure* measure, Frames* webViewFrame) { - if (!frame || !frame->name || !frame->injected) return; + if (!webViewFrame || !webViewFrame->frame || !webViewFrame->injected) return; - frame->name->ExecuteScript( + webViewFrame->frame->ExecuteScript( measure->allowDualControl ? L"rm_SetAllowDualControl(true);" : L"rm_SetAllowDualControl(false);", @@ -432,18 +432,18 @@ PLUGIN_EXPORT void Reload(void* data, void* rm, double* /*maxValue*/) if (measure->webViewFrames.empty()) return; // Frames - for (Frames& frame : measure->webViewFrames) + for (auto& webViewFramePtr : measure->webViewFrames) { - if (!frame.name) + if (!webViewFramePtr->frame) continue; - if (!frame.injected) - { - InjectAllowDualControlFrame(measure, &frame); + if (!webViewFramePtr->injected) + { + InjectAllowDualControlFrame(measure, webViewFramePtr.get()); } else { - UpdateAllowDualControlFrame(measure, &frame); + UpdateAllowDualControlFrame(measure, webViewFramePtr.get()); } } } @@ -582,12 +582,12 @@ PLUGIN_EXPORT void ExecuteBang(void* data, LPCWSTR args) if (measure->webViewFrames.empty()) return; // Frames - for (Frames& frame : measure->webViewFrames) + for (auto& webViewFramePtr : measure->webViewFrames) { - if (!frame.name) + if (!webViewFramePtr->frame) continue; - frame.name->ExecuteScript( + webViewFramePtr->frame->ExecuteScript( param.c_str(), Callback( [](HRESULT errorCode, LPCWSTR resultObjectAsJson) -> HRESULT @@ -669,12 +669,12 @@ PLUGIN_EXPORT LPCWSTR CallJS(void* data, const int argc, const WCHAR* argv[]) if (!measure->webViewFrames.empty()) { // Frames - for (Frames& frame : measure->webViewFrames) + for (auto& webViewFramePtr : measure->webViewFrames) { - if (!frame.name) + if (!webViewFramePtr->frame) continue; - frame.name->ExecuteScript( + webViewFramePtr->frame->ExecuteScript( jsCode.c_str(), Callback( [](HRESULT errorCode, LPCWSTR resultObjectAsJson) -> HRESULT From 2d495df105f3040828969b14610b27cd1ef8405f Mon Sep 17 00:00:00 2001 From: RicardoTM05 <168048518+RicardoTM05@users.noreply.github.com> Date: Mon, 22 Dec 2025 19:47:29 -0600 Subject: [PATCH 6/6] Update WebView2.cpp --- WebView2/WebView2.cpp | 179 ++++++++++++++++++++++++++++-------------- 1 file changed, 120 insertions(+), 59 deletions(-) diff --git a/WebView2/WebView2.cpp b/WebView2/WebView2.cpp index 6262403..5a05a36 100644 --- a/WebView2/WebView2.cpp +++ b/WebView2/WebView2.cpp @@ -33,6 +33,112 @@ std::wstring NormalizeUri(const std::wstring& uri) return uri.substr(0, path_start + 1); } +void RegisterFrames(Measure* measure, ICoreWebView2Frame* rawFrame) +{ + wil::com_ptr frame = rawFrame; + + wil::com_ptr frame2; + frame->QueryInterface(IID_PPV_ARGS(&frame2)); + + wil::com_ptr frame5; + frame->QueryInterface(IID_PPV_ARGS(&frame5)); + + // Only proceed if we have valid interfaces + if (!frame2 || !frame5) return; + + // Add host object + wil::com_ptr hostObject = + Microsoft::WRL::Make(measure, g_typeLib); + + wil::unique_variant hostObjectVariant; + hostObject.query_to(&hostObjectVariant.pdispVal); + hostObjectVariant.vt = VT_DISPATCH; + + std::wstring origin = NormalizeUri(measure->currentUrl); + LPCWSTR origins = L"*"; // all-origins + + frame2->AddHostObjectToScriptWithOrigins(L"RainmeterAPI", &hostObjectVariant, 1, &origins); + + auto newFrameState = std::make_shared(); + newFrameState->frame = frame2; + newFrameState->injected = false; + + measure->webViewFrames.push_back(newFrameState); + + Frames* frameState = newFrameState.get(); + + // Inject frame ancestor to nested frames to allow framing websites. (Requires http-server). + frame2->add_NavigationStarting( + Microsoft::WRL::Callback( + [measure, origin](ICoreWebView2Frame* sender, ICoreWebView2NavigationStartingEventArgs* args) -> HRESULT + { + wil::com_ptr navigationStartArgs; + if (SUCCEEDED(args->QueryInterface(IID_PPV_ARGS(&navigationStartArgs)))) + { + navigationStartArgs->put_AdditionalAllowedFrameAncestors(origin.c_str()); + } + return S_OK; + } + ).Get(), nullptr + ); + + // Inject AllowDualControl script to frames. + frame2->add_DOMContentLoaded( + Callback( + [measure, frameState](ICoreWebView2Frame* sender, ICoreWebView2DOMContentLoadedEventArgs* args) -> HRESULT + { + frameState->injected = false; + if (measure->allowDualControl) + { + InjectAllowDualControlFrame(measure, frameState); + } + return S_OK; + } + ).Get(), nullptr + ); + + frame2->add_Destroyed( + Callback( + [measure](ICoreWebView2Frame* sender, IUnknown* args)->HRESULT + { + if (measure->isStopping) + return S_OK; + // Remove frame + auto it = std::remove_if( + measure->webViewFrames.begin(), + measure->webViewFrames.end(), + [sender](const std::shared_ptr& f) + { + // Check equality against the COM pointer inside the struct + return f->frame.get() == sender; + } + ); + if (it != measure->webViewFrames.end()) + { + measure->webViewFrames.erase(it, measure->webViewFrames.end()); + } + return S_OK; + } + ).Get(), nullptr + ); + + // Subscribe to FrameCreated on THIS frame to catch its children + wil::com_ptr frame7; + if (SUCCEEDED(frame->QueryInterface(IID_PPV_ARGS(&frame7)))) + { + frame7->add_FrameCreated( + Microsoft::WRL::Callback( + [measure](ICoreWebView2Frame* sender, ICoreWebView2FrameCreatedEventArgs* args) -> HRESULT + { + wil::com_ptr childFrame; + RETURN_IF_FAILED(args->get_Frame(&childFrame)); + // RECURSIVE CALL: Treat the child exactly like the parent + RegisterFrames(measure, childFrame.get()); + return S_OK; + }).Get(), nullptr); + } +} + // Create WebView2 environment and controller void CreateWebView2(Measure* measure) { @@ -229,6 +335,7 @@ HRESULT Measure::CreateControllerHandler(HRESULT result, ICoreWebView2Controller wil::com_ptr webView4; webView->QueryInterface(IID_PPV_ARGS(&webView4)); + if (webView4) { webView4->add_FrameCreated( Microsoft::WRL::Callback( @@ -237,61 +344,11 @@ HRESULT Measure::CreateControllerHandler(HRESULT result, ICoreWebView2Controller wil::com_ptr frame; RETURN_IF_FAILED(args->get_Frame(&frame)); - wil::com_ptr frame2; - RETURN_IF_FAILED(frame->QueryInterface(IID_PPV_ARGS(&frame2))); - - if (frame2) { - // Add host object - wil::com_ptr hostObject = - Microsoft::WRL::Make(this, g_typeLib); - - wil::unique_variant hostObjectVariant; - hostObject.query_to(&hostObjectVariant.pdispVal); - hostObjectVariant.vt = VT_DISPATCH; - - std::wstring origin = NormalizeUri(currentUrl); - LPCWSTR origins = L"*"; // all-origins - - frame2->AddHostObjectToScriptWithOrigins(L"RainmeterAPI", &hostObjectVariant, 1, &origins); - - this->webViewFrames.push_back(Frames{ frame2, false }); - Frames* frameState = &this->webViewFrames.back(); - - // Inject frame ancestor to nested frames to allow framing websites. (Requires http-server). - frame2->add_NavigationStarting( - Microsoft::WRL::Callback( - [origin](ICoreWebView2Frame* sender, ICoreWebView2NavigationStartingEventArgs* args) -> HRESULT - { - wil::com_ptr navigationStartArgs; - if (SUCCEEDED(args->QueryInterface(IID_PPV_ARGS(&navigationStartArgs)))) - { - navigationStartArgs->put_AdditionalAllowedFrameAncestors(origin.c_str()); - } - return S_OK; - - } - ).Get(), nullptr - ); - - // Inject AllowDualControl script to frames. - frame2->add_DOMContentLoaded( - Callback( - [this, frameState](ICoreWebView2Frame* sender, ICoreWebView2DOMContentLoadedEventArgs* args) -> HRESULT { - - frameState->injected = false; - - if (this->allowDualControl) - { - InjectAllowDualControlFrame(this, frameState); - } - return S_OK; - } - ).Get(), nullptr - ); - } + RegisterFrames(this, frame.get()); return S_OK; - }).Get(), nullptr + } + ).Get(), nullptr ); } @@ -316,7 +373,7 @@ HRESULT Measure::CreateControllerHandler(HRESULT result, ICoreWebView2Controller } return S_OK; } - ).Get(),nullptr + ).Get(), nullptr ); // Avoid browser from opening links on different windows and block not user requested popups @@ -398,7 +455,7 @@ HRESULT Measure::CreateControllerHandler(HRESULT result, ICoreWebView2Controller } return S_OK; } - ).Get(),nullptr + ).Get(), nullptr ); // Add SourceChanged event to detect changes in URL @@ -432,7 +489,7 @@ HRESULT Measure::CreateControllerHandler(HRESULT result, ICoreWebView2Controller } return S_OK; } - ).Get(),nullptr + ).Get(), nullptr ); // Add ContentLoading event to call action when page starts loading @@ -450,7 +507,7 @@ HRESULT Measure::CreateControllerHandler(HRESULT result, ICoreWebView2Controller } return S_OK; } - ).Get(),nullptr + ).Get(), nullptr ); // Add DOMContentLoaded event to inject AllowDualControl @@ -583,10 +640,13 @@ void StopWebView2(Measure* measure) measure->isCreationInProgress = false; - // Hide (prevents flicker) + measure->webViewFrames.clear(); + + // Close the Controller if (measure->webViewController) { measure->webViewController->put_IsVisible(FALSE); + measure->webViewController->Close(); } // Stop navigation @@ -596,8 +656,8 @@ void StopWebView2(Measure* measure) } // Release - measure->webView.reset(); measure->webViewController.reset(); + measure->webView.reset(); measure->webViewEnvironment.reset(); // Reset flags @@ -620,6 +680,7 @@ void StopWebView2(Measure* measure) { RmLog(measure->rm, LOG_NOTICE, L"WebView2: Stopped sucessfully"); } + measure->isStopping = false; }