diff --git a/Build-CPP.ps1 b/Build-CPP.ps1 index 1e001a1..19cfe87 100644 --- a/Build-CPP.ps1 +++ b/Build-CPP.ps1 @@ -1,4 +1,4 @@ -#usage -----> powershell -ExecutionPolicy Bypass -Command "& {. .\Build-CPP.ps1; Dist -major 1 -minor 2 -patch 0}" +#usage -----> powershell -ExecutionPolicy Bypass -Command "& {. .\Build-CPP.ps1; Dist -major 0 -minor 0 -patch 3}" $msbuild = "C:\Program Files\Microsoft Visual Studio\2022\Community\Msbuild\Current\Bin\MSBuild.exe" function Add-RMSkinFooter { diff --git a/README.md b/README.md index 791debb..0f6e747 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,9 @@ A powerful Rainmeter plugin that embeds Microsoft Edge WebView2 control to display web content or local HTML files directly in your Rainmeter skins. -![Version](https://img.shields.io/badge/version-1.0.0-blue) +![Version](https://img.shields.io/badge/version-0.0.3-blue) ![Platform](https://img.shields.io/badge/platform-Windows%2010%2F11-lightgrey) -![License](https://img.shields.io/badge/license-GPL--2.0-green) +![License](https://img.shields.io/badge/license-MIT-green) ## ✨ Features @@ -12,9 +12,10 @@ A powerful Rainmeter plugin that embeds Microsoft Edge WebView2 control to displ - 📄 **Local HTML Files** - Display custom HTML/CSS/JavaScript content - 🪟 **Seamless Integration** - WebView window automatically parents to skin window - 🎮 **Full Control** - Navigate, reload, go back/forward via bang commands -- 💻 **JavaScript Support** - Execute JavaScript code in the WebView +- 💻 **JavaScript Support** - Full JavaScript execution with event handling - 🎨 **Customizable** - Configure size, position, and visibility - ⚡ **Modern** - Uses Microsoft Edge WebView2 (Chromium-based) +- 🔌 **Rainmeter API Bridge** - Access Rainmeter functions from JavaScript ## 📋 Requirements @@ -27,8 +28,8 @@ A powerful Rainmeter plugin that embeds Microsoft Edge WebView2 control to displ ### Installation -1. Download the latest release from [Releases](https://github.com/nstechbytes/WebView2/releases) -2. Install the `.rmskin` package, or +1. Download the latest release `.rmskin` package +2. Double-click to install, or 3. Manually copy `WebView2.dll` to `%APPDATA%\Rainmeter\Plugins\` ### Basic Usage @@ -40,12 +41,11 @@ Update=1000 [MeasureWebView] Measure=Plugin Plugin=WebView2 -Url=https://www.google.com +URL=https://www.google.com Width=1000 Height=700 X=0 Y=0 -Visible=1 ``` ## 📖 Documentation @@ -54,7 +54,7 @@ Visible=1 | Option | Type | Default | Description | |--------|------|---------|-------------| -| `Url` | String | (empty) | URL or file path to load. Supports web URLs and local file paths | +| `URL` | String | (empty) | URL or file path to load. Supports web URLs and local file paths | | `Width` | Integer | 800 | Width of the WebView window in pixels | | `Height` | Integer | 600 | Height of the WebView window in pixels | | `X` | Integer | 0 | X position relative to skin window | @@ -65,93 +65,74 @@ Visible=1 Execute commands using `!CommandMeasure`: -#### Navigate to URL ```ini +; Navigate to URL LeftMouseUpAction=[!CommandMeasure MeasureWebView "Navigate https://example.com"] -``` -#### Reload Current Page -```ini +; Reload current page LeftMouseUpAction=[!CommandMeasure MeasureWebView "Reload"] -``` -#### Navigation Controls -```ini +; Navigation controls LeftMouseUpAction=[!CommandMeasure MeasureWebView "GoBack"] LeftMouseUpAction=[!CommandMeasure MeasureWebView "GoForward"] -``` -#### Show/Hide WebView -```ini +; Show/Hide WebView LeftMouseUpAction=[!CommandMeasure MeasureWebView "Show"] LeftMouseUpAction=[!CommandMeasure MeasureWebView "Hide"] -``` -#### Execute JavaScript -```ini -LeftMouseUpAction=[!CommandMeasure MeasureWebView "ExecuteScript alert('Hello from Rainmeter!')"] -``` +; Execute JavaScript +LeftMouseUpAction=[!CommandMeasure MeasureWebView "ExecuteScript alert('Hello!')"] -## 💡 Examples +;Open DevTools +LeftMouseUpAction=[!CommandMeasure MeasureWebView "OpenDevTools"] -### Example 1: Web Browser Skin +; SetWidth +LeftMouseUpAction=[!CommandMeasure MeasureWebView "SetWidth 500"] +; Dynamically sets the width of the WebView2 control in pixels. -```ini -[Rainmeter] -Update=1000 -BackgroundMode=2 -SolidColor=30,30,30,255 +; SetHeight +LeftMouseUpAction=[!CommandMeasure MeasureWebView "SetHeight 400"] +; Dynamically sets the height of the WebView2 control in pixels. + +; SetX +LeftMouseUpAction=[!CommandMeasure MeasureWebView "SetX 100"] +; Dynamically sets the X position of the WebView2 control relative to the skin window. + +;SetY +LeftMouseUpAction=[!CommandMeasure MeasureWebView "SetY 50"] +;Dynamically sets the Y position of the WebView2 control relative to the skin window. +``` + +## 💡 Examples -[Variables] -WebWidth=1200 -WebHeight=800 +### Example 1: Mouse Drag Test +```ini [MeasureWebView] Measure=Plugin Plugin=WebView2 -Url=https://www.rainmeter.net -Width=#WebWidth# -Height=#WebHeight# +URL=#@#mouse-drag-test.html +Width=600 +Height=400 X=0 -Y=50 -Visible=1 - -[MeterNavigate] -Meter=String -X=20 -Y=15 -FontSize=12 -FontColor=100,150,255 -Text="Go to Google" -LeftMouseUpAction=[!CommandMeasure MeasureWebView "Navigate https://www.google.com"] - -[MeterReload] -Meter=String -X=150 -Y=15 -FontSize=12 -FontColor=200,200,200 -Text="Reload" -LeftMouseUpAction=[!CommandMeasure MeasureWebView "Reload"] +Y=0 ``` -### Example 2: Local HTML Dashboard +### Example 2: Web Browser Skin ```ini -[Rainmeter] -Update=1000 - [MeasureWebView] Measure=Plugin Plugin=WebView2 -Url=#@#dashboard.html -Width=800 -Height=600 +URL=https://www.rainmeter.net +Width=1200 +Height=800 X=0 -Y=0 -Visible=1 +Y=50 ``` +### Example 3: Local HTML Dashboard + Create `@Resources\dashboard.html`: ```html @@ -164,52 +145,50 @@ Create `@Resources\dashboard.html`: color: white; padding: 40px; } - h1 { font-size: 3em; }

My Dashboard

-

Current time:

+

Time:

``` -## JavaScript API Bridge +## 🔌 JavaScript API Bridge -The WebView2 plugin automatically injects a global `rm` object into all loaded web pages, providing seamless access to Rainmeter API functions from JavaScript. +The plugin automatically injects a global `rm` object into all loaded web pages, providing seamless access to Rainmeter API functions from JavaScript. -## API Reference Tables - -### Reading Options +### Reading Options from Current Measure | Method | Parameters | Returns | Description | |--------|------------|---------|-------------| -| `rm.ReadString(option, default)` | `option` (string), `default` (string) | Promise | Read a string option from the skin | -| `rm.ReadInt(option, default)` | `option` (string), `default` (number) | Promise | Read an integer option from the skin | -| `rm.ReadDouble(option, default)` | `option` (string), `default` (number) | Promise | Read a double/float option from the skin | -| `rm.ReadFormula(option, default)` | `option` (string), `default` (number) | Promise | Read and evaluate a formula option | -| `rm.ReadPath(option, default)` | `option` (string), `default` (string) | Promise | Read a file path option from the skin | +| `rm.ReadString(option, default)` | `option` (string), `default` (string) | Promise\ | Read a string option from the current measure | +| `rm.ReadInt(option, default)` | `option` (string), `default` (number) | Promise\ | Read an integer option from the current measure | +| `rm.ReadDouble(option, default)` | `option` (string), `default` (number) | Promise\ | Read a double/float option from the current measure | +| `rm.ReadFormula(option, default)` | `option` (string), `default` (number) | Promise\ | Read and evaluate a formula option | +| `rm.ReadPath(option, default)` | `option` (string), `default` (string) | Promise\ | Read a file path option from the current measure | ### Reading from Other Sections | Method | Parameters | Returns | Description | |--------|------------|---------|-------------| -| `rm.ReadStringFromSection(section, option, default)` | `section` (string), `option` (string), `default` (string) | Promise | Read a string from another section/measure | -| `rm.ReadIntFromSection(section, option, default)` | `section` (string), `option` (string), `default` (number) | Promise | Read an integer from another section/measure | -| `rm.ReadDoubleFromSection(section, option, default)` | `section` (string), `option` (string), `default` (number) | Promise | Read a double from another section/measure | -| `rm.ReadFormulaFromSection(section, option, default)` | `section` (string), `option` (string), `default` (number) | Promise | Read and evaluate a formula from another section | +| `rm.ReadStringFromSection(section, option, default)` | `section` (string), `option` (string), `default` (string) | Promise\ | Read a string from another section/measure | +| `rm.ReadIntFromSection(section, option, default)` | `section` (string), `option` (string), `default` (number) | Promise\ | Read an integer from another section/measure | +| `rm.ReadDoubleFromSection(section, option, default)` | `section` (string), `option` (string), `default` (number) | Promise\ | Read a double from another section/measure | +| `rm.ReadFormulaFromSection(section, option, default)` | `section` (string), `option` (string), `default` (number) | Promise\ | Read and evaluate a formula from another section | ### Utility Functions | Method | Parameters | Returns | Description | |--------|------------|---------|-------------| -| `rm.ReplaceVariables(text)` | `text` (string) | Promise | Replace Rainmeter variables in text (e.g., `#CURRENTCONFIG#`) | -| `rm.PathToAbsolute(path)` | `path` (string) | Promise | Convert relative path to absolute path | +| `rm.ReplaceVariables(text)` | `text` (string) | Promise\ | Replace Rainmeter variables in text (e.g., `#CURRENTCONFIG#`) | +| `rm.PathToAbsolute(path)` | `path` (string) | Promise\ | Convert relative path to absolute path | | `rm.Execute(command)` | `command` (string) | void | Execute a Rainmeter bang command | | `rm.Log(message, level)` | `message` (string), `level` (string) | void | Log a message to Rainmeter log. Levels: `'Notice'`, `'Warning'`, `'Error'`, `'Debug'` | @@ -217,62 +196,67 @@ The WebView2 plugin automatically injects a global `rm` object into all loaded w | Property | Returns | Description | |----------|---------|-------------| -| `rm.MeasureName` | Promise | Get the name of the current measure | -| `rm.SkinName` | Promise | Get the name of the current skin | -| `rm.SkinWindowHandle` | Promise | Get the window handle of the skin | -| `rm.SettingsFile` | Promise | Get the path to Rainmeter settings file | +| `rm.MeasureName` | Promise\ | Get the name of the current measure | +| `rm.SkinName` | Promise\ | Get the name of the current skin | +| `rm.SkinWindowHandle` | Promise\ | Get the window handle of the skin | +| `rm.SettingsFile` | Promise\ | Get the path to Rainmeter settings file | -## Usage Examples +### Usage Examples #### Reading Options ```javascript // Read string option -const url = await rm.ReadString('Url', 'default'); +const url = await rm.ReadString('URL', 'https://default.com'); // Read integer option const width = await rm.ReadInt('Width', 800); // Read double/float option -const height = await rm.ReadDouble('Height', 600.0); +const opacity = await rm.ReadDouble('Opacity', 1.0); // Read formula option -const value = await rm.ReadFormula('SomeFormula', 0); +const calculated = await rm.ReadFormula('MyFormula', 0); // Read path option -const path = await rm.ReadPath('FilePath', ''); +const filePath = await rm.ReadPath('DataFile', ''); ``` #### Reading from Other Sections ```javascript -// Read string from another section -const value = await rm.ReadStringFromSection('MeasureName', 'Option', 'default'); +// Read string from another measure +const cpuValue = await rm.ReadStringFromSection('MeasureCPU', 'String', '0%'); -// Read int from another section -const num = await rm.ReadIntFromSection('MeasureName', 'Option', 0); +// Read integer from another section +const memoryUsage = await rm.ReadIntFromSection('MeasureRAM', 'Value', 0); // Read double from another section -const dbl = await rm.ReadDoubleFromSection('MeasureName', 'Option', 0.0); +const temperature = await rm.ReadDoubleFromSection('MeasureTemp', 'Value', 0.0); // Read formula from another section -const formula = await rm.ReadFormulaFromSection('MeasureName', 'Option', 0.0); +const result = await rm.ReadFormulaFromSection('MeasureCalc', 'Formula', 0.0); ``` #### Utility Functions ```javascript // Replace Rainmeter variables -const replaced = await rm.ReplaceVariables('#CURRENTCONFIG#'); +const currentPath = await rm.ReplaceVariables('#CURRENTPATH#'); +const skinPath = await rm.ReplaceVariables('#@#'); // Convert relative path to absolute -const absolutePath = await rm.PathToAbsolute('#@#file.txt'); +const absolutePath = await rm.PathToAbsolute('#@#data.json'); -// Execute Rainmeter bang +// Execute Rainmeter bang commands rm.Execute('[!SetVariable MyVar "Hello"]'); +rm.Execute('[!UpdateMeter *][!Redraw]'); -// Log message to Rainmeter log -rm.Log('Message from JavaScript', 'Notice'); // Levels: Notice, Warning, Error, Debug +// Log messages to Rainmeter log +rm.Log('JavaScript initialized', 'Notice'); +rm.Log('Warning: Low memory', 'Warning'); +rm.Log('Error occurred', 'Error'); +rm.Log('Debug info', 'Debug'); ``` #### Information Properties @@ -280,9 +264,11 @@ rm.Log('Message from JavaScript', 'Notice'); // Levels: Notice, Warning, Error, ```javascript // Get measure name const measureName = await rm.MeasureName; +console.log('Measure:', measureName); // Get skin name const skinName = await rm.SkinName; +console.log('Skin:', skinName); // Get skin window handle const handle = await rm.SkinWindowHandle; @@ -310,10 +296,14 @@ const settingsFile = await rm.SettingsFile; // Read values from Rainmeter const width = await rm.ReadInt('Width', 800); const skinName = await rm.SkinName; + const measureName = await rm.MeasureName; // Display results - document.getElementById('output').innerHTML = - `Skin: ${skinName}
Width: ${width}px`; + document.getElementById('output').innerHTML = ` + Skin: ${skinName}
+ Measure: ${measureName}
+ Width: ${width}px + `; // Log to Rainmeter rm.Log('Updated from JavaScript', 'Notice'); @@ -322,6 +312,7 @@ const settingsFile = await rm.SettingsFile; rm.Execute('[!UpdateMeter *][!Redraw]'); } catch (error) { console.error('Error:', error); + rm.Log('Error: ' + error.message, 'Error'); } } @@ -329,13 +320,14 @@ const settingsFile = await rm.SettingsFile; ``` -### Notes +### Important Notes + +- ✅ All read methods return **Promises** and should be used with `await` or `.then()` +- ✅ Execute and Log methods are **fire-and-forget** (no return value) +- ✅ Property getters (MeasureName, SkinName, etc.) also return **Promises** +- ✅ The `rm` object is **automatically available** in all pages loaded by the plugin +- ✅ No additional setup or imports required -- All read methods return Promises and should be used with `await` or `.then()` -- Execute and Log methods are fire-and-forget (no return value) -- Property getters (MeasureName, SkinName, etc.) also return Promises -- The `rm` object is automatically available in all pages loaded by the plugin -- No additional setup or imports required ## 🔧 Building from Source @@ -345,82 +337,33 @@ const settingsFile = await rm.SettingsFile; - Windows 10/11 SDK - NuGet Package Manager -### Build Steps - -1. Clone the repository: - ```bash - git clone https://github.com/nstechbytes/WebView2.git - cd WebView2 - ``` - -2. Open `WebView2-Plugin.sln` in Visual Studio - -3. Restore NuGet packages (right-click solution → Restore NuGet Packages) - -4. Build the solution (Ctrl+Shift+B) - -5. Find the compiled DLLs in: - - `WebView2\x64\Release\WebView2.dll` (64-bit) - - `WebView2\x32\Release\WebView2.dll` (32-bit) - ### Build via PowerShell ```powershell -powershell -ExecutionPolicy Bypass -Command "& { . .\Build-CPP.ps1; Dist -major 1 -minor 0 -patch 0 }" +powershell -ExecutionPolicy Bypass -Command "& { . .\Build-CPP.ps1; Dist -major 0 -minor 0 -patch 3 }" ``` This creates: - Plugin DLLs in `dist\` folder - Complete `.rmskin` package for distribution -## 🐛 Troubleshooting - -### WebView2 doesn't appear -- Ensure WebView2 Runtime is installed -- Check Rainmeter log for error messages -- Verify the skin window is visible and has appropriate dimensions - -### File paths not loading -- Use `#@#` for @Resources folder: `Url=#@#mypage.html` -- Or use absolute paths: `Url=C:\MyFolder\page.html` -- The plugin automatically converts file paths to `file:///` format - -### Access Denied Error -- The plugin uses TEMP directory for WebView2 data -- Ensure you have write permissions to `%TEMP%\RainmeterWebView2` - -## 📝 Technical Details - -- **WebView2 SDK**: Microsoft.Web.WebView2 (v1.0.2792.45) -- **Runtime**: Uses Windows Implementation Library (WIL) -- **Architecture**: Supports both x86 and x64 -- **Language**: C++17 -- **User Data**: Stored in `%TEMP%\RainmeterWebView2` - ## 🤝 Contributing Contributions are welcome! Please feel free to submit a Pull Request. -1. Fork the repository -2. Create your feature branch (`git checkout -b feature/AmazingFeature`) -3. Commit your changes (`git commit -m 'Add some AmazingFeature'`) -4. Push to the branch (`git push origin feature/AmazingFeature`) -5. Open a Pull Request - ## 📄 License -This project is licensed under the GPL-2.0 License - see the [LICENSE](LICENSE) file for details. +This project is licensed under the MIT License. ## 🙏 Acknowledgments - Microsoft Edge WebView2 team for the excellent SDK - Rainmeter community for inspiration and support -- ModernSearchBar plugin for reference implementation ## 📧 Contact - **Author**: nstechbytes -- **GitHub**: [https://github.com/nstechbytes/WebView2](https://github.com/nstechbytes/WebView2) +- **GitHub**: [WebView2 Plugin](https://github.com/nstechbytes/WebView2) ## 🔗 Related Links @@ -430,4 +373,4 @@ This project is licensed under the GPL-2.0 License - see the [LICENSE](LICENSE) --- -**Made with ❤️ for the Rainmeter community** \ No newline at end of file +**Made with ❤️ by nstechbytes** \ No newline at end of file diff --git a/Resources/Skins/WebView2/@Resources/api-demo.html b/Resources/Skins/WebView2/@Resources/api-demo.html index 125c116..394a8e5 100644 --- a/Resources/Skins/WebView2/@Resources/api-demo.html +++ b/Resources/Skins/WebView2/@Resources/api-demo.html @@ -205,12 +205,12 @@

Interactive Test

+ + diff --git a/Resources/Skins/WebView2/APIDemo.ini b/Resources/Skins/WebView2/APIDemo.ini index 8faab8a..930c942 100644 --- a/Resources/Skins/WebView2/APIDemo.ini +++ b/Resources/Skins/WebView2/APIDemo.ini @@ -5,7 +5,7 @@ SolidColor=0,0,0,1 [Metadata] Name=WebView2 API Bridge Demo -Author=NSTechBytes +Author=nstechbytes Information=Demonstrates Rainmeter API bridge in JavaScript Version=0.0.3 License=MIT diff --git a/Resources/Skins/WebView2/EventTest.ini b/Resources/Skins/WebView2/EventTest.ini index f7d3350..337c6b2 100644 --- a/Resources/Skins/WebView2/EventTest.ini +++ b/Resources/Skins/WebView2/EventTest.ini @@ -5,7 +5,7 @@ SolidColor=0,0,0,1 [Metadata] Name=Event Handler Test -Author=NSTechBytes +Author=nstechbytes Information=Test page to verify mouse and keyboard events work in WebView2 Version=0.0.3 License=MIT diff --git a/Resources/Skins/WebView2/MouseDragTest.ini b/Resources/Skins/WebView2/MouseDragTest.ini new file mode 100644 index 0000000..1e07c58 --- /dev/null +++ b/Resources/Skins/WebView2/MouseDragTest.ini @@ -0,0 +1,29 @@ +[Rainmeter] +Update=1000 +DynamicWindowSize=1 +AccurateText=1 + +[Metadata] +Name=Mouse Drag Test +Information=Test skin to verify mouse drag events work in WebView2 +Version=0.0.3 +License=MIT +Author=nstechbytes + +[Variables] +Width=600 +Height=400 + +[MeasureWebView] +Measure=Plugin +Plugin=WebView2 +X=0 +Y=0 +Width=#Width# +Height=#Height# +URL=#@#mouse-drag-test.html + +[MeterBackground] +Meter=Shape +Shape=Rectangle 0,0,#Width#,#Height# | Fill Color 20,20,30,255 | StrokeWidth 0 +DynamicVariables=1 diff --git a/WebView2/HostObject.idl b/WebView2/HostObject.idl new file mode 100644 index 0000000..d553246 --- /dev/null +++ b/WebView2/HostObject.idl @@ -0,0 +1,46 @@ +// Copyright (C) 2024 WebView2 Plugin. All rights reserved. + +import "oaidl.idl"; +import "ocidl.idl"; + +[uuid(a1b2c3d4-e5f6-4a5b-8c9d-0e1f2a3b4c5d), version(1.0)] +library WebView2Library +{ + //! [HostObjectRmAPIInterface] + [uuid(b2c3d4e5-f6a7-4b5c-9d0e-1f2a3b4c5d6e), object, local] + interface IHostObjectRmAPI : IUnknown + { + // Basic option reading + HRESULT ReadString([in] BSTR option, [in] BSTR defaultValue, [out, retval] BSTR* result); + HRESULT ReadInt([in] BSTR option, [in] int defaultValue, [out, retval] int* result); + HRESULT ReadDouble([in] BSTR option, [in] double defaultValue, [out, retval] double* result); + HRESULT ReadFormula([in] BSTR option, [in] double defaultValue, [out, retval] double* result); + HRESULT ReadPath([in] BSTR option, [in] BSTR defaultValue, [out, retval] BSTR* result); + + // Section reading + HRESULT ReadStringFromSection([in] BSTR section, [in] BSTR option, [in] BSTR defaultValue, [out, retval] BSTR* result); + HRESULT ReadIntFromSection([in] BSTR section, [in] BSTR option, [in] int defaultValue, [out, retval] int* result); + HRESULT ReadDoubleFromSection([in] BSTR section, [in] BSTR option, [in] double defaultValue, [out, retval] double* result); + HRESULT ReadFormulaFromSection([in] BSTR section, [in] BSTR option, [in] double defaultValue, [out, retval] double* result); + + // Utility functions + HRESULT ReplaceVariables([in] BSTR text, [out, retval] BSTR* result); + HRESULT PathToAbsolute([in] BSTR path, [out, retval] BSTR* result); + HRESULT Execute([in] BSTR command); + HRESULT Log([in] BSTR message, [in] BSTR level); + + // Properties + [propget] HRESULT MeasureName([out, retval] BSTR* result); + [propget] HRESULT SkinName([out, retval] BSTR* result); + [propget] HRESULT SkinWindowHandle([out, retval] BSTR* result); + [propget] HRESULT SettingsFile([out, retval] BSTR* result); + }; + //! [HostObjectRmAPIInterface] + + [uuid(c3d4e5f6-a7b8-4c5d-0e1f-2a3b4c5d6e7f)] + coclass HostObjectRmAPI + { + [default] interface IHostObjectRmAPI; + interface IDispatch; + }; +}; diff --git a/WebView2/HostObjectRmAPI.cpp b/WebView2/HostObjectRmAPI.cpp new file mode 100644 index 0000000..295e217 --- /dev/null +++ b/WebView2/HostObjectRmAPI.cpp @@ -0,0 +1,240 @@ +// Copyright (C) 2024 WebView2 Plugin. All rights reserved. + +#include "HostObjectRmAPI.h" +#include "Plugin.h" + +HostObjectRmAPI::HostObjectRmAPI(Measure* m, wil::com_ptr tLib) + : measure(m), rm(m->rm), skin(m->skin), typeLib(tLib) +{ +} + +// Basic option reading +STDMETHODIMP HostObjectRmAPI::ReadString(BSTR option, BSTR defaultValue, BSTR* result) +{ + if (!option || !result || !rm) + return E_INVALIDARG; + + LPCWSTR value = RmReadString(rm, option, defaultValue ? defaultValue : L"", TRUE); + *result = SysAllocString(value ? value : L""); + return S_OK; +} + +STDMETHODIMP HostObjectRmAPI::ReadInt(BSTR option, int defaultValue, int* result) +{ + if (!option || !result || !rm) + return E_INVALIDARG; + + double value = RmReadFormula(rm, option, defaultValue); + *result = static_cast(value); + return S_OK; +} + +STDMETHODIMP HostObjectRmAPI::ReadDouble(BSTR option, double defaultValue, double* result) +{ + if (!option || !result || !rm) + return E_INVALIDARG; + + *result = RmReadFormula(rm, option, defaultValue); + return S_OK; +} + +STDMETHODIMP HostObjectRmAPI::ReadFormula(BSTR option, double defaultValue, double* result) +{ + if (!option || !result || !rm) + return E_INVALIDARG; + + *result = RmReadFormula(rm, option, defaultValue); + return S_OK; +} + +STDMETHODIMP HostObjectRmAPI::ReadPath(BSTR option, BSTR defaultValue, BSTR* result) +{ + if (!option || !result || !rm) + return E_INVALIDARG; + + LPCWSTR value = RmReadString(rm, option, defaultValue ? defaultValue : L"", TRUE); + if (value) + { + LPCWSTR absolutePath = RmPathToAbsolute(rm, value); + *result = SysAllocString(absolutePath ? absolutePath : value); + } + else + { + *result = SysAllocString(L""); + } + return S_OK; +} + +// Section reading +STDMETHODIMP HostObjectRmAPI::ReadStringFromSection(BSTR section, BSTR option, BSTR defaultValue, BSTR* result) +{ + if (!section || !option || !result || !rm) + return E_INVALIDARG; + + // Note: Rainmeter API doesn't have direct section reading, so we'd need to implement this differently + // For now, return empty string + *result = SysAllocString(defaultValue ? defaultValue : L""); + return S_OK; +} + +STDMETHODIMP HostObjectRmAPI::ReadIntFromSection(BSTR section, BSTR option, int defaultValue, int* result) +{ + if (!section || !option || !result || !rm) + return E_INVALIDARG; + + *result = defaultValue; + return S_OK; +} + +STDMETHODIMP HostObjectRmAPI::ReadDoubleFromSection(BSTR section, BSTR option, double defaultValue, double* result) +{ + if (!section || !option || !result || !rm) + return E_INVALIDARG; + + *result = defaultValue; + return S_OK; +} + +STDMETHODIMP HostObjectRmAPI::ReadFormulaFromSection(BSTR section, BSTR option, double defaultValue, double* result) +{ + if (!section || !option || !result || !rm) + return E_INVALIDARG; + + *result = defaultValue; + return S_OK; +} + +// Utility functions +STDMETHODIMP HostObjectRmAPI::ReplaceVariables(BSTR text, BSTR* result) +{ + if (!text || !result || !rm) + return E_INVALIDARG; + + LPCWSTR value = RmReplaceVariables(rm, text); + *result = SysAllocString(value ? value : L""); + return S_OK; +} + +STDMETHODIMP HostObjectRmAPI::PathToAbsolute(BSTR path, BSTR* result) +{ + if (!path || !result || !rm) + return E_INVALIDARG; + + LPCWSTR value = RmPathToAbsolute(rm, path); + *result = SysAllocString(value ? value : L""); + return S_OK; +} + +STDMETHODIMP HostObjectRmAPI::Execute(BSTR command) +{ + if (!command || !skin) + return E_INVALIDARG; + + RmExecute(skin, command); + return S_OK; +} + +STDMETHODIMP HostObjectRmAPI::Log(BSTR message, BSTR level) +{ + if (!message || !rm) + return E_INVALIDARG; + + int logLevel = LOG_NOTICE; + if (level) + { + std::wstring levelStr(level); + if (levelStr == L"Error") logLevel = LOG_ERROR; + else if (levelStr == L"Warning") logLevel = LOG_WARNING; + else if (levelStr == L"Debug") logLevel = LOG_DEBUG; + else if (levelStr == L"Notice") logLevel = LOG_NOTICE; + } + + RmLog(rm, logLevel, message); + return S_OK; +} + +// Properties +STDMETHODIMP HostObjectRmAPI::get_MeasureName(BSTR* result) +{ + if (!result || !measure) + return E_INVALIDARG; + + *result = SysAllocString(measure->measureName ? measure->measureName : L""); + return S_OK; +} + +STDMETHODIMP HostObjectRmAPI::get_SkinName(BSTR* result) +{ + if (!result || !rm) + return E_INVALIDARG; + + LPCWSTR skinName = RmGetSkinName(rm); + *result = SysAllocString(skinName ? skinName : L""); + return S_OK; +} + +STDMETHODIMP HostObjectRmAPI::get_SkinWindowHandle(BSTR* result) +{ + if (!result || !measure) + return E_INVALIDARG; + + std::wstring handle = std::to_wstring(reinterpret_cast(measure->skinWindow)); + *result = SysAllocString(handle.c_str()); + return S_OK; +} + +STDMETHODIMP HostObjectRmAPI::get_SettingsFile(BSTR* result) +{ + if (!result) + return E_INVALIDARG; + + LPCWSTR settingsFile = RmGetSettingsFile(); + *result = SysAllocString(settingsFile ? settingsFile : L""); + return S_OK; +} + +// IDispatch implementation +STDMETHODIMP HostObjectRmAPI::GetTypeInfoCount(UINT* pctinfo) +{ + if (!pctinfo) + return E_INVALIDARG; + + *pctinfo = 1; + return S_OK; +} + +STDMETHODIMP HostObjectRmAPI::GetTypeInfo(UINT iTInfo, LCID lcid, ITypeInfo** ppTInfo) +{ + if (!ppTInfo) + return E_INVALIDARG; + + if (iTInfo != 0 || !typeLib) + return TYPE_E_ELEMENTNOTFOUND; + + return typeLib->GetTypeInfoOfGuid(__uuidof(IHostObjectRmAPI), ppTInfo); +} + +STDMETHODIMP HostObjectRmAPI::GetIDsOfNames(REFIID riid, LPOLESTR* rgszNames, + UINT cNames, LCID lcid, DISPID* rgDispId) +{ + wil::com_ptr typeInfo; + HRESULT hr = GetTypeInfo(0, lcid, &typeInfo); + if (FAILED(hr)) + return hr; + + return typeInfo->GetIDsOfNames(rgszNames, cNames, rgDispId); +} + +STDMETHODIMP HostObjectRmAPI::Invoke(DISPID dispIdMember, REFIID riid, LCID lcid, + WORD wFlags, DISPPARAMS* pDispParams, + VARIANT* pVarResult, EXCEPINFO* pExcepInfo, + UINT* puArgErr) +{ + wil::com_ptr typeInfo; + HRESULT hr = GetTypeInfo(0, lcid, &typeInfo); + if (FAILED(hr)) + return hr; + + return typeInfo->Invoke(this, dispIdMember, wFlags, pDispParams, + pVarResult, pExcepInfo, puArgErr); +} diff --git a/WebView2/HostObjectRmAPI.h b/WebView2/HostObjectRmAPI.h new file mode 100644 index 0000000..6ec0f37 --- /dev/null +++ b/WebView2/HostObjectRmAPI.h @@ -0,0 +1,57 @@ +// Copyright (C) 2024 WebView2 Plugin. All rights reserved. + +#pragma once +#include "HostObject_h.h" +#include "../API/RainmeterAPI.h" +#include +#include + +struct Measure; + +class HostObjectRmAPI : public Microsoft::WRL::RuntimeClass< + Microsoft::WRL::RuntimeClassFlags, + IHostObjectRmAPI, IDispatch> +{ +public: + HostObjectRmAPI(Measure* measure, wil::com_ptr typeLib); + + // IHostObjectRmAPI methods - Basic option reading + STDMETHODIMP ReadString(BSTR option, BSTR defaultValue, BSTR* result) override; + STDMETHODIMP ReadInt(BSTR option, int defaultValue, int* result) override; + STDMETHODIMP ReadDouble(BSTR option, double defaultValue, double* result) override; + STDMETHODIMP ReadFormula(BSTR option, double defaultValue, double* result) override; + STDMETHODIMP ReadPath(BSTR option, BSTR defaultValue, BSTR* result) override; + + // Section reading + STDMETHODIMP ReadStringFromSection(BSTR section, BSTR option, BSTR defaultValue, BSTR* result) override; + STDMETHODIMP ReadIntFromSection(BSTR section, BSTR option, int defaultValue, int* result) override; + STDMETHODIMP ReadDoubleFromSection(BSTR section, BSTR option, double defaultValue, double* result) override; + STDMETHODIMP ReadFormulaFromSection(BSTR section, BSTR option, double defaultValue, double* result) override; + + // Utility functions + STDMETHODIMP ReplaceVariables(BSTR text, BSTR* result) override; + STDMETHODIMP PathToAbsolute(BSTR path, BSTR* result) override; + STDMETHODIMP Execute(BSTR command) override; + STDMETHODIMP Log(BSTR message, BSTR level) override; + + // Properties + STDMETHODIMP get_MeasureName(BSTR* result) override; + STDMETHODIMP get_SkinName(BSTR* result) override; + STDMETHODIMP get_SkinWindowHandle(BSTR* result) override; + STDMETHODIMP get_SettingsFile(BSTR* result) override; + + // IDispatch methods + STDMETHODIMP GetTypeInfoCount(UINT* pctinfo) override; + STDMETHODIMP GetTypeInfo(UINT iTInfo, LCID lcid, ITypeInfo** ppTInfo) override; + STDMETHODIMP GetIDsOfNames(REFIID riid, LPOLESTR* rgszNames, UINT cNames, + LCID lcid, DISPID* rgDispId) override; + STDMETHODIMP Invoke(DISPID dispIdMember, REFIID riid, LCID lcid, WORD wFlags, + DISPPARAMS* pDispParams, VARIANT* pVarResult, + EXCEPINFO* pExcepInfo, UINT* puArgErr) override; + +private: + Measure* measure; + void* rm; + void* skin; + wil::com_ptr typeLib; +}; diff --git a/WebView2/Plugin.cpp b/WebView2/Plugin.cpp index 71bd572..8cb28bb 100644 --- a/WebView2/Plugin.cpp +++ b/WebView2/Plugin.cpp @@ -7,9 +7,49 @@ // Global COM initialization tracking static bool g_comInitialized = false; +// Global TypeLib for COM objects +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; +} + // Measure constructor Measure::Measure() : rm(nullptr), skin(nullptr), skinWindow(nullptr), - webViewWindow(nullptr), measureName(nullptr), + measureName(nullptr), width(800), height(600), x(0), y(0), visible(true), initialized(false), webMessageToken{} { @@ -27,37 +67,6 @@ Measure::Measure() : rm(nullptr), skin(nullptr), skinWindow(nullptr), // Measure destructor Measure::~Measure() { - // Proper cleanup sequence to prevent crashes - - // 1. Remove event handlers first - if (webView && webMessageToken.value != 0) - { - webView->remove_WebMessageReceived(webMessageToken); - webMessageToken = {}; - } - - // 2. Close and release WebView2 controller - if (webViewController) - { - webViewController->Close(); - webViewController.reset(); // Explicit release - } - - // 3. Release WebView COM pointer - if (webView) - { - webView.reset(); // Explicit release - } - - // 4. Small delay to allow async cleanup - Sleep(50); - - // 5. Destroy window last - if (webViewWindow && IsWindow(webViewWindow)) - { - DestroyWindow(webViewWindow); - webViewWindow = nullptr; - } } // Rainmeter Plugin Exports @@ -122,39 +131,9 @@ PLUGIN_EXPORT void Reload(void* data, void* rm, double* maxValue) // Read visibility measure->visible = RmReadInt(rm, L"Visible", 1) != 0; - // Create WebView2 if not already created - if (!measure->initialized) - { - CreateWebView2(measure); - } - else - { - // Update existing WebView - if (measure->webView && !measure->url.empty()) - { - measure->webView->Navigate(measure->url.c_str()); - } - - // Update window position and size - if (measure->webViewWindow) - { - SetWindowPos( - measure->webViewWindow, - nullptr, - measure->x, measure->y, - measure->width, measure->height, - SWP_NOZORDER | SWP_NOACTIVATE - ); - - ShowWindow(measure->webViewWindow, measure->visible ? SW_SHOW : SW_HIDE); - - // Update WebView2 controller visibility - if (measure->webViewController) - { - measure->webViewController->put_IsVisible(measure->visible ? TRUE : FALSE); - } - } - } + // Always create fresh WebView2 instance on every Reload + // This matches the stable PluginWebView-main pattern and prevents race conditions + CreateWebView2(measure); } PLUGIN_EXPORT double Update(void* data) @@ -217,30 +196,22 @@ PLUGIN_EXPORT void ExecuteBang(void* data, LPCWSTR args) } else if (_wcsicmp(action.c_str(), L"Show") == 0) { - if (measure->webViewWindow) + measure->visible = true; + + // Make WebView2 controller visible + if (measure->webViewController) { - ShowWindow(measure->webViewWindow, SW_SHOW); - measure->visible = true; - - // Also make WebView2 controller visible - if (measure->webViewController) - { - measure->webViewController->put_IsVisible(TRUE); - } + measure->webViewController->put_IsVisible(TRUE); } } else if (_wcsicmp(action.c_str(), L"Hide") == 0) { - if (measure->webViewWindow) + measure->visible = false; + + // Hide WebView2 controller + if (measure->webViewController) { - ShowWindow(measure->webViewWindow, SW_HIDE); - measure->visible = false; - - // Also hide WebView2 controller - if (measure->webViewController) - { - measure->webViewController->put_IsVisible(FALSE); - } + measure->webViewController->put_IsVisible(FALSE); } } else if (_wcsicmp(action.c_str(), L"ExecuteScript") == 0) @@ -258,6 +229,105 @@ PLUGIN_EXPORT void ExecuteBang(void* data, LPCWSTR args) ); } } + else if (_wcsicmp(action.c_str(), L"SetWidth") == 0) + { + if (!param.empty()) + { + measure->width = _wtoi(param.c_str()); + + // Update WebView2 bounds + if (measure->webViewController) + { + RECT bounds; + GetClientRect(measure->skinWindow, &bounds); + bounds.left = measure->x; + bounds.top = measure->y; + bounds.right = measure->x + measure->width; + if (measure->height > 0) + { + bounds.bottom = measure->y + measure->height; + } + measure->webViewController->put_Bounds(bounds); + } + } + } + else if (_wcsicmp(action.c_str(), L"SetHeight") == 0) + { + if (!param.empty()) + { + measure->height = _wtoi(param.c_str()); + + // Update WebView2 bounds + if (measure->webViewController) + { + RECT bounds; + GetClientRect(measure->skinWindow, &bounds); + bounds.left = measure->x; + bounds.top = measure->y; + if (measure->width > 0) + { + bounds.right = measure->x + measure->width; + } + bounds.bottom = measure->y + measure->height; + measure->webViewController->put_Bounds(bounds); + } + } + } + else if (_wcsicmp(action.c_str(), L"SetX") == 0) + { + if (!param.empty()) + { + measure->x = _wtoi(param.c_str()); + + // Update WebView2 bounds + if (measure->webViewController) + { + RECT bounds; + GetClientRect(measure->skinWindow, &bounds); + bounds.left = measure->x; + bounds.top = measure->y; + if (measure->width > 0) + { + bounds.right = measure->x + measure->width; + } + if (measure->height > 0) + { + bounds.bottom = measure->y + measure->height; + } + measure->webViewController->put_Bounds(bounds); + } + } + } + else if (_wcsicmp(action.c_str(), L"SetY") == 0) + { + if (!param.empty()) + { + measure->y = _wtoi(param.c_str()); + + // Update WebView2 bounds + if (measure->webViewController) + { + RECT bounds; + GetClientRect(measure->skinWindow, &bounds); + bounds.left = measure->x; + bounds.top = measure->y; + if (measure->width > 0) + { + bounds.right = measure->x + measure->width; + } + if (measure->height > 0) + { + bounds.bottom = measure->y + measure->height; + } + measure->webViewController->put_Bounds(bounds); + } + } + } + else if (_wcsicmp(action.c_str(), L"OpenDevTools") == 0) + { + measure->webView->OpenDevToolsWindow(); + } + } PLUGIN_EXPORT void Finalize(void* data) diff --git a/WebView2/Plugin.h b/WebView2/Plugin.h index 9aa2782..0d96fea 100644 --- a/WebView2/Plugin.h +++ b/WebView2/Plugin.h @@ -8,13 +8,15 @@ using namespace Microsoft::WRL; +// Global TypeLib for COM objects +extern wil::com_ptr g_typeLib; + // Measure structure containing WebView2 state struct Measure { void* rm; void* skin; HWND skinWindow; - HWND webViewWindow; LPCWSTR measureName; std::wstring url; @@ -31,11 +33,12 @@ struct Measure Measure(); ~Measure(); + + // Member callback functions for WebView2 creation + HRESULT CreateEnvironmentHandler(HRESULT result, ICoreWebView2Environment* env); + HRESULT CreateControllerHandler(HRESULT result, ICoreWebView2Controller* controller); }; // WebView2 functions void CreateWebView2(Measure* measure); -void RegisterWebViewWindowClass(); -void InjectJavaScriptBridge(Measure* measure); -void HandleWebMessage(Measure* measure, LPCWSTR message); -LRESULT CALLBACK WebViewWindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam); + diff --git a/WebView2/WebView2.cpp b/WebView2/WebView2.cpp index 55537e0..54b2a8b 100644 --- a/WebView2/WebView2.cpp +++ b/WebView2/WebView2.cpp @@ -1,553 +1,157 @@ #include "Plugin.h" +#include "HostObjectRmAPI.h" #include "../API/RainmeterAPI.h" -#include -#include -// Window class registration flag -static bool g_windowClassRegistered = false; - -// Window procedure for WebView2 host window -LRESULT CALLBACK WebViewWindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) +// Create WebView2 environment and controller +void CreateWebView2(Measure* measure) { - switch (uMsg) + if (!measure || !measure->skinWindow) { - case WM_SIZE: - { - // Get measure from window user data - Measure* measure = reinterpret_cast(GetWindowLongPtr(hwnd, GWLP_USERDATA)); - if (measure && measure->webViewController) - { - RECT bounds; - GetClientRect(hwnd, &bounds); - measure->webViewController->put_Bounds(bounds); - } - return 0; - } - - case WM_DESTROY: - return 0; - - default: - return DefWindowProc(hwnd, uMsg, wParam, lParam); - } -} - -// Register window class for WebView2 host -void RegisterWebViewWindowClass() -{ - if (g_windowClassRegistered) + if (measure && measure->rm) + RmLog(measure->rm, LOG_ERROR, L"WebView2: Invalid measure or skin window"); return; - - WNDCLASSEX wc = { 0 }; - wc.cbSize = sizeof(WNDCLASSEX); - wc.lpfnWndProc = WebViewWindowProc; - wc.hInstance = GetModuleHandle(nullptr); - wc.lpszClassName = L"RainmeterWebView2Host"; - wc.hCursor = LoadCursor(nullptr, IDC_ARROW); - - if (RegisterClassEx(&wc)) - { - g_windowClassRegistered = true; } -} - -// Inject JavaScript bridge code -void InjectJavaScriptBridge(Measure* measure) -{ - if (!measure || !measure->webView) - return; - // JavaScript bridge code - const wchar_t* bridgeScript = LR"( -(function() { - // Create Rainmeter API object - window.rm = { - _messageId: 0, - _pendingCalls: {}, - - // Internal: Send message to C++ - _sendMessage: function(method, args) { - return new Promise((resolve, reject) => { - const id = ++this._messageId; - this._pendingCalls[id] = { resolve, reject }; - - const message = { - type: 'rainmeter_api', - method: method, - args: args || [], - id: id - }; - - window.chrome.webview.postMessage(JSON.stringify(message)); - - // Timeout after 5 seconds - setTimeout(() => { - if (this._pendingCalls[id]) { - delete this._pendingCalls[id]; - reject(new Error('Request timeout')); - } - }, 5000); - }); - }, - - // Internal: Handle response from C++ - _handleResponse: function(response) { - const call = this._pendingCalls[response.id]; - if (call) { - delete this._pendingCalls[response.id]; - if (response.success) { - call.resolve(response.result); - } else { - call.reject(new Error(response.error || 'Unknown error')); - } - } - }, - - // Read string option - ReadString: function(option, defaultValue) { - return this._sendMessage('ReadString', [option, defaultValue || '']); - }, - - // Read integer option - ReadInt: function(option, defaultValue) { - return this._sendMessage('ReadInt', [option, defaultValue || 0]); - }, - - // Read double option - ReadDouble: function(option, defaultValue) { - return this._sendMessage('ReadDouble', [option, defaultValue || 0.0]); - }, - - // Read formula option - ReadFormula: function(option, defaultValue) { - return this._sendMessage('ReadFormula', [option, defaultValue || 0.0]); - }, - - // Read path option - ReadPath: function(option, defaultValue) { - return this._sendMessage('ReadPath', [option, defaultValue || '']); - }, - - // Read string from section - ReadStringFromSection: function(section, option, defaultValue) { - return this._sendMessage('ReadStringFromSection', [section, option, defaultValue || '']); - }, - - // Read int from section - ReadIntFromSection: function(section, option, defaultValue) { - return this._sendMessage('ReadIntFromSection', [section, option, defaultValue || 0]); - }, - - // Read double from section - ReadDoubleFromSection: function(section, option, defaultValue) { - return this._sendMessage('ReadDoubleFromSection', [section, option, defaultValue || 0.0]); - }, - - // Read formula from section - ReadFormulaFromSection: function(section, option, defaultValue) { - return this._sendMessage('ReadFormulaFromSection', [section, option, defaultValue || 0.0]); - }, - - // Replace variables - ReplaceVariables: function(text) { - return this._sendMessage('ReplaceVariables', [text]); - }, - - // Path to absolute - PathToAbsolute: function(path) { - return this._sendMessage('PathToAbsolute', [path]); - }, - - // Execute bang (synchronous, no return value) - Execute: function(command) { - this._sendMessage('Execute', [command]); - }, - - // Log message - Log: function(message, level) { - this._sendMessage('Log', [message, level || 'Notice']); - }, - - // Get measure name - get MeasureName() { - return this._sendMessage('GetMeasureName', []); - }, - - // Get skin name - get SkinName() { - return this._sendMessage('GetSkinName', []); - }, - - // Get skin window handle - get SkinWindowHandle() { - return this._sendMessage('GetSkinWindowHandle', []); - }, - - // Get settings file - get SettingsFile() { - return this._sendMessage('GetSettingsFile', []); - } - }; + // 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"; - // Listen for responses from C++ - window.chrome.webview.addEventListener('message', function(event) { - try { - const response = JSON.parse(event.data); - if (response.type === 'rainmeter_response') { - window.rm._handleResponse(response); - } - } catch (e) { - console.error('Failed to parse message from Rainmeter:', e); - } - }); + // Create the directory if it doesn't exist + CreateDirectoryW(userDataFolder.c_str(), nullptr); - console.log('Rainmeter API bridge initialized'); -})(); -)"; + // Create WebView2 environment with user data folder + HRESULT hr = CreateCoreWebView2EnvironmentWithOptions( + nullptr, userDataFolder.c_str(), nullptr, + Callback( + measure, + &Measure::CreateEnvironmentHandler + ).Get() + ); - measure->webView->AddScriptToExecuteOnDocumentCreated(bridgeScript, nullptr); + 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); + } + } } -// Handle web message from JavaScript -void HandleWebMessage(Measure* measure, LPCWSTR message) +// Environment creation callback +HRESULT Measure::CreateEnvironmentHandler(HRESULT result, ICoreWebView2Environment* env) { - if (!measure || !measure->rm || !message) - return; - - // Parse JSON message - std::wstring msgStr(message); - - // Simple JSON parsing (looking for specific fields) - // Format: {"type":"rainmeter_api","method":"ReadString","args":["Width","800"],"id":1} - - size_t typePos = msgStr.find(L"\"type\""); - size_t methodPos = msgStr.find(L"\"method\""); - size_t argsPos = msgStr.find(L"\"args\""); - size_t idPos = msgStr.find(L"\"id\""); - - if (typePos == std::wstring::npos || methodPos == std::wstring::npos || idPos == std::wstring::npos) - return; - - // Extract method name - size_t methodStart = msgStr.find(L"\"", methodPos + 9) + 1; - size_t methodEnd = msgStr.find(L"\"", methodStart); - std::wstring method = msgStr.substr(methodStart, methodEnd - methodStart); - - // Extract ID - size_t idStart = msgStr.find(L":", idPos) + 1; - size_t idEnd = msgStr.find_first_of(L",}", idStart); - int id = _wtoi(msgStr.substr(idStart, idEnd - idStart).c_str()); - - // Extract args array - std::vector args; - if (argsPos != std::wstring::npos) + if (FAILED(result)) { - size_t argsStart = msgStr.find(L"[", argsPos) + 1; - size_t argsEnd = msgStr.find(L"]", argsStart); - std::wstring argsStr = msgStr.substr(argsStart, argsEnd - argsStart); - - // Parse args (simple string extraction) - size_t pos = 0; - while (pos < argsStr.length()) + if (rm) { - size_t start = argsStr.find(L"\"", pos); - if (start == std::wstring::npos) break; - start++; - size_t end = argsStr.find(L"\"", start); - if (end == std::wstring::npos) break; - args.push_back(argsStr.substr(start, end - start)); - pos = end + 1; + wchar_t errorMsg[256]; + swprintf_s(errorMsg, L"WebView2: Failed to create environment (HRESULT: 0x%08X)", result); + RmLog(rm, LOG_ERROR, errorMsg); } + return result; } - // Process API call and build response - std::wstring result; - bool success = true; - std::wstring error; + // Create WebView2 controller using skin window directly + env->CreateCoreWebView2Controller( + skinWindow, + Callback( + this, + &Measure::CreateControllerHandler + ).Get() + ); - try + return S_OK; +} + +// Controller creation callback +HRESULT Measure::CreateControllerHandler(HRESULT result, ICoreWebView2Controller* controller) +{ + if (FAILED(result)) { - if (method == L"ReadString") - { - LPCWSTR value = RmReadString(measure->rm, args.size() > 0 ? args[0].c_str() : L"", - args.size() > 1 ? args[1].c_str() : L"", TRUE); - result = value ? value : L""; - } - else if (method == L"ReadInt") - { - int defaultVal = args.size() > 1 ? _wtoi(args[1].c_str()) : 0; - double value = RmReadFormula(measure->rm, args.size() > 0 ? args[0].c_str() : L"", defaultVal); - result = std::to_wstring(static_cast(value)); - } - else if (method == L"ReadDouble") - { - double defaultVal = args.size() > 1 ? _wtof(args[1].c_str()) : 0.0; - double value = RmReadFormula(measure->rm, args.size() > 0 ? args[0].c_str() : L"", defaultVal); - result = std::to_wstring(value); - } - else if (method == L"ReadFormula") - { - double defaultVal = args.size() > 1 ? _wtof(args[1].c_str()) : 0.0; - double value = RmReadFormula(measure->rm, args.size() > 0 ? args[0].c_str() : L"", defaultVal); - result = std::to_wstring(value); - } - else if (method == L"ReadPath") + if (rm) { - LPCWSTR value = RmReadString(measure->rm, args.size() > 0 ? args[0].c_str() : L"", - args.size() > 1 ? args[1].c_str() : L"", TRUE); - result = value ? value : L""; - } - else if (method == L"ReplaceVariables") - { - LPCWSTR value = RmReplaceVariables(measure->rm, args.size() > 0 ? args[0].c_str() : L""); - result = value ? value : L""; - } - else if (method == L"PathToAbsolute") - { - LPCWSTR value = RmPathToAbsolute(measure->rm, args.size() > 0 ? args[0].c_str() : L""); - result = value ? value : L""; - } - else if (method == L"Execute") - { - RmExecute(measure->skin, args.size() > 0 ? args[0].c_str() : L""); - result = L""; - } - else if (method == L"Log") - { - int level = LOG_NOTICE; - if (args.size() > 1) - { - if (args[1] == L"Error") level = LOG_ERROR; - else if (args[1] == L"Warning") level = LOG_WARNING; - else if (args[1] == L"Debug") level = LOG_DEBUG; - } - RmLog(measure->rm, level, args.size() > 0 ? args[0].c_str() : L""); - result = L""; - } - else if (method == L"GetMeasureName") - { - result = measure->measureName ? measure->measureName : L""; - } - else if (method == L"GetSkinName") - { - LPCWSTR skinName = RmGetSkinName(measure->rm); - result = skinName ? skinName : L""; - } - else if (method == L"GetSkinWindowHandle") - { - result = std::to_wstring(reinterpret_cast(measure->skinWindow)); - } - else if (method == L"GetSettingsFile") - { - LPCWSTR settingsFile = RmGetSettingsFile(); - result = settingsFile ? settingsFile : L""; - } - else - { - success = false; - error = L"Unknown method: " + method; + wchar_t errorMsg[256]; + swprintf_s(errorMsg, L"WebView2: Failed to create controller (HRESULT: 0x%08X)", result); + RmLog(rm, LOG_ERROR, errorMsg); } + return result; } - catch (...) + + if (controller == nullptr) { - success = false; - error = L"Exception occurred while processing request"; + if (rm) + RmLog(rm, LOG_ERROR, L"WebView2: Controller is null"); + return S_FALSE; } - // Build JSON response - std::wostringstream response; - response << L"{\"type\":\"rainmeter_response\",\"id\":" << id << L",\"success\":" - << (success ? L"true" : L"false"); + webViewController = controller; + webViewController->get_CoreWebView2(&webView); - if (success) + // Set bounds within the skin window + RECT bounds; + GetClientRect(skinWindow, &bounds); + bounds.left = x; + bounds.top = y; + if (width > 0) { - // Escape backslashes and quotes in result for JSON - std::wstring escapedResult = result; - size_t pos = 0; - - // First escape backslashes - while ((pos = escapedResult.find(L"\\", pos)) != std::wstring::npos) - { - escapedResult.replace(pos, 1, L"\\\\"); - pos += 2; - } - - // Then escape quotes - pos = 0; - while ((pos = escapedResult.find(L"\"", pos)) != std::wstring::npos) - { - escapedResult.replace(pos, 1, L"\\\""); - pos += 2; - } - - response << L",\"result\":\"" << escapedResult << L"\""; + bounds.right = x + width; } - else + if (height > 0) { - // Escape error message too - std::wstring escapedError = error; - size_t pos = 0; - while ((pos = escapedError.find(L"\\", pos)) != std::wstring::npos) - { - escapedError.replace(pos, 1, L"\\\\"); - pos += 2; - } - pos = 0; - while ((pos = escapedError.find(L"\"", pos)) != std::wstring::npos) - { - escapedError.replace(pos, 1, L"\\\""); - pos += 2; - } - response << L",\"error\":\"" << escapedError << L"\""; + bounds.bottom = y + height; } + webViewController->put_Bounds(bounds); - response << L"}"; + // Set initial visibility + webViewController->put_IsVisible(visible ? TRUE : FALSE); - // Send response back to JavaScript - if (measure->webView) - { - measure->webView->PostWebMessageAsString(response.str().c_str()); - } -} - -// Create WebView2 environment and controller -void CreateWebView2(Measure* measure) -{ - if (!measure || !measure->skinWindow) + // Transparent background + auto controller2 = webViewController.query(); + if (controller2) { - if (measure && measure->rm) - RmLog(measure->rm, LOG_ERROR, L"WebView2: Invalid measure or skin window"); - return; + COREWEBVIEW2_COLOR transparentColor = { 0, 0, 0, 0 }; + controller2->put_DefaultBackgroundColor(transparentColor); } - // Register window class - RegisterWebViewWindowClass(); - - // Create host window as child of skin window - measure->webViewWindow = CreateWindowEx( - 0, - L"RainmeterWebView2Host", - L"WebView2", - WS_CHILD | (measure->visible ? WS_VISIBLE : 0), - measure->x, measure->y, - measure->width, measure->height, - measure->skinWindow, - nullptr, - GetModuleHandle(nullptr), + // 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"rm", &variant); + variant.pdispVal->Release(); + + // Add script to make rm available globally + webView->AddScriptToExecuteOnDocumentCreated( + L"window.rm = chrome.webview.hostObjects.sync.rm", nullptr ); - if (!measure->webViewWindow) + // Navigate to URL + if (!url.empty()) { - if (measure->rm) - RmLog(measure->rm, LOG_ERROR, L"WebView2: Failed to create host window"); - return; + webView->Navigate(url.c_str()); } - // Store measure pointer in window - SetWindowLongPtr(measure->webViewWindow, GWLP_USERDATA, reinterpret_cast(measure)); - - // 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); + initialized = true; - // Create WebView2 environment with user data folder - HRESULT hr = CreateCoreWebView2EnvironmentWithOptions( - nullptr, userDataFolder.c_str(), nullptr, - Callback( - [measure](HRESULT result, ICoreWebView2Environment* env) -> HRESULT - { - if (FAILED(result)) - { - if (measure->rm) - { - wchar_t errorMsg[256]; - swprintf_s(errorMsg, L"WebView2: Failed to create environment (HRESULT: 0x%08X)", result); - RmLog(measure->rm, LOG_ERROR, errorMsg); - } - return result; - } - - // Create WebView2 controller - env->CreateCoreWebView2Controller( - measure->webViewWindow, - Callback( - [measure](HRESULT result, ICoreWebView2Controller* controller) -> HRESULT - { - if (FAILED(result)) - { - if (measure->rm) - { - wchar_t errorMsg[256]; - swprintf_s(errorMsg, L"WebView2: Failed to create controller (HRESULT: 0x%08X)", result); - RmLog(measure->rm, LOG_ERROR, errorMsg); - } - return result; - } - - measure->webViewController = controller; - measure->webViewController->get_CoreWebView2(&measure->webView); - - // Set bounds - RECT bounds; - GetClientRect(measure->webViewWindow, &bounds); - measure->webViewController->put_Bounds(bounds); - - // Set initial visibility - measure->webViewController->put_IsVisible(measure->visible ? TRUE : FALSE); - - // Set up web message handler and store token for cleanup - measure->webView->add_WebMessageReceived( - Callback( - [measure](ICoreWebView2* sender, ICoreWebView2WebMessageReceivedEventArgs* args) -> HRESULT - { - wil::unique_cotaskmem_string message; - args->TryGetWebMessageAsString(&message); - if (message) - { - HandleWebMessage(measure, message.get()); - } - return S_OK; - } - ).Get(), - &measure->webMessageToken - ); - - // Inject JavaScript bridge - InjectJavaScriptBridge(measure); - - // Navigate to URL - if (!measure->url.empty()) - { - measure->webView->Navigate(measure->url.c_str()); - } - - measure->initialized = true; - - if (measure->rm) - RmLog(measure->rm, LOG_NOTICE, L"WebView2: Initialized successfully with JavaScript bridge"); - - return S_OK; - } - ).Get() - ); - - return S_OK; - } - ).Get() - ); + if (rm) + RmLog(rm, LOG_NOTICE, L"WebView2: Initialized successfully with COM Host Objects"); - 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); - } - } + return S_OK; } diff --git a/WebView2/WebView2.rc b/WebView2/WebView2.rc index 9e7c779..ddcb688 100644 Binary files a/WebView2/WebView2.rc and b/WebView2/WebView2.rc differ diff --git a/WebView2/WebView2.vcxproj b/WebView2/WebView2.vcxproj index 31ab383..9cbaa8b 100644 --- a/WebView2/WebView2.vcxproj +++ b/WebView2/WebView2.vcxproj @@ -22,15 +22,22 @@ + + + + + + + {64FDEE97-6B7E-40E5-A489-ECA322825BC8} Win32Proj @@ -119,6 +126,9 @@ ..\API\x32\Rainmeter.lib;%(AdditionalDependencies) Windows + + $(IntDir);%(AdditionalIncludeDirectories) + @@ -137,6 +147,7 @@ _WIN64;%(PreprocessorDefinitions) + $(IntDir);%(AdditionalIncludeDirectories) @@ -159,6 +170,9 @@ ..\API\x32\Rainmeter.lib;%(AdditionalDependencies) Windows + + $(IntDir);%(AdditionalIncludeDirectories) + @@ -182,6 +196,7 @@ _WIN64;%(PreprocessorDefinitions) + $(IntDir);%(AdditionalIncludeDirectories) diff --git a/WebView2/WebView2.vcxproj.filters b/WebView2/WebView2.vcxproj.filters index fcad2b1..6b93abb 100644 --- a/WebView2/WebView2.vcxproj.filters +++ b/WebView2/WebView2.vcxproj.filters @@ -5,5 +5,22 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/WebView2/packages.config b/WebView2/packages.config index 4b13e9e..4662d01 100644 --- a/WebView2/packages.config +++ b/WebView2/packages.config @@ -1,5 +1,5 @@ - - + + diff --git a/WebView2/resource.h b/WebView2/resource.h new file mode 100644 index 0000000..7e6f6a4 --- /dev/null +++ b/WebView2/resource.h @@ -0,0 +1,15 @@ +// Microsoft Visual C++ generated include file. +// Used by WebView2.rc + +#define IDR_TYPELIB1 1 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 101 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif